Blob Blame History Raw
diff --git a/.ci/ovn-kubernetes/Dockerfile b/.ci/ovn-kubernetes/Dockerfile
index e74b620be..1884724ec 100644
--- a/.ci/ovn-kubernetes/Dockerfile
+++ b/.ci/ovn-kubernetes/Dockerfile
@@ -1,5 +1,5 @@
 ARG OVNKUBE_COMMIT=master
-ARG LIBOVSDB_COMMIT=8081fe24e48f
+ARG LIBOVSDB_COMMIT=a6a173993830
 
 FROM fedora:35 AS ovnbuilder
 
@@ -47,9 +47,17 @@ RUN GO111MODULE=on go install github.com/ovn-org/libovsdb/cmd/modelgen@${LIBOVSD
 # Clone OVN Kubernetes and build the binary based on the commit passed as argument
 WORKDIR /root
 RUN git clone https://github.com/ovn-org/ovn-kubernetes.git
-WORKDIR /root/ovn-kubernetes/go-controller
+WORKDIR /root/ovn-kubernetes
 RUN git checkout ${OVNKUBE_COMMIT} && git log -n 1
 
+# Copy the ovn-kubernetes scripts from the OVN sources and apply any
+# custom changes if needed.
+RUN mkdir -p /tmp/ovn/.ci/ovn-kubernetes
+COPY .ci/ovn-kubernetes /tmp/ovn/.ci/ovn-kubernetes
+WORKDIR /tmp/ovn
+RUN .ci/ovn-kubernetes/prepare.sh /root/ovn-kubernetes
+
+WORKDIR /root/ovn-kubernetes/go-controller
 # Make sure we use the OVN NB/SB schema from the local code.
 COPY --from=ovnbuilder /tmp/ovn/ovn-nb.ovsschema pkg/nbdb/ovn-nb.ovsschema
 COPY --from=ovnbuilder /tmp/ovn/ovn-sb.ovsschema pkg/sbdb/ovn-sb.ovsschema
diff --git a/.ci/ovn-kubernetes/custom.patch b/.ci/ovn-kubernetes/custom.patch
new file mode 100644
index 000000000..e69de29bb
diff --git a/.ci/ovn-kubernetes/prepare.sh b/.ci/ovn-kubernetes/prepare.sh
new file mode 100755
index 000000000..8fc9652af
--- /dev/null
+++ b/.ci/ovn-kubernetes/prepare.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+set -ev
+
+ovnk8s_path=$1
+topdir=$PWD
+
+pushd ${ovnk8s_path}
+
+# Add here any custom operations that need to performed on the
+# ovn-kubernetes cloned repo, e.g., custom patches.
+
+# git apply --allow-empty is too new so not all git versions from major
+# distros support it, just check if the custom patch file is not empty
+# before applying it.
+[ -s ${topdir}/.ci/ovn-kubernetes/custom.patch ] && \
+    git apply -v ${topdir}/.ci/ovn-kubernetes/custom.patch
+
+popd # ${ovnk8s_path}
+exit 0
diff --git a/.github/workflows/ovn-kubernetes.yml b/.github/workflows/ovn-kubernetes.yml
index 344937e53..840ecd1e0 100644
--- a/.github/workflows/ovn-kubernetes.yml
+++ b/.github/workflows/ovn-kubernetes.yml
@@ -16,7 +16,7 @@ env:
   GO_VERSION: "1.18.4"
   K8S_VERSION: v1.24.0
   OVNKUBE_COMMIT: "master"
-  LIBOVSDB_COMMIT: "98c0bad3cff1"
+  LIBOVSDB_COMMIT: "a6a173993830"
   KIND_CLUSTER_NAME: ovn
   KIND_INSTALL_INGRESS: true
   KIND_ALLOW_SYSTEM_WRITES: true
@@ -73,6 +73,7 @@ jobs:
     env:
       JOB_NAME: "${{ matrix.target }}-${{ matrix.ha }}-${{ matrix.gateway-mode }}-${{ matrix.ipfamily }}-${{ matrix.disable-snat-multiple-gws }}-${{ matrix.second-bridge }}"
       OVN_HYBRID_OVERLAY_ENABLE: "${{ matrix.target == 'control-plane' }}"
+      KIND_INSTALL_METALLB: "${{ matrix.target == 'control-plane' }}"
       OVN_MULTICAST_ENABLE:  "${{ matrix.target == 'control-plane' }}"
       OVN_EMPTY_LB_EVENTS: "${{ matrix.target == 'control-plane' }}"
       OVN_HA: "true"
@@ -91,12 +92,19 @@ jobs:
         go-version: ${{ env.GO_VERSION }}
       id: go
 
+    - name: Check out ovn
+      uses: actions/checkout@v3
+
     - name: Check out ovn-kubernetes
       uses: actions/checkout@v3
       with:
           path: src/github.com/ovn-org/ovn-kubernetes
           repository: ovn-org/ovn-kubernetes
 
+    - name: Prepare
+      run: |
+        .ci/ovn-kubernetes/prepare.sh src/github.com/ovn-org/ovn-kubernetes
+
     - name: Set up environment
       run: |
         export GOPATH=$(go env GOPATH)
diff --git a/Makefile.am b/Makefile.am
index c8f770146..f7758d114 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -90,6 +90,8 @@ EXTRA_DIST = \
 	.ci/osx-build.sh \
 	.ci/osx-prepare.sh \
 	.ci/ovn-kubernetes/Dockerfile \
+	.ci/ovn-kubernetes/prepare.sh \
+	.ci/ovn-kubernetes/custom.patch \
 	.github/workflows/test.yml \
 	.github/workflows/ovn-kubernetes.yml \
 	boot.sh \
diff --git a/NEWS b/NEWS
index 0920b44e2..acb8065bc 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,6 @@
+OVN v22.12.1 - xx xxx xxxx
+--------------------------
+
 OVN v22.12.0 - 16 Dec 2022
 --------------------------
   - Add load balancer "affinity_timeout" option to configure load balancing
diff --git a/build-aux/sodepends.py b/build-aux/sodepends.py
index 343fda1af..7b1f9c840 100755
--- a/build-aux/sodepends.py
+++ b/build-aux/sodepends.py
@@ -63,7 +63,8 @@ def sodepends(include_info, filenames, dst):
             continue
 
         # Open file.
-        include_dirs = [info[0] for info in include_info]
+        include_dirs = [info[1] if len(info) == 2 else info[0]
+                        for info in include_info]
         fn = soutil.find_file(include_dirs, toplevel)
         if not fn:
             ok = False
diff --git a/configure.ac b/configure.ac
index 101467253..357758e0c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 AC_PREREQ(2.63)
-AC_INIT(ovn, 22.12.0, bugs@openvswitch.org)
+AC_INIT(ovn, 22.12.1, bugs@openvswitch.org)
 AC_CONFIG_MACRO_DIR([m4])
 AC_CONFIG_AUX_DIR([build-aux])
 AC_CONFIG_HEADERS([config.h])
diff --git a/controller/chassis.c b/controller/chassis.c
index 685d9b2ae..98f8da2be 100644
--- a/controller/chassis.c
+++ b/controller/chassis.c
@@ -352,6 +352,7 @@ chassis_build_other_config(const struct ovs_chassis_cfg *ovs_cfg,
     smap_replace(config, OVN_FEATURE_PORT_UP_NOTIF, "true");
     smap_replace(config, OVN_FEATURE_CT_NO_MASKED_LABEL, "true");
     smap_replace(config, OVN_FEATURE_MAC_BINDING_TIMESTAMP, "true");
+    smap_replace(config, OVN_FEATURE_CT_LB_RELATED, "true");
 }
 
 /*
@@ -469,6 +470,12 @@ chassis_other_config_changed(const struct ovs_chassis_cfg *ovs_cfg,
         return true;
     }
 
+    if (!smap_get_bool(&chassis_rec->other_config,
+                       OVN_FEATURE_CT_LB_RELATED,
+                       false)) {
+        return true;
+    }
+
     return false;
 }
 
diff --git a/controller/lflow.c b/controller/lflow.c
index bb47bb0c7..4b1cfe318 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -1567,9 +1567,6 @@ add_lb_vip_hairpin_reply_action(struct in6_addr *vip6, ovs_be32 vip,
     /* Hairpin replies have the same nw_proto as packets that created the
      * session.
      */
-    union mf_value imm_proto = {
-        .u8 = lb_proto,
-    };
     ol_spec = ofpbuf_put_zeros(ofpacts, sizeof *ol_spec);
     ol_spec->dst.field = mf_from_id(MFF_IP_PROTO);
     ol_spec->src.field = mf_from_id(MFF_IP_PROTO);
@@ -1577,16 +1574,21 @@ add_lb_vip_hairpin_reply_action(struct in6_addr *vip6, ovs_be32 vip,
     ol_spec->dst.n_bits = ol_spec->dst.field->n_bits;
     ol_spec->n_bits = ol_spec->dst.n_bits;
     ol_spec->dst_type = NX_LEARN_DST_MATCH;
-    ol_spec->src_type = NX_LEARN_SRC_IMMEDIATE;
-    mf_write_subfield_value(&ol_spec->dst, &imm_proto, &match);
-
-    /* Push value last, as this may reallocate 'ol_spec' */
-    imm_bytes = DIV_ROUND_UP(ol_spec->dst.n_bits, 8);
-    src_imm = ofpbuf_put_zeros(ofpacts, OFPACT_ALIGN(imm_bytes));
-    memcpy(src_imm, &imm_proto, imm_bytes);
 
     /* Hairpin replies have source port == <backend-port>. */
     if (has_l4_port) {
+        union mf_value imm_proto = {
+            .u8 = lb_proto,
+        };
+
+        ol_spec->src_type = NX_LEARN_SRC_IMMEDIATE;
+        mf_write_subfield_value(&ol_spec->dst, &imm_proto, &match);
+
+        /* Push value last, as this may reallocate 'ol_spec' */
+        imm_bytes = DIV_ROUND_UP(ol_spec->dst.n_bits, 8);
+        src_imm = ofpbuf_put_zeros(ofpacts, OFPACT_ALIGN(imm_bytes));
+        memcpy(src_imm, &imm_proto, imm_bytes);
+
         ol_spec = ofpbuf_put_zeros(ofpacts, sizeof *ol_spec);
         switch (lb_proto) {
         case IPPROTO_TCP:
@@ -1610,6 +1612,8 @@ add_lb_vip_hairpin_reply_action(struct in6_addr *vip6, ovs_be32 vip,
         ol_spec->n_bits = ol_spec->dst.n_bits;
         ol_spec->dst_type = NX_LEARN_DST_MATCH;
         ol_spec->src_type = NX_LEARN_SRC_FIELD;
+    } else {
+        ol_spec->src_type = NX_LEARN_SRC_FIELD;
     }
 
     /* Set MLF_LOOKUP_LB_HAIRPIN_BIT for hairpin replies. */
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index 73c33a6bf..c899283dc 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -755,6 +755,11 @@ update_ct_zones(const struct shash *binding_lports,
             }
             bitmap_set1(ct_zone_bitmap, snat_req_node->data);
             node->data = snat_req_node->data;
+        } else {
+            add_pending_ct_zone_entry(pending_ct_zones, CT_ZONE_OF_QUEUED,
+                                      snat_req_node->data, true, snat_req_node->name);
+            bitmap_set1(ct_zone_bitmap, snat_req_node->data);
+            simap_put(ct_zones, snat_req_node->name, snat_req_node->data);
         }
     }
 
diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index 82da6ae73..e4d530138 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -1419,7 +1419,6 @@ prepare_ipv6_prefixd(struct ovsdb_idl_txn *ovnsb_idl_txn,
 
 struct buffer_info {
     struct ofpbuf ofpacts;
-    ofp_port_t ofp_port;
     struct dp_packet *p;
 };
 
@@ -1495,7 +1494,6 @@ buffered_push_packet(struct buffered_packets *bp,
     union mf_value pkt_mark_value;
     mf_get_value(pkt_mark_field, &md->flow, &pkt_mark_value);
     ofpact_put_set_field(&bi->ofpacts, pkt_mark_field, &pkt_mark_value, NULL);
-    bi->ofp_port = md->flow.in_port.ofp_port;
 
     struct ofpact_resubmit *resubmit = ofpact_put_RESUBMIT(&bi->ofpacts);
     resubmit->in_port = OFPP_CONTROLLER;
@@ -1531,7 +1529,7 @@ buffered_send_packets(struct rconn *swconn, struct buffered_packets *bp,
             .ofpacts = bi->ofpacts.data,
             .ofpacts_len = bi->ofpacts.size,
         };
-        match_set_in_port(&po.flow_metadata, bi->ofp_port);
+        match_set_in_port(&po.flow_metadata, OFPP_CONTROLLER);
         queue_msg(swconn, ofputil_encode_packet_out(&po, proto));
 
         ofpbuf_uninit(&bi->ofpacts);
diff --git a/debian/changelog b/debian/changelog
index 6f5a9ac2a..d658774f6 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+OVN (22.12.1-1) unstable; urgency=low
+   [ OVN team ]
+   * New upstream version
+
+ -- OVN team <dev@openvswitch.org>  Fri, 16 Dec 2022 09:52:44 -0500
+
 ovn (22.12.0-1) unstable; urgency=low
 
    * New upstream version
diff --git a/ic/ovn-ic.c b/ic/ovn-ic.c
index 73ce77e5c..9a80a7f68 100644
--- a/ic/ovn-ic.c
+++ b/ic/ovn-ic.c
@@ -1911,13 +1911,112 @@ main(int argc, char *argv[])
     struct ovsdb_idl_loop ovnisb_idl_loop = OVSDB_IDL_LOOP_INITIALIZER(
         ovsdb_idl_create(ovn_ic_sb_db, &icsbrec_idl_class, true, true));
 
-    /* ovn-nb db. XXX: add only needed tables and columns */
+    /* ovn-nb db. */
     struct ovsdb_idl_loop ovnnb_idl_loop = OVSDB_IDL_LOOP_INITIALIZER(
-        ovsdb_idl_create(ovnnb_db, &nbrec_idl_class, true, true));
-
-    /* ovn-sb db. XXX: add only needed tables and columns */
+        ovsdb_idl_create(ovnnb_db, &nbrec_idl_class, false, true));
+
+    ovsdb_idl_add_table(ovnnb_idl_loop.idl, &nbrec_table_nb_global);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl, &nbrec_nb_global_col_name);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl, &nbrec_nb_global_col_options);
+
+    ovsdb_idl_add_table(ovnnb_idl_loop.idl,
+                        &nbrec_table_logical_router_static_route);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_static_route_col_route_table);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_static_route_col_ip_prefix);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_static_route_col_nexthop);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_static_route_col_external_ids);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_static_route_col_options);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_static_route_col_policy);
+
+    ovsdb_idl_add_table(ovnnb_idl_loop.idl, &nbrec_table_logical_router);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_col_name);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_col_static_routes);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_col_ports);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_col_options);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_col_external_ids);
+
+    ovsdb_idl_add_table(ovnnb_idl_loop.idl, &nbrec_table_logical_router_port);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_port_col_name);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_port_col_networks);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_port_col_external_ids);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_router_port_col_options);
+
+    ovsdb_idl_add_table(ovnnb_idl_loop.idl, &nbrec_table_logical_switch);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_col_name);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_col_ports);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_col_other_config);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_col_external_ids);
+
+    ovsdb_idl_add_table(ovnnb_idl_loop.idl, &nbrec_table_logical_switch_port);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_port_col_name);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_port_col_addresses);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_port_col_options);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_port_col_type);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_port_col_up);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_port_col_addresses);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_port_col_enabled);
+    ovsdb_idl_add_column(ovnnb_idl_loop.idl,
+                         &nbrec_logical_switch_port_col_external_ids);
+
+    /* ovn-sb db. */
     struct ovsdb_idl_loop ovnsb_idl_loop = OVSDB_IDL_LOOP_INITIALIZER(
-        ovsdb_idl_create(ovnsb_db, &sbrec_idl_class, true, true));
+        ovsdb_idl_create(ovnsb_db, &sbrec_idl_class, false, true));
+
+    ovsdb_idl_add_table(ovnsb_idl_loop.idl, &sbrec_table_chassis);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_chassis_col_encaps);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_chassis_col_name);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_chassis_col_hostname);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_chassis_col_other_config);
+
+    ovsdb_idl_add_table(ovnsb_idl_loop.idl, &sbrec_table_encap);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_encap_col_chassis_name);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_encap_col_type);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_encap_col_ip);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_encap_col_options);
+
+    ovsdb_idl_add_table(ovnsb_idl_loop.idl, &sbrec_table_datapath_binding);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl,
+                         &sbrec_datapath_binding_col_external_ids);
+
+    ovsdb_idl_add_table(ovnsb_idl_loop.idl, &sbrec_table_port_binding);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl,
+                         &sbrec_port_binding_col_datapath);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl,
+                         &sbrec_port_binding_col_mac);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl,
+                         &sbrec_port_binding_col_options);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl,
+                         &sbrec_port_binding_col_logical_port);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl,
+                         &sbrec_port_binding_col_external_ids);
+    ovsdb_idl_add_column(ovnsb_idl_loop.idl,
+                         &sbrec_port_binding_col_chassis);
 
     /* Create IDL indexes */
     struct ovsdb_idl_index *nbrec_ls_by_name
diff --git a/include/ovn/features.h b/include/ovn/features.h
index 679f67457..5bcd68739 100644
--- a/include/ovn/features.h
+++ b/include/ovn/features.h
@@ -24,6 +24,7 @@
 #define OVN_FEATURE_PORT_UP_NOTIF      "port-up-notif"
 #define OVN_FEATURE_CT_NO_MASKED_LABEL "ct-no-masked-label"
 #define OVN_FEATURE_MAC_BINDING_TIMESTAMP "mac-binding-timestamp"
+#define OVN_FEATURE_CT_LB_RELATED "ovn-ct-lb-related"
 
 /* OVS datapath supported features.  Based on availability OVN might generate
  * different types of openflows.
diff --git a/include/ovn/lex.h b/include/ovn/lex.h
index 9159b7a26..64d33361f 100644
--- a/include/ovn/lex.h
+++ b/include/ovn/lex.h
@@ -29,6 +29,8 @@
 
 struct ds;
 
+#define LEX_TEMPLATE_PREFIX '^'
+
 /* Token type. */
 enum lex_type {
     LEX_T_END,                  /* end of input */
diff --git a/lib/lb.c b/lib/lb.c
index 43628bba7..c13d07b99 100644
--- a/lib/lb.c
+++ b/lib/lb.c
@@ -314,11 +314,10 @@ ovn_lb_vip_destroy(struct ovn_lb_vip *vip)
     free(vip->backends);
 }
 
