Blob Blame History Raw
From 8788ac191a4e0689f0287695c181fe1a781b0d31 Mon Sep 17 00:00:00 2001
From: Dumitru Ceara <dceara@redhat.com>
Date: Fri, 15 Jan 2021 19:26:13 +0100
Subject: [PATCH 1/2] Support configuring Load Balancer hairpin source IP.

In case traffic that gets load balanced is DNAT-ed to a backend IP that
happens to be the source of the traffic then OVN performs an additional
SNAT to ensure that return traffic is directed through OVN.

Until now the load balancer VIP was chosen as SNAT IP.  However, in
specific scenarios, the CMS may prefer a different IP, e.g., a single
cluster-wide IP.  This commit adds support, through the newly added
Load_Balancer.option 'hairpin_snat_ip', to allow the CMS to explicitly
chose a SNAT IP.

Due to the fact that now traffic that was hairpinned might need to be
SNAT-ed to different IPs for different load balancers that share the
same VIP address value we need to also explicitly match on L4 protocol
and ports in the 'OFTABLE_CT_SNAT_FOR_VIP' table.

Signed-off-by: Dumitru Ceara <dceara@redhat.com>
Signed-off-by: Numan Siddique <numans@ovn.org>
(cherry picked from upstream commit cc4d5520064f294d2b011be10ec5ff5f1a85bfd0)

Conflicts:
	NEWS

Change-Id: Ie5ba5d9f3811ee577377e3e2cd700d2949a174da
---
 NEWS                |   3 +
 controller/lflow.c  |  53 +++++++++------
 lib/lb.c            |  26 ++++++++
 lib/lb.h            |  11 ++++
 lib/ovn-util.c      |  21 ++++++
 lib/ovn-util.h      |   1 +
 northd/ovn-northd.c |   9 +--
 ovn-nb.xml          |   8 +++
 ovn-sb.ovsschema    |   9 ++-
 ovn-sb.xml          |   8 +++
 tests/ovn-northd.at |   6 ++
 tests/ovn.at        | 182 ++++++++++++++++++++++++++++++++++++++--------------
 12 files changed, 261 insertions(+), 76 deletions(-)

diff --git a/NEWS b/NEWS
index e89c5f4..57a9ba9 100644
--- a/NEWS
+++ b/NEWS
@@ -10,6 +10,9 @@ Post-v20.12.0
     "ovn-installed".  This external-id is set by ovn-controller only after all
     openflow operations corresponding to the OVS interface being added have
     been processed.
+  - Add a new option to Load_Balancer.options, "hairpin_snat_ip", to allow
+    users to explicitly select which source IP should be used for load
+    balancer hairpin traffic.
 
 OVN v20.12.0 - 18 Dec 2020
 --------------------------