-void
-ovn_lb_vip_format(const struct ovn_lb_vip *vip, struct ds *s, bool template)
+static void
+ovn_lb_vip_format__(const struct ovn_lb_vip *vip, struct ds *s,
+                    bool needs_brackets)
 {
-    bool needs_brackets = vip->address_family == AF_INET6 && vip->port_str
-                          && !template;
     if (needs_brackets) {
         ds_put_char(s, '[');
     }
@@ -331,6 +330,30 @@ ovn_lb_vip_format(const struct ovn_lb_vip *vip, struct ds *s, bool template)
     }
 }
 
+/* Formats the VIP in the way the ovn-controller expects it, that is,
+ * template IPv6 variables need to be between brackets too.
+ */
+static char *
+ovn_lb_vip6_template_format_internal(const struct ovn_lb_vip *vip)
+{
+    struct ds s = DS_EMPTY_INITIALIZER;
+
+    if (vip->vip_str && *vip->vip_str == LEX_TEMPLATE_PREFIX) {
+        ovn_lb_vip_format__(vip, &s, true);
+    } else {
+        ovn_lb_vip_format(vip, &s, !!vip->port_str);
+    }
+    return ds_steal_cstr(&s);
+}
+
+void
+ovn_lb_vip_format(const struct ovn_lb_vip *vip, struct ds *s, bool template)
+{
+    bool needs_brackets = vip->address_family == AF_INET6 && vip->port_str
+                          && !template;
+    ovn_lb_vip_format__(vip, s, needs_brackets);
+}
+
 void
 ovn_lb_vip_backends_format(const struct ovn_lb_vip *vip, struct ds *s,
                            bool template)
@@ -512,6 +535,7 @@ ovn_northd_lb_create(const struct nbrec_load_balancer *nbrec_lb)
     lb->n_vips = smap_count(&nbrec_lb->vips);
     lb->vips = xcalloc(lb->n_vips, sizeof *lb->vips);
     lb->vips_nb = xcalloc(lb->n_vips, sizeof *lb->vips_nb);
+    smap_init(&lb->template_vips);
     lb->controller_event = smap_get_bool(&nbrec_lb->options, "event", false);
 
     bool routable = smap_get_bool(&nbrec_lb->options, "add_route", false);
@@ -560,6 +584,12 @@ ovn_northd_lb_create(const struct nbrec_load_balancer *nbrec_lb)
         } else {
             sset_add(&lb->ips_v6, lb_vip->vip_str);
         }
+
+        if (lb->template && address_family == AF_INET6) {
+            smap_add_nocopy(&lb->template_vips,
+                            ovn_lb_vip6_template_format_internal(lb_vip),
+                            xstrdup(node->value));
+        }
         n_vips++;
     }
 
@@ -604,6 +634,15 @@ ovn_northd_lb_find(const struct hmap *lbs, const struct uuid *uuid)
     return NULL;
 }
 
+const struct smap *
+ovn_northd_lb_get_vips(const struct ovn_northd_lb *lb)
+{
+    if (!smap_is_empty(&lb->template_vips)) {
+        return &lb->template_vips;
+    }
+    return &lb->nlb->vips;
+}
+
 void
 ovn_northd_lb_add_lr(struct ovn_northd_lb *lb, size_t n,
                      struct ovn_datapath **ods)
@@ -637,6 +676,7 @@ ovn_northd_lb_destroy(struct ovn_northd_lb *lb)
     }
     free(lb->vips);
     free(lb->vips_nb);
+    smap_destroy(&lb->template_vips);
     sset_destroy(&lb->ips_v4);
     sset_destroy(&lb->ips_v6);
     free(lb->selection_fields);
diff --git a/lib/lb.h b/lib/lb.h
index 55a41ae0b..55becc1bf 100644
--- a/lib/lb.h
+++ b/lib/lb.h
@@ -19,6 +19,7 @@
 
 #include <sys/types.h>
 #include <netinet/in.h>
+#include "lib/smap.h"
 #include "openvswitch/hmap.h"
 #include "ovn-util.h"
 #include "sset.h"
@@ -62,6 +63,9 @@ struct ovn_northd_lb {
     char *selection_fields;
     struct ovn_lb_vip *vips;
     struct ovn_northd_lb_vip *vips_nb;
+    struct smap template_vips; /* Slightly changed template VIPs, populated
+                                * if needed.  Until now it's only required
+                                * for IPv6 template load balancers. */
     size_t n_vips;
 
     enum lb_neighbor_responder_mode neigh_mode;
@@ -130,6 +134,7 @@ struct ovn_northd_lb_backend {
 struct ovn_northd_lb *ovn_northd_lb_create(const struct nbrec_load_balancer *);
 struct ovn_northd_lb *ovn_northd_lb_find(const struct hmap *,
                                          const struct uuid *);
+const struct smap *ovn_northd_lb_get_vips(const struct ovn_northd_lb *);
 void ovn_northd_lb_destroy(struct ovn_northd_lb *);
 void ovn_northd_lb_add_lr(struct ovn_northd_lb *lb, size_t n,
                           struct ovn_datapath **ods);
diff --git a/lib/lex.c b/lib/lex.c
index 5251868b5..a8b9812bb 100644
--- a/lib/lex.c
+++ b/lib/lex.c
@@ -782,7 +782,7 @@ next:
         p = lex_parse_port_group(p, token);
         break;
 
-    case '^':
+    case LEX_TEMPLATE_PREFIX:
         p = lex_parse_template(p, token);
         break;
 
@@ -1061,7 +1061,7 @@ lexer_parse_template_string(const char *s, const struct smap *template_vars,
                             struct sset *template_vars_ref)
 {
     /* No '^' means no templates. */
-    if (!strchr(s, '^')) {
+    if (!strchr(s, LEX_TEMPLATE_PREFIX)) {
         return lex_str_use(s);
     }
 
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index 86b98acf7..69ab56423 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -825,24 +825,6 @@ ovn_get_internal_version(void)
                      N_OVNACTS, OVN_INTERNAL_MINOR_VER);
 }
 
-unsigned int
-ovn_parse_internal_version_minor(const char *ver)
-{
-    const char *p = ver + strlen(ver);
-    for (int i = 0; i < strlen(ver); i++) {
-        if (*p == '.') {
-            break;
-        }
-        p--;
-    }
-
-    unsigned int minor;
-    if (ovs_scan(p, ".%u", &minor)) {
-        return minor;
-    }
-    return 0;
-}
-
 #ifdef DDLOG
 /* Callbacks used by the ddlog northd code to print warnings and errors. */
 void
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index 809ff1d36..48dc846ad 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -70,6 +70,23 @@ struct lport_addresses {
     struct ipv6_netaddr *ipv6_addrs;
 };
 
+static inline bool
+ipv6_is_all_router(const struct in6_addr *addr)
+{
+    return ipv6_addr_equals(addr, &in6addr_all_routers);
+}
+
+static const struct in6_addr in6addr_all_site_routers = {{{
+    0xff,0x05,0x00,0x00,0x00,0x00,0x00,0x00,
+    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02
+}}};
+
+static inline bool
+ipv6_is_all_site_router(const struct in6_addr *addr)
+{
+    return ipv6_addr_equals(addr, &in6addr_all_site_routers);
+}
+
 bool is_dynamic_lsp_address(const char *address);
 bool extract_addresses(const char *address, struct lport_addresses *,
                        int *ofs);
@@ -248,11 +265,6 @@ bool ip_address_and_port_from_lb_key(const char *key, char **ip_address,
  * value. */
 char *ovn_get_internal_version(void);
 
-/* Parse the provided internal version string and return the "minor" part which
- * is expected to be an unsigned integer followed by the last "." in the
- * string. Returns 0 if the string can't be parsed. */
-unsigned int ovn_parse_internal_version_minor(const char *ver);
-
 /* OVN Packet definitions. These may eventually find a home in OVS's
  * packets.h file. For the time being, they live here because OVN uses them
  * and OVS does not.
diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
index 363e384bd..a7b735333 100644
--- a/northd/inc-proc-northd.c
+++ b/northd/inc-proc-northd.c
@@ -34,10 +34,13 @@
 #include "en-lflow.h"
 #include "en-northd-output.h"
 #include "en-sync-sb.h"
+#include "unixctl.h"
 #include "util.h"
 
 VLOG_DEFINE_THIS_MODULE(inc_proc_northd);
 
+static unixctl_cb_func chassis_features_list;
+
 #define NB_NODES \
     NB_NODE(nb_global, "nb_global") \
     NB_NODE(copp, "copp") \
@@ -306,6 +309,12 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_ovsdb_node_add_index(&en_sb_address_set,
                                 "sbrec_address_set_by_name",
                                 sbrec_address_set_by_name);
+
+    struct northd_data *northd_data =
+        engine_get_internal_data(&en_northd);
+    unixctl_command_register("debug/chassis-features-list", "", 0, 0,
+                             chassis_features_list,
+                             &northd_data->features);
 }
 
 void inc_proc_northd_run(struct ovsdb_idl_txn *ovnnb_txn,
@@ -354,3 +363,20 @@ void inc_proc_northd_cleanup(void)
     engine_cleanup();
     engine_set_context(NULL);
 }
+
+static void
+chassis_features_list(struct unixctl_conn *conn, int argc OVS_UNUSED,
+                      const char *argv[] OVS_UNUSED, void *features_)
+{
+    struct chassis_features *features = features_;
+    struct ds ds = DS_EMPTY_INITIALIZER;
+
+    ds_put_format(&ds, "ct_no_masked_label:    %s\n",
+                  features->ct_no_masked_label ? "true" : "false");
+    ds_put_format(&ds, "ct_lb_related:         %s\n",
+                  features->ct_lb_related ? "true" : "false");
+    ds_put_format(&ds, "mac_binding_timestamp: %s\n",
+                  features->mac_binding_timestamp ? "true" : "false");
+    unixctl_command_reply(conn, ds_cstr(&ds));
+    ds_destroy(&ds);
+}
diff --git a/northd/northd.c b/northd/northd.c
index 841ae9cc5..9cedec909 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -125,11 +125,11 @@ enum ovn_stage {
     PIPELINE_STAGE(SWITCH, IN,  LB_AFF_CHECK,  11, "ls_in_lb_aff_check")  \
     PIPELINE_STAGE(SWITCH, IN,  LB,            12, "ls_in_lb")            \
     PIPELINE_STAGE(SWITCH, IN,  LB_AFF_LEARN,  13, "ls_in_lb_aff_learn")  \
-    PIPELINE_STAGE(SWITCH, IN,  ACL_AFTER_LB,  14, "ls_in_acl_after_lb")  \
-    PIPELINE_STAGE(SWITCH, IN,  STATEFUL,      15, "ls_in_stateful")      \
-    PIPELINE_STAGE(SWITCH, IN,  PRE_HAIRPIN,   16, "ls_in_pre_hairpin")   \
-    PIPELINE_STAGE(SWITCH, IN,  NAT_HAIRPIN,   17, "ls_in_nat_hairpin")   \
-    PIPELINE_STAGE(SWITCH, IN,  HAIRPIN,       18, "ls_in_hairpin")       \
+    PIPELINE_STAGE(SWITCH, IN,  PRE_HAIRPIN,   14, "ls_in_pre_hairpin")   \
+    PIPELINE_STAGE(SWITCH, IN,  NAT_HAIRPIN,   15, "ls_in_nat_hairpin")   \
+    PIPELINE_STAGE(SWITCH, IN,  HAIRPIN,       16, "ls_in_hairpin")       \
+    PIPELINE_STAGE(SWITCH, IN,  ACL_AFTER_LB,  17, "ls_in_acl_after_lb")  \
+    PIPELINE_STAGE(SWITCH, IN,  STATEFUL,      18, "ls_in_stateful")      \
     PIPELINE_STAGE(SWITCH, IN,  ARP_ND_RSP,    19, "ls_in_arp_rsp")       \
     PIPELINE_STAGE(SWITCH, IN,  DHCP_OPTIONS,  20, "ls_in_dhcp_options")  \
     PIPELINE_STAGE(SWITCH, IN,  DHCP_RESPONSE, 21, "ls_in_dhcp_response") \
@@ -215,6 +215,7 @@ enum ovn_stage {
 #define REGBIT_ACL_LABEL          "reg0[13]"
 #define REGBIT_FROM_RAMP          "reg0[14]"
 #define REGBIT_PORT_SEC_DROP      "reg0[15]"
+#define REGBIT_ACL_HINT_ALLOW_REL "reg0[17]"
 
 #define REG_ORIG_DIP_IPV4         "reg1"
 #define REG_ORIG_DIP_IPV6         "xxreg1"
@@ -430,6 +431,13 @@ build_chassis_features(const struct northd_input *input_data,
     const struct sbrec_chassis *chassis;
 
     SBREC_CHASSIS_TABLE_FOR_EACH (chassis, input_data->sbrec_chassis) {
+        /* Only consider local AZ chassis.  Remote ones don't install
+         * flows generated by the local northd.
+         */
+        if (smap_get_bool(&chassis->other_config, "is-remote", false)) {
+            continue;
+        }
+
         bool ct_no_masked_label =
             smap_get_bool(&chassis->other_config,
                           OVN_FEATURE_CT_NO_MASKED_LABEL,
@@ -446,6 +454,15 @@ build_chassis_features(const struct northd_input *input_data,
             chassis_features->mac_binding_timestamp) {
             chassis_features->mac_binding_timestamp = false;
         }
+
+        bool ct_lb_related =
+            smap_get_bool(&chassis->other_config,
+                          OVN_FEATURE_CT_LB_RELATED,
+                          false);
+        if (!ct_lb_related &&
+            chassis_features->ct_lb_related) {
+            chassis_features->ct_lb_related = false;
+        }
     }
 }
 
@@ -4410,7 +4427,7 @@ sync_lbs(struct northd_input *input_data, struct ovsdb_idl_txn *ovnsb_txn,
 
         /* Update columns. */
         sbrec_load_balancer_set_name(lb->slb, lb->nlb->name);
-        sbrec_load_balancer_set_vips(lb->slb, &lb->nlb->vips);
+        sbrec_load_balancer_set_vips(lb->slb, ovn_northd_lb_get_vips(lb));
         sbrec_load_balancer_set_protocol(lb->slb, lb->nlb->protocol);
         sbrec_load_balancer_set_datapath_group(lb->slb, dpg->dp_group);
         sbrec_load_balancer_set_options(lb->slb, &options);
@@ -4849,7 +4866,7 @@ ovn_igmp_group_get_ports(const struct sbrec_igmp_group *sb_igmp_group,
         struct ovn_port *port =
             ovn_port_find(ovn_ports, sb_igmp_group->ports[i]->logical_port);
 
-        if (!port) {
+        if (!port || !port->nbsp) {
             continue;
         }
 
@@ -6758,7 +6775,8 @@ build_acls(struct ovn_datapath *od, const struct chassis_features *features,
                       ct_blocked_match);
         ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX - 3,
                       ds_cstr(&match), REGBIT_ACL_HINT_DROP" = 0; "
-                      REGBIT_ACL_HINT_BLOCK" = 0; next;");
+                      REGBIT_ACL_HINT_BLOCK" = 0; "
+                      REGBIT_ACL_HINT_ALLOW_REL" = 1; next;");
         ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
                       ds_cstr(&match), "next;");
 
@@ -6774,14 +6792,21 @@ build_acls(struct ovn_datapath *od, const struct chassis_features *features,
          * a dynamically negotiated FTP data channel), but will allow
          * related traffic such as an ICMP Port Unreachable through
          * that's generated from a non-listening UDP port.  */
+        const char *ct_in_acl_action =
+            features->ct_lb_related
+            ? REGBIT_ACL_HINT_ALLOW_REL" = 1; ct_commit_nat;"
+            : REGBIT_ACL_HINT_ALLOW_REL" = 1; next;";
+        const char *ct_out_acl_action = features->ct_lb_related
+                                        ? "ct_commit_nat;"
+                                        : "next;";
         ds_clear(&match);
         ds_put_format(&match, "!ct.est && ct.rel && !ct.new%s && %s == 0",
                       use_ct_inv_match ? " && !ct.inv" : "",
                       ct_blocked_match);
         ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX - 3,
-                      ds_cstr(&match), "ct_commit_nat;");
+                      ds_cstr(&match), ct_in_acl_action);
         ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
-                      ds_cstr(&match), "ct_commit_nat;");
+                      ds_cstr(&match), ct_out_acl_action);
 
         /* Ingress and Egress ACL Table (Priority 65532).
          *
@@ -6790,6 +6815,11 @@ build_acls(struct ovn_datapath *od, const struct chassis_features *features,
                       "nd || nd_ra || nd_rs || mldv1 || mldv2", "next;");
         ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
                       "nd || nd_ra || nd_rs || mldv1 || mldv2", "next;");
+
+        /* Reply and related traffic matched by an "allow-related" ACL
+         * should be allowed in the ls_in_acl_after_lb stage too. */
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL_AFTER_LB, UINT16_MAX - 3,
+                      REGBIT_ACL_HINT_ALLOW_REL" == 1", "next;");
     }
 
     /* Ingress or Egress ACL Table (Various priorities). */
@@ -7838,7 +7868,7 @@ build_lrouter_groups(struct hmap *ports, struct ovs_list *lr_list)
 }
 
 /*
- * Ingress table 24: Flows that flood self originated ARP/RARP/ND packets in
+ * Ingress table 25: Flows that flood self originated ARP/RARP/ND packets in
  * the switching domain.
  */
 static void
@@ -7952,7 +7982,7 @@ lrouter_port_ipv6_reachable(const struct ovn_port *op,
 }
 
 /*
- * Ingress table 24: Flows that forward ARP/ND requests only to the routers
+ * Ingress table 25: Flows that forward ARP/ND requests only to the routers
  * that own the addresses. Other ARP/ND packets are still flooded in the
  * switching domain as regular broadcast.
  */