diff --git a/controller/lflow.c b/controller/lflow.c
index 9f6aece..946c1e0 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -1189,26 +1189,30 @@ add_lb_vip_hairpin_flows(struct ovn_controller_lb *lb,
     struct match hairpin_reply_match = MATCH_CATCHALL_INITIALIZER;
 
     if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
-        ovs_be32 ip4 = in6_addr_get_mapped_ipv4(&lb_backend->ip);
+        ovs_be32 bip4 = in6_addr_get_mapped_ipv4(&lb_backend->ip);
+        ovs_be32 vip4 = lb->hairpin_snat_ips.n_ipv4_addrs
+                        ? lb->hairpin_snat_ips.ipv4_addrs[0].addr
+                        : in6_addr_get_mapped_ipv4(&lb_vip->vip);
 
         match_set_dl_type(&hairpin_match, htons(ETH_TYPE_IP));
-        match_set_nw_src(&hairpin_match, ip4);
-        match_set_nw_dst(&hairpin_match, ip4);
-
-        match_set_dl_type(&hairpin_reply_match,
-                          htons(ETH_TYPE_IP));
-        match_set_nw_src(&hairpin_reply_match, ip4);
-        match_set_nw_dst(&hairpin_reply_match,
-                         in6_addr_get_mapped_ipv4(&lb_vip->vip));
+        match_set_nw_src(&hairpin_match, bip4);
+        match_set_nw_dst(&hairpin_match, bip4);
+
+        match_set_dl_type(&hairpin_reply_match, htons(ETH_TYPE_IP));
+        match_set_nw_src(&hairpin_reply_match, bip4);
+        match_set_nw_dst(&hairpin_reply_match, vip4);
     } else {
+        struct in6_addr *bip6 = &lb_backend->ip;
+        struct in6_addr *vip6 = lb->hairpin_snat_ips.n_ipv6_addrs
+                                ? &lb->hairpin_snat_ips.ipv6_addrs[0].addr
+                                : &lb_vip->vip;
         match_set_dl_type(&hairpin_match, htons(ETH_TYPE_IPV6));
-        match_set_ipv6_src(&hairpin_match, &lb_backend->ip);
-        match_set_ipv6_dst(&hairpin_match, &lb_backend->ip);
+        match_set_ipv6_src(&hairpin_match, bip6);
+        match_set_ipv6_dst(&hairpin_match, bip6);
 
-        match_set_dl_type(&hairpin_reply_match,
-                          htons(ETH_TYPE_IPV6));
-        match_set_ipv6_src(&hairpin_reply_match, &lb_backend->ip);
-        match_set_ipv6_dst(&hairpin_reply_match, &lb_vip->vip);
+        match_set_dl_type(&hairpin_reply_match, htons(ETH_TYPE_IPV6));
+        match_set_ipv6_src(&hairpin_reply_match, bip6);
+        match_set_ipv6_dst(&hairpin_reply_match, vip6);
     }
 
     if (lb_backend->port) {
@@ -1254,6 +1258,7 @@ add_lb_vip_hairpin_flows(struct ovn_controller_lb *lb,
 static void
 add_lb_ct_snat_vip_flows(struct ovn_controller_lb *lb,
                          struct ovn_lb_vip *lb_vip,
+                         uint8_t lb_proto,
                          struct ovn_desired_flow_table *flow_table)
 {
     uint64_t stub[1024 / 8];
@@ -1277,10 +1282,16 @@ add_lb_ct_snat_vip_flows(struct ovn_controller_lb *lb,
 
     if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
         nat->range_af = AF_INET;
-        nat->range.addr.ipv4.min = in6_addr_get_mapped_ipv4(&lb_vip->vip);
+        nat->range.addr.ipv4.min =
+            lb->hairpin_snat_ips.n_ipv4_addrs
+            ? lb->hairpin_snat_ips.ipv4_addrs[0].addr
+            : in6_addr_get_mapped_ipv4(&lb_vip->vip);
     } else {
         nat->range_af = AF_INET6;
-        nat->range.addr.ipv6.min = lb_vip->vip;
+        nat->range.addr.ipv6.min
+            = lb->hairpin_snat_ips.n_ipv6_addrs
+            ? lb->hairpin_snat_ips.ipv6_addrs[0].addr
+            : lb_vip->vip;
     }
     ofpacts.header = ofpbuf_push_uninit(&ofpacts, nat_offset);
     ofpact_finish(&ofpacts, &ct->ofpact);
@@ -1288,12 +1299,16 @@ add_lb_ct_snat_vip_flows(struct ovn_controller_lb *lb,
     struct match match = MATCH_CATCHALL_INITIALIZER;
     if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
         match_set_dl_type(&match, htons(ETH_TYPE_IP));
-        match_set_ct_nw_dst(&match, nat->range.addr.ipv4.min);
+        match_set_ct_nw_dst(&match, in6_addr_get_mapped_ipv4(&lb_vip->vip));
     } else {
         match_set_dl_type(&match, htons(ETH_TYPE_IPV6));
         match_set_ct_ipv6_dst(&match, &lb_vip->vip);
     }
 
+    match_set_nw_proto(&match, lb_proto);
+    match_set_ct_nw_proto(&match, lb_proto);
+    match_set_ct_tp_dst(&match, htons(lb_vip->vip_port));
+
     uint32_t ct_state = OVS_CS_F_TRACKED | OVS_CS_F_DST_NAT;
     match_set_ct_state_masked(&match, ct_state, ct_state);
 
@@ -1349,7 +1364,7 @@ consider_lb_hairpin_flows(const struct sbrec_load_balancer *sbrec_lb,
                                      flow_table);
         }
 
-        add_lb_ct_snat_vip_flows(lb, lb_vip, flow_table);
+        add_lb_ct_snat_vip_flows(lb, lb_vip, lb_proto, flow_table);
     }
 
     ovn_controller_lb_destroy(lb);
diff --git a/lib/lb.c b/lib/lb.c
index 2517c02..e11ac00 100644
--- a/lib/lb.c
+++ b/lib/lb.c
@@ -170,6 +170,24 @@ void ovn_northd_lb_vip_destroy(struct ovn_northd_lb_vip *vip)
     free(vip->backends_nb);
 }
 
+static void
+ovn_lb_get_hairpin_snat_ip(const struct uuid *lb_uuid,
+                           const struct smap *lb_options,
+                           struct lport_addresses *hairpin_addrs)
+{
+    const char *addresses = smap_get(lb_options, "hairpin_snat_ip");
+
+    if (!addresses) {
+        return;
+    }
+
+    if (!extract_ip_address(addresses, hairpin_addrs)) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+        VLOG_WARN_RL(&rl, "bad hairpin_snat_ip %s in load balancer "UUID_FMT,
+                     addresses, UUID_ARGS(lb_uuid));
+    }
+}
+
 struct ovn_northd_lb *
 ovn_northd_lb_create(const struct nbrec_load_balancer *nbrec_lb,
                      struct hmap *ports,
@@ -224,6 +242,9 @@ ovn_northd_lb_create(const struct nbrec_load_balancer *nbrec_lb,
         ds_chomp(&sel_fields, ',');
         lb->selection_fields = ds_steal_cstr(&sel_fields);
     }
+
+    ovn_lb_get_hairpin_snat_ip(&nbrec_lb->header_.uuid, &nbrec_lb->options,
+                               &lb->hairpin_snat_ips);
     return lb;
 }
 
@@ -260,6 +281,7 @@ ovn_northd_lb_destroy(struct ovn_northd_lb *lb)
     free(lb->vips);
     free(lb->vips_nb);
     free(lb->selection_fields);
+    destroy_lport_addresses(&lb->hairpin_snat_ips);
     free(lb->dps);
     free(lb);
 }
@@ -289,6 +311,9 @@ ovn_controller_lb_create(const struct sbrec_load_balancer *sbrec_lb)
      * correct value.
      */
     lb->n_vips = n_vips;
+
+    ovn_lb_get_hairpin_snat_ip(&sbrec_lb->header_.uuid, &sbrec_lb->options,
+                               &lb->hairpin_snat_ips);
     return lb;
 }
 
@@ -299,5 +324,6 @@ ovn_controller_lb_destroy(struct ovn_controller_lb *lb)
         ovn_lb_vip_destroy(&lb->vips[i]);
     }
     free(lb->vips);
+    destroy_lport_addresses(&lb->hairpin_snat_ips);
     free(lb);
 }
diff --git a/lib/lb.h b/lib/lb.h
index 42c580b..dfce51c 100644
--- a/lib/lb.h
+++ b/lib/lb.h
@@ -20,6 +20,7 @@
 #include <sys/types.h>
 #include <netinet/in.h>
 #include "openvswitch/hmap.h"
+#include "ovn-util.h"
 
 struct nbrec_load_balancer;
 struct sbrec_load_balancer;