@@ -7989,7 +8019,7 @@ build_lswitch_rport_arp_req_flow(const char *ips,
 }
 
 /*
- * Ingress table 24: Flows that forward ARP/ND requests only to the routers
+ * Ingress table 25: Flows that forward ARP/ND requests only to the routers
  * that own the addresses.
  * Priorities:
  * - 80: self originated GARPs that need to follow regular processing.
@@ -8318,7 +8348,8 @@ build_lswitch_flows(const struct hmap *datapaths,
 
     struct ovn_datapath *od;
 
-    /* Ingress table 25: Destination lookup for unknown MACs (priority 0). */
+    /* Ingress table 25/26: Destination lookup for unknown MACs
+     * (priority 0). */
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbs) {
             continue;
@@ -8393,7 +8424,7 @@ build_lswitch_lflows_admission_control(struct ovn_datapath *od,
     }
 }
 
-/* Ingress table 18: ARP/ND responder, skip requests coming from localnet
+/* Ingress table 19: ARP/ND responder, skip requests coming from localnet
  * ports. (priority 100); see ovn-northd.8.xml for the rationale. */
 
 static void
@@ -8411,7 +8442,7 @@ build_lswitch_arp_nd_responder_skip_local(struct ovn_port *op,
     }
 }
 
-/* Ingress table 18: ARP/ND responder, reply for known IPs.
+/* Ingress table 19: ARP/ND responder, reply for known IPs.
  * (priority 50). */
 static void
 build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
@@ -8671,7 +8702,7 @@ build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
     }
 }
 
-/* Ingress table 18: ARP/ND responder, by default goto next.
+/* Ingress table 19: ARP/ND responder, by default goto next.
  * (priority 0)*/
 static void
 build_lswitch_arp_nd_responder_default(struct ovn_datapath *od,
@@ -8682,7 +8713,7 @@ build_lswitch_arp_nd_responder_default(struct ovn_datapath *od,
     }
 }
 
-/* Ingress table 18: ARP/ND responder for service monitor source ip.
+/* Ingress table 19: ARP/ND responder for service monitor source ip.
  * (priority 110)*/
 static void
 build_lswitch_arp_nd_service_monitor(struct ovn_northd_lb *lb,
@@ -8730,7 +8761,7 @@ build_lswitch_arp_nd_service_monitor(struct ovn_northd_lb *lb,
 }
 
 
-/* Logical switch ingress table 19 and 20: DHCP options and response
+/* Logical switch ingress table 20 and 21: DHCP options and response
  * priority 100 flows. */
 static void
 build_lswitch_dhcp_options_and_response(struct ovn_port *op,
@@ -8782,11 +8813,11 @@ build_lswitch_dhcp_options_and_response(struct ovn_port *op,
     }
 }
 
-/* Ingress table 19 and 20: DHCP options and response, by default goto
+/* Ingress table 20 and 21: DHCP options and response, by default goto
  * next. (priority 0).
- * Ingress table 21 and 22: DNS lookup and response, by default goto next.
+ * Ingress table 22 and 23: DNS lookup and response, by default goto next.
  * (priority 0).
- * Ingress table 23 - External port handling, by default goto next.
+ * Ingress table 24 - External port handling, by default goto next.
  * (priority 0). */
 static void
 build_lswitch_dhcp_and_dns_defaults(struct ovn_datapath *od,
@@ -8801,7 +8832,7 @@ build_lswitch_dhcp_and_dns_defaults(struct ovn_datapath *od,
     }
 }
 
-/* Logical switch ingress table 21 and 22: DNS lookup and response
+/* Logical switch ingress table 22 and 23: DNS lookup and response
 * priority 100 flows.
 */
 static void
@@ -8829,7 +8860,7 @@ build_lswitch_dns_lookup_and_response(struct ovn_datapath *od,
     }
 }
 
-/* Table 23: External port. Drop ARP request for router ips from
+/* Table 24: External port. Drop ARP request for router ips from
  * external ports  on chassis not binding those ports.
  * This makes the router pipeline to be run only on the chassis
  * binding the external ports. */
@@ -8846,7 +8877,7 @@ build_lswitch_external_port(struct ovn_port *op,
     }
 }
 