@@ -37,6 +38,11 @@ struct ovn_northd_lb {
     struct ovn_northd_lb_vip *vips_nb;
     size_t n_vips;
 
+    struct lport_addresses hairpin_snat_ips; /* IP (v4 and/or v6) to be used
+                                              * as source for hairpinned
+                                              * traffic.
+                                              */
+
     size_t n_dps;
     size_t n_allocated_dps;
     const struct sbrec_datapath_binding **dps;
@@ -89,6 +95,11 @@ struct ovn_controller_lb {
 
     struct ovn_lb_vip *vips;
     size_t n_vips;
+
+    struct lport_addresses hairpin_snat_ips; /* IP (v4 and/or v6) to be used
+                                              * as source for hairpinned
+                                              * traffic.
+                                              */
 };
 
 struct ovn_controller_lb *ovn_controller_lb_create(
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index 2136f90..b647106 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -232,6 +232,27 @@ extract_ip_addresses(const char *address, struct lport_addresses *laddrs)
     return false;
 }
 
+/* Extracts at most one IPv4 and at most one IPv6 address from 'address'
+ * which should be of the format 'IP1 [IP2]'.
+ *
+ * Return true if at most one IPv4 address and at most one IPv6 address
+ * is found in 'address'.  IPs must be host IPs, i.e., no unmasked bits.
+ *
+ * The caller must call destroy_lport_addresses().
+ */
+bool extract_ip_address(const char *address, struct lport_addresses *laddrs)
+{
+    if (!extract_ip_addresses(address, laddrs) ||
+            laddrs->n_ipv4_addrs > 1 ||
+            laddrs->n_ipv6_addrs > 1 ||
+            (laddrs->n_ipv4_addrs && laddrs->ipv4_addrs[0].plen != 32) ||
+            (laddrs->n_ipv6_addrs && laddrs->ipv6_addrs[0].plen != 128)) {
+        destroy_lport_addresses(laddrs);
+        return false;
+    }
+    return true;
+}
+
 /* Extracts the mac, IPv4 and IPv6 addresses from the
  * "nbrec_logical_router_port" parameter 'lrp'.  Stores the IPv4 and
  * IPv6 addresses in the 'ipv4_addrs' and 'ipv6_addrs' fields of
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index 679f47a..a711363 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -72,6 +72,7 @@ bool extract_addresses(const char *address, struct lport_addresses *,
                        int *ofs);
 bool extract_lsp_addresses(const char *address, struct lport_addresses *);
 bool extract_ip_addresses(const char *address, struct lport_addresses *);
+bool extract_ip_address(const char *address, struct lport_addresses *);
 bool extract_lrp_networks(const struct nbrec_logical_router_port *,
                           struct lport_addresses *);
 bool extract_sbrec_binding_first_mac(const struct sbrec_port_binding *binding,
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 62d45f9..d9bcd6f 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -3606,6 +3606,7 @@ build_ovn_lbs(struct northd_context *ctx, struct hmap *datapaths,
         sbrec_load_balancer_set_name(lb->slb, lb->nlb->name);
         sbrec_load_balancer_set_vips(lb->slb, &lb->nlb->vips);
         sbrec_load_balancer_set_protocol(lb->slb, lb->nlb->protocol);
+        sbrec_load_balancer_set_options(lb->slb, &lb->nlb->options);
         sbrec_load_balancer_set_datapaths(
             lb->slb, (struct sbrec_datapath_binding **)lb->dps,
             lb->n_dps);
@@ -8593,15 +8594,10 @@ get_force_snat_ip(struct ovn_datapath *od, const char *key_type,
         return false;
     }
 
-    if (!extract_ip_addresses(addresses, laddrs) ||
-        laddrs->n_ipv4_addrs > 1 ||
-        laddrs->n_ipv6_addrs > 1 ||
-        (laddrs->n_ipv4_addrs && laddrs->ipv4_addrs[0].plen != 32) ||
-        (laddrs->n_ipv6_addrs && laddrs->ipv6_addrs[0].plen != 128)) {
+    if (!extract_ip_address(addresses, laddrs)) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
         VLOG_WARN_RL(&rl, "bad ip %s in options of router "UUID_FMT"",
                      addresses, UUID_ARGS(&od->key));
-        destroy_lport_addresses(laddrs);
         return false;
     }
 
@@ -13852,6 +13848,7 @@ main(int argc, char *argv[])
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_load_balancer_col_name);
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_load_balancer_col_vips);
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_load_balancer_col_protocol);
+    add_column_noalert(ovnsb_idl_loop.idl, &sbrec_load_balancer_col_options);
     add_column_noalert(ovnsb_idl_loop.idl,
                        &sbrec_load_balancer_col_external_ids);
 
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 105d869..86aa438 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -1644,6 +1644,14 @@
         Please note using <code>--reject</code> option will disable empty_lb
         SB controller event for this load balancer.
       </column>
+
+      <column name="options" key="hairpin_snat_ip">
+        IP to be used as source IP for packets that have been hair-pinned
+        after load balancing.  The default behavior when the option is not set
+        is to use the load balancer VIP as source IP.  This option may have
+        exactly one IPv4 and/or one IPv6 address on it, separated by a space
+        character.
+      </column>
     </group>
   </table>
 
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index b418434..0d20f08 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@
 {
     "name": "OVN_Southbound",
-    "version": "20.14.0",
-    "cksum": "1412040198 25748",
+    "version": "20.15.0",
+    "cksum": "539683023 25965",
     "tables": {
         "SB_Global": {
             "columns": {
@@ -482,6 +482,11 @@
                     "type": {"key": {"type": "uuid",
                                      "refTable": "Datapath_Binding"},
                              "min": 0, "max": "unlimited"}},
+                "options": {
+                     "type": {"key": "string",
+                              "value": "string",
+                              "min": 0,
+                              "max": "unlimited"}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 980a096..2f251bd 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -4238,6 +4238,14 @@ tcp.flags = RST;
       Datapaths to which this load balancer applies to.
     </column>
 
+    <group title="Load_Balancer options">
+    <column name="options" key="hairpin_snat_ip">
+      IP to be used as source IP for packets that have been hair-pinned after
+      load balancing.  This value is automatically populated by
+      <code>ovn-northd</code>.
+    </column>
+    </group>
+
     <group title="Common Columns">
       <column name="external_ids">
         See <em>External IDs</em> at the beginning of this document.
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index d52aeed..c02c5d7 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -2131,6 +2131,12 @@ echo
 echo "__file__:__line__: check that datapath sw1 has lb0 and lb1 set in the load_balancers column."
 check_column "$lb0_uuid $lb1_uuid" sb:datapath_binding load_balancers external_ids:name=sw1
 
+
+echo
+echo "__file__:__line__: Set hairpin_snat_ip on lb1 and check that SB DB is updated."
+check ovn-nbctl --wait=sb set Load_Balancer lb1 options:hairpin_snat_ip="42.42.42.42 4242::4242"
+check_column "$lb1_uuid" sb:load_balancer _uuid name=lb1 options='{hairpin_snat_ip="42.42.42.42 4242::4242"}'
+
 echo
 echo "__file__:__line__: Delete load balancer lb1 an check that datapath sw1's load_balancers are updated accordingly."
 
diff --git a/tests/ovn.at b/tests/ovn.at
index 9f2e152..14072ec 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -20742,8 +20742,9 @@ build_tcp_syn() {
 
 send_ipv4_pkt() {
     local hv=$1 inport=$2 eth_src=$3 eth_dst=$4
-    local ip_src=$5 ip_dst=$6 ip_proto=$7 ip_len=$8 ip_chksum=$9
-    local l4_payload=${10}
+    local ip_src=$5 ip_dst=$6 ip_proto=$7 ip_len=$8
+    local l4_payload=$9
+    local hp_ip_src=${10}
     local hp_l4_payload=${11}
     local outfile=${12}
 
@@ -20751,8 +20752,10 @@ send_ipv4_pkt() {
 
     local eth=${eth_dst}${eth_src}0800
     local hp_eth=${eth_src}${eth_dst}0800
-    local ip=4500${ip_len}00004000${ip_ttl}${ip_proto}${ip_chksum}${ip_src}${ip_dst}
-    local hp_ip=4500${ip_len}00004000${ip_ttl}${ip_proto}${ip_chksum}${ip_dst}${ip_src}
+    local ip=4500${ip_len}00004000${ip_ttl}${ip_proto}0000${ip_src}${ip_dst}
+    ip=$(ip4_csum_inplace $ip)
+    local hp_ip=4500${ip_len}00004000${ip_ttl}${ip_proto}0000${hp_ip_src}${ip_src}
+    hp_ip=$(ip4_csum_inplace ${hp_ip})
     local packet=${eth}${ip}${l4_payload}
     local hp_packet=${hp_eth}${hp_ip}${hp_l4_payload}
 
@@ -20764,15 +20767,16 @@ send_ipv6_pkt() {
     local hv=$1 inport=$2 eth_src=$3 eth_dst=$4
     local ip_src=$5 ip_dst=$6 ip_proto=$7 ip_len=$8
     local l4_payload=$9
-    local hp_l4_payload=${10}
-    local outfile=${11}
+    local hp_ip_src=${10}
+    local hp_l4_payload=${11}
+    local outfile=${12}
 
     local ip_ttl=40
 
     local eth=${eth_dst}${eth_src}86dd
     local hp_eth=${eth_src}${eth_dst}86dd
     local ip=60000000${ip_len}${ip_proto}${ip_ttl}${ip_src}${ip_dst}
-    local hp_ip=60000000${ip_len}${ip_proto}${ip_ttl}${ip_dst}${ip_src}
+    local hp_ip=60000000${ip_len}${ip_proto}${ip_ttl}${hp_ip_src}${ip_src}
     local packet=${eth}${ip}${l4_payload}
     local hp_packet=${hp_eth}${hp_ip}${hp_l4_payload}
 
@@ -20814,18 +20818,35 @@ ovn-nbctl lsp-add sw sw-rtr                       \
 
 ovn-nbctl --wait=hv sync
 
-# Inject IPv4 TCP packet from lsp.
+ovn-sbctl dump-flows > sbflows
+AT_CAPTURE_FILE([sbflows])
 > expected
+
+# Inject IPv4 TCP packet from lsp.
 tcp_payload=$(build_tcp_syn 84d0 1f90 05a7)
 hp_tcp_payload=$(build_tcp_syn 84d0 0fc9 156e)
 send_ipv4_pkt hv1 hv1-vif1 000000000001 000000000100 \
     $(ip_to_hex 42 42 42 1) $(ip_to_hex 88 88 88 88) \
-    06 0028 35f5 \
-    ${tcp_payload} ${hp_tcp_payload} \
+    06 0028 \
+    ${tcp_payload} \
+    $(ip_to_hex 88 88 88 88) ${hp_tcp_payload} \
     expected
 
-ovn-sbctl dump-flows > sbflows
-AT_CAPTURE_FILE([sbflows])
+# Check that traffic is hairpinned.
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [expected])
+
+# Change LB Hairpin SNAT IP.
+# Also flush conntrack to avoid reusing an existing entry.
+as hv1 ovs-appctl dpctl/flush-conntrack
+ovn-nbctl --wait=hv set load_balancer lb-ipv4-tcp options:hairpin_snat_ip="88.88.88.87"
+# Inject IPv4 TCP packet from lsp.
+hp_tcp_payload=$(build_tcp_syn 84d0 0fc9 156f)
+send_ipv4_pkt hv1 hv1-vif1 000000000001 000000000100 \
+    $(ip_to_hex 42 42 42 1) $(ip_to_hex 88 88 88 88) \
+    06 0028 \
+    ${tcp_payload} \
+    $(ip_to_hex 88 88 88 87) ${hp_tcp_payload} \
+    expected
 
 # Check that traffic is hairpinned.
 OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [expected])
@@ -20835,8 +20856,25 @@ udp_payload=$(build_udp 84d0 0fc8 6666)
 hp_udp_payload=$(build_udp 84d0 07e5 6e49)
 send_ipv4_pkt hv1 hv1-vif1 000000000001 000000000100 \
     $(ip_to_hex 42 42 42 1) $(ip_to_hex 88 88 88 88) \
-    11 001e 35f4 \
-    ${udp_payload} ${hp_udp_payload} \
+    11 001e \
+    ${udp_payload} \
+    $(ip_to_hex 88 88 88 88) ${hp_udp_payload} \
+    expected
+
+# Check that traffic is hairpinned.
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [expected])
+
+# Change LB Hairpin SNAT IP.
+# Also flush conntrack to avoid reusing an existing entry.
+as hv1 ovs-appctl dpctl/flush-conntrack
+ovn-nbctl --wait=hv set load_balancer lb-ipv4-udp options:hairpin_snat_ip="88.88.88.87"
+# Inject IPv4 UDP packet from lsp.
+hp_udp_payload=$(build_udp 84d0 07e5 6e4a)
+send_ipv4_pkt hv1 hv1-vif1 000000000001 000000000100 \
+    $(ip_to_hex 42 42 42 1) $(ip_to_hex 88 88 88 88) \
+    11 001e \
+    ${udp_payload} \
+    $(ip_to_hex 88 88 88 87) ${hp_udp_payload} \
     expected
 
 # Check that traffic is hairpinned.
@@ -20848,7 +20886,25 @@ hp_tcp_payload=$(build_tcp_syn 84d0 0fc9 4fc0)
 send_ipv6_pkt hv1 hv1-vif1 000000000001 000000000100 \
     42000000000000000000000000000001 88000000000000000000000000000088 \
     06 0014 \
-    ${tcp_payload} ${hp_tcp_payload} \
+    ${tcp_payload} \
+    88000000000000000000000000000088 ${hp_tcp_payload} \
+    expected
+
+# Check that traffic is hairpinned.
+OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [expected])
+
+# Change LB Hairpin SNAT IP.
+# Also flush conntrack to avoid reusing an existing entry.
+as hv1 ovs-appctl dpctl/flush-conntrack
+ovn-nbctl --wait=hv set load_balancer lb-ipv6-tcp options:hairpin_snat_ip="8800::0087"
+
+# Inject IPv6 TCP packet from lsp.
+hp_tcp_payload=$(build_tcp_syn 84d0 0fc9 4fc1)
+send_ipv6_pkt hv1 hv1-vif1 000000000001 000000000100 \
+    42000000000000000000000000000001 88000000000000000000000000000088 \
+    06 0014 \
+    ${tcp_payload} \
+    88000000000000000000000000000087 ${hp_tcp_payload} \
     expected
 
 # Check that traffic is hairpinned.
@@ -20860,12 +20916,27 @@ hp_udp_payload=$(build_udp 84d0 07e5 a89b)
 send_ipv6_pkt hv1 hv1-vif1 000000000001 000000000100 \
     42000000000000000000000000000001 88000000000000000000000000000088 \
     11 000a \
-    ${udp_payload} ${hp_udp_payload} \
+    ${udp_payload} \
+    88000000000000000000000000000088 ${hp_udp_payload} \
     expected
 
-# Check that traffic is hairpinned.
+Check that traffic is hairpinned.
 OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [expected])
 
+# Change LB Hairpin SNAT IP.
+# Also flush conntrack to avoid reusing an existing entry.
+as hv1 ovs-appctl dpctl/flush-conntrack
+ovn-nbctl --wait=hv set load_balancer lb-ipv6-udp options:hairpin_snat_ip="8800::0087"
+
+# Inject IPv6 UDP packet from lsp.
+hp_udp_payload=$(build_udp 84d0 07e5 a89b)
+send_ipv6_pkt hv1 hv1-vif1 000000000001 000000000100 \
+    42000000000000000000000000000001 88000000000000000000000000000088 \
+    11 000a \
+    ${udp_payload} \
+    88000000000000000000000000000087 ${hp_udp_payload} \
+    expected
+
 OVN_CLEANUP([hv1])
 AT_CLEANUP
 
@@ -23156,7 +23227,7 @@ priority=100,tcp,metadata=0x1,nw_src=42.42.42.1,nw_dst=88.88.88.88,tp_src=4041 a
 ])
 
 AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8-], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=68], [0], [dnl
@@ -23190,8 +23261,8 @@ priority=100,tcp,metadata=0x1,nw_src=52.52.52.52,nw_dst=88.88.88.90,tp_src=4042
 ])
 
 AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8-], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=68], [0], [dnl
@@ -23227,8 +23298,8 @@ priority=100,tcp,metadata=0x1,nw_src=52.52.52.52,nw_dst=88.88.88.90,tp_src=4042
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8-], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 check ovn-nbctl --wait=hv ls-lb-add sw0 lb-ipv4-udp
@@ -23256,8 +23327,9 @@ priority=100,udp,metadata=0x1,nw_src=42.42.42.1,nw_dst=88.88.88.88,tp_src=2021 a
 ])
 
 AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=17,ct_tp_dst=4040,udp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=68 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
@@ -23275,8 +23347,9 @@ priority=100,udp,metadata=0x1,nw_src=42.42.42.1,nw_dst=88.88.88.88,tp_src=2021 a
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=17,ct_tp_dst=4040,udp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 check ovn-nbctl --wait=hv ls-lb-add sw0 lb-ipv6-tcp
@@ -23306,9 +23379,10 @@ priority=100,udp,metadata=0x1,nw_src=42.42.42.1,nw_dst=88.88.88.88,tp_src=2021 a
 ])
 
 AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=6,ct_tp_dst=8080,tcp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=17,ct_tp_dst=4040,udp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=68 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
@@ -23328,9 +23402,10 @@ priority=100,udp,metadata=0x1,nw_src=42.42.42.1,nw_dst=88.88.88.88,tp_src=2021 a
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=6,ct_tp_dst=8080,tcp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=17,ct_tp_dst=4040,udp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 check ovn-nbctl --wait=hv ls-lb-add sw0 lb-ipv6-udp
@@ -23362,9 +23437,11 @@ priority=100,udp6,metadata=0x1,ipv6_src=4200::1,ipv6_dst=8800::88,tp_src=2021 ac
 ])
 
 AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=17,ct_tp_dst=4040,udp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=6,ct_tp_dst=8080,tcp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=17,ct_tp_dst=4040,udp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=68 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
@@ -23386,9 +23463,11 @@ priority=100,udp6,metadata=0x1,ipv6_src=4200::1,ipv6_dst=8800::88,tp_src=2021 ac
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=17,ct_tp_dst=4040,udp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=6,ct_tp_dst=8080,tcp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=17,ct_tp_dst=4040,udp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 check ovn-nbctl --wait=hv ls-lb-add sw1 lb-ipv6-udp
@@ -23423,10 +23502,12 @@ priority=100,udp6,metadata=0x2,ipv6_src=4200::1,ipv6_dst=8800::88,tp_src=2021 ac
 ])
 
 AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x2 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=17,ct_tp_dst=4040,udp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=17,ct_tp_dst=4040,udp6,metadata=0x2 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=6,ct_tp_dst=8080,tcp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=17,ct_tp_dst=4040,udp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=68 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
@@ -23449,10 +23530,12 @@ priority=100,udp6,metadata=0x2,ipv6_src=4200::1,ipv6_dst=8800::88,tp_src=2021 ac
 ])
 
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x2 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=17,ct_tp_dst=4040,udp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=17,ct_tp_dst=4040,udp6,metadata=0x2 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=6,ct_tp_dst=8080,tcp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=17,ct_tp_dst=4040,udp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.90,ct_nw_proto=6,ct_tp_dst=8080,tcp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.90))
 ])
 
 as hv2 ovs-vsctl del-port hv2-vif1
@@ -23501,9 +23584,10 @@ priority=100,udp6,metadata=0x2,ipv6_src=4200::1,ipv6_dst=8800::88,tp_src=2021 ac
 ])
 
 AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=70 | grep -v NXST | cut -d ' ' -f8- | sort], [0], [dnl
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ipv6,metadata=0x2 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
-priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ip,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=17,ct_tp_dst=4040,udp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=17,ct_tp_dst=4040,udp6,metadata=0x2 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_ipv6_dst=8800::88,ct_nw_proto=6,ct_tp_dst=8080,tcp6,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=8800::88))
+priority=100,ct_state=+trk+dnat,ct_nw_dst=88.88.88.88,ct_nw_proto=17,ct_tp_dst=4040,udp,metadata=0x1 actions=ct(commit,zone=NXM_NX_REG12[[0..15]],nat(src=88.88.88.88))
 ])
 
 check ovn-nbctl --wait=hv ls-del sw0
-- 
1.8.3.1