-/* Ingress table 24: Destination lookup, broadcast and multicast handling
+/* Ingress table 25: Destination lookup, broadcast and multicast handling
  * (priority 70 - 100). */
 static void
 build_lswitch_destination_lookup_bmcast(struct ovn_datapath *od,
@@ -8931,7 +8962,7 @@ build_lswitch_destination_lookup_bmcast(struct ovn_datapath *od,
 }
 
 
-/* Ingress table 24: Add IP multicast flows learnt from IGMP/MLD
+/* Ingress table 25: Add IP multicast flows learnt from IGMP/MLD
  * (priority 90). */
 static void
 build_lswitch_ip_mcast_igmp_mld(struct ovn_igmp_group *igmp_group,
@@ -8973,9 +9004,11 @@ build_lswitch_ip_mcast_igmp_mld(struct ovn_igmp_group *igmp_group,
                           igmp_group->mcgroup.name);
         } else {
             /* RFC 4291, section 2.7.1: Skip groups that correspond to all
-             * hosts.
+             * hosts, all link-local routers and all site routers.
              */
-            if (ipv6_is_all_hosts(&igmp_group->address)) {
+            if (ipv6_is_all_hosts(&igmp_group->address) ||
+                ipv6_is_all_router(&igmp_group->address) ||
+                ipv6_is_all_site_router(&igmp_group->address)) {
                 return;
             }
             if (atomic_compare_exchange_strong(
@@ -9013,7 +9046,7 @@ build_lswitch_ip_mcast_igmp_mld(struct ovn_igmp_group *igmp_group,
 
 static struct ovs_mutex mcgroup_mutex = OVS_MUTEX_INITIALIZER;
 
-/* Ingress table 24: Destination lookup, unicast handling (priority 50), */
+/* Ingress table 25: Destination lookup, unicast handling (priority 50), */
 static void
 build_lswitch_ip_unicast_lookup(struct ovn_port *op,
                                 struct hmap *lflows,
@@ -10471,9 +10504,11 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip *lb_vip,
                                struct hmap *lflows,
                                struct ds *match, struct ds *action,
                                const struct shash *meter_groups,
-                               bool ct_lb_mark)
+                               const struct chassis_features *features)
 {
-    const char *ct_natted = ct_lb_mark ? "ct_mark.natted" : "ct_label.natted";
+    const char *ct_natted = features->ct_no_masked_label
+                            ? "ct_mark.natted"
+                            : "ct_label.natted";
     char *skip_snat_new_action = NULL;
     char *skip_snat_est_action = NULL;
     char *new_match;
@@ -10484,7 +10519,7 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip *lb_vip,
 
     bool reject = build_lb_vip_actions(lb_vip, vips_nb, action,
                                        lb->selection_fields, false,
-                                       ct_lb_mark);
+                                       features->ct_no_masked_label);
     bool drop = !!strncmp(ds_cstr(action), "ct_lb", strlen("ct_lb"));
     if (!drop) {
         /* Remove the trailing ");". */
@@ -10506,9 +10541,11 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip *lb_vip,
     }
 
     if (lb->skip_snat) {
-        skip_snat_new_action = xasprintf("flags.skip_snat_for_lb = 1; %s%s",
-                                         ds_cstr(action),
-                                         drop ? "" : "; skip_snat);");
+        const char *skip_snat = features->ct_lb_related && !drop
+                                ? "; skip_snat"
+                                : "";
+        skip_snat_new_action = xasprintf("flags.skip_snat_for_lb = 1; %s%s);",
+                                         ds_cstr(action), skip_snat);
         skip_snat_est_action = xasprintf("flags.skip_snat_for_lb = 1; "
                                          "next;");
     }
@@ -10641,9 +10678,11 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip *lb_vip,
             skip_snat_new_action, est_match,
             skip_snat_est_action, lflows, prio, meter_groups);
 
-    char *new_actions = xasprintf("flags.force_snat_for_lb = 1; %s%s",
-                                  ds_cstr(action),
-                                  drop ? "" : "; force_snat);");
+    const char *force_snat = features->ct_lb_related && !drop
+                             ? "; force_snat"
+                             : "";
+    char *new_actions = xasprintf("flags.force_snat_for_lb = 1; %s%s);",
+                                  ds_cstr(action), force_snat);
     build_gw_lrouter_nat_flows_for_lb(lb, gw_router_force_snat,
             n_gw_router_force_snat, reject, new_match,
             new_actions, est_match,
@@ -10898,7 +10937,7 @@ build_lrouter_flows_for_lb(struct ovn_northd_lb *lb, struct hmap *lflows,
 
         build_lrouter_nat_flows_for_lb(lb_vip, lb, &lb->vips_nb[i],
                                        lflows, match, action, meter_groups,
-                                       features->ct_no_masked_label);
+                                       features);
 
         if (!build_empty_lb_event_flow(lb_vip, lb, match, action)) {
             continue;
@@ -14208,7 +14247,7 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
                                 const struct hmap *ports, struct ds *match,
                                 struct ds *actions,
                                 const struct shash *meter_groups,
-                                bool ct_lb_mark)
+                                const struct chassis_features *features)
 {
     if (!od->nbr) {
         return;
@@ -14239,9 +14278,11 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
      * a dynamically negotiated FTP data channel), but will allow
      * related traffic such as an ICMP Port Unreachable through
      * that's generated from a non-listening UDP port.  */
-    if (od->has_lb_vip) {
+    if (od->has_lb_vip && features->ct_lb_related) {
         ds_clear(match);
-        const char *ct_flag_reg = ct_lb_mark ? "ct_mark" : "ct_label";
+        const char *ct_flag_reg = features->ct_no_masked_label
+                                  ? "ct_mark"
+                                  : "ct_label";
 
         ds_put_cstr(match, "ct.rel && !ct.est && !ct.new");
         size_t match_len = match->length;
@@ -14328,6 +14369,23 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
             sset_add(&nat_entries, nat->external_ip);
         } else {
             if (!sset_contains(&nat_entries, nat->external_ip)) {
+                /* Drop packets coming in from external that still has
+                 * destination IP equals to the NAT external IP, to avoid loop.
+                 * The packets must have gone through DNAT/unSNAT stage but
+                 * failed to convert the destination. */
+                ds_clear(match);
+                ds_put_format(
+                    match, "inport == %s && outport == %s && ip%s.dst == %s",
+                    l3dgw_port->json_key, l3dgw_port->json_key,
+                    is_v6 ? "6" : "4", nat->external_ip);
+                ovn_lflow_add_with_hint(lflows, od,
+                                        S_ROUTER_IN_ARP_RESOLVE,
+                                        150, ds_cstr(match),
+                                        debug_drop_action(),
+                                        &nat->header_);
+                /* Now for packets coming from other (downlink) LRPs, allow ARP
+                 * resolve for the NAT IP, so that such packets can be
+                 * forwarded for E/W NAT. */
                 ds_clear(match);
                 ds_put_format(
                     match, "outport == %s && %s == %s",
@@ -14464,7 +14522,7 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od, struct hmap *lflows,
 
     if (od->nbr->n_nat) {
         ds_clear(match);
-        const char *ct_natted = ct_lb_mark ?
+        const char *ct_natted = features->ct_no_masked_label ?
                                 "ct_mark.natted" :
                                 "ct_label.natted";
         ds_put_format(match, "ip && %s == 1", ct_natted);
@@ -14581,7 +14639,7 @@ build_lswitch_and_lrouter_iterate_by_od(struct ovn_datapath *od,
     build_lrouter_arp_nd_for_datapath(od, lsi->lflows, lsi->meter_groups);
     build_lrouter_nat_defrag_and_lb(od, lsi->lflows, lsi->ports, &lsi->match,
                                     &lsi->actions, lsi->meter_groups,
-                                    lsi->features->ct_no_masked_label);
+                                    lsi->features);
     build_lb_affinity_default_flows(od, lsi->lflows);
 }
 
@@ -16073,6 +16131,7 @@ northd_init(struct northd_data *data)
     data->features = (struct chassis_features) {
         .ct_no_masked_label = true,
         .mac_binding_timestamp = true,
+        .ct_lb_related = true,
     };
     data->ovn_internal_version_changed = false;
 }
diff --git a/northd/northd.h b/northd/northd.h
index ff8727cb7..4d9055296 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -71,6 +71,7 @@ struct northd_input {
 struct chassis_features {
     bool ct_no_masked_label;
     bool mac_binding_timestamp;
+    bool ct_lb_related;
 };
 
 struct northd_data {
diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index 058cbf71a..4de015e40 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -790,8 +790,9 @@
         policy, <code>ct_mark.blocked</code> will get set and packets in the
         reply direction will no longer be allowed, either. This flow also
         clears the register bits <code>reg0[9]</code> and
-        <code>reg0[10]</code>.  If ACL logging and logging of related packets
-        is enabled, then a companion priority-65533 flow will be installed that
+        <code>reg0[10]</code> and sets register bit <code>reg0[17]</code>.
+        If ACL logging and logging of related packets is enabled, then a
+        companion priority-65533 flow will be installed that
         accomplishes the same thing but also logs the traffic.
       </li>
 
@@ -1028,92 +1029,7 @@
       </li>
     </ul>
 
-    <h3>Ingress table 14: <code>from-lport</code> ACLs after LB</h3>
-
-    <p>
-      Logical flows in this table closely reproduce those in the
-      <code>ACL</code> table in the <code>OVN_Northbound</code> database
-      for the <code>from-lport</code> direction with the option
-      <code>apply-after-lb</code> set to <code>true</code>.
-      The <code>priority</code> values from the <code>ACL</code> table have a
-      limited range and have 1000 added to them to leave room for OVN default
-      flows at both higher and lower priorities.
-    </p>
-
-    <ul>
-      <li>
-        <code>allow</code> apply-after-lb ACLs translate into logical flows
-        with the <code>next;</code> action.  If there are any stateful ACLs
-        (including both before-lb and after-lb ACLs)
-        on this datapath, then <code>allow</code> ACLs translate to
-        <code>ct_commit; next;</code> (which acts as a hint for the next tables
-        to commit the connection to conntrack). In case the <code>ACL</code>
-        has a label then <code>reg3</code> is loaded with the label value and
-        <code>reg0[13]</code> bit is set to 1 (which acts as a hint for the
-        next tables to commit the label to conntrack).
-      </li>
-      <li>
-        <code>allow-related</code> apply-after-lb ACLs translate into logical
-        flows with the <code>ct_commit(ct_label=0/1); next;</code> actions
-        for new connections and <code>reg0[1] = 1; next;</code> for existing
-        connections.  In case the <code>ACL</code> has a label then
-        <code>reg3</code> is loaded with the label value and
-        <code>reg0[13]</code> bit is set to 1 (which acts as a hint for the
-        next tables to commit the label to conntrack).
-      </li>
-      <li>
-        <code>allow-stateless</code> apply-after-lb ACLs translate into logical
-        flows with the <code>next;</code> action.
-      </li>
-      <li>
-        <code>reject</code> apply-after-lb ACLs translate into logical
-        flows with the
-        <code>tcp_reset { output &lt;-&gt; inport;
-        next(pipeline=egress,table=5);}</code>
-        action for TCP connections,<code>icmp4/icmp6</code> action
-        for UDP connections, and <code>sctp_abort {output &lt;-%gt; inport;
-        next(pipeline=egress,table=5);}</code> action for SCTP associations.
-      </li>
-      <li>
-        Other apply-after-lb ACLs translate to <code>drop;</code> for new
-        or untracked connections and <code>ct_commit(ct_label=1/1);</code> for
-        known connections.  Setting <code>ct_label</code> marks a connection
-        as one that was previously allowed, but should no longer be
-        allowed due to a policy change.
-      </li>
-    </ul>
-
-    <ul>
-      <li>
-        One priority-0 fallback flow that matches all packets and advances to
-        the next table.
-      </li>
-    </ul>
-
-    <h3>Ingress Table 15: Stateful</h3>
-
-    <ul>
-      <li>
-        A priority 100 flow is added which commits the packet to the conntrack
-        and sets the most significant 32-bits of <code>ct_label</code> with the
-        <code>reg3</code> value based on the hint provided by previous tables
-        (with a match for <code>reg0[1] == 1 &amp;&amp; reg0[13] == 1</code>).
-        This is used by the <code>ACLs</code> with label to commit the label
-        value to conntrack.
-      </li>
-
-      <li>
-        For <code>ACLs</code> without label, a second priority-100 flow commits
-        packets to connection tracker using <code>ct_commit; next;</code>
-        action based on a hint provided by the previous tables (with a match
-        for <code>reg0[1] == 1 &amp;&amp; reg0[13] == 0</code>).
-      </li>
-      <li>
-        A priority-0 flow that simply moves traffic to the next table.
-      </li>
-    </ul>
-
-    <h3>Ingress Table 16: Pre-Hairpin</h3>
+    <h3>Ingress Table 14: Pre-Hairpin</h3>
     <ul>
       <li>
         If the logical switch has load balancer(s) configured, then a
@@ -1131,7 +1047,7 @@
       </li>
     </ul>
 
-    <h3>Ingress Table 17: Nat-Hairpin</h3>
+    <h3>Ingress Table 15: Nat-Hairpin</h3>
     <ul>
       <li>
          If the logical switch has load balancer(s) configured, then a
@@ -1166,7 +1082,7 @@
       </li>
     </ul>
 
-    <h3>Ingress Table 18: Hairpin</h3>
+    <h3>Ingress Table 16: Hairpin</h3>
     <ul>
       <li>
         <p>
@@ -1200,6 +1116,100 @@
       </li>
     </ul>
 
+    <h3>Ingress table 17: <code>from-lport</code> ACLs after LB</h3>
+
+    <p>
+      Logical flows in this table closely reproduce those in the
+      <code>ACL</code> table in the <code>OVN_Northbound</code> database
+      for the <code>from-lport</code> direction with the option
+      <code>apply-after-lb</code> set to <code>true</code>.
+      The <code>priority</code> values from the <code>ACL</code> table have a
+      limited range and have 1000 added to them to leave room for OVN default
+      flows at both higher and lower priorities.
+    </p>
+
+    <ul>
+      <li>
+        <code>allow</code> apply-after-lb ACLs translate into logical flows
+        with the <code>next;</code> action.  If there are any stateful ACLs
+        (including both before-lb and after-lb ACLs)
+        on this datapath, then <code>allow</code> ACLs translate to
+        <code>ct_commit; next;</code> (which acts as a hint for the next tables
+        to commit the connection to conntrack). In case the <code>ACL</code>
+        has a label then <code>reg3</code> is loaded with the label value and
+        <code>reg0[13]</code> bit is set to 1 (which acts as a hint for the
+        next tables to commit the label to conntrack).
+      </li>
+      <li>
+        <code>allow-related</code> apply-after-lb ACLs translate into logical
+        flows with the <code>ct_commit(ct_label=0/1); next;</code> actions
+        for new connections and <code>reg0[1] = 1; next;</code> for existing
+        connections.  In case the <code>ACL</code> has a label then
+        <code>reg3</code> is loaded with the label value and
+        <code>reg0[13]</code> bit is set to 1 (which acts as a hint for the
+        next tables to commit the label to conntrack).
+      </li>
+      <li>
+        <code>allow-stateless</code> apply-after-lb ACLs translate into logical
+        flows with the <code>next;</code> action.
+      </li>
+      <li>
+        <code>reject</code> apply-after-lb ACLs translate into logical
+        flows with the
+        <code>tcp_reset { output &lt;-&gt; inport;
+        next(pipeline=egress,table=5);}</code>
+        action for TCP connections,<code>icmp4/icmp6</code> action
+        for UDP connections, and <code>sctp_abort {output &lt;-%gt; inport;
+        next(pipeline=egress,table=5);}</code> action for SCTP associations.
+      </li>
+      <li>
+        Other apply-after-lb ACLs translate to <code>drop;</code> for new
+        or untracked connections and <code>ct_commit(ct_label=1/1);</code> for
+        known connections.  Setting <code>ct_label</code> marks a connection
+        as one that was previously allowed, but should no longer be
+        allowed due to a policy change.
+      </li>
+    </ul>
+
+    <ul>
+      <li>
+        One priority-65532 flow matching packets with <code>reg0[17]</code>
+        set (either replies to existing sessions or traffic related to
+        existing sessions) and allows these by advancing to the next
+        table.
+      </li>
+    </ul>
+
+    <ul>
+      <li>
+        One priority-0 fallback flow that matches all packets and advances to
+        the next table.
+      </li>
+    </ul>
+
+    <h3>Ingress Table 18: Stateful</h3>
+
+    <ul>
+      <li>
+        A priority 100 flow is added which commits the packet to the conntrack
+        and sets the most significant 32-bits of <code>ct_label</code> with the
+        <code>reg3</code> value based on the hint provided by previous tables
+        (with a match for <code>reg0[1] == 1 &amp;&amp; reg0[13] == 1</code>).
+        This is used by the <code>ACLs</code> with label to commit the label
+        value to conntrack.
+      </li>
+
+      <li>
+        For <code>ACLs</code> without label, a second priority-100 flow commits
+        packets to connection tracker using <code>ct_commit; next;</code>
+        action based on a hint provided by the previous tables (with a match
+        for <code>reg0[1] == 1 &amp;&amp; reg0[13] == 0</code>).
+      </li>
+      <li>
+        A priority-0 flow that simply moves traffic to the next table.
+      </li>
+    </ul>
+
     <h3>Ingress Table 19: ARP/ND responder</h3>
 
     <p>
@@ -4257,13 +4267,17 @@ outport = <var>P</var>
           For each row in the <code>NAT</code> table with IPv4 address
           <var>A</var> in the <ref column="external_ip"
           table="NAT" db="OVN_Northbound"/> column of
-          <ref table="NAT" db="OVN_Northbound"/> table, a priority-100
-          flow with the match <code>outport === <var>P</var> &amp;&amp;
-          reg0 == <var>A</var></code> has actions <code>eth.dst = <var>E</var>;
-          next;</code>, where <code>P</code> is the distributed logical router
-          port, <var>E</var> is the Ethernet address if set in the
-          <ref column="external_mac" table="NAT" db="OVN_Northbound"/> column
-          of <ref table="NAT" db="OVN_Northbound"/> table for of type
+          <ref table="NAT" db="OVN_Northbound"/> table, below two flows are
+          programmed:
+        </p>
+
+        <p>
+          A priority-100 flow with the match <code>outport == <var>P</var>
+          &amp;&amp; reg0 == <var>A</var></code> has actions <code>eth.dst =
+          <var>E</var>; next;</code>, where <code>P</code> is the distributed
+          logical router port, <var>E</var> is the Ethernet address if set in
+          the <ref column="external_mac" table="NAT" db="OVN_Northbound"/>
+          column of <ref table="NAT" db="OVN_Northbound"/> table for of type
           <code>dnat_and_snat</code>, otherwise the Ethernet address of the
           distributed logical router port. Note that if the
           <ref column="external_ip" table="NAT" db="OVN_Northbound"/> is not
@@ -4273,9 +4287,18 @@ outport = <var>P</var>
           will be added.
         </p>
 
+        <p>
+          Corresponding to the above flow, a priority-150 flow with the match
+          <code>inport == <var>P</var> &amp;&amp; outport == <var>P</var>
+          &amp;&amp; ip4.dst == <var>A</var></code> has actions
+          <code>drop;</code> to exclude packets that have gone through
+          DNAT/unSNAT stage but failed to convert the destination, to avoid
+          loop.
+        </p>
+
         <p>
           For IPv6 NAT entries, same flows are added, but using the register
-          <code>xxreg0</code> for the match.
+          <code>xxreg0</code> and field <code>ip6</code> for the match.
         </p>
       </li>
 
diff --git a/ovn-architecture.7.xml b/ovn-architecture.7.xml
index b2e00d6e4..cb1064f71 100644
--- a/ovn-architecture.7.xml
+++ b/ovn-architecture.7.xml
@@ -2832,8 +2832,7 @@
       The maximum number of networks is reduced to 4096.
     </li>
     <li>
-      The maximum number of ports per network is reduced to 4096. (Including
-      multicast group ports.)
+      The maximum number of ports per network is reduced to 2048.
     </li>
     <li>
       ACLs matching against logical ingress port identifiers are not supported.
diff --git a/tests/atlocal.in b/tests/atlocal.in
index 0b9a31276..02e9ce9bb 100644
--- a/tests/atlocal.in
+++ b/tests/atlocal.in
@@ -166,6 +166,9 @@ fi
 # Set HAVE_TCPDUMP
 find_command tcpdump
 
+# Set HAVE_XXD
+find_command xxd
+
 # Set HAVE_LFTP
 find_command lftp
 
diff --git a/tests/network-functions.at b/tests/network-functions.at
index c583bc31e..a2481c55c 100644
--- a/tests/network-functions.at
+++ b/tests/network-functions.at
@@ -128,12 +128,18 @@ OVS_START_SHELL_HELPERS
 # hex_to_binary HEXDIGITS
 #
 # Converts the pairs of HEXDIGITS into bytes and prints them on stdout.
-hex_to_binary() {
-    printf $(while test -n "$1"; do
-                 printf '\\%03o' 0x$(expr "$1" : '\(..\)')
-                 set -- "${1##??}"
-             done)
-}
+if test x$HAVE_XXD = xno; then
+    hex_to_binary() {
+        printf $(while test -n "$1"; do
+                     printf '\\%03o' 0x$(expr "$1" : '\(..\)')
+                     set -- "${1##??}"
+                 done)
+    }
+else
+    hex_to_binary() {
+        echo $1 | xxd -r -p
+    }
+fi
 
 # tcpdump_hex TITLE PACKET
 #
diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
index 6bc9ba75d..e2f4fc85c 100644
--- a/tests/ovn-controller.at
+++ b/tests/ovn-controller.at
@@ -2499,3 +2499,30 @@ AT_CHECK([GET_LOCAL_TEMPLATE_VARS], [1], [])
 
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn-controller - Requested SNAT Zone in router creation transaction])
+ovn_start
+
+net_add n1
+sim_add hv1
+as hv1
+check ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+
+dnl This is key. Add the snat-ct-zone when creating the logical router and then
+dnl do not make any further changes to the logical router settings.
+check ovn-nbctl lr-add lr0 -- set Logical_Router lr0 options:snat-ct-zone=666
+check ovn-nbctl lrp-add lr0 lrp-gw 01:00:00:00:00:01 172.16.0.1
+check ovn-nbctl lrp-set-gateway-chassis lrp-gw hv1
+
+check ovn-nbctl --wait=hv sync
+
+lr_uuid=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+ct_zones=$(ovn-appctl -t ovn-controller ct-zone-list)
+zone_num=$(printf "$ct_zones" | grep ${lr_uuid}_snat | cut -d ' ' -f 2)
+
+check test "$zone_num" -eq 666
+
+AT_CLEANUP
+])
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index c25d1122c..072102f36 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -2232,9 +2232,9 @@ check ovn-nbctl acl-add sw0 to-lport 1002 'outport == "sw0-p1" && ip4.src == 10.
 check ovn-nbctl acl-add sw0 to-lport 1002 'outport == "sw0-p1" && ip4.src == 10.0.0.13' allow
 check ovn-nbctl acl-add pg0 to-lport 1002 'outport == "pg0" && ip4.src == 10.0.0.11' drop
 
-acl1=$(ovn-nbctl --bare --column _uuid,match find acl | grep -B1 '10.0.0.12' | head -1)
-acl2=$(ovn-nbctl --bare --column _uuid,match find acl | grep -B1 '10.0.0.13' | head -1)
-acl3=$(ovn-nbctl --bare --column _uuid,match find acl | grep -B1 '10.0.0.11' | head -1)
+acl1=$(ovn-nbctl --bare --column _uuid,match find acl | grep -F -B1 '10.0.0.12' | head -1)
+acl2=$(ovn-nbctl --bare --column _uuid,match find acl | grep -F -B1 '10.0.0.13' | head -1)
+acl3=$(ovn-nbctl --bare --column _uuid,match find acl | grep -F -B1 '10.0.0.11' | head -1)
 check ovn-nbctl set acl $acl1 log=true severity=alert meter=meter_me name=acl_one
 check ovn-nbctl set acl $acl2 log=true severity=info  meter=meter_me name=acl_two
 check ovn-nbctl set acl $acl3 log=true severity=info  meter=meter_me name=acl_three
@@ -2472,8 +2472,8 @@ AT_CHECK([ovn-sbctl lflow-list ls | grep -e ls_in_acl_hint -e ls_out_acl_hint -e
   table=7 (ls_in_acl_hint     ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=8 (ls_in_acl          ), priority=1    , match=(ip && !ct.est), action=(reg0[[1]] = 1; next;)
   table=8 (ls_in_acl          ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; next;)
-  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=8 (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
 ])
 
@@ -2485,7 +2485,8 @@ check ovn-nbctl --wait=sb \
     -- ls-lb-add ls lb
 
 AT_CHECK([ovn-sbctl lflow-list ls | grep -e ls_in_acl_hint -e ls_out_acl_hint -e ls_in_acl -e ls_out_acl | sort], [0], [dnl
-  table=14(ls_in_acl_after_lb ), priority=0    , match=(1), action=(next;)
+  table=17(ls_in_acl_after_lb ), priority=0    , match=(1), action=(next;)
+  table=17(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
   table=3 (ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
   table=3 (ls_out_acl_hint    ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
@@ -2518,8 +2519,8 @@ AT_CHECK([ovn-sbctl lflow-list ls | grep -e ls_in_acl_hint -e ls_out_acl_hint -e
   table=8 (ls_in_acl          ), priority=1001 , match=(reg0[[7]] == 1 && (ip)), action=(reg0[[1]] = 1; next;)
   table=8 (ls_in_acl          ), priority=1001 , match=(reg0[[8]] == 1 && (ip)), action=(next;)
   table=8 (ls_in_acl          ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(next;)
-  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=8 (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=8 (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
 ])
@@ -2528,7 +2529,7 @@ ovn-nbctl --wait=sb clear logical_switch ls acls
 ovn-nbctl --wait=sb clear logical_switch ls load_balancer
 
 AT_CHECK([ovn-sbctl lflow-list ls | grep -e ls_in_acl_hint -e ls_out_acl_hint -e ls_in_acl -e ls_out_acl | sort], [0], [dnl
-  table=14(ls_in_acl_after_lb ), priority=0    , match=(1), action=(next;)
+  table=17(ls_in_acl_after_lb ), priority=0    , match=(1), action=(next;)
   table=3 (ls_out_acl_hint    ), priority=65535, match=(1), action=(next;)
   table=4 (ls_out_acl         ), priority=65535, match=(1), action=(next;)
   table=7 (ls_in_acl_hint     ), priority=65535, match=(1), action=(next;)
@@ -4360,8 +4361,8 @@ ovn-sbctl dump-flows sw0 > sw0flows
 AT_CAPTURE_FILE([sw0flows])
 
 AT_CHECK([grep -w "ls_in_acl" sw0flows | grep 6553 | sort | sed 's/table=./table=?/'], [0], [dnl
-  table=? (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=? (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=? (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=? (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=? (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=? (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
 ])
@@ -4380,9 +4381,9 @@ ovn-sbctl dump-flows sw0 > sw0flows
 AT_CAPTURE_FILE([sw0flows])
 
 AT_CHECK([grep -w "ls_in_acl" sw0flows | grep 6553 | sort | sed 's/table=./table=?/'], [0], [dnl
-  table=? (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && ct_mark.blocked == 0), action=(ct_commit_nat;)
+  table=? (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
   table=? (ls_in_acl          ), priority=65532, match=((ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
-  table=? (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=? (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=? (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
 ])
 
@@ -4404,8 +4405,8 @@ ovn-sbctl dump-flows sw0 > sw0flows
 AT_CAPTURE_FILE([sw0flows])
 
 AT_CHECK([grep -w "ls_in_acl" sw0flows | grep 6553 | sort | sed 's/table=./table=?/'], [0], [dnl
-  table=? (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=? (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=? (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=? (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=? (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=? (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
 ])
@@ -5139,7 +5140,8 @@ AT_CHECK([grep "lr_out_snat" lr0flows | sed 's/table=./table=?/' | sort], [0], [
 ])
 
 check ovn-sbctl chassis-add gw1 geneve 127.0.0.1 \
-  -- set chassis gw1 other_config:ct-no-masked-label="true"
+  -- set chassis gw1 other_config:ct-no-masked-label="true" \
+  -- set chassis gw1 other_config:ovn-ct-lb-related="true"
 
 # Create a distributed gw port on lr0
 check ovn-nbctl ls-add public
@@ -6685,11 +6687,12 @@ AT_CHECK([grep -e "ls_in_acl" lsflows | sed 's/table=../table=??/' | sort], [0],
   table=??(ls_in_acl          ), priority=2004 , match=(reg0[[10]] == 1 && (ip4 && ip4.dst == 10.0.0.2)), action=(ct_commit { ct_mark.blocked = 1; }; /* drop */)
   table=??(ls_in_acl          ), priority=2004 , match=(reg0[[9]] == 1 && (ip4 && ip4.dst == 10.0.0.2)), action=(/* drop */)
   table=??(ls_in_acl          ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(next;)
-  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=??(ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=??(ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
   table=??(ls_in_acl_after_lb ), priority=0    , match=(1), action=(next;)
+  table=??(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
@@ -6730,8 +6733,8 @@ AT_CHECK([grep -e "ls_in_acl" lsflows | sed 's/table=../table=??/' | sort], [0],
   table=??(ls_in_acl          ), priority=1    , match=(ip && !ct.est), action=(reg0[[1]] = 1; next;)
   table=??(ls_in_acl          ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; next;)
   table=??(ls_in_acl          ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(next;)
-  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=??(ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=??(ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
   table=??(ls_in_acl_after_lb ), priority=0    , match=(1), action=(next;)
@@ -6743,6 +6746,7 @@ AT_CHECK([grep -e "ls_in_acl" lsflows | sed 's/table=../table=??/' | sort], [0],
   table=??(ls_in_acl_after_lb ), priority=2003 , match=(reg0[[8]] == 1 && (ip4 && icmp)), action=(next;)
   table=??(ls_in_acl_after_lb ), priority=2004 , match=(reg0[[10]] == 1 && (ip4 && ip4.dst == 10.0.0.2)), action=(ct_commit { ct_mark.blocked = 1; }; /* drop */)
   table=??(ls_in_acl_after_lb ), priority=2004 , match=(reg0[[9]] == 1 && (ip4 && ip4.dst == 10.0.0.2)), action=(/* drop */)
+  table=??(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
@@ -6787,8 +6791,8 @@ AT_CHECK([grep -e "ls_in_acl" lsflows | sed 's/table=../table=??/' | sort], [0],
   table=??(ls_in_acl          ), priority=2003 , match=(reg0[[7]] == 1 && (ip4 && icmp)), action=(reg0[[1]] = 1; next;)
   table=??(ls_in_acl          ), priority=2003 , match=(reg0[[8]] == 1 && (ip4 && icmp)), action=(next;)
   table=??(ls_in_acl          ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(next;)
-  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=??(ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=??(ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
   table=??(ls_in_acl_after_lb ), priority=0    , match=(1), action=(next;)
@@ -6796,6 +6800,7 @@ AT_CHECK([grep -e "ls_in_acl" lsflows | sed 's/table=../table=??/' | sort], [0],
   table=??(ls_in_acl_after_lb ), priority=2001 , match=(reg0[[9]] == 1 && (ip4)), action=(/* drop */)
   table=??(ls_in_acl_after_lb ), priority=2004 , match=(reg0[[10]] == 1 && (ip4 && ip4.dst == 10.0.0.2)), action=(ct_commit { ct_mark.blocked = 1; }; /* drop */)
   table=??(ls_in_acl_after_lb ), priority=2004 , match=(reg0[[9]] == 1 && (ip4 && ip4.dst == 10.0.0.2)), action=(/* drop */)
+  table=??(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
@@ -7219,11 +7224,12 @@ AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | sed 's/table=../table=??/
   table=??(ls_in_acl          ), priority=1001 , match=(reg0[[7]] == 1 && (ip4 && tcp)), action=(reg0[[1]] = 1; next;)
   table=??(ls_in_acl          ), priority=1001 , match=(reg0[[8]] == 1 && (ip4 && tcp)), action=(next;)
   table=??(ls_in_acl          ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(next;)
-  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=??(ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=??(ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
   table=??(ls_in_acl_after_lb ), priority=0    , match=(1), action=(drop;)
+  table=??(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
@@ -7342,13 +7348,14 @@ AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | sed 's/table=../table=??/
   table=??(ls_in_acl          ), priority=1    , match=(ip && !ct.est), action=(drop;)
   table=??(ls_in_acl          ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; next;)
   table=??(ls_in_acl          ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(next;)
-  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=??(ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=??(ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
   table=??(ls_in_acl_after_lb ), priority=0    , match=(1), action=(drop;)
   table=??(ls_in_acl_after_lb ), priority=1001 , match=(reg0[[7]] == 1 && (ip4 && tcp)), action=(reg0[[1]] = 1; next;)
   table=??(ls_in_acl_after_lb ), priority=1001 , match=(reg0[[8]] == 1 && (ip4 && tcp)), action=(next;)
+  table=??(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
@@ -7467,11 +7474,12 @@ AT_CHECK([ovn-sbctl dump-flows | grep -E "ls_.*_acl" | sed 's/table=../table=??/
   table=??(ls_in_acl          ), priority=1    , match=(ip && !ct.est), action=(drop;)
   table=??(ls_in_acl          ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; next;)
   table=??(ls_in_acl          ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(next;)
-  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=??(ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=??(ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=??(ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=??(ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
   table=??(ls_in_acl_after_lb ), priority=0    , match=(1), action=(drop;)
+  table=??(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
   table=??(ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
   table=??(ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
@@ -7775,7 +7783,7 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
   table=??(ls_in_check_port_sec), priority=100  , match=(vlan.present), action=(drop;)
   table=??(ls_in_check_port_sec), priority=50   , match=(1), action=(reg0[[15]] = check_in_port_sec(); next;)
   table=??(ls_in_check_port_sec), priority=70   , match=(inport == "localnetport"), action=(set_queue(10); reg0[[15]] = check_in_port_sec(); next;)
-  table=??(ls_in_check_port_sec), priority=70   , match=(inport == "sw0p1"), action=(reg0[[14]] = 1; next(pipeline=ingress, table=18);)
+  table=??(ls_in_check_port_sec), priority=70   , match=(inport == "sw0p1"), action=(reg0[[14]] = 1; next(pipeline=ingress, table=16);)
   table=??(ls_in_check_port_sec), priority=70   , match=(inport == "sw0p2"), action=(set_queue(10); reg0[[15]] = check_in_port_sec(); next;)
   table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
   table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), action=(drop;)
@@ -7832,6 +7840,22 @@ AT_CHECK([ovn-sbctl lflow-list | grep -e natted -e ct_lb], [0], [dnl
   table=2 (ls_out_pre_stateful), priority=110  , match=(reg0[[2]] == 1), action=(ct_lb;)
 ])
 
+check ovn-nbctl --wait=sb set logical_router lr options:lb_force_snat_ip="42.42.42.1"
+AT_CHECK([ovn-sbctl lflow-list | grep lr_in_dnat], [0], [dnl
+  table=7 (lr_in_dnat         ), priority=110  , match=(ct.est && !ct.rel && ip4 && reg0 == 66.66.66.66 && ct_label.natted == 1), action=(flags.force_snat_for_lb = 1; next;)
+  table=7 (lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && reg0 == 66.66.66.66), action=(flags.force_snat_for_lb = 1; ct_lb(backends=42.42.42.2);)
+  table=7 (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+])
+check ovn-nbctl remove logical_router lr options lb_force_snat_ip
+
+check ovn-nbctl --wait=sb set load_balancer lb-test options:skip_snat="true"
+AT_CHECK([ovn-sbctl lflow-list | grep lr_in_dnat], [0], [dnl
+  table=7 (lr_in_dnat         ), priority=110  , match=(ct.est && !ct.rel && ip4 && reg0 == 66.66.66.66 && ct_label.natted == 1), action=(flags.skip_snat_for_lb = 1; next;)
+  table=7 (lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && reg0 == 66.66.66.66), action=(flags.skip_snat_for_lb = 1; ct_lb(backends=42.42.42.2);)
+  table=7 (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+])
+check ovn-nbctl remove load_balancer lb-test options skip_snat
+
 AS_BOX([Chassis upgrades and supports ct_lb_mark - use ct_lb_mark and ct_mark.natted])
 check ovn-sbctl set chassis hv other_config:ct-no-masked-label=true
 check ovn-nbctl --wait=sb sync
@@ -7865,8 +7889,8 @@ AT_CHECK([ovn-sbctl lflow-list | grep 'ls.*acl.*blocked' ], [0], [dnl
   table=7 (ls_in_acl_hint     ), priority=4    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0), action=(reg0[[8]] = 1; reg0[[10]] = 1; next;)
   table=7 (ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
   table=7 (ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
-  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=8 (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=8 (ls_in_acl          ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
@@ -7887,15 +7911,15 @@ AT_CHECK([ovn-sbctl lflow-list | grep 'ls.*acl.*blocked' ], [0], [dnl
   table=7 (ls_in_acl_hint     ), priority=4    , match=(!ct.new && ct.est && !ct.rpl && ct_label.blocked == 0), action=(reg0[[8]] = 1; reg0[[10]] = 1; next;)
   table=7 (ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_label.blocked == 1), action=(reg0[[9]] = 1; next;)
   table=7 (ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_label.blocked == 0), action=(reg0[[10]] = 1; next;)
-  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(ct_commit_nat;)
-  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(reg0[[17]] = 1; next;)
+  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=8 (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
   table=8 (ls_in_acl          ), priority=1    , match=(ip && ct.est && ct_label.blocked == 1), action=(reg0[[1]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_label.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=4    , match=(!ct.new && ct.est && !ct.rpl && ct_label.blocked == 0), action=(reg0[[8]] = 1; reg0[[10]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=2    , match=(ct.est && ct_label.blocked == 1), action=(reg0[[9]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=1    , match=(ct.est && ct_label.blocked == 0), action=(reg0[[10]] = 1; next;)
-  table=4 (ls_out_acl         ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(ct_commit_nat;)
+  table=4 (ls_out_acl         ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
   table=4 (ls_out_acl         ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
   table=4 (ls_out_acl         ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
   table=4 (ls_out_acl         ), priority=1    , match=(ip && ct.est && ct_label.blocked == 1), action=(reg0[[1]] = 1; next;)
@@ -7909,15 +7933,15 @@ AT_CHECK([ovn-sbctl lflow-list | grep 'ls.*acl.*blocked' ], [0], [dnl
   table=7 (ls_in_acl_hint     ), priority=4    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0), action=(reg0[[8]] = 1; reg0[[10]] = 1; next;)
   table=7 (ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
   table=7 (ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
-  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
-  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; next;)
+  table=8 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; next;)
+  table=8 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
   table=8 (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=8 (ls_in_acl          ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=4    , match=(!ct.new && ct.est && !ct.rpl && ct_mark.blocked == 0), action=(reg0[[8]] = 1; reg0[[10]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=2    , match=(ct.est && ct_mark.blocked == 1), action=(reg0[[9]] = 1; next;)
   table=3 (ls_out_acl_hint    ), priority=1    , match=(ct.est && ct_mark.blocked == 0), action=(reg0[[10]] = 1; next;)
-  table=4 (ls_out_acl         ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
+  table=4 (ls_out_acl         ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(next;)
   table=4 (ls_out_acl         ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(next;)
   table=4 (ls_out_acl         ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
   table=4 (ls_out_acl         ), priority=1    , match=(ip && ct.est && ct_mark.blocked == 1), action=(reg0[[1]] = 1; next;)
@@ -8437,3 +8461,156 @@ check_row_count sb:Chassis_Template_Var 0
 
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([Load balancer CT related backwards compatibility])
+AT_KEYWORDS([lb])
+ovn_start
+
+check ovn-nbctl                                               \
+  -- ls-add ls                                                \
+  -- lr-add lr -- set logical_router lr options:chassis=local \
+  -- lb-add lb-test 192.168.0.1 192.168.1.10                  \
+  -- ls-lb-add ls lb-test                                     \
+  -- lr-lb-add lr lb-test
+
+m4_define([DUMP_FLOWS_SORTED], [sed 's/table=[[0-9]]\{1,2\}/table=?/' | sort])
+
+AS_BOX([No chassis registered - CT related flows should be installed])
+check ovn-nbctl --wait=sb sync
+ovn-sbctl dump-flows | DUMP_FLOWS_SORTED > lflows0
+
+AT_CHECK([grep -e "lr_in_defrag" -e "lr_in_dnat" lflows0], [0], [dnl
+  table=? (lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=? (lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 192.168.0.1), action=(reg0 = 192.168.0.1; ct_dnat;)
+  table=? (lr_in_defrag       ), priority=50   , match=(icmp || icmp6), action=(ct_dnat;)
+  table=? (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=? (lr_in_dnat         ), priority=110  , match=(ct.est && !ct.rel && ip4 && reg0 == 192.168.0.1 && ct_mark.natted == 1), action=(next;)
+  table=? (lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && reg0 == 192.168.0.1), action=(ct_lb_mark(backends=192.168.1.10);)
+  table=? (lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
+  table=? (lr_in_dnat         ), priority=70   , match=(ct.rel && !ct.est && !ct.new && ct_mark.force_snat == 1), action=(flags.force_snat_for_lb = 1; ct_commit_nat;)
+  table=? (lr_in_dnat         ), priority=70   , match=(ct.rel && !ct.est && !ct.new && ct_mark.skip_snat == 1), action=(flags.skip_snat_for_lb = 1; ct_commit_nat;)
+])
+
+AT_CHECK([grep -e "ls_in_acl" -e "ls_out_acl" lflows0 | grep "priority=65532"], [0], [dnl
+  table=? (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=? (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
+  table=? (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
+  table=? (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+  table=? (ls_out_acl         ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
+  table=? (ls_out_acl         ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(next;)
+  table=? (ls_out_acl         ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
+  table=? (ls_out_acl         ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+  table=?(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
+])
+
+
+AS_BOX([Chassis registered that doesn't support CT related])
+check ovn-sbctl chassis-add hv geneve 127.0.0.1
+check ovn-nbctl --wait=sb sync
+ovn-sbctl dump-flows | DUMP_FLOWS_SORTED > lflows1
+
+AT_CHECK([grep -e "lr_in_defrag" -e "lr_in_dnat" lflows1], [0], [dnl
+  table=? (lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=? (lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 192.168.0.1), action=(reg0 = 192.168.0.1; ct_dnat;)
+  table=? (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=? (lr_in_dnat         ), priority=110  , match=(ct.est && !ct.rel && ip4 && reg0 == 192.168.0.1 && ct_label.natted == 1), action=(next;)
+  table=? (lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && reg0 == 192.168.0.1), action=(ct_lb(backends=192.168.1.10);)
+])
+
+check ovn-nbctl --wait=sb set logical_router lr options:lb_force_snat_ip="192.168.1.1"
+AT_CHECK([ovn-sbctl lflow-list | grep lr_in_dnat], [0], [dnl
+  table=7 (lr_in_dnat         ), priority=110  , match=(ct.est && !ct.rel && ip4 && reg0 == 192.168.0.1 && ct_label.natted == 1), action=(flags.force_snat_for_lb = 1; next;)
+  table=7 (lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && reg0 == 192.168.0.1), action=(flags.force_snat_for_lb = 1; ct_lb(backends=192.168.1.10);)
+  table=7 (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+])
+check ovn-nbctl remove logical_router lr options lb_force_snat_ip
+
+check ovn-nbctl --wait=sb set load_balancer lb-test options:skip_snat="true"
+AT_CHECK([ovn-sbctl lflow-list | grep lr_in_dnat], [0], [dnl
+  table=7 (lr_in_dnat         ), priority=110  , match=(ct.est && !ct.rel && ip4 && reg0 == 192.168.0.1 && ct_label.natted == 1), action=(flags.skip_snat_for_lb = 1; next;)
+  table=7 (lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && reg0 == 192.168.0.1), action=(flags.skip_snat_for_lb = 1; ct_lb(backends=192.168.1.10);)
+  table=7 (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+])
+check ovn-nbctl remove load_balancer lb-test options skip_snat
+
+AT_CHECK([grep -e "ls_in_acl" -e "ls_out_acl" lflows1 | grep "priority=65532"], [0], [dnl
+  table=? (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(reg0[[17]] = 1; next;)
+  table=? (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
+  table=? (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=? (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+  table=? (ls_out_acl         ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
+  table=? (ls_out_acl         ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
+  table=? (ls_out_acl         ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=? (ls_out_acl         ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+  table=?(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
+])
+
+AS_BOX([Chassis upgrades and supports CT related])
+check ovn-sbctl set chassis hv other_config:ct-no-masked-label=true
+check ovn-sbctl set chassis hv other_config:ovn-ct-lb-related=true
+check ovn-nbctl --wait=sb sync
+ovn-sbctl dump-flows | DUMP_FLOWS_SORTED > lflows2
+
+AT_CHECK([grep -e "lr_in_defrag" -e "lr_in_dnat" lflows2], [0], [dnl
+  table=? (lr_in_defrag       ), priority=0    , match=(1), action=(next;)
+  table=? (lr_in_defrag       ), priority=100  , match=(ip && ip4.dst == 192.168.0.1), action=(reg0 = 192.168.0.1; ct_dnat;)
+  table=? (lr_in_defrag       ), priority=50   , match=(icmp || icmp6), action=(ct_dnat;)
+  table=? (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=? (lr_in_dnat         ), priority=110  , match=(ct.est && !ct.rel && ip4 && reg0 == 192.168.0.1 && ct_mark.natted == 1), action=(next;)
+  table=? (lr_in_dnat         ), priority=110  , match=(ct.new && !ct.rel && ip4 && reg0 == 192.168.0.1), action=(ct_lb_mark(backends=192.168.1.10);)
+  table=? (lr_in_dnat         ), priority=50   , match=(ct.rel && !ct.est && !ct.new), action=(ct_commit_nat;)
+  table=? (lr_in_dnat         ), priority=70   , match=(ct.rel && !ct.est && !ct.new && ct_mark.force_snat == 1), action=(flags.force_snat_for_lb = 1; ct_commit_nat;)
+  table=? (lr_in_dnat         ), priority=70   , match=(ct.rel && !ct.est && !ct.new && ct_mark.skip_snat == 1), action=(flags.skip_snat_for_lb = 1; ct_commit_nat;)
+])
+
+AT_CHECK([grep -e "ls_in_acl" -e "ls_out_acl" lflows2 | grep "priority=65532"], [0], [dnl
+  table=? (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(reg0[[17]] = 1; ct_commit_nat;)
+  table=? (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(reg0[[9]] = 0; reg0[[10]] = 0; reg0[[17]] = 1; next;)
+  table=? (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
+  table=? (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+  table=? (ls_out_acl         ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_mark.blocked == 0), action=(ct_commit_nat;)
+  table=? (ls_out_acl         ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_mark.blocked == 0), action=(next;)
+  table=? (ls_out_acl         ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_mark.blocked == 1)), action=(drop;)
+  table=? (ls_out_acl         ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+  table=?(ls_in_acl_after_lb ), priority=65532, match=(reg0[[17]] == 1), action=(next;)
+])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([Chassis-feature compatibitility - remote chassis])
+ovn_start
+
+AS_BOX([Local chassis])
+check ovn-sbctl chassis-add hv1 geneve 127.0.0.1 \
+  -- set chassis hv1 other_config:ct-no-masked-label=true \
+  -- set chassis hv1 other_config:ovn-ct-lb-related=true \
+  -- set chassis hv1 other_config:mac-binding-timestamp=true
+
+check ovn-nbctl --wait=sb sync
+
+AT_CHECK([as northd ovn-appctl -t NORTHD_TYPE debug/chassis-features-list], [0], [dnl
+ct_no_masked_label:    true
+ct_lb_related:         true
+mac_binding_timestamp: true
+])
+
+AS_BOX([Remote chassis])
+check ovn-sbctl chassis-add hv2 geneve 127.0.0.2 \
+  -- set chassis hv2 other_config:is-remote=true \
+  -- set chassis hv2 other_config:ct-no-masked-label=false \
+  -- set chassis hv2 other_config:ovn-ct-lb-related=false \
+  -- set chassis hv2 other_config:mac-binding-timestamp=false
+
+check ovn-nbctl --wait=sb sync
+
+AT_CHECK([as northd ovn-appctl -t NORTHD_TYPE debug/chassis-features-list], [0], [dnl
+ct_no_masked_label:    true
+ct_lb_related:         true
+mac_binding_timestamp: true
+])
+
+AT_CLEANUP
+])
diff --git a/tests/ovn.at b/tests/ovn.at
index ad2014de6..f77a4983d 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -4461,7 +4461,12 @@ for i in 1 2 3; do
 done
 
 # Gracefully terminate daemons
-OVN_CLEANUP([hv1],[hv2],[vtep])
+
+OVN_CLEANUP_SBOX([hv1])
+OVN_CLEANUP_SBOX([hv2])
+OVS_WAIT_UNTIL([test `as vtep ovs-vsctl list-ports vtep_bfd | wc -l` -eq 0])
+OVN_CLEANUP([vtep])
+
 OVN_CLEANUP_VSWITCH([hv3])
 
 AT_CLEANUP
@@ -25064,8 +25069,10 @@ OVN_FOR_EACH_NORTHD([
 AT_SETUP([interconnection])
 
 ovn_init_ic_db
-n_az=5
-n_ts=5
+# The number needs to stay relatively low due to high memory consumption
+# with address sanitizers enabled.
+n_az=3
+n_ts=3
 for i in `seq 1 $n_az`; do
     ovn_start az$i
 done
@@ -28416,24 +28423,39 @@ wait_row_count Port_Binding 1 logical_port=lsp-cont1 chassis=$ch
 OVN_CLEANUP([hv1])
 AT_CLEANUP
 
+# TEST_LR_DROP_TRAFFIC_FOR_OWN_IPS [ DGP | GR ]
 # Test dropping traffic destined to router owned IPs.
-OVN_FOR_EACH_NORTHD([
-AT_SETUP([gateway router drop traffic for own IPs])
+m4_define([TEST_LR_DROP_TRAFFIC_FOR_OWN_IPS], [
 ovn_start
 
-ovn-nbctl lr-add r1 -- set logical_router r1 options:chassis=hv1
-ovn-nbctl ls-add s1
-
-# Connnect r1 to s1.
-ovn-nbctl lrp-add r1 lrp-r1-s1 00:00:00:00:01:01 10.0.1.1/24
-ovn-nbctl lsp-add s1 lsp-s1-r1 -- set Logical_Switch_Port lsp-s1-r1 type=router \
-    options:router-port=lrp-r1-s1 addresses=router
-
-# Create logical port p1 in s1
-ovn-nbctl lsp-add s1 p1 \
+ovn-nbctl lr-add r1 # Gateway router or LR with DGP on the ext side
+ovn-nbctl ls-add ext # simulate external LS
+ovn-nbctl ls-add s2 # simulate internal LS
+
+# Connnect r1 to ext.
+ovn-nbctl lrp-add r1 lrp-r1-ext 00:00:00:00:01:01 10.0.1.1/24
+if test X"$1" = X"DGP"; then
+    ovn-nbctl lrp-set-gateway-chassis lrp-r1-ext hv1 1
+else
+    ovn-nbctl set logical_router r1 options:chassis=hv1
+fi
+ovn-nbctl lsp-add ext lsp-ext-r1 -- set Logical_Switch_Port lsp-ext-r1 type=router \
+    options:router-port=lrp-r1-ext addresses=router
+
+# Connnect r1 to s2.
+ovn-nbctl lrp-add r1 lrp-r1-s2 00:00:00:00:02:01 10.0.2.1/24
+ovn-nbctl lsp-add s2 lsp-s2-r1 -- set Logical_Switch_Port lsp-s2-r1 type=router \
+    options:router-port=lrp-r1-s2 addresses=router
+
+# Create logical port p1 in ext
+ovn-nbctl lsp-add ext p1 \
 -- lsp-set-addresses p1 "f0:00:00:00:01:02 10.0.1.2" \
 -- lsp-set-port-security p1 "f0:00:00:00:01:02 10.0.1.2"
 
+# Create logical port p2 in s2
+ovn-nbctl lsp-add s2 p2 \
+-- lsp-set-addresses p2 "f0:00:00:00:02:02 10.0.2.2"
+
 # Create two hypervisor and create OVS ports corresponding to logical ports.
 net_add n1
 
@@ -28447,6 +28469,12 @@ ovs-vsctl -- add-port br-int hv1-vif1 -- \
     options:rxq_pcap=hv1/vif1-rx.pcap \
     ofport-request=1
 
+ovs-vsctl -- add-port br-int hv1-vif2 -- \
+    set interface hv1-vif2 external-ids:iface-id=p2 \
+    options:tx_pcap=hv1/vif2-tx.pcap \
+    options:rxq_pcap=hv1/vif2-rx.pcap \
+    ofport-request=2
+
 # Pre-populate the hypervisors' ARP tables so that we don't lose any
 # packets for ARP resolution (native tunneling doesn't queue packets
 # for ARP resolution).
@@ -28457,9 +28485,10 @@ ovn-nbctl --wait=hv sync
 
 sw_key=$(ovn-sbctl --bare --columns tunnel_key list datapath_binding r1)
 
+echo sw_key: $sw_key
 AT_CHECK([ovn-sbctl lflow-list | grep lr_in_arp_resolve | grep 10.0.1.1], [1], [])
 
-# Send ip packets from p1 to lrp-r1-s1
+# Send ip packets from p1 to lrp-r1-ext
 src_mac="f00000000102"
 dst_mac="000000000101"
 src_ip=`ip_to_hex 10 0 1 2`
@@ -28478,10 +28507,10 @@ AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=11, n_packets=1,.*
 ])
 
 # Use the router IP as SNAT IP.
-ovn-nbctl set logical_router r1 options:lb_force_snat_ip=10.0.1.1
+ovn-nbctl lr-nat-add r1 snat 10.0.1.1 10.8.8.0/24
 ovn-nbctl --wait=hv sync
 
-# Send ip packets from p1 to lrp-r1-s1
+# Send ip packets from p1 to lrp-r1-ext
 src_mac="f00000000102"
 dst_mac="000000000101"
 src_ip=`ip_to_hex 10 0 1 2`
@@ -28496,11 +28525,53 @@ AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep "actions=controller" | grep
 ])
 
 # The packet should've been dropped in the lr_in_arp_resolve stage.
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=25, n_packets=1,.* priority=2,ip,metadata=0x${sw_key},nw_dst=10.0.1.1 actions=drop" -c], [0], [dnl
+if test X"$1" = X"DGP"; then
+    prio=150
+    inport=reg14
+    outport=reg15
+else
+    prio=2
+fi
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=25, n_packets=1,.* priority=$prio,ip,$inport.*$outport.*metadata=0x${sw_key},nw_dst=10.0.1.1 actions=drop" -c], [0], [dnl
 1
 ])
 
+# Send ip packets from p2 to lrp-r1-ext
+src_mac="f00000000202"
+dst_mac="000000000201"
+src_ip=`ip_to_hex 10 0 2 2`
+dst_ip=`ip_to_hex 10 0 1 1`
+packet=${dst_mac}${src_mac}08004500001c0000000040110000${src_ip}${dst_ip}0035111100080000
+as hv1 ovs-appctl netdev-dummy/receive hv1-vif2 $packet
+
+# Still no packet-ins should reach ovn-controller.
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep "actions=controller" | grep -v n_packets=0 -c], [1], [dnl
+0
+])
+
+if test X"$1" = X"DGP"; then
+    # The packet dst should be resolved once for E/W centralized NAT purpose.
+    AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=25, n_packets=1,.* priority=100,reg0=0xa000101,reg15=.*metadata=0x${sw_key} actions=mod_dl_dst:00:00:00:00:01:01,resubmit" -c], [0], [dnl
+1
+])
+fi
+
+# The packet should've been finally dropped in the lr_in_arp_resolve stage.
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=25, n_packets=2,.* priority=$prio,ip,$inport.*$outport.*metadata=0x${sw_key},nw_dst=10.0.1.1 actions=drop" -c], [0], [dnl
+1
+])
 OVN_CLEANUP([hv1])
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([gateway router drop traffic for own IPs])
+TEST_LR_DROP_TRAFFIC_FOR_OWN_IPS(GR)
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([distributed gateway port drop traffic for own IPs])
+TEST_LR_DROP_TRAFFIC_FOR_OWN_IPS(DGP)
 AT_CLEANUP
 ])
 
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 99ad14aa5..1e6767846 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -1618,8 +1618,8 @@ OVS_WAIT_UNTIL([
 ovn-nbctl --reject lb-add lb3 30.0.0.10:80 ""
 ovn-nbctl ls-lb-add foo lb3
 # Filter reset segments
-NS_CHECK_EXEC([foo1], [tcpdump -c 1 -neei foo1 ip[[33:1]]=0x14 > rst.pcap 2>/dev/null &])
-sleep 1
+NS_CHECK_EXEC([foo1], [tcpdump -l -c 1 -neei foo1 ip[[33:1]]=0x14 > rst.pcap 2>tcpdump_err &])
+OVS_WAIT_UNTIL([grep "listening" tcpdump_err])
 NS_CHECK_EXEC([foo1], [wget -q 30.0.0.10],[4])
 
 OVS_WAIT_UNTIL([
@@ -1734,13 +1734,11 @@ OVS_START_L7([bar2], [http6])
 OVS_START_L7([bar3], [http6])
 
 dnl Should work with the virtual IP fd03::1 address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([foo1], [wget http://[[fd03::1]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log || (ovs-ofctl -O OpenFlow13 dump-flows br-int && false)])
+OVS_WAIT_FOR_OUTPUT([
+for i in `seq 1 10`; do
+    NS_EXEC([foo1], [wget http://[[fd03::1]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log || (ovs-ofctl -O OpenFlow13 dump-flows br-int && false)])
 done
-
-dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::1) | grep -v fe80 | \
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::1) | grep -v fe80 | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd01::2,dst=fd03::1,sport=<cleared>,dport=<cleared>),reply=(src=fd02::2,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd01::2,dst=fd03::1,sport=<cleared>,dport=<cleared>),reply=(src=fd02::3,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
@@ -1748,27 +1746,25 @@ tcp,orig=(src=fd01::2,dst=fd03::1,sport=<cleared>,dport=<cleared>),reply=(src=fd
 ])
 
 dnl Should work with the virtual IP fd03::3 address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([foo1], [wget http://[[fd03::3]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT([
+for i in `seq 1 10`; do
+    NS_EXEC([foo1], [wget http://[[fd03::3]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
-
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::3) | grep -v fe80 | \
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::3) | grep -v fe80 | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd01::2,dst=fd03::3,sport=<cleared>,dport=<cleared>),reply=(src=fd02::2,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd01::2,dst=fd03::3,sport=<cleared>,dport=<cleared>),reply=(src=fd02::3,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd01::2,dst=fd03::3,sport=<cleared>,dport=<cleared>),reply=(src=fd02::4,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 ])
 
+OVS_WAIT_FOR_OUTPUT([
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([foo1], [wget http://[[fd03::2]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+for i in `seq 1 10`; do
+    NS_EXEC([foo1], [wget http://[[fd03::2]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
-
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::2) | grep -v fe80 | \
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::2) | grep -v fe80 | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd01::2,dst=fd03::2,sport=<cleared>,dport=<cleared>),reply=(src=fd02::2,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd01::2,dst=fd03::2,sport=<cleared>,dport=<cleared>),reply=(src=fd02::3,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
@@ -1784,14 +1780,14 @@ OVS_WAIT_UNTIL([
 
 AT_CHECK([ovs-appctl dpctl/flush-conntrack])
 
+OVS_WAIT_FOR_OUTPUT([
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([foo1], [wget http://[[fd03::2]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+for i in `seq 1 10`; do
+    NS_EXEC([foo1], [wget http://[[fd03::2]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::2) | grep -v fe80 | \
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::2) | grep -v fe80 | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd01::2,dst=fd03::2,sport=<cleared>,dport=<cleared>),reply=(src=fd02::2,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd01::2,dst=fd03::2,sport=<cleared>,dport=<cleared>),reply=(src=fd02::3,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
@@ -1933,13 +1929,13 @@ OVS_START_L7([foo3], [http])
 OVS_START_L7([foo4], [http])
 
 dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([foo1], [wget 30.0.0.1 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT([
+for i in `seq 1 10`; do
+    NS_EXEC([foo1], [wget 30.0.0.1 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) | \
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=192.168.1.2,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.3,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=192.168.1.2,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.4,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
@@ -1947,20 +1943,19 @@ tcp,orig=(src=192.168.1.2,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(s
 ])
 
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([foo1], [wget 30.0.0.2:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT([
+for i in `seq 1 10`; do
+    NS_EXEC([foo1], [wget 30.0.0.2:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) | \
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=192.168.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.3,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=192.168.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.4,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=192.168.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.5,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 ])
 
-
 OVS_APP_EXIT_AND_WAIT([ovn-controller])
 
 as ovn-sb
@@ -2044,13 +2039,13 @@ OVS_START_L7([foo3], [http6])
 OVS_START_L7([foo4], [http6])
 
 dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([foo1], [wget http://[[fd03::1]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT([
+for i in `seq 1 10`; do
+    NS_EXEC([foo1], [wget http://[[fd03::1]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::1) | grep -v fe80 | \
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::1) | grep -v fe80 | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd01::2,dst=fd03::1,sport=<cleared>,dport=<cleared>),reply=(src=fd01::3,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd01::2,dst=fd03::1,sport=<cleared>,dport=<cleared>),reply=(src=fd01::4,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
@@ -2058,20 +2053,19 @@ tcp,orig=(src=fd01::2,dst=fd03::1,sport=<cleared>,dport=<cleared>),reply=(src=fd
 ])
 
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([foo1], [wget http://[[fd03::2]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT([
+for i in `seq 1 10`; do
+    NS_EXEC([foo1], [wget http://[[fd03::2]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::2) | grep -v fe80 | \
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd03::2) | grep -v fe80 | \
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd01::2,dst=fd03::2,sport=<cleared>,dport=<cleared>),reply=(src=fd01::3,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd01::2,dst=fd03::2,sport=<cleared>,dport=<cleared>),reply=(src=fd01::4,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd01::2,dst=fd03::2,sport=<cleared>,dport=<cleared>),reply=(src=fd01::5,dst=fd01::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 ])
 
-
 OVS_APP_EXIT_AND_WAIT([ovn-controller])
 
 as ovn-sb
@@ -2199,27 +2193,27 @@ OVS_START_L7([bar1], [http])
 
 check ovs-appctl dpctl/flush-conntrack
 dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget 30.0.0.1 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget 30.0.0.1 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) |
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) |
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=172.16.1.2,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=172.16.1.2,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 ])
 
 check ovs-appctl dpctl/flush-conntrack
+OVS_WAIT_FOR_OUTPUT([
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget 30.0.0.2:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget 30.0.0.2:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) |
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) |
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
@@ -2256,23 +2250,23 @@ OVS_WAIT_UNTIL([ovs-ofctl -O OpenFlow13 dump-flows br-int table=43 | \
 grep 'nat(src=20.0.0.2)'])
 
 check ovs-appctl dpctl/flush-conntrack
+exp_ct1="tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
+tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)"
+exp_ct2="tcp,orig=(src=172.16.1.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=172.16.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)"
+
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget 30.0.0.2:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget 30.0.0.2:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-])
+ct1=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
+ct2=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0.2) | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
 
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0.2) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=172.16.1.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
-tcp,orig=(src=172.16.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+test "x$ct1 = x$exp_ct1" && test "x$ct2 = x$exp_ct2"
+], [0], [dnl
 ])
 
 OVS_WAIT_UNTIL([check_est_flows], [check established flows])
@@ -2298,22 +2292,21 @@ rm -f wget*.log
 
 check ovs-appctl dpctl/flush-conntrack
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget 30.0.0.2:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+exp_ct1="tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
+tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)"
+exp_ct2="tcp,orig=(src=172.16.1.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=172.16.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)"
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget 30.0.0.2:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-tcp,orig=(src=172.16.1.2,dst=30.0.0.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-])
-
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0.2) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=172.16.1.2,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
-tcp,orig=(src=172.16.1.2,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+ct1=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.2) | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
+ct2=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0.2) | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
+test "x$ct1 = x$exp_ct1" && test "x$ct2 = x$exp_ct2"
+], [0], [dnl
 ])
 
 OVS_WAIT_UNTIL([check_est_flows], [check established flows])
@@ -2549,26 +2542,26 @@ OVS_START_L7([foo1], [http6])
 OVS_START_L7([bar1], [http6])
 
 dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget http://[[fd30::1]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget http://[[fd30::1]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd30::1) | grep -v fe80 |
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd30::1) | grep -v fe80 |
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd72::2,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd72::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd72::2,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd72::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 ])
 
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget http://[[fd30::2]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget http://[[fd30::2]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd30::2) | grep -v fe80 |
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd30::2) | grep -v fe80 |
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd72::2,dst=fd30::2,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd72::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd72::2,dst=fd30::2,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd72::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
@@ -2727,24 +2720,24 @@ OVS_START_L7([foo1], [http])
 OVS_START_L7([bar1], [http])
 
 dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget 30.0.0.1 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+exp_ct1="tcp,orig=(src=172.16.1.3,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
+tcp,orig=(src=172.16.1.3,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)"
+exp_ct2="tcp,orig=(src=172.16.1.3,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=172.16.1.3,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)"
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget 30.0.0.1 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=172.16.1.3,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-tcp,orig=(src=172.16.1.3,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-])
-
+ct1=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
 dnl Force SNAT should have worked.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=172.16.1.3,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
-tcp,orig=(src=172.16.1.3,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+ct2=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0) | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
+test "x$ct1 = x$exp_ct1" && test "x$ct2 = x$exp_ct2"
+], [0], [dnl
 ])
+
 OVS_APP_EXIT_AND_WAIT([ovn-controller])
 
 as ovn-sb
@@ -2900,24 +2893,24 @@ OVS_START_L7([foo1], [http6])
 OVS_START_L7([bar1], [http6])
 
 dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget http://[[fd30::1]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+exp_ct1="tcp,orig=(src=fd72::3,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd72::3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
+tcp,orig=(src=fd72::3,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd72::3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
+exp_ct2=tcp,orig=(src=fd72::3,dst=fd11::2,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd20::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=fd72::3,dst=fd12::2,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd20::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)"
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget http://[[fd30::1]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd30::1) | grep -v fe80 |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=fd72::3,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd72::3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-tcp,orig=(src=fd72::3,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd72::3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-])
-
+ct1=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd30::1) | grep -v fe80 | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
 dnl Force SNAT should have worked.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd20::2) | grep -v fe80 |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=fd72::3,dst=fd11::2,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd20::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
-tcp,orig=(src=fd72::3,dst=fd12::2,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd20::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+ct2=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd20::2) | grep -v fe80 | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
+test "x$ct1 = x$exp_ct1" && test "x$ct2 = x$exp_ct2"
+], [0], [dnl
 ])
+
 OVS_APP_EXIT_AND_WAIT([ovn-controller])
 
 as ovn-sb
@@ -3111,39 +3104,32 @@ OVS_START_L7([foo16], [http6])
 OVS_START_L7([bar16], [http6])
 
 dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget 30.0.0.1 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
-done
-
-for i in `seq 1 20`; do
-    echo Request ${i}_6
-    NS_CHECK_EXEC([alice16], [wget http://[[fd30::1]] -t 5 -T 1 --retry-connrefused -v -o wget${i}_6.log])
+exp_ct1="tcp,orig=(src=172.16.1.3,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
+tcp,orig=(src=172.16.1.3,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)"
+exp_ct2="tcp,orig=(src=fd72::3,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd72::3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
+tcp,orig=(src=fd72::3,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd72::3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)"
+exp_ct3="tcp,orig=(src=172.16.1.3,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=172.16.1.3,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)"
+exp_ct4="tcp,orig=(src=fd72::3,dst=fd11::2,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd20::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+tcp,orig=(src=fd72::3,dst=fd12::2,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd20::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)"
+
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget 30.0.0.1 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+    NS_EXEC([alice16], [wget http://[[fd30::1]] -t 5 -T 1 --retry-connrefused -v -o wget${i}_6.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=172.16.1.3,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-tcp,orig=(src=172.16.1.3,dst=30.0.0.1,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-])
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd30::1) | grep -v fe80 |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=fd72::3,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd72::3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-tcp,orig=(src=fd72::3,dst=fd30::1,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd72::3,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=10,protoinfo=(state=<cleared>)
-])
+ct1=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(30.0.0.1) | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
+ct2=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd30::1) | grep -v fe80 | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
 
 dnl Force SNAT should have worked.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0) |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=172.16.1.3,dst=192.168.1.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
-tcp,orig=(src=172.16.1.3,dst=192.168.2.2,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=20.0.0.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
-])
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd20::2) | grep -v fe80 |
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
-tcp,orig=(src=fd72::3,dst=fd11::2,sport=<cleared>,dport=<cleared>),reply=(src=fd11::2,dst=fd20::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
-tcp,orig=(src=fd72::3,dst=fd12::2,sport=<cleared>,dport=<cleared>),reply=(src=fd12::2,dst=fd20::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,protoinfo=(state=<cleared>)
+ct3=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(20.0.0) | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
+ct4=$(ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd20::2) | grep -v fe80 | sed -e 's/zone=[[0-9]]*/zone=<cleared>/')
+test "x$ct1 = x$exp_ct1" && test "x$ct2 = x$exp_ct2" && test "x$ct3 = x$exp_ct3" && test "x$ct4 = x$exp_ct4"
+], [0], [dnl
 ])
+
 OVS_APP_EXIT_AND_WAIT([ovn-controller])
 
 as ovn-sb
@@ -3262,26 +3248,26 @@ OVS_START_L7([foo1], [http])
 OVS_START_L7([bar1], [http])
 
 dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget 172.16.1.10 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget 172.16.1.10 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.10) |
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.10) |
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=172.16.1.2,dst=172.16.1.10,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=172.16.1.2,dst=172.16.1.10,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 ])
 
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget 172.16.1.11:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget 172.16.1.11:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.11) |
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(172.16.1.11) |
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=172.16.1.2,dst=172.16.1.11,sport=<cleared>,dport=<cleared>),reply=(src=192.168.1.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=172.16.1.2,dst=172.16.1.11,sport=<cleared>,dport=<cleared>),reply=(src=192.168.2.2,dst=172.16.1.2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
@@ -3405,26 +3391,26 @@ OVS_START_L7([foo1], [http6])
 OVS_START_L7([bar1], [http6])
 
 dnl Should work with the virtual IP address through NAT
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget http://[[fd72::10]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget http://[[fd72::10]] -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd72::10) | grep -v fe80 |
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd72::10) | grep -v fe80 |
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd72::2,dst=fd72::10,sport=<cleared>,dport=<cleared>),reply=(src=fd01::2,dst=fd72::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd72::2,dst=fd72::10,sport=<cleared>,dport=<cleared>),reply=(src=fd02::2,dst=fd72::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 ])
 
 dnl Test load-balancing that includes L4 ports in NAT.
-for i in `seq 1 20`; do
-    echo Request $i
-    NS_CHECK_EXEC([alice1], [wget http://[[fd72::11]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+OVS_WAIT_FOR_OUTPUT_UNQUOTED([
+for i in `seq 1 10`; do
+    NS_EXEC([alice1], [wget http://[[fd72::11]]:8000 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
 done
 
 dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd72::11) | grep -v fe80 |
+ovs-appctl dpctl/dump-conntrack | FORMAT_CT(fd72::11) | grep -v fe80 |
 sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=fd72::2,dst=fd72::11,sport=<cleared>,dport=<cleared>),reply=(src=fd01::2,dst=fd72::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=fd72::2,dst=fd72::11,sport=<cleared>,dport=<cleared>),reply=(src=fd02::2,dst=fd72::2,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
@@ -3598,8 +3584,8 @@ icmp,orig=(src=192.168.2.2,dst=172.16.1.2,id=<cleared>,type=8,code=0),reply=(src
 ])
 
 # Try to ping external network
-NS_CHECK_EXEC([ext-net], [tcpdump -n -c 3 -i ext-veth dst 172.16.1.3 and icmp > ext-net.pcap &])
-sleep 1
+NS_CHECK_EXEC([ext-net], [tcpdump -l -n -c 3 -i ext-veth dst 172.16.1.3 and icmp > ext-net.pcap 2>tcpdump_err &])
+OVS_WAIT_UNTIL([grep "listening" tcpdump_err])
 AT_CHECK([ovn-nbctl lr-nat-del R1 snat])
 NS_CHECK_EXEC([foo1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.1 | FORMAT_PING], \
 [0], [dnl
@@ -4507,17 +4493,15 @@ OVS_WAIT_UNTIL(
     [ovn-sbctl dump-flows sw0 | grep ct_lb_mark | grep priority=120 | grep "ip4.dst == 10.0.0.10" > lflows.txt
      test 1 = `cat lflows.txt | grep "ct_lb_mark(backends=10.0.0.3:80,20.0.0.3:80)" | wc -l`]
 )
-
 # From sw0-p2 send traffic to vip - 10.0.0.10
-for i in `seq 1 20`; do
-    echo Request $i
-    ovn-sbctl list service_monitor
-    NS_CHECK_EXEC([sw0-p2], [wget 10.0.0.10 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
-done
+#dnl Each server should have at least one connection.
+OVS_WAIT_FOR_OUTPUT([
+    for i in `seq 1 10`; do
+        NS_EXEC([sw0-p2], [wget 10.0.0.10 -t 5 -T 1 --retry-connrefused -v -o wget$i.log])
+    done
 
-dnl Each server should have at least one connection.
-AT_CHECK([ovs-appctl dpctl/dump-conntrack | FORMAT_CT(10.0.0.10) | \
-sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
+    ovs-appctl dpctl/dump-conntrack | FORMAT_CT(10.0.0.10) | \
+      sed -e 's/zone=[[0-9]]*/zone=<cleared>/'], [0], [dnl
 tcp,orig=(src=10.0.0.4,dst=10.0.0.10,sport=<cleared>,dport=<cleared>),reply=(src=10.0.0.3,dst=10.0.0.4,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 tcp,orig=(src=10.0.0.4,dst=10.0.0.10,sport=<cleared>,dport=<cleared>),reply=(src=20.0.0.3,dst=10.0.0.4,sport=<cleared>,dport=<cleared>),zone=<cleared>,mark=2,protoinfo=(state=<cleared>)
 ])
@@ -4649,10 +4633,12 @@ ovn-nbctl lb-add lb-ipv4-tcp     88.88.88.88:8080 42.42.42.1:4041 tcp
 ovn-nbctl lb-add lb-ipv4-tcp-dup 88.88.88.89:8080 42.42.42.1:4041 tcp
 ovn-nbctl lb-add lb-ipv4-udp     88.88.88.88:4040 42.42.42.1:2021 udp
 ovn-nbctl lb-add lb-ipv4-udp-dup 88.88.88.89:4040 42.42.42.1:2021 udp
+ovn-nbctl lb-add lb-ipv4 88.88.88.90 42.42.42.1
 ovn-nbctl ls-lb-add sw lb-ipv4-tcp
 ovn-nbctl ls-lb-add sw lb-ipv4-tcp-dup
 ovn-nbctl ls-lb-add sw lb-ipv4-udp
 ovn-nbctl ls-lb-add sw lb-ipv4-udp-dup
+ovn-nbctl ls-lb-add sw lb-ipv4
 
 ovn-nbctl lr-add rtr
 ovn-nbctl lrp-add rtr rtr-sw 00:00:00:00:01:00 42.42.42.254/24
@@ -4668,28 +4654,39 @@ ADD_VETH(lsp, lsp, br-int, "42.42.42.1/24", "00:00:00:00:00:01", \
 ovn-nbctl --wait=hv -t 3 sync
 
 # Start IPv4 TCP server on lsp.
-NS_CHECK_EXEC([lsp], [timeout 2s nc -k -l 42.42.42.1 4041 &], [0])
+NETNS_DAEMONIZE([lsp], [nc -l -k 42.42.42.1 4041], [lsp0.pid])
 
 # Check that IPv4 TCP hairpin connection succeeds on both VIPs.
 NS_CHECK_EXEC([lsp], [nc 88.88.88.88 8080 -z], [0], [ignore], [ignore])
 NS_CHECK_EXEC([lsp], [nc 88.88.88.89 8080 -z], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc 88.88.88.90 4041 -z], [0], [ignore], [ignore])
 
 # Capture IPv4 UDP hairpinned packets.
 filter="dst 42.42.42.1 and dst port 2021 and udp"
-NS_CHECK_EXEC([lsp], [tcpdump -nn -c 2 -i lsp ${filter} > lsp.pcap &])
-
-sleep 1
+NS_CHECK_EXEC([lsp], [tcpdump -l -nn -c 3 -i lsp ${filter} > lsp.pcap 2>tcpdump_err &])
+OVS_WAIT_UNTIL([grep "listening" tcpdump_err])
 
 # Generate IPv4 UDP hairpin traffic.
 NS_CHECK_EXEC([lsp], [echo a | nc -u 88.88.88.88 4040 &], [0])
 NS_CHECK_EXEC([lsp], [echo a | nc -u 88.88.88.89 4040 &], [0])
+NS_CHECK_EXEC([lsp], [echo a | nc -u 88.88.88.90 2021 &], [0])
 
 # Check hairpin traffic.
 OVS_WAIT_UNTIL([
     total_pkts=$(cat lsp.pcap | wc -l)
-    test "${total_pkts}" = "2"
+    test "${total_pkts}" = "3"
 ])
 
+ovn-nbctl pg-add pg0 lsp
+ovn-nbctl --apply-after-lb acl-add pg0 from-lport 1004 "ip4 && ip4.dst == 10.0.0.2" drop
+ovn-nbctl --apply-after-lb acl-add pg0 from-lport 1002 "ip4 && tcp" allow-related
+ovn-nbctl --apply-after-lb acl-add pg0 from-lport 1002 "ip4 && udp" allow
+ovn-nbctl --wait=hv sync
+
+## Check that IPv4 TCP hairpin connection succeeds on both VIPs.
+NS_CHECK_EXEC([lsp], [nc 88.88.88.88 8080 -z], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc 88.88.88.89 8080 -z], [0], [ignore], [ignore])
+
 OVS_APP_EXIT_AND_WAIT([ovn-controller])
 
 as ovn-sb
@@ -4736,10 +4733,12 @@ ovn-nbctl lb-add lb-ipv6-tcp     [[8800::0088]]:8080 [[4200::1]]:4041 tcp
 ovn-nbctl lb-add lb-ipv6-tcp-dup [[8800::0089]]:8080 [[4200::1]]:4041 tcp
 ovn-nbctl lb-add lb-ipv6-udp     [[8800::0088]]:4040 [[4200::1]]:2021 udp
 ovn-nbctl lb-add lb-ipv6-udp-dup [[8800::0089]]:4040 [[4200::1]]:2021 udp
+ovn-nbctl lb-add lb-ipv6 8800::0090 4200::1
 ovn-nbctl ls-lb-add sw lb-ipv6-tcp
 ovn-nbctl ls-lb-add sw lb-ipv6-tcp-dup
 ovn-nbctl ls-lb-add sw lb-ipv6-udp
 ovn-nbctl ls-lb-add sw lb-ipv6-udp-dup
+ovn-nbctl ls-lb-add sw lb-ipv6
 
 ovn-nbctl lr-add rtr
 ovn-nbctl lrp-add rtr rtr-sw 00:00:00:00:01:00 4200::00ff/64
@@ -4754,28 +4753,39 @@ OVS_WAIT_UNTIL([test "$(ip netns exec lsp ip a | grep 4200::1 | grep tentative)"
 ovn-nbctl --wait=hv -t 3 sync
 
 # Start IPv6 TCP server on lsp.
-NS_CHECK_EXEC([lsp], [timeout 2s nc -k -l 4200::1 4041 &], [0])
+NETNS_DAEMONIZE([lsp], [nc -l -k 4200::1 4041], [lsp0.pid])
 
 # Check that IPv6 TCP hairpin connection succeeds on both VIPs.
 NS_CHECK_EXEC([lsp], [nc 8800::0088 8080 -z], [0], [ignore], [ignore])
 NS_CHECK_EXEC([lsp], [nc 8800::0089 8080 -z], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc 8800::0090 4041 -z], [0], [ignore], [ignore])
 
 # Capture IPv6 UDP hairpinned packets.
 filter="dst 4200::1 and dst port 2021 and udp"
-NS_CHECK_EXEC([lsp], [tcpdump -nn -c 2 -i lsp $filter > lsp.pcap &])
-
-sleep 1
+NS_CHECK_EXEC([lsp], [tcpdump -l -nn -c 3 -i lsp $filter > lsp.pcap 2>tcpdump_err &])
+OVS_WAIT_UNTIL([grep "listening" tcpdump_err])
 
 # Generate IPv6 UDP hairpin traffic.
 NS_CHECK_EXEC([lsp], [echo a | nc -u 8800::0088 4040 &], [0])
 NS_CHECK_EXEC([lsp], [echo a | nc -u 8800::0089 4040 &], [0])
+NS_CHECK_EXEC([lsp], [echo a | nc -u 8800::0090 2021 &], [0])
 
 # Check hairpin traffic.
 OVS_WAIT_UNTIL([
     total_pkts=$(cat lsp.pcap | wc -l)
-    test "${total_pkts}" = "2"
+    test "${total_pkts}" = "3"
 ])
 
+ovn-nbctl pg-add pg0 lsp
+ovn-nbctl --apply-after-lb acl-add pg0 from-lport 1002 "ip6 && tcp" allow-related
+ovn-nbctl --apply-after-lb acl-add pg0 from-lport 1002 "ip6 && udp" allow
+ovn-nbctl --apply-after-lb acl-add pg0 from-lport 1000 "ip6" drop
+ovn-nbctl --wait=hv sync
+
+# Check that IPv6 TCP hairpin connection succeeds on both VIPs.
+NS_CHECK_EXEC([lsp], [nc 8800::0088 8080 -z], [0], [ignore], [ignore])
+NS_CHECK_EXEC([lsp], [nc 8800::0089 8080 -z], [0], [ignore], [ignore])
+
 OVS_APP_EXIT_AND_WAIT([ovn-controller])
 
 as ovn-sb
@@ -4938,7 +4948,7 @@ OVS_WAIT_UNTIL([
     ip netns exec sw0-p1-rej nc -u 10.0.0.4 90 < foo
     c=$(cat sw0-p1-rej-icmp.pcap | grep \
 "10.0.0.4 > 10.0.0.3: ICMP 10.0.0.4 udp port 90 unreachable" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 rm -f *.pcap
@@ -4950,7 +4960,7 @@ OVS_WAIT_UNTIL([
     ip netns exec sw0-p1-rej nc -u 10.0.0.4 94 < foo
     c=$(cat sw0-p1-rej-icmp.pcap | grep \
 "10.0.0.4 > 10.0.0.3: ICMP 10.0.0.4 udp port 94 unreachable" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 # Now test for IPv6 UDP.
@@ -4962,7 +4972,7 @@ OVS_WAIT_UNTIL([
     c=$(cat sw0-p2-rej-icmp6.pcap | grep \
 "IP6 aef0::3 > aef0::4: ICMP6, destination unreachable, unreachable port, \
 aef0::3 udp port 90" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 rm -f *.pcap
@@ -4975,7 +4985,7 @@ OVS_WAIT_UNTIL([
     c=$(cat sw0-p2-rej-icmp6.pcap | grep \
 "IP6 aef0::3 > aef0::4: ICMP6, destination unreachable, unreachable port, \
 aef0::3 udp port 94" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 # Delete all the ACLs of pg0 and add the ACL with a generic match with reject action.
@@ -5000,7 +5010,7 @@ OVS_WAIT_UNTIL([
     ip netns exec sw0-p1-rej nc -u 10.0.0.4 90 < foo
     c=$(cat sw0-p1-rej-icmp.pcap | grep \
 "10.0.0.4 > 10.0.0.3: ICMP 10.0.0.4 udp port 90 unreachable" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 rm -f *.pcap
@@ -5012,7 +5022,7 @@ OVS_WAIT_UNTIL([
     c=$(cat sw0-p2-rej-icmp6.pcap | grep \
 "IP6 aef0::3 > aef0::4: ICMP6, destination unreachable, unreachable port, \
 aef0::3 udp port 90" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 
@@ -5179,7 +5189,7 @@ OVS_WAIT_UNTIL([
     ip netns exec sw0-p1-rej nc -u 10.0.0.4 90 < foo
     c=$(cat sw0-p1-rej-icmp.pcap | grep \
 "10.0.0.4 > 10.0.0.3: ICMP 10.0.0.4 udp port 90 unreachable" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 rm -f *.pcap
@@ -5191,7 +5201,7 @@ OVS_WAIT_UNTIL([
     ip netns exec sw0-p1-rej nc -u 10.0.0.4 94 < foo
     c=$(cat sw0-p1-rej-icmp.pcap | grep \
 "10.0.0.4 > 10.0.0.3: ICMP 10.0.0.4 udp port 94 unreachable" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 # Now test for IPv6 UDP.
@@ -5203,7 +5213,7 @@ OVS_WAIT_UNTIL([
     c=$(cat sw0-p2-rej-icmp6.pcap | grep \
 "IP6 aef0::3 > aef0::4: ICMP6, destination unreachable, unreachable port, \
 aef0::3 udp port 90" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 rm -f *.pcap
@@ -5216,7 +5226,7 @@ OVS_WAIT_UNTIL([
     c=$(cat sw0-p2-rej-icmp6.pcap | grep \
 "IP6 aef0::3 > aef0::4: ICMP6, destination unreachable, unreachable port, \
 aef0::3 udp port 94" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 # Delete all the ACLs of pg0 and add the ACL with a generic match with reject action.
@@ -5241,7 +5251,7 @@ OVS_WAIT_UNTIL([
     ip netns exec sw0-p1-rej nc -u 10.0.0.4 90 < foo
     c=$(cat sw0-p1-rej-icmp.pcap | grep \
 "10.0.0.4 > 10.0.0.3: ICMP 10.0.0.4 udp port 90 unreachable" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 rm -f *.pcap
@@ -5253,7 +5263,7 @@ OVS_WAIT_UNTIL([
     c=$(cat sw0-p2-rej-icmp6.pcap | grep \
 "IP6 aef0::3 > aef0::4: ICMP6, destination unreachable, unreachable port, \
 aef0::3 udp port 90" | uniq | wc -l)
-    test $c -eq 1
+    test $c -ge 1
 ])
 
 
@@ -9277,13 +9287,15 @@ test_related_traffic() {
 
     check ovs-appctl dpctl/flush-conntrack
 
-    NETNS_DAEMONIZE([client], [tcpdump -U -i client -w client.pcap], [tcpdump0.pid])
-    NETNS_DAEMONIZE([server], [tcpdump -U -i server -w server.pcap], [tcpdump1.pid])
+    NETNS_DAEMONIZE([client], [tcpdump -l -U -i client -w client.pcap 2>client_err], [tcpdump0.pid])
+    NETNS_DAEMONIZE([server], [tcpdump -l -U -i server -w server.pcap 2>server_err], [tcpdump1.pid])
 
     # Setup a dummy UDP listeners so we don't get "port unreachable".
     NETNS_DAEMONIZE([client], [nc -l -u 1], [nc0.pid])
     NETNS_DAEMONIZE([server], [nc -l -u 2], [nc1.pid])
-    sleep 1
+
+    OVS_WAIT_UNTIL([grep "listening" client_err])
+    OVS_WAIT_UNTIL([grep "listening" server_err])
 
     # Send UDP client -> server
     check ovs-ofctl packet-out br-int "in_port=ovs-client,packet=$client_udp,actions=resubmit(,0)"
@@ -9479,7 +9491,8 @@ name: 'vport' value: '666'
 # Start IPv4 TCP server on vm1.
 NETNS_DAEMONIZE([vm1], [nc -k -l 42.42.42.2 4242], [nc-vm1.pid])
 
-# Make sure connecting to the VIP works.
+# Make sure connecting to the VIP works (hairpin, via ls and via lr).
+NS_CHECK_EXEC([vm1], [nc 66.66.66.66 666 -z], [0], [ignore], [ignore])
 NS_CHECK_EXEC([vm2], [nc 66.66.66.66 666 -z], [0], [ignore], [ignore])
 NS_CHECK_EXEC([vm3], [nc 66.66.66.66 666 -z], [0], [ignore], [ignore])
 
@@ -9572,9 +9585,263 @@ name: 'vport' value: '666'
 # Start IPv6 TCP server on vm1.
 NETNS_DAEMONIZE([vm1], [nc -k -l 4242::2 4242], [nc-vm1.pid])
 
-# Make sure connecting to the VIP works.
+# Make sure connecting to the VIP works (hairpin, via ls and via lr).
+NS_CHECK_EXEC([vm1], [nc 6666::1 666 -z], [0], [ignore], [ignore])
 NS_CHECK_EXEC([vm2], [nc 6666::1 666 -z], [0], [ignore], [ignore])
 NS_CHECK_EXEC([vm3], [nc 6666::1 666 -z], [0], [ignore], [ignore])
 
 AT_CLEANUP
 ])
+
+###########################################################
+## ls1 -- cluster-router -- join - gr1 -- public1 -- ln1 ##
+###########################################################
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([Gateway router with dynamic_neigh_routers])
+
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+ADD_BR([br-ex], [set Bridge br-ex fail-mode=standalone])
+
+check ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=provider:br-ex
+
+# Set external-ids in br-int needed for ovn-controller
+ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+# Start ovn-controller
+start_daemon ovn-controller
+
+# Add routers
+check ovn-nbctl lr-add gr1
+check ovn-nbctl lr-add cluster-router
+
+# Add switches
+check ovn-nbctl ls-add join
+check ovn-nbctl ls-add public1
+check ovn-nbctl ls-add ls1
+
+# Add ls1 ports
+check ovn-nbctl lsp-add ls1 ls1p1 \
+    -- lsp-set-addresses ls1p1 "00:00:00:00:01:11 10.244.2.11"
+
+check ovn-nbctl lsp-add ls1 ls1-to-cluster-router \
+    -- lsp-set-type ls1-to-cluster-router router \
+    -- lsp-set-options ls1-to-cluster-router router-port=cluster-router-to-ls1 \
+    -- lsp-set-addresses ls1-to-cluster-router router
+
+# Add cluster-router ports
+check ovn-nbctl lrp-add cluster-router cluster-router-to-ls1 "00:00:00:0f:01:01" 10.244.2.1/24 \
+    -- lrp-add cluster-router cluster-router-to-join "00:00:00:0f:02:01" 100.64.0.1/16 \
+    -- lrp-set-gateway-chassis cluster-router-to-ls1 hv1 10 \
+    -- --policy=src-ip lr-route-add cluster-router 10.244.2.0/24 100.64.0.3
+
+# Add join ports
+check ovn-nbctl lsp-add join join-to-cluster-router \
+    -- lsp-set-type join-to-cluster-router router \
+    -- lsp-set-options join-to-cluster-router router-port=cluster-router-to-join \
+    -- lsp-set-addresses join-to-cluster-router router \
+    -- lsp-add join join-to-gr1 \
+    -- lsp-set-type join-to-gr1 router \
+    -- lsp-set-options join-to-gr1 router-port=gr1-to-join \
+    -- lsp-set-addresses join-to-gr1 router
+
+check ovn-nbctl set logical_router gr1 options:lb_force_snat_ip=router_ip \
+    -- set logical_router gr1 options:snat-ct-zone=0 \
+    -- set logical_router gr1 options:dynamic_neigh_routers=true
+
+# Add gr1 ports and set natting
+check ovn-nbctl lrp-add gr1 gr1-to-join "00:00:00:0f:02:03" 100.64.0.3/16 \
+    -- lr-route-add gr1 10.244.0.0/16 100.64.0.1 \
+    -- lr-nat-add gr1 snat 10.89.189.12 10.244.0.0/16 \
+    -- lrp-add gr1 gr1-to-public1 "0a:0a:b6:fc:03:12" 10.89.189.12/24 \
+    -- set logical_router gr1 options:chassis=hv1
+
+# Add public1 ports
+check ovn-nbctl lsp-add public1 public1-to-gr1 \
+    -- lsp-set-type public1-to-gr1 router \
+    -- lsp-set-options public1-to-gr1 router-port=gr1-to-public1 \
+    -- lsp-set-addresses public1-to-gr1 router \
+    -- lsp-add public1 ln1 \
+    -- lsp-set-type ln1 localnet \
+    -- lsp-set-options ln1 network_name=provider \
+    -- lsp-set-addresses ln1 unknown
+
+check ovn-nbctl --wait=hv sync
+
+ADD_NAMESPACES(ns_ls1p1)
+ADD_VETH(ls1p1, ns_ls1p1, br-int, "10.244.2.11/24", "00:00:00:00:01:11", "10.244.2.1")
+
+ADD_NAMESPACES(ns_ext1)
+ADD_VETH(ln1, ns_ext1, br-ex, "10.89.189.1/24", "0a:0a:b6:fc:03:01")
+
+NS_CHECK_EXEC([ns_ls1p1], [ping -q -c 3 -i 0.3 -w 2 10.89.189.1 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+
+as ovn-sb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-nb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as northd
+OVS_APP_EXIT_AND_WAIT([NORTHD_TYPE])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ACL default_acl_drop])
+AT_KEYWORDS([acl default_acl_drop])
+
+CHECK_CONNTRACK()
+ovn_start
+
+OVS_TRAFFIC_VSWITCHD_START()
+ADD_BR([br-int])
+
+# Set external-ids in br-int needed for ovn-controller
+ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+# Start ovn-controller
+start_daemon ovn-controller
+
+ovn-nbctl ls-add sw
+
+# Logical port 'vm1' in switch 'sw'.
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "10.0.0.1/24", "f0:00:00:01:02:03", \
+         "10.0.0.254")
+check ovn-nbctl lsp-add sw vm1 \
+-- lsp-set-addresses vm1 "f0:00:00:01:02:03 10.0.0.1"
+
+# Logical port 'vm2' in switch 'sw'.
+ADD_NAMESPACES(vm2)
+ADD_VETH(vm2, vm2, br-int, "10.0.0.2/24", "f0:00:00:01:02:05", \
+"10.0.0.254")
+check ovn-nbctl lsp-add sw vm2 \
+-- lsp-set-addresses vm2 "f0:00:00:01:02:05 10.0.0.2"
+
+# Wait for ovn-controller to catch up.
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+AS_BOX([from-lport acl, default_acl_drop false])
+check ovn-nbctl acl-del sw
+check ovn-nbctl set NB_Global . options:default_acl_drop=false \
+    -- acl-add sw from-lport 20 "ip4 && icmp" allow-related \
+    -- acl-add sw from-lport 10 "ip4" drop
+check ovn-nbctl --wait=hv sync
+
+# 'vm1' should be able to ping 'vm2' directly.
+NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.2 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+AS_BOX([from-lport acl, default_acl_drop true])
+check ovn-nbctl acl-del sw
+check ovn-nbctl set NB_Global . options:default_acl_drop=true \
+    -- acl-add sw from-lport 20 "ip4 && icmp" allow-related \
+    -- acl-add sw from-lport 10 "arp" allow \
+    -- --apply-after-lb acl-add sw from-lport 1 1 allow \
+    -- acl-add sw to-lport 1 1 allow
+check ovn-nbctl --wait=hv sync
+
+# 'vm1' should be able to ping 'vm2' directly.
+NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.2 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+AS_BOX([from-lport acl, after LB, default_acl_drop false])
+check ovn-nbctl acl-del sw
+check ovn-nbctl set NB_Global . options:default_acl_drop=false \
+    -- --apply-after-lb acl-add sw from-lport 20 "ip4 && icmp" allow-related \
+    -- --apply-after-lb acl-add sw from-lport 10 "ip4" drop
+check ovn-nbctl --wait=hv sync
+
+# 'vm1' should be able to ping 'vm2' directly.
+NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.2 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+AS_BOX([from-lport acl, after LB, default_acl_drop true])
+check ovn-nbctl acl-del sw
+check ovn-nbctl set NB_Global . options:default_acl_drop=true \
+    -- acl-add sw from-lport 1 1 allow \
+    -- --apply-after-lb acl-add sw from-lport 20 "ip4 && icmp" allow-related \
+    -- --apply-after-lb acl-add sw from-lport 20 "arp" allow-related \
+    -- acl-add sw to-lport 1 1 allow
+check ovn-nbctl --wait=hv sync
+
+# 'vm1' should be able to ping 'vm2' directly.
+NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.2 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+AS_BOX([to-lport acl, default_acl_drop false])
+check ovn-nbctl acl-del sw
+check ovn-nbctl set NB_Global . options:default_acl_drop=false \
+    -- acl-add sw to-lport 20 "ip4 && icmp" allow-related \
+    -- acl-add sw to-lport 10 "ip4" drop
+check ovn-nbctl --wait=hv sync
+
+# 'vm1' should be able to ping 'vm2' directly.
+NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.2 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+AS_BOX([to-lport acl, default_acl_drop true])
+check ovn-nbctl acl-del sw
+check ovn-nbctl set NB_Global . options:default_acl_drop=true \
+    -- acl-add sw from-lport 1 1 allow \
+    -- --apply-after-lb acl-add sw from-lport 1 1 allow \
+    -- acl-add sw to-lport 20 "ip4 && icmp" allow-related \
+    -- acl-add sw to-lport 20 "arp" allow
+check ovn-nbctl --wait=hv sync
+
+# 'vm1' should be able to ping 'vm2' directly.
+NS_CHECK_EXEC([vm1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.2 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+
+as ovn-sb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-nb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as northd
+OVS_APP_EXIT_AND_WAIT([NORTHD_TYPE])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
+/connection dropped.*/d"])
+AT_CLEANUP
+])
diff --git a/utilities/ovn-dbctl.c b/utilities/ovn-dbctl.c
index a850c2f31..5edb82e7f 100644
--- a/utilities/ovn-dbctl.c
+++ b/utilities/ovn-dbctl.c
@@ -109,6 +109,15 @@ static void server_loop(const struct ovn_dbctl_options *dbctl_options,
                         struct ovsdb_idl *idl, int argc, char *argv[]);
 static void ovn_dbctl_exit(int status);
 
+static void
+destroy_argv(int argc, char **argv)
+{
+    for (int i = 0; i < argc; i++) {
+        free(argv[i]);
+    }
+    free(argv);
+}
+
 int
 ovn_dbctl_main(int argc, char *argv[],
                const struct ovn_dbctl_options *dbctl_options)
@@ -151,6 +160,7 @@ ovn_dbctl_main(int argc, char *argv[],
     char *error_s = ovs_cmdl_parse_all(argc, argv_, get_all_options(),
                                        &parsed_options, &n_parsed_options);
     if (error_s) {
+        destroy_argv(argc, argv_);
         ctl_fatal("%s", error_s);
     }
 
@@ -179,6 +189,7 @@ ovn_dbctl_main(int argc, char *argv[],
     bool daemon_mode = false;
     if (get_detach()) {
         if (argc != optind) {
+            destroy_argv(argc, argv_);
             ctl_fatal("non-option arguments not supported with --detach "
                       "(use --help for help)");
         }
@@ -204,11 +215,8 @@ ovn_dbctl_main(int argc, char *argv[],
         if (error) {
             ovsdb_idl_destroy(idl);
             idl = the_idl = NULL;
+            destroy_argv(argc, argv_);
 
-            for (int i = 0; i < argc; i++) {
-                free(argv_[i]);
-            }
-            free(argv_);
             ctl_fatal("%s", error);
         }
 
@@ -237,21 +245,15 @@ cleanup:
         }
         free(commands);
         if (error) {
-            for (int i = 0; i < argc; i++) {
-                free(argv_[i]);
-            }
-            free(argv_);
+            destroy_argv(argc, argv_);
             ctl_fatal("%s", error);
         }
     }
 
     ovsdb_idl_destroy(idl);
     idl = the_idl = NULL;
+    destroy_argv(argc, argv_);
 
-    for (int i = 0; i < argc; i++) {
-        free(argv_[i]);
-    }
-    free(argv_);
     exit(EXIT_SUCCESS);
 }
 
@@ -1238,40 +1240,53 @@ dbctl_client(const struct ovn_dbctl_options *dbctl_options,
 
     ctl_timeout_setup(timeout);
 
+    char *cmd_result = NULL;
+    char *cmd_error = NULL;
     struct jsonrpc *client;
+    int exit_status;
+    char *error_str;
+
     int error = unixctl_client_create(socket_name, &client);
     if (error) {
-        ctl_fatal("%s: could not connect to %s daemon (%s); "
-                  "unset %s to avoid using daemon",
-                  socket_name, program_name, ovs_strerror(error),
-                  dbctl_options->daemon_env_var_name);
+        error_str = xasprintf("%s: could not connect to %s daemon (%s); "
+                              "unset %s to avoid using daemon",
+                              socket_name, program_name, ovs_strerror(error),
+                              dbctl_options->daemon_env_var_name);
+        goto log_error;
     }
 
-    char *cmd_result;
-    char *cmd_error;
     error = unixctl_client_transact(client, "run",
                                     args.n, args.names,
                                     &cmd_result, &cmd_error);
     if (error) {
-        ctl_fatal("%s: transaction error (%s)",
-                  socket_name, ovs_strerror(error));
+        error_str = xasprintf("%s: transaction error (%s)",
+                              socket_name, ovs_strerror(error));
+        goto log_error;
     }
-    svec_destroy(&args);
 
-    int exit_status;
     if (cmd_error) {
-        exit_status = EXIT_FAILURE;
         fprintf(stderr, "%s: %s", program_name, cmd_error);
-    } else {
-        exit_status = EXIT_SUCCESS;
-        fputs(cmd_result, stdout);
+        goto error;
     }
+
+    exit_status = EXIT_SUCCESS;
+    fputs(cmd_result, stdout);
+    goto cleanup;
+
+log_error:
+    VLOG_ERR("%s", error_str);
+    ovs_error(0, "%s", error_str);
+    free(error_str);
+
+error:
+    exit_status = EXIT_FAILURE;
+
+cleanup:
     free(cmd_result);
     free(cmd_error);
     jsonrpc_close(client);
-    for (int i = 0; i < argc; i++) {
-        free(argv[i]);
-    }
-    free(argv);
+    svec_destroy(&args);
+    destroy_argv(argc, argv);
+
     exit(exit_status);
 }
diff --git a/utilities/ovn-nbctl.8.xml b/utilities/ovn-nbctl.8.xml
index 92e10c012..72d4088f0 100644
--- a/utilities/ovn-nbctl.8.xml
+++ b/utilities/ovn-nbctl.8.xml
@@ -814,7 +814,7 @@
         Attaches the mirror <var>m</var> to the logical port <var>port</var>.
       </dd>
 
-      <dt><code>lsp-dettach-mirror</code> <var>port</var> <var>m</var></dt>
+      <dt><code>lsp-detach-mirror</code> <var>port</var> <var>m</var></dt>
       <dd>
         Detaches the mirror <var>m</var> from the logical port <var>port</var>.
       </dd>
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index 07ebac5e5..e5766ed67 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -1486,9 +1486,8 @@ ovntrace_node_prune_hard(struct ovs_list *nodes)
 }
 
 static void
-execute_load(const struct ovnact_load *load,
-             const struct ovntrace_datapath *dp, struct flow *uflow,
-             struct ovs_list *super OVS_UNUSED)
+execute_load(const struct ovnact *ovnact, const struct ovntrace_datapath *dp,
+             struct flow *uflow, struct ovs_list *super OVS_UNUSED)
 {
     const struct ovnact_encode_params ep = {
         .lookup_port = ovntrace_lookup_port,
@@ -1498,7 +1497,7 @@ execute_load(const struct ovnact_load *load,
     uint64_t stub[512 / 8];
     struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(stub);
 
-    ovnacts_encode(&load->ovnact, sizeof *load, &ep, &ofpacts);
+    ovnacts_encode(ovnact, OVNACT_ALIGN(ovnact->len), &ep, &ofpacts);
 
     struct ofpact *a;
     OFPACT_FOR_EACH (a, ofpacts.data, ofpacts.size) {
@@ -1506,12 +1505,11 @@ execute_load(const struct ovnact_load *load,
 
         if (!mf_is_register(sf->field->id)) {
             struct ds s = DS_EMPTY_INITIALIZER;
-            ovnacts_format(&load->ovnact, OVNACT_LOAD_SIZE, &s);
-            ds_chomp(&s, ';');
 
-            char *friendly = ovntrace_make_names_friendly(ds_cstr(&s));
-            ovntrace_node_append(super, OVNTRACE_NODE_MODIFY, "%s", friendly);
-            free(friendly);
+            ovnacts_format(ovnact, OVNACT_ALIGN(ovnact->len), &s);
+            ds_chomp(&s, ';');
+            ovntrace_node_append(super, OVNTRACE_NODE_MODIFY, "%s",
+                                 ds_cstr(&s));
 
             ds_destroy(&s);
         }
@@ -3057,7 +3055,7 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
     const struct ovnact *a;
     OVNACT_FOR_EACH (a, ovnacts, ovnacts_len) {
         ds_clear(&s);
-        ovnacts_format(a, sizeof *a * (ovnact_next(a) - a), &s);
+        ovnacts_format(a, OVNACT_ALIGN(a->len), &s);
         char *friendly = ovntrace_make_names_friendly(ds_cstr(&s));
         ovntrace_node_append(super, OVNTRACE_NODE_ACTION, "%s", friendly);
         free(friendly);
@@ -3072,7 +3070,7 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             break;
 
         case OVNACT_LOAD:
-            execute_load(ovnact_get_LOAD(a), dp, uflow, super);
+            execute_load(a, dp, uflow, super);
             break;
 
         case OVNACT_MOVE: