Blob Blame History Raw
diff --git a/.ci/linux-prepare.sh b/.ci/linux-prepare.sh
index 0bb0ff096..83ad3958b 100755
--- a/.ci/linux-prepare.sh
+++ b/.ci/linux-prepare.sh
@@ -12,5 +12,5 @@ set -ev
 git clone git://git.kernel.org/pub/scm/devel/sparse/sparse.git
 cd sparse && make -j4 HAVE_LLVM= HAVE_SQLITE= install && cd ..
 
-pip install --disable-pip-version-check --user six flake8 hacking
-pip install --user --upgrade docutils
+pip3 install --disable-pip-version-check --user flake8 hacking sphinx pyOpenSSL
+pip3 install --upgrade --user docutils
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f3a53a8b6..91bd1e538 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,7 +13,6 @@ jobs:
       dependencies: |
         automake libtool gcc bc libjemalloc1 libjemalloc-dev    \
         libssl-dev llvm-dev libelf-dev libnuma-dev libpcap-dev  \
-        python3-openssl python3-pip python3-sphinx              \
         selinux-policy-dev
       m32_dependecies: gcc-multilib
       CC:          ${{ matrix.compiler }}
@@ -88,11 +87,21 @@ jobs:
       if:   matrix.m32 != ''
       run:  sudo apt install -y ${{ env.m32_dependecies }}
 
+    - name: update PATH
+      run:  |
+        echo "$HOME/bin"        >> $GITHUB_PATH
+        echo "$HOME/.local/bin" >> $GITHUB_PATH
+
+    - name: set up python
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.x'
+
     - name: prepare
       run:  ./.ci/linux-prepare.sh
 
     - name: build
-      run:  PATH="$PATH:$HOME/bin" ./.ci/linux-build.sh
+      run:  ./.ci/linux-build.sh
 
     - name: copy logs on failure
       if: failure() || cancelled()
@@ -145,10 +154,18 @@ jobs:
         ref: 'master'
     - name: install dependencies
       run:  brew install automake libtool
+    - name: update PATH
+      run:  |
+        echo "$HOME/bin"        >> $GITHUB_PATH
+        echo "$HOME/.local/bin" >> $GITHUB_PATH
+    - name: set up python
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.x'
     - name: prepare
       run:  ./.ci/osx-prepare.sh
     - name: build
-      run:  PATH="$PATH:$HOME/bin" ./.ci/osx-build.sh
+      run:  ./.ci/osx-build.sh
     - name: upload logs on failure
       if: failure()
       uses: actions/upload-artifact@v2
diff --git a/Makefile.am b/Makefile.am
index 80247b62d..1fe730dc4 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -221,6 +221,7 @@ dist-hook-git: distfiles
 	    grep -v '\.gitattributes$$' | \
 	    grep -v '\.gitmodules$$' | \
 	    grep -v "$(submodules)" | \
+		grep -v 'redhat' | \
 	    LC_ALL=C sort -u > all-gitfiles; \
 	  LC_ALL=C comm -1 -3 distfiles all-gitfiles > missing-distfiles; \
 	  if test -s missing-distfiles; then \
@@ -332,7 +333,7 @@ check-tabs:
 	@cd $(srcdir); \
 	if test -e .git && (git --version) >/dev/null 2>&1 && \
 	  grep -ln "^	" \
-	    `git ls-files | grep -v $(submodules) \
+	    `git ls-files | grep -v $(submodules) | grep -v redhat \
 	      | grep -v -f build-aux/initial-tab-whitelist` /dev/null \
 	      | $(EGREP) -v ':[ 	]*/?\*'; \
 	then \
diff --git a/NEWS b/NEWS
index 5372668bf..530c5d42f 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,13 @@
+Post-v21.03.0
+-------------------------
+  - ovn-northd-ddlog: New implementation of northd, based on DDlog.  This
+    implementation is incremental, meaning that it only recalculates what is
+    needed for the southbound database when northbound changes occur.  It is
+    expected to scale better than the C implementation, for large deployments.
+    (This may take testing and tuning to be effective.)  This version of OVN
+    requires DDLog 0.36.
+  - Introduce ovn-controller incremetal processing engine statistics
+
 OVN v21.03.0 - 12 Mar 2021
 -------------------------
   - Support ECMP multiple nexthops for reroute router policies.
diff --git a/configure.ac b/configure.ac
index 37b476d53..f3de6fef2 100644
--- a/configure.ac
+++ b/configure.ac
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 AC_PREREQ(2.63)
-AC_INIT(ovn, 21.03.0, bugs@openvswitch.org)
+AC_INIT(ovn, 21.03.1, bugs@openvswitch.org)
 AC_CONFIG_MACRO_DIR([m4])
 AC_CONFIG_AUX_DIR([build-aux])
 AC_CONFIG_HEADERS([config.h])
diff --git a/controller/automake.mk b/controller/automake.mk
index e664f1980..2f6c50890 100644
--- a/controller/automake.mk
+++ b/controller/automake.mk
@@ -10,6 +10,8 @@ controller_ovn_controller_SOURCES = \
 	controller/encaps.h \
 	controller/ha-chassis.c \
 	controller/ha-chassis.h \
+	controller/if-status.c \
+	controller/if-status.h \
 	controller/ip-mcast.c \
 	controller/ip-mcast.h \
 	controller/lflow.c \
diff --git a/controller/binding.c b/controller/binding.c
index 4e6c75696..31f3a210f 100644
--- a/controller/binding.c
+++ b/controller/binding.c
@@ -16,9 +16,9 @@
 #include <config.h>
 #include "binding.h"
 #include "ha-chassis.h"
+#include "if-status.h"
 #include "lflow.h"
 #include "lport.h"
-#include "ofctrl-seqno.h"
 #include "patch.h"
 
 #include "lib/bitmap.h"
@@ -41,32 +41,6 @@ VLOG_DEFINE_THIS_MODULE(binding);
  */
 #define OVN_INSTALLED_EXT_ID "ovn-installed"
 
-/* Set of OVS interface IDs that have been released in the most recent
- * processing iterations.  This gets updated in release_lport() and is
- * periodically emptied in binding_seqno_run().
- */
-static struct sset binding_iface_released_set =
-    SSET_INITIALIZER(&binding_iface_released_set);
-
-/* Set of OVS interface IDs that have been bound in the most recent
- * processing iterations.  This gets updated in release_lport() and is
- * periodically emptied in binding_seqno_run().
- */
-static struct sset binding_iface_bound_set =
-    SSET_INITIALIZER(&binding_iface_bound_set);
-
-static void
-binding_iface_released_add(const char *iface_id)
-{
-    sset_add(&binding_iface_released_set, iface_id);
-}
-
-static void
-binding_iface_bound_add(const char *iface_id)
-{
-    sset_add(&binding_iface_bound_set, iface_id);
-}
-
 #define OVN_QOS_TYPE "linux-htb"
 
 struct qos_queue {
@@ -597,6 +571,23 @@ remove_local_lport_ids(const struct sbrec_port_binding *pb,
     }
 }
 
+/* Corresponds to each Port_Binding.type. */
+enum en_lport_type {
+    LP_UNKNOWN,
+    LP_VIF,
+    LP_CONTAINER,
+    LP_PATCH,
+    LP_L3GATEWAY,
+    LP_LOCALNET,
+    LP_LOCALPORT,
+    LP_L2GATEWAY,
+    LP_VTEP,
+    LP_CHASSISREDIRECT,
+    LP_VIRTUAL,
+    LP_EXTERNAL,
+    LP_REMOTE
+};
+
 /* Local bindings. binding.c module binds the logical port (represented by
  * Port_Binding rows) and sets the 'chassis' column when it sees the
  * OVS interface row (of type "" or "internal") with the
@@ -608,134 +599,270 @@ remove_local_lport_ids(const struct sbrec_port_binding *pb,
  * 'struct local_binding' is used. A shash of these local bindings is
  * maintained with the 'external_ids:iface-id' as the key to the shash.
  *
- * struct local_binding (defined in binding.h) has 3 main fields:
- *    - type
- *    - OVS interface row object
- *    - Port_Binding row object
- *
- * An instance of 'struct local_binding' can be one of 3 types.
- *
- *  BT_VIF:     Represent a local binding for an OVS interface of
- *              type "" or "internal" with the external_ids:iface-id
- *              set.
- *
- *              This can be a
- *                 * probable local binding - external_ids:iface-id is
- *                   set, but the corresponding Port_Binding row is not
- *                   created or is not visible to the local ovn-controller
- *                   instance.
- *
- *                 * a local binding - external_ids:iface-id is set and
- *                   which is already bound to the corresponding Port_Binding
- *                   row.
- *
- *              It maintains a list of children
- *              (of type BT_CONTAINER/BT_VIRTUAL) if any.
- *
- *  BT_CONTAINER:   Represents a local binding which has a parent of type
- *                  BT_VIF. Its Port_Binding row's 'parent' column is set to
- *                  its parent's Port_Binding. It shares the OVS interface row
- *                  with the parent.
- *                  Each ovn-controller when it sees a container Port_Binding,
- *                  it creates 'struct local_binding' for the parent
- *                  Port_Binding and for its even if the OVS interface row for
- *                  the parent is not present.
- *
- *  BT_VIRTUAL: Represents a local binding which has a parent of type BT_VIF.
- *              Its Port_Binding type is "virtual" and it shares the OVS
- *              interface row with the parent.
- *              Port_Binding of type "virtual" is claimed by pinctrl module
- *              when it sees the ARP packet from the parent's VIF.
- *
+ * struct local_binding has 3 main fields:
+ *    - name : 'external_ids:iface-id' of the OVS interface (key).
+ *    - OVS interface row object.
+ *    - List of 'binding_lport' objects with the primary lport
+ *      in the front of the list (if present).
  *
  *  An object of 'struct local_binding' is created:
- *    - For each interface that has iface-id configured with the type - BT_VIF.
+ *    - For each interface that has external_ids:iface-id configured.
+ *
+ *    - For each port binding (also referred as lport) of type 'LP_VIF'
+ *      if it is a parent lport of container lports even if there is no
+ *      corresponding OVS interface.
+ */
+struct local_binding {
+    char *name;
+    const struct ovsrec_interface *iface;
+    struct ovs_list binding_lports;
+};
+
+/* This structure represents a logical port (or port binding)
+ * which is associated with 'struct local_binding'.
  *
- *    - For each container Port Binding (of type BT_CONTAINER) and its
- *      parent Port_Binding (of type BT_VIF), no matter if
- *      they are bound to this chassis i.e even if OVS interface row for the
- *      parent is not present.
+ * An instance of 'struct binding_lport' is created for a logical port
+ *  - If the OVS interface's iface-id corresponds to the logical port.
+ *  - If it is a container or virtual logical port and its parent
+ *    has a 'local binding'.
  *
- *   - For each 'virtual' Port Binding (of type BT_VIRTUAL) provided its parent
- *     is bound to this chassis.
  */
+struct binding_lport {
+    struct ovs_list list_node; /* Node in local_binding.binding_lports. */
 
-static struct local_binding *
-local_binding_create(const char *name, const struct ovsrec_interface *iface,
-                     const struct sbrec_port_binding *pb,
-                     enum local_binding_type type)
+    char *name;
+    const struct sbrec_port_binding *pb;
+    struct local_binding *lbinding;
+    enum en_lport_type type;
+};
+
+static struct local_binding *local_binding_create(
+    const char *name, const struct ovsrec_interface *);
+static void local_binding_add(struct shash *local_bindings,
+                              struct local_binding *);
+static struct local_binding *local_binding_find(
+    struct shash *local_bindings, const char *name);
+static void local_binding_destroy(struct local_binding *,
+                                  struct shash *binding_lports);
+static void local_binding_delete(struct local_binding *,
+                                 struct shash *local_bindings,
+                                 struct shash *binding_lports,
+                                 struct if_status_mgr *if_mgr);
+static struct binding_lport *local_binding_add_lport(
+    struct shash *binding_lports,
+    struct local_binding *,
+    const struct sbrec_port_binding *,
+    enum en_lport_type);
+static struct binding_lport *local_binding_get_primary_lport(
+    struct local_binding *);
+static bool local_binding_handle_stale_binding_lports(
+    struct local_binding *lbinding, struct binding_ctx_in *b_ctx_in,
+    struct binding_ctx_out *b_ctx_out, struct hmap *qos_map);
+
+static struct binding_lport *binding_lport_create(
+    const struct sbrec_port_binding *,
+    struct local_binding *, enum en_lport_type);
+static void binding_lport_destroy(struct binding_lport *);
+static void binding_lport_delete(struct shash *binding_lports,
+                                 struct binding_lport *);
+static void binding_lport_add(struct shash *binding_lports,
+                              struct binding_lport *);
+static void binding_lport_set_up(struct binding_lport *, bool sb_readonly);
+static void binding_lport_set_down(struct binding_lport *, bool sb_readonly);
+static struct binding_lport *binding_lport_find(
+    struct shash *binding_lports, const char *lport_name);
+static const struct sbrec_port_binding *binding_lport_get_parent_pb(
+    struct binding_lport *b_lprt);
+static struct binding_lport *binding_lport_check_and_cleanup(
+    struct binding_lport *, struct shash *b_lports);
+
+static char *get_lport_type_str(enum en_lport_type lport_type);
+
+void
+local_binding_data_init(struct local_binding_data *lbinding_data)
 {
-    struct local_binding *lbinding = xzalloc(sizeof *lbinding);
-    lbinding->name = xstrdup(name);
-    lbinding->type = type;
-    lbinding->pb = pb;
-    lbinding->iface = iface;
-    shash_init(&lbinding->children);
-    return lbinding;
+    shash_init(&lbinding_data->bindings);
+    shash_init(&lbinding_data->lports);
 }
 
-static void
-local_binding_add(struct shash *local_bindings, struct local_binding *lbinding)
+void
+local_binding_data_destroy(struct local_binding_data *lbinding_data)
 {
-    shash_add(local_bindings, lbinding->name, lbinding);
+    struct shash_node *node, *next;
+
+    SHASH_FOR_EACH_SAFE (node, next, &lbinding_data->lports) {
+        struct binding_lport *b_lport = node->data;
+        binding_lport_destroy(b_lport);
+        shash_delete(&lbinding_data->lports, node);
+    }
+
+    SHASH_FOR_EACH_SAFE (node, next, &lbinding_data->bindings) {
+        struct local_binding *lbinding = node->data;
+        local_binding_destroy(lbinding, &lbinding_data->lports);
+        shash_delete(&lbinding_data->bindings, node);
+    }
+
+    shash_destroy(&lbinding_data->lports);
+    shash_destroy(&lbinding_data->bindings);
 }
 
-static void
-local_binding_destroy(struct local_binding *lbinding)
+const struct sbrec_port_binding *
+local_binding_get_primary_pb(struct shash *local_bindings, const char *pb_name)
 {
-    local_bindings_destroy(&lbinding->children);
+    struct local_binding *lbinding =
+        local_binding_find(local_bindings, pb_name);
+    struct binding_lport *b_lport = local_binding_get_primary_lport(lbinding);
 
-    free(lbinding->name);
-    free(lbinding);
+    return b_lport ? b_lport->pb : NULL;
 }
 
-void
-local_bindings_init(struct shash *local_bindings)
+bool
+local_binding_is_up(struct shash *local_bindings, const char *pb_name)
 {
-    shash_init(local_bindings);
+    struct local_binding *lbinding =
+        local_binding_find(local_bindings, pb_name);
+    struct binding_lport *b_lport = local_binding_get_primary_lport(lbinding);
+    if (lbinding && b_lport && lbinding->iface) {
+        if (b_lport->pb->n_up && !b_lport->pb->up[0]) {
+            return false;
+        }
+        return smap_get_bool(&lbinding->iface->external_ids,
+                             OVN_INSTALLED_EXT_ID, false);
+    }
+    return false;
 }
 
-void
-local_bindings_destroy(struct shash *local_bindings)
+bool
+local_binding_is_down(struct shash *local_bindings, const char *pb_name)
 {
-    struct shash_node *node, *next;
-    SHASH_FOR_EACH_SAFE (node, next, local_bindings) {
-        struct local_binding *lbinding = node->data;
-        local_binding_destroy(lbinding);
-        shash_delete(local_bindings, node);
+    struct local_binding *lbinding =
+        local_binding_find(local_bindings, pb_name);
+
+    struct binding_lport *b_lport = local_binding_get_primary_lport(lbinding);
+
+    if (!lbinding) {
+        return true;
     }
 
-    shash_destroy(local_bindings);
-}
+    if (lbinding->iface && smap_get_bool(&lbinding->iface->external_ids,
+                                         OVN_INSTALLED_EXT_ID, false)) {
+        return false;
+    }
 
-static
-void local_binding_delete(struct shash *local_bindings,
-                          struct local_binding *lbinding)
-{
-    shash_find_and_delete(local_bindings, lbinding->name);
-    local_binding_destroy(lbinding);
+    if (b_lport && b_lport->pb->n_up && b_lport->pb->up[0]) {
+        return false;
+    }
+
+    return true;
 }
 
-static void
-local_binding_add_child(struct local_binding *lbinding,
-                        struct local_binding *child)
+void
+local_binding_set_up(struct shash *local_bindings, const char *pb_name,
+                     bool sb_readonly, bool ovs_readonly)
 {
-    local_binding_add(&lbinding->children, child);
-    child->parent = lbinding;
+    struct local_binding *lbinding =
+        local_binding_find(local_bindings, pb_name);
+    struct binding_lport *b_lport = local_binding_get_primary_lport(lbinding);
+
+    if (!ovs_readonly && lbinding && lbinding->iface
+            && !smap_get_bool(&lbinding->iface->external_ids,
+                              OVN_INSTALLED_EXT_ID, false)) {
+        ovsrec_interface_update_external_ids_setkey(lbinding->iface,
+                                                    OVN_INSTALLED_EXT_ID,
+                                                    "true");
+    }
+
+    if (!sb_readonly && lbinding && b_lport && b_lport->pb->n_up) {
+        binding_lport_set_up(b_lport, sb_readonly);
+        LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) {
+            binding_lport_set_up(b_lport, sb_readonly);
+        }
+    }
 }
 
-static struct local_binding *
-local_binding_find_child(struct local_binding *lbinding,
-                         const char *child_name)
+void
+local_binding_set_down(struct shash *local_bindings, const char *pb_name,
+                       bool sb_readonly, bool ovs_readonly)
 {
-    return local_binding_find(&lbinding->children, child_name);
+    struct local_binding *lbinding =
+        local_binding_find(local_bindings, pb_name);
+    struct binding_lport *b_lport = local_binding_get_primary_lport(lbinding);
+
+    if (!ovs_readonly && lbinding && lbinding->iface
+            && smap_get_bool(&lbinding->iface->external_ids,
+                             OVN_INSTALLED_EXT_ID, false)) {
+        ovsrec_interface_update_external_ids_delkey(lbinding->iface,
+                                                    OVN_INSTALLED_EXT_ID);
+    }
+
+    if (!sb_readonly && b_lport && b_lport->pb->n_up) {
+        binding_lport_set_down(b_lport, sb_readonly);
+        LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) {
+            binding_lport_set_down(b_lport, sb_readonly);
+        }
+    }
 }
 
-static void
-local_binding_delete_child(struct local_binding *lbinding,
-                           struct local_binding *child)
+void
+binding_dump_local_bindings(struct local_binding_data *lbinding_data,
+                            struct ds *out_data)
 {
-    shash_find_and_delete(&lbinding->children, child->name);
+    const struct shash_node **nodes;
+
+    nodes = shash_sort(&lbinding_data->bindings);
+    size_t n = shash_count(&lbinding_data->bindings);
+
+    ds_put_cstr(out_data, "Local bindings:\n");
+    for (size_t i = 0; i < n; i++) {
+        const struct shash_node *node = nodes[i];
+        struct local_binding *lbinding = node->data;
+        size_t num_lports = ovs_list_size(&lbinding->binding_lports);
+        ds_put_format(out_data, "name: [%s], OVS interface name : [%s], "
+                      "num binding lports : [%"PRIuSIZE"]\n",
+                      lbinding->name,
+                      lbinding->iface ? lbinding->iface->name : "NULL",
+                      num_lports);
+
+        if (num_lports) {
+            struct shash child_lports = SHASH_INITIALIZER(&child_lports);
+            struct binding_lport *primary_lport = NULL;
+            struct binding_lport *b_lport;
+            bool first_elem = true;
+
+            LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) {
+                if (first_elem && b_lport->type == LP_VIF) {
+                    primary_lport = b_lport;
+                } else {
+                    shash_add(&child_lports, b_lport->name, b_lport);
+                }
+                first_elem = false;
+            }
+
+            if (primary_lport) {
+                ds_put_format(out_data, "primary lport : [%s]\n",
+                              primary_lport->name);
+            } else {
+                ds_put_format(out_data, "no primary lport\n");
+            }
+
+            if (!shash_is_empty(&child_lports)) {
+                const struct shash_node **c_nodes =
+                    shash_sort(&child_lports);
+                for (size_t j = 0; j < shash_count(&child_lports); j++) {
+                    b_lport = c_nodes[j]->data;
+                    ds_put_format(out_data, "child lport[%"PRIuSIZE"] : [%s], "
+                                  "type : [%s]\n", j + 1, b_lport->name,
+                                  get_lport_type_str(b_lport->type));
+                }
+                free(c_nodes);
+            }
+            shash_destroy(&child_lports);
+        }
+
+        ds_put_cstr(out_data, "----------------------------------------\n");
+    }
+
+    free(nodes);
 }
 
 static bool
@@ -744,12 +871,6 @@ is_lport_vif(const struct sbrec_port_binding *pb)
     return !pb->type[0];
 }
 
-static bool
-is_lport_container(const struct sbrec_port_binding *pb)
-{
-    return is_lport_vif(pb) && pb->parent_port && pb->parent_port[0];
-}
-
 static struct tracked_binding_datapath *
 tracked_binding_datapath_create(const struct sbrec_datapath_binding *dp,
                                 bool is_new,
@@ -818,26 +939,13 @@ binding_tracked_dp_destroy(struct hmap *tracked_datapaths)
     hmap_destroy(tracked_datapaths);
 }
 
-/* Corresponds to each Port_Binding.type. */
-enum en_lport_type {
-    LP_UNKNOWN,
-    LP_VIF,
-    LP_PATCH,
-    LP_L3GATEWAY,
-    LP_LOCALNET,
-    LP_LOCALPORT,
-    LP_L2GATEWAY,
-    LP_VTEP,
-    LP_CHASSISREDIRECT,
-    LP_VIRTUAL,
-    LP_EXTERNAL,
-    LP_REMOTE
-};
-
 static enum en_lport_type
 get_lport_type(const struct sbrec_port_binding *pb)
 {
     if (is_lport_vif(pb)) {
+        if (pb->parent_port && pb->parent_port[0]) {
+            return LP_CONTAINER;
+        }
         return LP_VIF;
     } else if (!strcmp(pb->type, "patch")) {
         return LP_PATCH;
@@ -864,6 +972,41 @@ get_lport_type(const struct sbrec_port_binding *pb)
     return LP_UNKNOWN;
 }
 
+static char *
+get_lport_type_str(enum en_lport_type lport_type)
+{
+    switch (lport_type) {
+    case LP_VIF:
+        return "VIF";
+    case LP_CONTAINER:
+        return "CONTAINER";
+    case LP_VIRTUAL:
+        return "VIRTUAL";
+    case LP_PATCH:
+        return "PATCH";
+    case LP_CHASSISREDIRECT:
+        return "CHASSISREDIRECT";
+    case LP_L3GATEWAY:
+        return "L3GATEWAT";
+    case LP_LOCALNET:
+        return "PATCH";
+    case LP_LOCALPORT:
+        return "LOCALPORT";
+    case LP_L2GATEWAY:
+        return "L2GATEWAY";
+    case LP_EXTERNAL:
+        return "EXTERNAL";
+    case LP_REMOTE:
+        return "REMOTE";
+    case LP_VTEP:
+        return "VTEP";
+    case LP_UNKNOWN:
+        return "UNKNOWN";
+    }
+
+    OVS_NOT_REACHED();
+}
+
 /* For newly claimed ports, if 'notify_up' is 'false':
  * - set the 'pb.up' field to true if 'pb' has no 'parent_pb'.
  * - set the 'pb.up' field to true if 'parent_pb.up' is 'true' (e.g., for
@@ -880,7 +1023,7 @@ static void
 claimed_lport_set_up(const struct sbrec_port_binding *pb,
                      const struct sbrec_port_binding *parent_pb,
                      const struct sbrec_chassis *chassis_rec,
-                     bool notify_up)
+                     bool notify_up, struct if_status_mgr *if_mgr)
 {
     if (!notify_up) {
         bool up = true;
@@ -891,7 +1034,7 @@ claimed_lport_set_up(const struct sbrec_port_binding *pb,
     }
 
     if (pb->chassis != chassis_rec || (pb->n_up && !pb->up[0])) {
-        binding_iface_bound_add(pb->logical_port);
+        if_status_mgr_claim_iface(if_mgr, pb->logical_port);
     }
 }
 
@@ -904,10 +1047,11 @@ claim_lport(const struct sbrec_port_binding *pb,
             const struct sbrec_chassis *chassis_rec,
             const struct ovsrec_interface *iface_rec,
             bool sb_readonly, bool notify_up,
-            struct hmap *tracked_datapaths)
+            struct hmap *tracked_datapaths,
+            struct if_status_mgr *if_mgr)
 {
     if (!sb_readonly) {
-        claimed_lport_set_up(pb, parent_pb, chassis_rec, notify_up);
+        claimed_lport_set_up(pb, parent_pb, chassis_rec, notify_up, if_mgr);
     }
 
     if (pb->chassis != chassis_rec) {
@@ -955,7 +1099,7 @@ claim_lport(const struct sbrec_port_binding *pb,
  */
 static bool
 release_lport(const struct sbrec_port_binding *pb, bool sb_readonly,
-              struct hmap *tracked_datapaths)
+              struct hmap *tracked_datapaths, struct if_status_mgr *if_mgr)
 {
     if (pb->encap) {
         if (sb_readonly) {
@@ -978,12 +1122,8 @@ release_lport(const struct sbrec_port_binding *pb, bool sb_readonly,
         sbrec_port_binding_set_virtual_parent(pb, NULL);
     }
 
-    if (pb->n_up) {
-        bool up = false;
-        sbrec_port_binding_set_up(pb, &up, 1);
-    }
     update_lport_tracking(pb, tracked_datapaths);
-    binding_iface_released_add(pb->logical_port);
+    if_status_mgr_release_iface(if_mgr, pb->logical_port);
     VLOG_INFO("Releasing lport %s from this chassis.", pb->logical_port);
     return true;
 }
@@ -991,14 +1131,15 @@ release_lport(const struct sbrec_port_binding *pb, bool sb_readonly,
 static bool
 is_lbinding_set(struct local_binding *lbinding)
 {
-    return lbinding && lbinding->pb && lbinding->iface;
+    return lbinding && lbinding->iface;
 }
 
 static bool
-is_lbinding_this_chassis(struct local_binding *lbinding,
-                         const struct sbrec_chassis *chassis)
+is_binding_lport_this_chassis(struct binding_lport *b_lport,
+                              const struct sbrec_chassis *chassis)
 {
-    return lbinding && lbinding->pb && lbinding->pb->chassis == chassis;
+    return (b_lport && b_lport->pb && chassis &&
+            b_lport->pb->chassis == chassis);
 }
 
 static bool
@@ -1010,15 +1151,14 @@ can_bind_on_this_chassis(const struct sbrec_chassis *chassis_rec,
            || !strcmp(requested_chassis, chassis_rec->hostname);
 }
 
-/* Returns 'true' if the 'lbinding' has children of type BT_CONTAINER,
+/* Returns 'true' if the 'lbinding' has binding lports of type LP_CONTAINER,
  * 'false' otherwise. */
 static bool
 is_lbinding_container_parent(struct local_binding *lbinding)
 {
-    struct shash_node *node;
-    SHASH_FOR_EACH (node, &lbinding->children) {
-        struct local_binding *l = node->data;
-        if (l->type == BT_CONTAINER) {
+    struct binding_lport *b_lport;
+    LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) {
+        if (b_lport->type == LP_CONTAINER) {
             return true;
         }
     }
@@ -1027,66 +1167,44 @@ is_lbinding_container_parent(struct local_binding *lbinding)
 }
 
 static bool
-release_local_binding_children(const struct sbrec_chassis *chassis_rec,
-                               struct local_binding *lbinding,
-                               bool sb_readonly,
-                               struct hmap *tracked_dp_bindings)
-{
-    struct shash_node *node;
-    SHASH_FOR_EACH (node, &lbinding->children) {
-        struct local_binding *l = node->data;
-        if (is_lbinding_this_chassis(l, chassis_rec)) {
-            if (!release_lport(l->pb, sb_readonly, tracked_dp_bindings)) {
-                return false;
-            }
+release_binding_lport(const struct sbrec_chassis *chassis_rec,
+                      struct binding_lport *b_lport, bool sb_readonly,
+                      struct binding_ctx_out *b_ctx_out)
+{
+    if (is_binding_lport_this_chassis(b_lport, chassis_rec)) {
+        remove_local_lport_ids(b_lport->pb, b_ctx_out);
+        if (!release_lport(b_lport->pb, sb_readonly,
+                           b_ctx_out->tracked_dp_bindings,
+                           b_ctx_out->if_mgr)) {
+            return false;
         }
-
-        /* Clear the local bindings' 'iface'. */
-        l->iface = NULL;
+        binding_lport_set_down(b_lport, sb_readonly);
     }
 
     return true;
 }
 
-static bool
-release_local_binding(const struct sbrec_chassis *chassis_rec,
-                      struct local_binding *lbinding, bool sb_readonly,
-                      struct hmap *tracked_dp_bindings)
-{
-    if (!release_local_binding_children(chassis_rec, lbinding,
-                                        sb_readonly, tracked_dp_bindings)) {
-        return false;
-    }
-
-    bool retval = true;
-    if (is_lbinding_this_chassis(lbinding, chassis_rec)) {
-        retval = release_lport(lbinding->pb, sb_readonly, tracked_dp_bindings);
-    }
-
-    lbinding->pb = NULL;
-    lbinding->iface = NULL;
-    return retval;
-}
-
 static bool
 consider_vif_lport_(const struct sbrec_port_binding *pb,
                     bool can_bind, const char *vif_chassis,
                     struct binding_ctx_in *b_ctx_in,
                     struct binding_ctx_out *b_ctx_out,
-                    struct local_binding *lbinding,
+                    struct binding_lport *b_lport,
                     struct hmap *qos_map)
 {
-    bool lbinding_set = is_lbinding_set(lbinding);
+    bool lbinding_set = b_lport && is_lbinding_set(b_lport->lbinding);
+
     if (lbinding_set) {
         if (can_bind) {
             /* We can claim the lport. */
             const struct sbrec_port_binding *parent_pb =
-                lbinding->parent ? lbinding->parent->pb : NULL;
+                binding_lport_get_parent_pb(b_lport);
 
             if (!claim_lport(pb, parent_pb, b_ctx_in->chassis_rec,
-                             lbinding->iface, !b_ctx_in->ovnsb_idl_txn,
-                             !lbinding->parent,
-                             b_ctx_out->tracked_dp_bindings)){
+                             b_lport->lbinding->iface,
+                             !b_ctx_in->ovnsb_idl_txn,
+                             !parent_pb, b_ctx_out->tracked_dp_bindings,
+                             b_ctx_out->if_mgr)){
                 return false;
             }
 
@@ -1098,7 +1216,7 @@ consider_vif_lport_(const struct sbrec_port_binding *pb,
                                b_ctx_out->tracked_dp_bindings);
             update_local_lport_ids(pb, b_ctx_out);
             update_local_lports(pb->logical_port, b_ctx_out);
-            if (lbinding->iface && qos_map && b_ctx_in->ovs_idl_txn) {
+            if (b_lport->lbinding->iface && qos_map && b_ctx_in->ovs_idl_txn) {
                 get_qos_params(pb, qos_map);
             }
         } else {
@@ -1117,7 +1235,8 @@ consider_vif_lport_(const struct sbrec_port_binding *pb,
         /* Release the lport if there is no lbinding. */
         if (!lbinding_set || !can_bind) {
             return release_lport(pb, !b_ctx_in->ovnsb_idl_txn,
-                                 b_ctx_out->tracked_dp_bindings);
+                                 b_ctx_out->tracked_dp_bindings,
+                                 b_ctx_out->if_mgr);
         }
     }
 
@@ -1136,16 +1255,19 @@ consider_vif_lport(const struct sbrec_port_binding *pb,
                                              vif_chassis);
 
     if (!lbinding) {
-        lbinding = local_binding_find(b_ctx_out->local_bindings,
+        lbinding = local_binding_find(&b_ctx_out->lbinding_data->bindings,
                                       pb->logical_port);
     }
 
+    struct binding_lport *b_lport = NULL;
     if (lbinding) {
-        lbinding->pb = pb;
+        struct shash *binding_lports =
+            &b_ctx_out->lbinding_data->lports;
+        b_lport = local_binding_add_lport(binding_lports, lbinding, pb, LP_VIF);
     }
 
     return consider_vif_lport_(pb, can_bind, vif_chassis, b_ctx_in,
-                               b_ctx_out, lbinding, qos_map);
+                               b_ctx_out, b_lport, qos_map);
 }
 
 static bool
@@ -1154,9 +1276,9 @@ consider_container_lport(const struct sbrec_port_binding *pb,
                          struct binding_ctx_out *b_ctx_out,
                          struct hmap *qos_map)
 {
+    struct shash *local_bindings = &b_ctx_out->lbinding_data->bindings;
     struct local_binding *parent_lbinding;
-    parent_lbinding = local_binding_find(b_ctx_out->local_bindings,
-                                         pb->parent_port);
+    parent_lbinding = local_binding_find(local_bindings, pb->parent_port);
 
     if (!parent_lbinding) {
         /* There is no local_binding for parent port. Create it
@@ -1171,54 +1293,62 @@ consider_container_lport(const struct sbrec_port_binding *pb,
          *     we want the these container ports also be claimed by the
          *     chassis.
          * */
-        parent_lbinding = local_binding_create(pb->parent_port, NULL, NULL,
-                                               BT_VIF);
-        local_binding_add(b_ctx_out->local_bindings, parent_lbinding);
+        parent_lbinding = local_binding_create(pb->parent_port, NULL);
+        local_binding_add(local_bindings, parent_lbinding);
     }
 
-    struct local_binding *container_lbinding =
-        local_binding_find_child(parent_lbinding, pb->logical_port);
+    struct shash *binding_lports = &b_ctx_out->lbinding_data->lports;
+    struct binding_lport *container_b_lport =
+        local_binding_add_lport(binding_lports, parent_lbinding, pb,
+                                LP_CONTAINER);
 
-    if (!container_lbinding) {
-        container_lbinding = local_binding_create(pb->logical_port,
-                                                  parent_lbinding->iface,
-                                                  pb, BT_CONTAINER);
-        local_binding_add_child(parent_lbinding, container_lbinding);
-    } else {
-        ovs_assert(container_lbinding->type == BT_CONTAINER);
-        container_lbinding->pb = pb;
-        container_lbinding->iface = parent_lbinding->iface;
-    }
+    struct binding_lport *parent_b_lport =
+        binding_lport_find(binding_lports, pb->parent_port);
 
-    if (!parent_lbinding->pb) {
-        parent_lbinding->pb = lport_lookup_by_name(
+    bool can_consider_c_lport = true;
+    if (!parent_b_lport || !parent_b_lport->pb) {
+        const struct sbrec_port_binding *parent_pb = lport_lookup_by_name(
             b_ctx_in->sbrec_port_binding_by_name, pb->parent_port);
 
-        if (parent_lbinding->pb) {
+        if (parent_pb && get_lport_type(parent_pb) == LP_VIF) {
             /* Its possible that the parent lport is not considered yet.
              * So call consider_vif_lport() to process it first. */
-            consider_vif_lport(parent_lbinding->pb, b_ctx_in, b_ctx_out,
+            consider_vif_lport(parent_pb, b_ctx_in, b_ctx_out,
                                parent_lbinding, qos_map);
+            parent_b_lport = binding_lport_find(binding_lports,
+                                                pb->parent_port);
         } else {
-            /* The parent lport doesn't exist. Call release_lport() to
-             * release the container lport, if it was bound earlier. */
-            if (is_lbinding_this_chassis(container_lbinding,
-                                         b_ctx_in->chassis_rec)) {
-               return release_lport(pb, !b_ctx_in->ovnsb_idl_txn,
-                                    b_ctx_out->tracked_dp_bindings);
-            }
+            /* The parent lport doesn't exist.  Cannot consider the container
+             * lport for binding. */
+            can_consider_c_lport = false;
+        }
+    }
 
-            return true;
+    if (parent_b_lport && parent_b_lport->type != LP_VIF) {
+        can_consider_c_lport = false;
+    }
+
+    if (!can_consider_c_lport) {
+        /* Call release_lport() to release the container lport,
+         * if it was bound earlier. */
+        if (is_binding_lport_this_chassis(container_b_lport,
+                                          b_ctx_in->chassis_rec)) {
+            return release_lport(pb, !b_ctx_in->ovnsb_idl_txn,
+                                 b_ctx_out->tracked_dp_bindings,
+                                 b_ctx_out->if_mgr);
         }
+
+        return true;
     }
 
-    const char *vif_chassis = smap_get(&parent_lbinding->pb->options,
+    ovs_assert(parent_b_lport && parent_b_lport->pb);
+    const char *vif_chassis = smap_get(&parent_b_lport->pb->options,
                                        "requested-chassis");
     bool can_bind = can_bind_on_this_chassis(b_ctx_in->chassis_rec,
                                              vif_chassis);
 
     return consider_vif_lport_(pb, can_bind, vif_chassis, b_ctx_in, b_ctx_out,
-                               container_lbinding, qos_map);
+                               container_b_lport, qos_map);
 }
 
 static bool
@@ -1227,46 +1357,58 @@ consider_virtual_lport(const struct sbrec_port_binding *pb,
                        struct binding_ctx_out *b_ctx_out,
                        struct hmap *qos_map)
 {
-    struct local_binding * parent_lbinding =
-        pb->virtual_parent ? local_binding_find(b_ctx_out->local_bindings,
+    struct shash *local_bindings = &b_ctx_out->lbinding_data->bindings;
+    struct local_binding *parent_lbinding =
+        pb->virtual_parent ? local_binding_find(local_bindings,
                                                 pb->virtual_parent)
         : NULL;
 
-    if (parent_lbinding && !parent_lbinding->pb) {
-        parent_lbinding->pb = lport_lookup_by_name(
-            b_ctx_in->sbrec_port_binding_by_name, pb->virtual_parent);
-
-        if (parent_lbinding->pb) {
-            /* Its possible that the parent lport is not considered yet.
-             * So call consider_vif_lport() to process it first. */
-            consider_vif_lport(parent_lbinding->pb, b_ctx_in, b_ctx_out,
-                               parent_lbinding, qos_map);
-        }
-    }
-
+    struct binding_lport *virtual_b_lport = NULL;
     /* Unlike container lports, we don't have to create parent_lbinding if
      * it is NULL. This is because, if parent_lbinding is not present, it
      * means the virtual port can't bind in this chassis.
      * Note: pinctrl module binds the virtual lport when it sees ARP
      * packet from the parent lport. */
-    struct local_binding *virtual_lbinding = NULL;
-    if (is_lbinding_this_chassis(parent_lbinding, b_ctx_in->chassis_rec)) {
-        virtual_lbinding =
-            local_binding_find_child(parent_lbinding, pb->logical_port);
-        if (!virtual_lbinding) {
-            virtual_lbinding = local_binding_create(pb->logical_port,
-                                                    parent_lbinding->iface,
-                                                    pb, BT_VIRTUAL);
-            local_binding_add_child(parent_lbinding, virtual_lbinding);
-        } else {
-            ovs_assert(virtual_lbinding->type == BT_VIRTUAL);
-            virtual_lbinding->pb = pb;
-            virtual_lbinding->iface = parent_lbinding->iface;
+    if (parent_lbinding) {
+        struct shash *binding_lports = &b_ctx_out->lbinding_data->lports;
+
+        struct binding_lport *parent_b_lport =
+            binding_lport_find(binding_lports, pb->virtual_parent);
+
+        if (!parent_b_lport || !parent_b_lport->pb) {
+            const struct sbrec_port_binding *parent_pb = lport_lookup_by_name(
+                b_ctx_in->sbrec_port_binding_by_name, pb->virtual_parent);
+
+            if (parent_pb && get_lport_type(parent_pb) == LP_VIF) {
+                /* Its possible that the parent lport is not considered yet.
+                 * So call consider_vif_lport() to process it first. */
+                consider_vif_lport(parent_pb, b_ctx_in, b_ctx_out,
+                                   parent_lbinding, qos_map);
+            }
+        }
+
+        parent_b_lport = local_binding_get_primary_lport(parent_lbinding);
+        if (is_binding_lport_this_chassis(parent_b_lport,
+                                          b_ctx_in->chassis_rec)) {
+            virtual_b_lport =
+                local_binding_add_lport(binding_lports, parent_lbinding, pb,
+                                        LP_VIRTUAL);
         }
     }
 
-    return consider_vif_lport_(pb, true, NULL, b_ctx_in, b_ctx_out,
-                               virtual_lbinding, qos_map);
+    if (!consider_vif_lport_(pb, true, NULL, b_ctx_in, b_ctx_out,
+                             virtual_b_lport, qos_map)) {
+        return false;
+    }
+
+    /* If the virtual lport is not bound to this chassis, then remove
+     * its entry from the local_lport_ids if present.  This is required
+     * when a virtual port moves from one chassis to other.*/
+    if (!virtual_b_lport) {
+        remove_local_lport_ids(pb, b_ctx_out);
+    }
+
+    return true;
 }
 
 /* Considers either claiming the lport or releasing the lport
@@ -1291,10 +1433,12 @@ consider_nonvif_lport_(const struct sbrec_port_binding *pb,
         update_local_lport_ids(pb, b_ctx_out);
         return claim_lport(pb, NULL, b_ctx_in->chassis_rec, NULL,
                            !b_ctx_in->ovnsb_idl_txn, false,
-                           b_ctx_out->tracked_dp_bindings);
+                           b_ctx_out->tracked_dp_bindings,
+                           b_ctx_out->if_mgr);
     } else if (pb->chassis == b_ctx_in->chassis_rec) {
         return release_lport(pb, !b_ctx_in->ovnsb_idl_txn,
-                             b_ctx_out->tracked_dp_bindings);
+                             b_ctx_out->tracked_dp_bindings,
+                             b_ctx_out->if_mgr);
     }
 
     return true;
@@ -1407,6 +1551,8 @@ build_local_bindings(struct binding_ctx_in *b_ctx_in,
             continue;
         }
 
+        struct shash *local_bindings =
+            &b_ctx_out->lbinding_data->bindings;
         for (j = 0; j < port_rec->n_interfaces; j++) {
             const struct ovsrec_interface *iface_rec;
 
@@ -1416,11 +1562,10 @@ build_local_bindings(struct binding_ctx_in *b_ctx_in,
 
             if (iface_id && ofport > 0) {
                 struct local_binding *lbinding =
-                    local_binding_find(b_ctx_out->local_bindings, iface_id);
+                    local_binding_find(local_bindings, iface_id);
                 if (!lbinding) {
-                    lbinding = local_binding_create(iface_id, iface_rec, NULL,
-                                                    BT_VIF);
-                    local_binding_add(b_ctx_out->local_bindings, lbinding);
+                    lbinding = local_binding_create(iface_id, iface_rec);
+                    local_binding_add(local_bindings, lbinding);
                 } else {
                     static struct vlog_rate_limit rl =
                         VLOG_RATE_LIMIT_INIT(1, 5);
@@ -1431,7 +1576,6 @@ build_local_bindings(struct binding_ctx_in *b_ctx_in,
                         "configuration on interface [%s]",
                         lbinding->iface->name, iface_rec->name,
                         iface_rec->name);
-                    ovs_assert(lbinding->type == BT_VIF);
                 }
 
                 update_local_lports(iface_id, b_ctx_out);
@@ -1494,11 +1638,11 @@ binding_run(struct binding_ctx_in *b_ctx_in, struct binding_ctx_out *b_ctx_out)
             break;
 
         case LP_VIF:
-            if (is_lport_container(pb)) {
-                consider_container_lport(pb, b_ctx_in, b_ctx_out, qos_map_ptr);
-            } else {
-                consider_vif_lport(pb, b_ctx_in, b_ctx_out, NULL, qos_map_ptr);
-            }
+            consider_vif_lport(pb, b_ctx_in, b_ctx_out, NULL, qos_map_ptr);
+            break;
+
+        case LP_CONTAINER:
+            consider_container_lport(pb, b_ctx_in, b_ctx_out, qos_map_ptr);
             break;
 
         case LP_VIRTUAL:
@@ -1799,39 +1943,44 @@ consider_iface_claim(const struct ovsrec_interface *iface_rec,
     update_local_lports(iface_id, b_ctx_out);
     smap_replace(b_ctx_out->local_iface_ids, iface_rec->name, iface_id);
 
-    struct local_binding *lbinding =
-        local_binding_find(b_ctx_out->local_bindings, iface_id);
+    struct shash *local_bindings = &b_ctx_out->lbinding_data->bindings;
+    struct shash *binding_lports = &b_ctx_out->lbinding_data->lports;
+    struct local_binding *lbinding = local_binding_find(local_bindings,
+                                                        iface_id);
 
     if (!lbinding) {
-        lbinding = local_binding_create(iface_id, iface_rec, NULL, BT_VIF);
-        local_binding_add(b_ctx_out->local_bindings, lbinding);
+        lbinding = local_binding_create(iface_id, iface_rec);
+        local_binding_add(local_bindings, lbinding);
     } else {
         lbinding->iface = iface_rec;
     }
 
-    if (!lbinding->pb || strcmp(lbinding->name, lbinding->pb->logical_port)) {
-        lbinding->pb = lport_lookup_by_name(
-            b_ctx_in->sbrec_port_binding_by_name, lbinding->name);
-        if (lbinding->pb && !strcmp(lbinding->pb->type, "virtual")) {
-            lbinding->pb = NULL;
+    struct binding_lport *b_lport = local_binding_get_primary_lport(lbinding);
+    const struct sbrec_port_binding *pb = NULL;
+    if (!b_lport) {
+        pb = lport_lookup_by_name(b_ctx_in->sbrec_port_binding_by_name,
+                                  lbinding->name);
+        if (pb && get_lport_type(pb) == LP_VIF) {
+            b_lport = local_binding_add_lport(binding_lports, lbinding, pb,
+                                              LP_VIF);
         }
     }
 
-    if (lbinding->pb) {
-        if (!consider_vif_lport(lbinding->pb, b_ctx_in, b_ctx_out,
-                                lbinding, qos_map)) {
-            return false;
-        }
+    if (!b_lport) {
+        /* There is no binding lport for this local binding. */
+        return true;
+    }
+
+    if (!consider_vif_lport(b_lport->pb, b_ctx_in, b_ctx_out,
+                            lbinding, qos_map)) {
+        return false;
     }
 
     /* Update the child local_binding's iface (if any children) and try to
      *  claim the container lbindings. */
-    struct shash_node *node;
-    SHASH_FOR_EACH (node, &lbinding->children) {
-        struct local_binding *child = node->data;
-        child->iface = iface_rec;
-        if (child->type == BT_CONTAINER) {
-            if (!consider_container_lport(child->pb, b_ctx_in, b_ctx_out,
+    LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) {
+        if (b_lport->type == LP_CONTAINER) {
+            if (!consider_container_lport(b_lport->pb, b_ctx_in, b_ctx_out,
                                           qos_map)) {
                 return false;
             }
@@ -1862,32 +2011,43 @@ consider_iface_release(const struct ovsrec_interface *iface_rec,
                        struct binding_ctx_out *b_ctx_out)
 {
     struct local_binding *lbinding;
-    lbinding = local_binding_find(b_ctx_out->local_bindings,
-                                  iface_id);
-    if (is_lbinding_this_chassis(lbinding, b_ctx_in->chassis_rec)) {
+    struct shash *local_bindings = &b_ctx_out->lbinding_data->bindings;
+    struct shash *binding_lports = &b_ctx_out->lbinding_data->lports;
+
+    lbinding = local_binding_find(local_bindings, iface_id);
+    struct binding_lport *b_lport = local_binding_get_primary_lport(lbinding);
+    if (is_binding_lport_this_chassis(b_lport, b_ctx_in->chassis_rec)) {
         struct local_datapath *ld =
             get_local_datapath(b_ctx_out->local_datapaths,
-                               lbinding->pb->datapath->tunnel_key);
+                               b_lport->pb->datapath->tunnel_key);
         if (ld) {
-            remove_pb_from_local_datapath(lbinding->pb,
-                                            b_ctx_in->chassis_rec,
-                                            b_ctx_out, ld);
+            remove_pb_from_local_datapath(b_lport->pb,
+                                          b_ctx_in->chassis_rec,
+                                          b_ctx_out, ld);
         }
 
-        /* Note: release_local_binding() resets lbinding->pb and
-         * lbinding->iface.
-         * Cannot access these members of lbinding after this call. */
-        if (!release_local_binding(b_ctx_in->chassis_rec, lbinding,
-                                   !b_ctx_in->ovnsb_idl_txn,
-                                   b_ctx_out->tracked_dp_bindings)) {
-            return false;
+        /* Release the primary binding lport and other children lports if
+         * any. */
+        LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) {
+            if (!release_binding_lport(b_ctx_in->chassis_rec, b_lport,
+                                       !b_ctx_in->ovnsb_idl_txn,
+                                       b_ctx_out)) {
+                return false;
+            }
         }
+
+    }
+
+    if (lbinding) {
+        /* Clear the iface of the local binding. */
+        lbinding->iface = NULL;
     }
 
     /* Check if the lbinding has children of type PB_CONTAINER.
      * If so, don't delete the local_binding. */
     if (lbinding && !is_lbinding_container_parent(lbinding)) {
-        local_binding_delete(b_ctx_out->local_bindings, lbinding);
+        local_binding_delete(lbinding, local_bindings, binding_lports,
+                             b_ctx_out->if_mgr);
     }
 
     remove_local_lports(iface_id, b_ctx_out);
@@ -2088,56 +2248,35 @@ handle_deleted_lport(const struct sbrec_port_binding *pb,
     }
 }
 
-static struct local_binding *
-get_lbinding_for_lport(const struct sbrec_port_binding *pb,
-                       enum en_lport_type lport_type,
-                       struct binding_ctx_out *b_ctx_out)
-{
-    ovs_assert(lport_type == LP_VIF || lport_type == LP_VIRTUAL);
-
-    if (lport_type == LP_VIF && !is_lport_container(pb)) {
-        return local_binding_find(b_ctx_out->local_bindings, pb->logical_port);
-    }
-
-    struct local_binding *parent_lbinding = NULL;
-
-    if (lport_type == LP_VIRTUAL) {
-        if (pb->virtual_parent) {
-            parent_lbinding = local_binding_find(b_ctx_out->local_bindings,
-                                                 pb->virtual_parent);
-        }
-    } else {
-        if (pb->parent_port) {
-            parent_lbinding = local_binding_find(b_ctx_out->local_bindings,
-                                                 pb->parent_port);
-        }
-    }
-
-    return parent_lbinding
-           ? local_binding_find(&parent_lbinding->children, pb->logical_port)
-           : NULL;
-}
-
 static bool
 handle_deleted_vif_lport(const struct sbrec_port_binding *pb,
                          enum en_lport_type lport_type,
                          struct binding_ctx_in *b_ctx_in,
                          struct binding_ctx_out *b_ctx_out)
 {
-    struct local_binding *lbinding =
-        get_lbinding_for_lport(pb, lport_type, b_ctx_out);
+    struct local_binding *lbinding = NULL;
+    bool bound = false;
 
-    if (lbinding) {
-        lbinding->pb = NULL;
-        /* The port_binding 'pb' is deleted. So there is no need to
-         * clear the 'chassis' column of 'pb'. But we need to do
-         * for the local_binding's children. */
-        if (lbinding->type == BT_VIF &&
-                !release_local_binding_children(
-                    b_ctx_in->chassis_rec, lbinding,
-                    !b_ctx_in->ovnsb_idl_txn,
-                    b_ctx_out->tracked_dp_bindings)) {
-            return false;
+    struct shash *binding_lports = &b_ctx_out->lbinding_data->lports;
+    struct binding_lport *b_lport = binding_lport_find(binding_lports, pb->logical_port);
+    if (b_lport) {
+        lbinding = b_lport->lbinding;
+        bound = is_binding_lport_this_chassis(b_lport, b_ctx_in->chassis_rec);
+
+         /* Remove b_lport from local_binding. */
+         binding_lport_delete(binding_lports, b_lport);
+    }
+
+    if (bound && lbinding && lport_type == LP_VIF) {
+        /* We need to release the container/virtual binding lports (if any) if
+         * deleted 'pb' type is LP_VIF. */
+        struct binding_lport *c_lport;
+        LIST_FOR_EACH (c_lport, list_node, &lbinding->binding_lports) {
+            if (!release_binding_lport(b_ctx_in->chassis_rec, c_lport,
+                                       !b_ctx_in->ovnsb_idl_txn,
+                                       b_ctx_out)) {
+                return false;
+            }
         }
     }
 
@@ -2147,18 +2286,8 @@ handle_deleted_vif_lport(const struct sbrec_port_binding *pb,
      * it from local_lports if there is a VIF entry.
      * consider_iface_release() takes care of removing from the local_lports
      * when the interface change happens. */
-    if (is_lport_container(pb)) {
+    if (lport_type == LP_CONTAINER) {
         remove_local_lports(pb->logical_port, b_ctx_out);
-
-        /* If the container port is removed we should also remove it from
-         * its parent's children set.
-         */
-        if (lbinding) {
-            if (lbinding->parent) {
-                local_binding_delete_child(lbinding->parent, lbinding);
-            }
-            local_binding_destroy(lbinding);
-        }
     }
 
     handle_deleted_lport(pb, b_ctx_in, b_ctx_out);
@@ -2177,7 +2306,7 @@ handle_updated_vif_lport(const struct sbrec_port_binding *pb,
 
     if (lport_type == LP_VIRTUAL) {
         handled = consider_virtual_lport(pb, b_ctx_in, b_ctx_out, qos_map);
-    } else if (lport_type == LP_VIF && is_lport_container(pb)) {
+    } else if (lport_type == LP_CONTAINER) {
         handled = consider_container_lport(pb, b_ctx_in, b_ctx_out, qos_map);
     } else {
         handled = consider_vif_lport(pb, b_ctx_in, b_ctx_out, NULL, qos_map);
@@ -2189,14 +2318,14 @@ handle_updated_vif_lport(const struct sbrec_port_binding *pb,
 
     bool now_claimed = (pb->chassis == b_ctx_in->chassis_rec);
 
-    if (lport_type == LP_VIRTUAL ||
-            (lport_type == LP_VIF && is_lport_container(pb)) ||
+    if (lport_type == LP_VIRTUAL || lport_type == LP_CONTAINER ||
             claimed == now_claimed) {
         return true;
     }
 
-    struct local_binding *lbinding =
-        local_binding_find(b_ctx_out->local_bindings, pb->logical_port);
+    struct shash *local_bindings = &b_ctx_out->lbinding_data->bindings;
+    struct local_binding *lbinding = local_binding_find(local_bindings,
+                                                        pb->logical_port);
 
     /* If the ovs port backing this binding previously was removed in the
      * meantime, we won't have a local_binding for it.
@@ -2206,12 +2335,11 @@ handle_updated_vif_lport(const struct sbrec_port_binding *pb,
         return true;
     }
 
-    struct shash_node *node;
-    SHASH_FOR_EACH (node, &lbinding->children) {
-        struct local_binding *child = node->data;
-        if (child->type == BT_CONTAINER) {
-            handled = consider_container_lport(child->pb, b_ctx_in, b_ctx_out,
-                                               qos_map);
+    struct binding_lport *b_lport;
+    LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) {
+        if (b_lport->type == LP_CONTAINER) {
+            handled = consider_container_lport(b_lport->pb, b_ctx_in,
+                                               b_ctx_out, qos_map);
             if (!handled) {
                 return false;
             }
@@ -2256,12 +2384,25 @@ binding_handle_port_binding_changes(struct binding_ctx_in *b_ctx_in,
 
         enum en_lport_type lport_type = get_lport_type(pb);
 
-        if (lport_type == LP_VIF) {
-            if (is_lport_container(pb)) {
-                shash_add(&deleted_container_pbs, pb->logical_port, pb);
-            } else {
-                shash_add(&deleted_vif_pbs, pb->logical_port, pb);
+        struct binding_lport *b_lport =
+            binding_lport_find(&b_ctx_out->lbinding_data->lports,
+                               pb->logical_port);
+        if (b_lport) {
+            /* If the 'b_lport->type' and 'lport_type' don't match, then update
+             * the b_lport->type to the updated 'lport_type'.  The function
+             * binding_lport_check_and_cleanup() will cleanup the 'b_lport'
+             * if required. */
+            if (b_lport->type != lport_type) {
+                b_lport->type = lport_type;
             }
+            b_lport = binding_lport_check_and_cleanup(
+                b_lport, &b_ctx_out->lbinding_data->lports);
+        }
+
+        if (lport_type == LP_VIF) {
+            shash_add(&deleted_vif_pbs, pb->logical_port, pb);
+        } else if (lport_type == LP_CONTAINER) {
+            shash_add(&deleted_container_pbs, pb->logical_port, pb);
         } else if (lport_type == LP_VIRTUAL) {
             shash_add(&deleted_virtual_pbs, pb->logical_port, pb);
         } else {
@@ -2272,7 +2413,7 @@ binding_handle_port_binding_changes(struct binding_ctx_in *b_ctx_in,
     struct shash_node *node;
     struct shash_node *node_next;
     SHASH_FOR_EACH_SAFE (node, node_next, &deleted_container_pbs) {
-        handled = handle_deleted_vif_lport(node->data, LP_VIF, b_ctx_in,
+        handled = handle_deleted_vif_lport(node->data, LP_CONTAINER, b_ctx_in,
                                            b_ctx_out);
         shash_delete(&deleted_container_pbs, node);
         if (!handled) {
@@ -2326,12 +2467,33 @@ delete_done:
 
         enum en_lport_type lport_type = get_lport_type(pb);
 
+        struct binding_lport *b_lport =
+            binding_lport_find(&b_ctx_out->lbinding_data->lports,
+                               pb->logical_port);
+        if (b_lport) {
+            ovs_assert(b_lport->pb == pb);
+
+            if (b_lport->type != lport_type) {
+                b_lport->type = lport_type;
+            }
+
+            if (b_lport->lbinding) {
+                handled = local_binding_handle_stale_binding_lports(
+                    b_lport->lbinding, b_ctx_in, b_ctx_out, qos_map_ptr);
+                if (!handled) {
+                    /* Backout from the handling. */
+                    break;
+                }
+            }
+        }
+
         struct local_datapath *ld =
             get_local_datapath(b_ctx_out->local_datapaths,
                                pb->datapath->tunnel_key);
 
         switch (lport_type) {
         case LP_VIF:
+        case LP_CONTAINER:
         case LP_VIRTUAL:
             handled = handle_updated_vif_lport(pb, lport_type, b_ctx_in,
                                                b_ctx_out, qos_map_ptr);
@@ -2440,154 +2602,327 @@ delete_done:
     return handled;
 }
 
-/* Registered ofctrl seqno type for port_binding flow installation. */
-static size_t binding_seq_type_pb_cfg;
+/* Static functions for local_lbindind and binding_lport. */
+static struct local_binding *
+local_binding_create(const char *name, const struct ovsrec_interface *iface)
+{
+    struct local_binding *lbinding = xzalloc(sizeof *lbinding);
+    lbinding->name = xstrdup(name);
+    lbinding->iface = iface;
+    ovs_list_init(&lbinding->binding_lports);
+
+    return lbinding;
+}
 
-/* Binding specific seqno to be acked by ofctrl when flows for new interfaces
- * have been installed.
- */
-static uint32_t binding_iface_seqno = 0;
+static struct local_binding *
+local_binding_find(struct shash *local_bindings, const char *name)
+{
+    return shash_find_data(local_bindings, name);
+}
 
-/* Map indexed by iface-id containing the sequence numbers that when acked
- * indicate that the OVS flows for the iface-id have been installed.
- */
-static struct simap binding_iface_seqno_map =
-    SIMAP_INITIALIZER(&binding_iface_seqno_map);
+static void
+local_binding_add(struct shash *local_bindings, struct local_binding *lbinding)
+{
+    shash_add(local_bindings, lbinding->name, lbinding);
+}
 
-void
-binding_init(void)
+static void
+local_binding_destroy(struct local_binding *lbinding,
+                      struct shash *binding_lports)
 {
-    binding_seq_type_pb_cfg = ofctrl_seqno_add_type();
+    struct binding_lport *b_lport;
+    LIST_FOR_EACH_POP (b_lport, list_node, &lbinding->binding_lports) {
+        b_lport->lbinding = NULL;
+        binding_lport_delete(binding_lports, b_lport);
+    }
+
+    free(lbinding->name);
+    free(lbinding);
 }
 
-/* Processes new release/bind operations OVN ports.  For newly bound ports
- * it creates ofctrl seqno update requests that will be acked when
- * corresponding OVS flows have been installed.
- *
- * NOTE: Should be called only when valid SB and OVS transactions are
- * available.
+static void
+local_binding_delete(struct local_binding *lbinding,
+                     struct shash *local_bindings,
+                     struct shash *binding_lports,
+                     struct if_status_mgr *if_mgr)
+{
+    shash_find_and_delete(local_bindings, lbinding->name);
+    if_status_mgr_delete_iface(if_mgr, lbinding->name);
+    local_binding_destroy(lbinding, binding_lports);
+}
+
+/* Returns the primary binding lport if present in lbinding's
+ * binding lports list.  A binding lport is considered primary
+ * if binding lport's type is LP_VIF and the name matches
+ * with the 'lbinding'.
  */
-void
-binding_seqno_run(struct shash *local_bindings)
+static struct binding_lport *
+local_binding_get_primary_lport(struct local_binding *lbinding)
 {
-    const char *iface_id;
-    const char *iface_id_next;
+    if (!lbinding) {
+        return NULL;
+    }
 
-    SSET_FOR_EACH_SAFE (iface_id, iface_id_next, &binding_iface_released_set) {
-        struct shash_node *lb_node = shash_find(local_bindings, iface_id);
+    if (!ovs_list_is_empty(&lbinding->binding_lports)) {
+        struct binding_lport *b_lport = NULL;
+        b_lport = CONTAINER_OF(ovs_list_front(&lbinding->binding_lports),
+                               struct binding_lport, list_node);
 
-        /* If the local binding still exists (i.e., the OVS interface is
-         * still configured locally) then remove the external id and remove
-         * it from the in-flight seqno map.
-         */
-        if (lb_node) {
-            struct local_binding *lb = lb_node->data;
+        if (b_lport->type == LP_VIF &&
+            !strcmp(lbinding->name, b_lport->name)) {
+            return b_lport;
+        }
+    }
 
-            if (lb->iface && smap_get(&lb->iface->external_ids,
-                                      OVN_INSTALLED_EXT_ID)) {
-                ovsrec_interface_update_external_ids_delkey(
-                    lb->iface, OVN_INSTALLED_EXT_ID);
-            }
+    return NULL;
+}
+
+static struct binding_lport *
+local_binding_add_lport(struct shash *binding_lports,
+                        struct local_binding *lbinding,
+                        const struct sbrec_port_binding *pb,
+                        enum en_lport_type b_type)
+{
+    struct binding_lport *b_lport =
+        binding_lport_find(binding_lports, pb->logical_port);
+    bool add_to_lport_list = false;
+    if (!b_lport) {
+        b_lport = binding_lport_create(pb, lbinding, b_type);
+        binding_lport_add(binding_lports, b_lport);
+        add_to_lport_list = true;
+    } else if (b_lport->lbinding != lbinding) {
+        add_to_lport_list = true;
+        if (!ovs_list_is_empty(&b_lport->list_node)) {
+            ovs_list_remove(&b_lport->list_node);
         }
-        simap_find_and_delete(&binding_iface_seqno_map, iface_id);
-        sset_delete(&binding_iface_released_set,
-                    SSET_NODE_FROM_NAME(iface_id));
+        b_lport->lbinding = lbinding;
+        b_lport->type = b_type;
     }
 
-    bool new_ifaces = false;
-    uint32_t new_seqno = binding_iface_seqno + 1;
+    if (add_to_lport_list) {
+        if (b_type == LP_VIF) {
+            ovs_list_push_front(&lbinding->binding_lports, &b_lport->list_node);
+        } else {
+            ovs_list_push_back(&lbinding->binding_lports, &b_lport->list_node);
+        }
+    }
 
-    SSET_FOR_EACH_SAFE (iface_id, iface_id_next, &binding_iface_bound_set) {
-        struct shash_node *lb_node = shash_find(local_bindings, iface_id);
+    return b_lport;
+}
 
-        struct local_binding *lb = lb_node ? lb_node->data : NULL;
+/* This function handles the stale binding lports of 'lbinding' if 'lbinding'
+ * doesn't have a primary binding lport.
+ */
+static bool
+local_binding_handle_stale_binding_lports(struct local_binding *lbinding,
+                                          struct binding_ctx_in *b_ctx_in,
+                                          struct binding_ctx_out *b_ctx_out,
+                                          struct hmap *qos_map)
+{
+    /* Check if this lbinding has a primary binding_lport or not. */
+    struct binding_lport *p_lport = local_binding_get_primary_lport(lbinding);
+    if (p_lport) {
+        /* Nothing to be done. */
+        return true;
+    }
 
-        /* Make sure the binding is still complete, i.e., both SB port_binding
-         * and OVS interface still exist.
-         *
-         * If so, then this is a newly bound interface, make sure we reset the
-         * Port_Binding 'up' field and the OVS Interface 'external-id'.
-         */
-        if (lb && lb->pb && lb->iface) {
-            new_ifaces = true;
-
-            if (smap_get(&lb->iface->external_ids, OVN_INSTALLED_EXT_ID)) {
-                ovsrec_interface_update_external_ids_delkey(
-                    lb->iface, OVN_INSTALLED_EXT_ID);
-            }
-            if (lb->pb->n_up) {
-                bool up = false;
-                sbrec_port_binding_set_up(lb->pb, &up, 1);
-            }
-            simap_put(&binding_iface_seqno_map, lb->name, new_seqno);
+    bool handled = true;
+    struct binding_lport *b_lport, *next;
+    const struct sbrec_port_binding *pb;
+    LIST_FOR_EACH_SAFE (b_lport, next, list_node, &lbinding->binding_lports) {
+        /* Get the lport type again from the pb.  Its possible that the
+         * pb type has changed. */
+        enum en_lport_type pb_lport_type = get_lport_type(b_lport->pb);
+        if (b_lport->type == LP_VIRTUAL && pb_lport_type == LP_VIRTUAL) {
+            pb = b_lport->pb;
+            binding_lport_delete(&b_ctx_out->lbinding_data->lports,
+                                 b_lport);
+            handled = consider_virtual_lport(pb, b_ctx_in, b_ctx_out, qos_map);
+        } else if (b_lport->type == LP_CONTAINER &&
+                   pb_lport_type == LP_CONTAINER) {
+            /* For container lport, binding_lport is preserved so that when
+             * the parent port is created, it can be considered.
+             * consider_container_lport() creates the binding_lport for the parent
+             * port (with iface set to NULL). */
+            handled = consider_container_lport(b_lport->pb, b_ctx_in, b_ctx_out, qos_map);
+        } else {
+            /* This can happen when the lport type changes from one type
+             * to another. Eg. from normal lport to external.  Release the
+             * lport if it was claimed earlier and delete the b_lport. */
+            handled = release_binding_lport(b_ctx_in->chassis_rec, b_lport,
+                                            !b_ctx_in->ovnsb_idl_txn,
+                                            b_ctx_out);
+            binding_lport_delete(&b_ctx_out->lbinding_data->lports,
+                                 b_lport);
+        }
+
+        if (!handled) {
+            return false;
         }
-        sset_delete(&binding_iface_bound_set, SSET_NODE_FROM_NAME(iface_id));
     }
 
-    /* Request a seqno update when the flows for new interfaces have been
-     * installed in OVS.
-     */
-    if (new_ifaces) {
-        binding_iface_seqno = new_seqno;
-        ofctrl_seqno_update_create(binding_seq_type_pb_cfg, new_seqno);
+    return handled;
+}
+
+static struct binding_lport *
+binding_lport_create(const struct sbrec_port_binding *pb,
+                     struct local_binding *lbinding,
+                     enum en_lport_type type)
+{
+    struct binding_lport *b_lport = xzalloc(sizeof *b_lport);
+    b_lport->name = xstrdup(pb->logical_port);
+    b_lport->pb = pb;
+    b_lport->type = type;
+    b_lport->lbinding = lbinding;
+    ovs_list_init(&b_lport->list_node);
+
+    return b_lport;
+}
+
+static void
+binding_lport_add(struct shash *binding_lports, struct binding_lport *b_lport)
+{
+    shash_add(binding_lports, b_lport->pb->logical_port, b_lport);
+}
+
+static struct binding_lport *
+binding_lport_find(struct shash *binding_lports, const char *lport_name)
+{
+    if (!lport_name) {
+        return NULL;
     }
+
+    return shash_find_data(binding_lports, lport_name);
 }
 
-/* Processes ofctrl seqno ACKs for new bindings.  Sets the
- * 'OVN_INSTALLED_EXT_ID' external-id in the OVS interface and the
- * Port_Binding.up field for all ports for which OVS flows have been
- * installed.
+static void
+binding_lport_destroy(struct binding_lport *b_lport)
+{
+    if (!ovs_list_is_empty(&b_lport->list_node)) {
+        ovs_list_remove(&b_lport->list_node);
+    }
+
+    free(b_lport->name);
+    free(b_lport);
+}
+
+static void
+binding_lport_delete(struct shash *binding_lports,
+                     struct binding_lport *b_lport)
+{
+    shash_find_and_delete(binding_lports, b_lport->name);
+    binding_lport_destroy(b_lport);
+}
+
+static void
+binding_lport_set_up(struct binding_lport *b_lport, bool sb_readonly)
+{
+    if (sb_readonly || !b_lport || !b_lport->pb->n_up || b_lport->pb->up[0]) {
+        return;
+    }
+
+    bool up = true;
+    sbrec_port_binding_set_up(b_lport->pb, &up, 1);
+}
+
+static void
+binding_lport_set_down(struct binding_lport *b_lport, bool sb_readonly)
+{
+    if (sb_readonly || !b_lport || !b_lport->pb->n_up || !b_lport->pb->up[0]) {
+        return;
+    }
+
+    bool up = false;
+    sbrec_port_binding_set_up(b_lport->pb, &up, 1);
+}
+
+static const struct sbrec_port_binding *
+binding_lport_get_parent_pb(struct binding_lport *b_lport)
+{
+    if (!b_lport) {
+        return NULL;
+    }
+
+    if (b_lport->type == LP_VIF) {
+        return NULL;
+    }
+
+    struct local_binding *lbinding = b_lport->lbinding;
+    ovs_assert(lbinding);
+
+    struct binding_lport *parent_b_lport =
+        local_binding_get_primary_lport(lbinding);
+
+    return parent_b_lport ? parent_b_lport->pb : NULL;
+}
+
+/* This function checks and cleans up the 'b_lport' if it is
+ * not in the correct state.
+ *
+ * If the 'b_lport' type is LP_VIF, then its name and its lbinding->name
+ * should match.  Otherwise this should be cleaned up.
  *
- * NOTE: Should be called only when valid SB and OVS transactions are
- * available.
+ * If the 'b_lport' type is LP_CONTAINER, then its parent_port name should
+ * be the same as its lbinding's name.  Otherwise this should be
+ * cleaned up.
+ *
+ * If the 'b_lport' type is LP_VIRTUAL, then its virtual parent name
+ * should be the same as its lbinding's name.  Otherwise this
+ * should be cleaned up.
+ *
+ * If the 'b_lport' type is not LP_VIF, LP_CONTAINER or LP_VIRTUAL, it
+ * should be cleaned up.  This can happen if the CMS changes
+ * the port binding type.
  */
-void
-binding_seqno_install(struct shash *local_bindings)
+static struct binding_lport *
+binding_lport_check_and_cleanup(struct binding_lport *b_lport,
+                                struct shash *binding_lports)
 {
-    struct ofctrl_acked_seqnos *acked_seqnos =
-            ofctrl_acked_seqnos_get(binding_seq_type_pb_cfg);
-    struct simap_node *node;
-    struct simap_node *node_next;
+    bool cleanup_blport = false;
 
-    SIMAP_FOR_EACH_SAFE (node, node_next, &binding_iface_seqno_map) {
-        struct shash_node *lb_node = shash_find(local_bindings, node->name);
-
-        if (!lb_node) {
-            goto del_seqno;
-        }
+    if (!b_lport->lbinding) {
+        cleanup_blport = true;
+        goto cleanup;
+    }
 
-        struct local_binding *lb = lb_node->data;
-        if (!lb->pb || !lb->iface) {
-            goto del_seqno;
+    switch (b_lport->type) {
+    case LP_VIF:
+        if (strcmp(b_lport->name, b_lport->lbinding->name)) {
+            cleanup_blport = true;
         }
+        break;
 
-        if (!ofctrl_acked_seqnos_contains(acked_seqnos, node->data)) {
-            continue;
+    case LP_CONTAINER:
+        if (strcmp(b_lport->pb->parent_port, b_lport->lbinding->name)) {
+            cleanup_blport = true;
         }
+        break;
 
-        ovsrec_interface_update_external_ids_setkey(lb->iface,
-                                                    OVN_INSTALLED_EXT_ID,
-                                                    "true");
-        if (lb->pb->n_up) {
-            bool up = true;
-
-            sbrec_port_binding_set_up(lb->pb, &up, 1);
-            struct shash_node *child_node;
-            SHASH_FOR_EACH (child_node, &lb->children) {
-                struct local_binding *lb_child = child_node->data;
-                sbrec_port_binding_set_up(lb_child->pb, &up, 1);
-            }
+    case LP_VIRTUAL:
+        if (!b_lport->pb->virtual_parent ||
+            strcmp(b_lport->pb->virtual_parent, b_lport->lbinding->name)) {
+            cleanup_blport = true;
         }
+        break;
 
-del_seqno:
-        simap_delete(&binding_iface_seqno_map, node);
+    case LP_PATCH:
+    case LP_LOCALPORT:
+    case LP_VTEP:
+    case LP_L2GATEWAY:
+    case LP_L3GATEWAY:
+    case LP_CHASSISREDIRECT:
+    case LP_EXTERNAL:
+    case LP_LOCALNET:
+    case LP_REMOTE:
+    case LP_UNKNOWN:
+        cleanup_blport = true;
     }
 
-    ofctrl_acked_seqnos_destroy(acked_seqnos);
-}
+cleanup:
+    if (cleanup_blport) {
+        binding_lport_delete(binding_lports, b_lport);
+        return NULL;
+    }
 
-void
-binding_seqno_flush(void)
-{
-    simap_clear(&binding_iface_seqno_map);
+    return b_lport;
 }
diff --git a/controller/binding.h b/controller/binding.h
index c9ebef4b1..7a6495320 100644
--- a/controller/binding.h
+++ b/controller/binding.h
@@ -36,6 +36,8 @@ struct sbrec_chassis;
 struct sbrec_port_binding_table;
 struct sset;
 struct sbrec_port_binding;
+struct ds;
+struct if_status_mgr;
 
 struct binding_ctx_in {
     struct ovsdb_idl_txn *ovnsb_idl_txn;
@@ -56,7 +58,7 @@ struct binding_ctx_in {
 
 struct binding_ctx_out {
     struct hmap *local_datapaths;
-    struct shash *local_bindings;
+    struct local_binding_data *lbinding_data;
 
     /* sset of (potential) local lports. */
     struct sset *local_lports;
@@ -84,30 +86,26 @@ struct binding_ctx_out {
      * binding_handle_port_binding_changes) fills in for
      * the changed datapaths and port bindings. */
     struct hmap *tracked_dp_bindings;
-};
 
-enum local_binding_type {
-    BT_VIF,
-    BT_CONTAINER,
-    BT_VIRTUAL
+    struct if_status_mgr *if_mgr;
 };
 
-struct local_binding {
-    char *name;
-    enum local_binding_type type;
-    const struct ovsrec_interface *iface;
-    const struct sbrec_port_binding *pb;
-
-    /* shash of 'struct local_binding' representing children. */
-    struct shash children;
-    struct local_binding *parent;
+struct local_binding_data {
+    struct shash bindings;
+    struct shash lports;
 };
 
-static inline struct local_binding *
-local_binding_find(struct shash *local_bindings, const char *name)
-{
-    return shash_find_data(local_bindings, name);
-}
+void local_binding_data_init(struct local_binding_data *);
+void local_binding_data_destroy(struct local_binding_data *);
+
+const struct sbrec_port_binding *local_binding_get_primary_pb(
+    struct shash *local_bindings, const char *pb_name);
+bool local_binding_is_up(struct shash *local_bindings, const char *pb_name);
+bool local_binding_is_down(struct shash *local_bindings, const char *pb_name);
+void local_binding_set_up(struct shash *local_bindings, const char *pb_name,
+                          bool sb_readonly, bool ovs_readonly);
+void local_binding_set_down(struct shash *local_bindings, const char *pb_name,
+                            bool sb_readonly, bool ovs_readonly);
 
 /* Represents a tracked binding logical port. */
 struct tracked_binding_lport {
@@ -128,16 +126,11 @@ bool binding_cleanup(struct ovsdb_idl_txn *ovnsb_idl_txn,
                      const struct sbrec_port_binding_table *,
                      const struct sbrec_chassis *);
 
-void local_bindings_init(struct shash *local_bindings);
-void local_bindings_destroy(struct shash *local_bindings);
 bool binding_handle_ovs_interface_changes(struct binding_ctx_in *,
                                           struct binding_ctx_out *);
 bool binding_handle_port_binding_changes(struct binding_ctx_in *,
                                          struct binding_ctx_out *);
 void binding_tracked_dp_destroy(struct hmap *tracked_datapaths);
 
-void binding_init(void);
-void binding_seqno_run(struct shash *local_bindings);
-void binding_seqno_install(struct shash *local_bindings);
-void binding_seqno_flush(void);
+void binding_dump_local_bindings(struct local_binding_data *, struct ds *);
 #endif /* controller/binding.h */
diff --git a/controller/if-status.c b/controller/if-status.c
new file mode 100644
index 000000000..8d8c8d436
--- /dev/null
+++ b/controller/if-status.c
@@ -0,0 +1,415 @@
+/* Copyright (c) 2021, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "binding.h"
+#include "if-status.h"
+#include "ofctrl-seqno.h"
+
+#include "lib/hmapx.h"
+#include "lib/util.h"
+#include "openvswitch/vlog.h"
+
+VLOG_DEFINE_THIS_MODULE(if_status);
+
+/* This module implements an interface manager that maintains the state of
+ * the interfaces wrt. their flows being completely installed in OVS and
+ * their corresponding bindings being marked up/down.
+ *
+ * A state machine is maintained for each interface.
+ *
+ * Transitions are triggered between states by three types of events:
+ * A. Events received from the binding module:
+ * - interface is claimed: if_status_mgr_claim_iface()
+ * - interface is released: if_status_mgr_release_iface()
+ * - interface is deleted: if_status_mgr_delete_iface()
+ *
+ * B. At every iteration, based on SB/OVS updates, handled in
+ *    if_status_mgr_update():
+ * - an interface binding has been marked "up" both in the Southbound and OVS
+ *   databases.
+ * - an interface binding has been marked "down" both in the Southbound and OVS
+ *   databases.
+ * - new interface has been claimed.
+ *
+ * C. At every iteration, based on ofctrl_seqno updates, handled in
+ *    if_status_mgr_run():
+ * - the flows for a previously claimed interface have been installed in OVS.
+ */
+
+enum if_state {
+    OIF_CLAIMED,       /* Newly claimed interface. */
+    OIF_INSTALL_FLOWS, /* Already claimed interface for which flows are still
+                        * being installed.
+                        */
+    OIF_MARK_UP,       /* Interface with flows successfully installed in OVS
+                        * but not yet marked "up" in the binding module (in
+                        * SB and OVS databases).
+                        */
+    OIF_MARK_DOWN,     /* Released interface but not yet marked "down" in the
+                        * binding module (in SB and/or OVS databases).
+                        */
+    OIF_INSTALLED,     /* Interface flows programmed in OVS and binding marked
+                        * "up" in the binding module.
+                        */
+    OIF_MAX,
+};
+
+static const char *if_state_names[] = {
+    [OIF_CLAIMED]       = "CLAIMED",
+    [OIF_INSTALL_FLOWS] = "INSTALL_FLOWS",
+    [OIF_MARK_UP]       = "MARK_UP",
+    [OIF_MARK_DOWN]     = "MARK_DOWN",
+    [OIF_INSTALLED]     = "INSTALLED",
+};
+
+struct ovs_iface {
+    char *id;               /* Extracted from OVS external_ids.iface_id. */
+    enum if_state state;    /* State of the interface in the state machine. */
+    uint32_t install_seqno; /* Seqno at which this interface is expected to
+                             * be fully programmed in OVS.  Only used in state
+                             * OIF_INSTALL_FLOWS.
+                             */
+};
+
+/* State machine manager for all local OVS interfaces. */
+struct if_status_mgr {
+    /* All local interfaces, mapping from 'iface-id' to 'struct ovs_iface'. */
+    struct shash ifaces;
+
+    /* All local interfaces, stored per state. */
+    struct hmapx ifaces_per_state[OIF_MAX];
+
+    /* Registered ofctrl seqno type for port_binding flow installation. */
+    size_t iface_seq_type_pb_cfg;
+
+    /* Interface specific seqno to be acked by ofctrl when flows for new
+     * interfaces have been installed.
+     */
+    uint32_t iface_seqno;
+};
+
+static struct ovs_iface *ovs_iface_create(struct if_status_mgr *,
+                                          const char *iface_id,
+                                          enum if_state );
+static void ovs_iface_destroy(struct if_status_mgr *, struct ovs_iface *);
+static void ovs_iface_set_state(struct if_status_mgr *, struct ovs_iface *,
+                                enum if_state);
+
+static void if_status_mgr_update_bindings(
+    struct if_status_mgr *mgr, struct local_binding_data *binding_data,
+    bool sb_readonly, bool ovs_readonly);
+
+struct if_status_mgr *
+if_status_mgr_create(void)
+{
+    struct if_status_mgr *mgr = xzalloc(sizeof *mgr);
+
+    mgr->iface_seq_type_pb_cfg = ofctrl_seqno_add_type();
+    for (size_t i = 0; i < ARRAY_SIZE(mgr->ifaces_per_state); i++) {
+        hmapx_init(&mgr->ifaces_per_state[i]);
+    }
+    shash_init(&mgr->ifaces);
+    return mgr;
+}
+
+void
+if_status_mgr_clear(struct if_status_mgr *mgr)
+{
+    struct shash_node *node_next;
+    struct shash_node *node;
+
+    SHASH_FOR_EACH_SAFE (node, node_next, &mgr->ifaces) {
+        ovs_iface_destroy(mgr, node->data);
+    }
+    ovs_assert(shash_is_empty(&mgr->ifaces));
+
+    for (size_t i = 0; i < ARRAY_SIZE(mgr->ifaces_per_state); i++) {
+        ovs_assert(hmapx_is_empty(&mgr->ifaces_per_state[i]));
+    }
+}
+
+void
+if_status_mgr_destroy(struct if_status_mgr *mgr)
+{
+    if_status_mgr_clear(mgr);
+    shash_destroy(&mgr->ifaces);
+    for (size_t i = 0; i < ARRAY_SIZE(mgr->ifaces_per_state); i++) {
+        hmapx_destroy(&mgr->ifaces_per_state[i]);
+    }
+    free(mgr);
+}
+
+void
+if_status_mgr_claim_iface(struct if_status_mgr *mgr, const char *iface_id)
+{
+    struct ovs_iface *iface = shash_find_data(&mgr->ifaces, iface_id);
+
+    if (!iface) {
+        iface = ovs_iface_create(mgr, iface_id, OIF_CLAIMED);
+    }
+
+    switch (iface->state) {
+    case OIF_CLAIMED:
+    case OIF_INSTALL_FLOWS:
+    case OIF_MARK_UP:
+        /* Nothing to do here. */
+        break;
+    case OIF_INSTALLED:
+    case OIF_MARK_DOWN:
+        ovs_iface_set_state(mgr, iface, OIF_CLAIMED);
+        break;
+    case OIF_MAX:
+        OVS_NOT_REACHED();
+        break;
+    }
+}
+
+void
+if_status_mgr_release_iface(struct if_status_mgr *mgr, const char *iface_id)
+{
+    struct ovs_iface *iface = shash_find_data(&mgr->ifaces, iface_id);
+
+    if (!iface) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+        VLOG_WARN_RL(&rl, "Trying to release unknown interface %s", iface_id);
+        return;
+    }
+
+    switch (iface->state) {
+    case OIF_CLAIMED:
+    case OIF_INSTALL_FLOWS:
+        /* Not yet fully installed interfaces can be safely deleted. */
+        ovs_iface_destroy(mgr, iface);
+        break;
+    case OIF_MARK_UP:
+    case OIF_INSTALLED:
+        /* Properly mark interfaces "down" if their flows were already
+         * programmed in OVS.
+         */
+        ovs_iface_set_state(mgr, iface, OIF_MARK_DOWN);
+        break;
+    case OIF_MARK_DOWN:
+        /* Nothing to do here. */
+        break;
+    case OIF_MAX:
+        OVS_NOT_REACHED();
+        break;
+    }
+}
+
+void
+if_status_mgr_delete_iface(struct if_status_mgr *mgr, const char *iface_id)
+{
+    struct ovs_iface *iface = shash_find_data(&mgr->ifaces, iface_id);
+
+    if (!iface) {
+        return;
+    }
+
+    switch (iface->state) {
+    case OIF_CLAIMED:
+    case OIF_INSTALL_FLOWS:
+        /* Not yet fully installed interfaces can be safely deleted. */
+        ovs_iface_destroy(mgr, iface);
+        break;
+    case OIF_MARK_UP:
+    case OIF_INSTALLED:
+        /* Properly mark interfaces "down" if their flows were already
+         * programmed in OVS.
+         */
+        ovs_iface_set_state(mgr, iface, OIF_MARK_DOWN);
+        break;
+    case OIF_MARK_DOWN:
+        /* Nothing to do here. */
+        break;
+    case OIF_MAX:
+        OVS_NOT_REACHED();
+        break;
+    }
+}
+
+void
+if_status_mgr_update(struct if_status_mgr *mgr,
+                     struct local_binding_data *binding_data)
+{
+    if (!binding_data) {
+        return;
+    }
+
+    struct shash *bindings = &binding_data->bindings;
+    struct hmapx_node *node_next;
+    struct hmapx_node *node;
+
+    /* Move all interfaces that have been confirmed "up" by the binding module,
+     * from OIF_MARK_UP to OIF_INSTALLED.
+     */
+    HMAPX_FOR_EACH_SAFE (node, node_next,
+                         &mgr->ifaces_per_state[OIF_MARK_UP]) {
+        struct ovs_iface *iface = node->data;
+
+        if (local_binding_is_up(bindings, iface->id)) {
+            ovs_iface_set_state(mgr, iface, OIF_INSTALLED);
+        }
+    }
+
+    /* Cleanup all interfaces that have been confirmed "down" by the binding
+     * module.
+     */
+    HMAPX_FOR_EACH_SAFE (node, node_next,
+                         &mgr->ifaces_per_state[OIF_MARK_DOWN]) {
+        struct ovs_iface *iface = node->data;
+
+        if (local_binding_is_down(bindings, iface->id)) {
+            ovs_iface_destroy(mgr, iface);
+        }
+    }
+
+    /* Register for a notification about flows being installed in OVS for all
+     * newly claimed interfaces.
+     *
+     * Move them from OIF_CLAIMED to OIF_INSTALL_FLOWS.
+     */
+    bool new_ifaces = false;
+    HMAPX_FOR_EACH_SAFE (node, node_next,
+                         &mgr->ifaces_per_state[OIF_CLAIMED]) {
+        struct ovs_iface *iface = node->data;
+
+        ovs_iface_set_state(mgr, iface, OIF_INSTALL_FLOWS);
+        iface->install_seqno = mgr->iface_seqno + 1;
+        new_ifaces = true;
+    }
+
+    /* Request a seqno update when the flows for new interfaces have been
+     * installed in OVS.
+     */
+    if (new_ifaces) {
+        mgr->iface_seqno++;
+        ofctrl_seqno_update_create(mgr->iface_seq_type_pb_cfg,
+                                   mgr->iface_seqno);
+        VLOG_DBG("Seqno requested: %"PRIu32, mgr->iface_seqno);
+    }
+}
+
+void
+if_status_mgr_run(struct if_status_mgr *mgr,
+                  struct local_binding_data *binding_data,
+                  bool sb_readonly, bool ovs_readonly)
+{
+    struct ofctrl_acked_seqnos *acked_seqnos =
+            ofctrl_acked_seqnos_get(mgr->iface_seq_type_pb_cfg);
+    struct hmapx_node *node_next;
+    struct hmapx_node *node;
+
+    /* Move interfaces from state OIF_INSTALL_FLOWS to OIF_MARK_UP if a
+     * notification has been received aabout their flows being installed
+     * in OVS.
+     */
+    HMAPX_FOR_EACH_SAFE (node, node_next,
+                         &mgr->ifaces_per_state[OIF_INSTALL_FLOWS]) {
+        struct ovs_iface *iface = node->data;
+
+        if (!ofctrl_acked_seqnos_contains(acked_seqnos,
+                                          iface->install_seqno)) {
+            continue;
+        }
+        ovs_iface_set_state(mgr, iface, OIF_MARK_UP);
+    }
+    ofctrl_acked_seqnos_destroy(acked_seqnos);
+
+    /* Update binding states. */
+    if_status_mgr_update_bindings(mgr, binding_data, sb_readonly,
+                                  ovs_readonly);
+}
+
+static struct ovs_iface *
+ovs_iface_create(struct if_status_mgr *mgr, const char *iface_id,
+                 enum if_state state)
+{
+    struct ovs_iface *iface = xzalloc(sizeof *iface);
+
+    VLOG_DBG("Interface %s create.", iface->id);
+    iface->id = xstrdup(iface_id);
+    shash_add(&mgr->ifaces, iface_id, iface);
+    ovs_iface_set_state(mgr, iface, state);
+    return iface;
+}
+
+static void
+ovs_iface_destroy(struct if_status_mgr *mgr, struct ovs_iface *iface)
+{
+    VLOG_DBG("Interface %s destroy: state %s", iface->id,
+             if_state_names[iface->state]);
+    hmapx_find_and_delete(&mgr->ifaces_per_state[iface->state], iface);
+    shash_find_and_delete(&mgr->ifaces, iface->id);
+    free(iface->id);
+    free(iface);
+}
+
+static void
+ovs_iface_set_state(struct if_status_mgr *mgr, struct ovs_iface *iface,
+                    enum if_state state)
+{
+    VLOG_DBG("Interface %s set state: old %s, new %s", iface->id,
+             if_state_names[iface->state],
+             if_state_names[state]);
+
+    hmapx_find_and_delete(&mgr->ifaces_per_state[iface->state], iface);
+    iface->state = state;
+    hmapx_add(&mgr->ifaces_per_state[iface->state], iface);
+    iface->install_seqno = 0;
+}
+
+static void
+if_status_mgr_update_bindings(struct if_status_mgr *mgr,
+                              struct local_binding_data *binding_data,
+                              bool sb_readonly, bool ovs_readonly)
+{
+    if (!binding_data) {
+        return;
+    }
+
+    struct shash *bindings = &binding_data->bindings;
+    struct hmapx_node *node;
+
+    /* Notify the binding module to set "down" all bindings that are still
+     * in the process of being installed in OVS, i.e., are not yet instsalled.
+     */
+    HMAPX_FOR_EACH (node, &mgr->ifaces_per_state[OIF_INSTALL_FLOWS]) {
+        struct ovs_iface *iface = node->data;
+
+        local_binding_set_down(bindings, iface->id, sb_readonly, ovs_readonly);
+    }
+
+    /* Notifiy the binding module to set "up" all bindings that have had
+     * their flows installed but are not yet marked "up" in the binding
+     * module.
+     */
+    HMAPX_FOR_EACH (node, &mgr->ifaces_per_state[OIF_MARK_UP]) {
+        struct ovs_iface *iface = node->data;
+
+        local_binding_set_up(bindings, iface->id, sb_readonly, ovs_readonly);
+    }
+
+    /* Notify the binding module to set "down" all bindings that have been
+     * released but are not yet marked as "down" in the binding module.
+     */
+    HMAPX_FOR_EACH (node, &mgr->ifaces_per_state[OIF_MARK_DOWN]) {
+        struct ovs_iface *iface = node->data;
+
+        local_binding_set_down(bindings, iface->id, sb_readonly, ovs_readonly);
+    }
+}
diff --git a/controller/if-status.h b/controller/if-status.h
new file mode 100644
index 000000000..51fe7c684
--- /dev/null
+++ b/controller/if-status.h
@@ -0,0 +1,37 @@
+/* Copyright (c) 2021, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef IF_STATUS_H
+#define IF_STATUS_H 1
+
+#include "openvswitch/shash.h"
+
+#include "binding.h"
+
+struct if_status_mgr;
+
+struct if_status_mgr *if_status_mgr_create(void);
+void if_status_mgr_clear(struct if_status_mgr *);
+void if_status_mgr_destroy(struct if_status_mgr *);
+
+void if_status_mgr_claim_iface(struct if_status_mgr *, const char *iface_id);
+void if_status_mgr_release_iface(struct if_status_mgr *, const char *iface_id);
+void if_status_mgr_delete_iface(struct if_status_mgr *, const char *iface_id);
+
+void if_status_mgr_update(struct if_status_mgr *, struct local_binding_data *);
+void if_status_mgr_run(struct if_status_mgr *mgr, struct local_binding_data *,
+                       bool sb_readonly, bool ovs_readonly);
+
+# endif /* controller/if-status.h */
diff --git a/controller/ovn-controller.8.xml b/controller/ovn-controller.8.xml
index 51c0c372c..8886df568 100644
--- a/controller/ovn-controller.8.xml
+++ b/controller/ovn-controller.8.xml
@@ -578,6 +578,28 @@
         Displays logical flow cache statistics: enabled/disabled, per cache
         type entry counts.
       </dd>
+
+      <dt><code>inc-engine/show-stats</code></dt>
+      <dd>
+        Display <code>ovn-controller</code> engine counters. For each engine
+        node the following counters have been added:
+        <ul>
+          <li>
+            <code>recompute</code>
+          </li>
+          <li>
+            <code>compute</code>
+          </li>
+          <li>
+            <code>abort</code>
+          </li>
+        </ul>
+      </dd>
+
+      <dt><code>inc-engine/clear-stats</code></dt>
+      <dd>
+        Reset <code>ovn-controller</code> engine counters.
+      </dd>
       </dl>
     </p>
 
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index 5dd643f52..b4eee4848 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -33,6 +33,7 @@
 #include "openvswitch/dynamic-string.h"
 #include "encaps.h"
 #include "fatal-signal.h"
+#include "if-status.h"
 #include "ip-mcast.h"
 #include "openvswitch/hmap.h"
 #include "lflow.h"
@@ -81,6 +82,7 @@ static unixctl_cb_func cluster_state_reset_cmd;
 static unixctl_cb_func debug_pause_execution;
 static unixctl_cb_func debug_resume_execution;
 static unixctl_cb_func debug_status_execution;
+static unixctl_cb_func debug_dump_local_bindings;
 static unixctl_cb_func lflow_cache_flush_cmd;
 static unixctl_cb_func lflow_cache_show_stats_cmd;
 static unixctl_cb_func debug_delay_nb_cfg_report;
@@ -102,6 +104,7 @@ OVS_NO_RETURN static void usage(void);
 
 struct controller_engine_ctx {
     struct lflow_cache *lflow_cache;
+    struct if_status_mgr *if_mgr;
 };
 
 /* Pending packet to be injected into connected OVS. */
@@ -258,23 +261,15 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
                                                    uuid);
         }
 
-        /* Updating conditions to receive logical flows that references
-         * datapath groups containing local datapaths. */
-        const struct sbrec_logical_dp_group *group;
-        SBREC_LOGICAL_DP_GROUP_FOR_EACH (group, ovnsb_idl) {
-            struct uuid *uuid = CONST_CAST(struct uuid *,
-                                           &group->header_.uuid);
-            size_t i;
-
-            for (i = 0; i < group->n_datapaths; i++) {
-                if (get_local_datapath(local_datapaths,
-                                       group->datapaths[i]->tunnel_key)) {
-                    sbrec_logical_flow_add_clause_logical_dp_group(
-                        &lf, OVSDB_F_EQ, uuid);
-                    break;
-                }
-            }
-        }
+        /* Datapath groups are immutable, which means a new group record is
+         * created when a datapath is added to a group.  The logical flows
+         * referencing a datapath group are also updated in such cases but the
+         * new group UUID is not known by ovn-controller until the SB update
+         * is received.  To avoid unnecessarily removing and adding lflows
+         * that reference datapath groups, set the monitor condition to always
+         * request all of them.
+         */
+        sbrec_logical_flow_add_clause_logical_dp_group(&lf, OVSDB_F_NE, NULL);
     }
 
 out:;
@@ -420,6 +415,10 @@ process_br_int(struct ovsdb_idl_txn *ovs_idl_txn,
         if (datapath_type && strcmp(br_int->datapath_type, datapath_type)) {
             ovsrec_bridge_set_datapath_type(br_int, datapath_type);
         }
+        if (!br_int->fail_mode || strcmp(br_int->fail_mode, "secure")) {
+            ovsrec_bridge_set_fail_mode(br_int, "secure");
+            VLOG_WARN("Integration bridge fail-mode changed to 'secure'.");
+        }
     }
     return br_int;
 }
@@ -1003,6 +1002,7 @@ en_ofctrl_is_connected_cleanup(void *data OVS_UNUSED)
 static void
 en_ofctrl_is_connected_run(struct engine_node *node, void *data)
 {
+    struct controller_engine_ctx *ctrl_ctx = engine_get_context()->client_ctx;
     struct ed_type_ofctrl_is_connected *of_data = data;
     if (of_data->connected != ofctrl_is_connected()) {
         of_data->connected = !of_data->connected;
@@ -1010,7 +1010,7 @@ en_ofctrl_is_connected_run(struct engine_node *node, void *data)
         /* Flush ofctrl seqno requests when the ofctrl connection goes down. */
         if (!of_data->connected) {
             ofctrl_seqno_flush();
-            binding_seqno_flush();
+            if_status_mgr_clear(ctrl_ctx->if_mgr);
         }
         engine_set_node_state(node, EN_UPDATED);
         return;
@@ -1182,8 +1182,7 @@ struct ed_type_runtime_data {
     /* Contains "struct local_datapath" nodes. */
     struct hmap local_datapaths;
 
-    /* Contains "struct local_binding" nodes. */
-    struct shash local_bindings;
+    struct local_binding_data lbinding_data;
 
     /* Contains the name of each logical port resident on the local
      * hypervisor.  These logical ports include the VIFs (and their child
@@ -1222,9 +1221,9 @@ struct ed_type_runtime_data {
  * |                      | Interface and Port Binding changes store the    |
  * | @tracked_dp_bindings | changed datapaths (datapaths added/removed from |
  * |                      | local_datapaths) and changed port bindings      |
- * |                      | (added/updated/deleted in 'local_bindings').    |
+ * |                      | (added/updated/deleted in 'lbinding_data').    |
  * |                      | So any changes to the runtime data -            |
- * |                      | local_datapaths and local_bindings is captured  |
+ * |                      | local_datapaths and lbinding_data is captured  |
  * |                      | here.                                           |
  *  ------------------------------------------------------------------------
  * |                      | This is a bool which represents if the runtime  |
@@ -1251,7 +1250,7 @@ struct ed_type_runtime_data {
  *
  *  ---------------------------------------------------------------------
  * | local_datapaths  | The changes to these runtime data is captured in |
- * | local_bindings   | the @tracked_dp_bindings indirectly and hence it |
+ * | lbinding_data   | the @tracked_dp_bindings indirectly and hence it |
  * | local_lport_ids  | is not tracked explicitly.                       |
  *  ---------------------------------------------------------------------
  * | local_iface_ids  | This is used internally within the runtime data  |
@@ -1294,7 +1293,7 @@ en_runtime_data_init(struct engine_node *node OVS_UNUSED,
     sset_init(&data->active_tunnels);
     sset_init(&data->egress_ifaces);
     smap_init(&data->local_iface_ids);
-    local_bindings_init(&data->local_bindings);
+    local_binding_data_init(&data->lbinding_data);
 
     /* Init the tracked data. */
     hmap_init(&data->tracked_dp_bindings);
@@ -1322,7 +1321,7 @@ en_runtime_data_cleanup(void *data)
         free(cur_node);
     }
     hmap_destroy(&rt_data->local_datapaths);
-    local_bindings_destroy(&rt_data->local_bindings);
+    local_binding_data_destroy(&rt_data->lbinding_data);
     hmapx_destroy(&rt_data->ct_updated_datapaths);
 }
 
@@ -1383,6 +1382,8 @@ init_binding_ctx(struct engine_node *node,
                 engine_get_input("SB_port_binding", node),
                 "datapath");
 
+    struct controller_engine_ctx *ctrl_ctx = engine_get_context()->client_ctx;
+
     b_ctx_in->ovnsb_idl_txn = engine_get_context()->ovnsb_idl_txn;
     b_ctx_in->ovs_idl_txn = engine_get_context()->ovs_idl_txn;
     b_ctx_in->sbrec_datapath_binding_by_key = sbrec_datapath_binding_by_key;
@@ -1405,10 +1406,10 @@ init_binding_ctx(struct engine_node *node,
     b_ctx_out->local_lport_ids_changed = false;
     b_ctx_out->non_vif_ports_changed = false;
     b_ctx_out->egress_ifaces = &rt_data->egress_ifaces;
-    b_ctx_out->local_bindings = &rt_data->local_bindings;
+    b_ctx_out->lbinding_data = &rt_data->lbinding_data;
     b_ctx_out->local_iface_ids = &rt_data->local_iface_ids;
     b_ctx_out->tracked_dp_bindings = NULL;
-    b_ctx_out->local_lports_changed = NULL;
+    b_ctx_out->if_mgr = ctrl_ctx->if_mgr;
 }
 
 static void
@@ -1449,7 +1450,7 @@ en_runtime_data_run(struct engine_node *node, void *data)
             free(cur_node);
         }
         hmap_clear(local_datapaths);
-        local_bindings_destroy(&rt_data->local_bindings);
+        local_binding_data_destroy(&rt_data->lbinding_data);
         sset_destroy(local_lports);
         sset_destroy(local_lport_ids);
         sset_destroy(active_tunnels);
@@ -1460,7 +1461,7 @@ en_runtime_data_run(struct engine_node *node, void *data)
         sset_init(active_tunnels);
         sset_init(&rt_data->egress_ifaces);
         smap_init(&rt_data->local_iface_ids);
-        local_bindings_init(&rt_data->local_bindings);
+        local_binding_data_init(&rt_data->lbinding_data);
         hmapx_clear(&rt_data->ct_updated_datapaths);
     }
 
@@ -1715,6 +1716,7 @@ en_physical_flow_changes_run(struct engine_node *node, void *data)
 {
     struct ed_type_pfc_data *pfc_tdata = data;
     pfc_tdata->recompute_physical_flows = true;
+    pfc_tdata->ovs_ifaces_changed = true;
     engine_set_node_state(node, EN_UPDATED);
 }
 
@@ -1822,7 +1824,7 @@ static void init_physical_ctx(struct engine_node *node,
     p_ctx->local_lports = &rt_data->local_lports;
     p_ctx->ct_zones = ct_zones;
     p_ctx->mff_ovn_geneve = ed_mff_ovn_geneve->mff_ovn_geneve;
-    p_ctx->local_bindings = &rt_data->local_bindings;
+    p_ctx->local_bindings = &rt_data->lbinding_data.bindings;
     p_ctx->ct_updated_datapaths = &rt_data->ct_updated_datapaths;
 }
 
@@ -2448,7 +2450,6 @@ main(int argc, char *argv[])
     /* Register ofctrl seqno types. */
     ofctrl_seq_type_nb_cfg = ofctrl_seqno_add_type();
 
-    binding_init();
     patch_init();
     pinctrl_init();
     lflow_init();
@@ -2685,7 +2686,8 @@ main(int argc, char *argv[])
         engine_get_internal_data(&en_flow_output);
     struct ed_type_ct_zones *ct_zones_data =
         engine_get_internal_data(&en_ct_zones);
-    struct ed_type_runtime_data *runtime_data = NULL;
+    struct ed_type_runtime_data *runtime_data =
+        engine_get_internal_data(&en_runtime_data);
 
     ofctrl_init(&flow_output_data->group_table,
                 &flow_output_data->meter_table,
@@ -2738,13 +2740,19 @@ main(int argc, char *argv[])
     unixctl_command_register("debug/delay-nb-cfg-report", "SECONDS", 1, 1,
                              debug_delay_nb_cfg_report, &delay_nb_cfg_report);
 
+    unixctl_command_register("debug/dump-local-bindings", "", 0, 0,
+                             debug_dump_local_bindings,
+                             &runtime_data->lbinding_data);
+
     unsigned int ovs_cond_seqno = UINT_MAX;
     unsigned int ovnsb_cond_seqno = UINT_MAX;
     unsigned int ovnsb_expected_cond_seqno = UINT_MAX;
 
     struct controller_engine_ctx ctrl_engine_ctx = {
         .lflow_cache = lflow_cache_create(),
+        .if_mgr = if_status_mgr_create(),
     };
+    struct if_status_mgr *if_mgr = ctrl_engine_ctx.if_mgr;
 
     char *ovn_version = ovn_get_internal_version();
     VLOG_INFO("OVN internal version is : [%s]", ovn_version);
@@ -2954,9 +2962,10 @@ main(int argc, char *argv[])
                                                        ovnsb_idl_loop.idl),
                                               ovnsb_cond_seqno,
                                               ovnsb_expected_cond_seqno));
-                    if (runtime_data && ovs_idl_txn && ovnsb_idl_txn) {
-                        binding_seqno_run(&runtime_data->local_bindings);
-                    }
+
+                    struct local_binding_data *binding_data =
+                        runtime_data ? &runtime_data->lbinding_data : NULL;
+                    if_status_mgr_update(if_mgr, binding_data);
 
                     flow_output_data = engine_get_data(&en_flow_output);
                     if (flow_output_data && ct_zones_data) {
@@ -2967,9 +2976,8 @@ main(int argc, char *argv[])
                                    engine_node_changed(&en_flow_output));
                     }
                     ofctrl_seqno_run(ofctrl_get_cur_cfg());
-                    if (runtime_data && ovs_idl_txn && ovnsb_idl_txn) {
-                        binding_seqno_install(&runtime_data->local_bindings);
-                    }
+                    if_status_mgr_run(if_mgr, binding_data, !ovnsb_idl_txn,
+                                      !ovs_idl_txn);
                 }
 
             }
@@ -3135,6 +3143,7 @@ loop_done:
     ofctrl_destroy();
     pinctrl_destroy();
     patch_destroy();
+    if_status_mgr_destroy(if_mgr);
 
     ovsdb_idl_loop_destroy(&ovs_idl_loop);
     ovsdb_idl_loop_destroy(&ovnsb_idl_loop);
@@ -3408,3 +3417,13 @@ debug_delay_nb_cfg_report(struct unixctl_conn *conn, int argc OVS_UNUSED,
         unixctl_command_reply(conn, "no delay for nb_cfg report.");
     }
 }
+
+static void
+debug_dump_local_bindings(struct unixctl_conn *conn, int argc OVS_UNUSED,
+                          const char *argv[] OVS_UNUSED, void *local_bindings)
+{
+    struct ds binding_data = DS_EMPTY_INITIALIZER;
+    binding_dump_local_bindings(local_bindings, &binding_data);
+    unixctl_command_reply(conn, ds_cstr(&binding_data));
+    ds_destroy(&binding_data);
+}
diff --git a/controller/physical.c b/controller/physical.c
index fa5d0d692..c7090b351 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -1160,6 +1160,11 @@ consider_port_binding(struct ovsdb_idl_index *sbrec_port_binding_by_name,
 
         load_logical_ingress_metadata(binding, &zone_ids, ofpacts_p);
 
+        if (!strcmp(binding->type, "localport")) {
+            /* mark the packet as incoming from a localport */
+            put_load(1, MFF_LOG_FLAGS, MLF_LOCALPORT_BIT, 1, ofpacts_p);
+        }
+
         /* Resubmit to first logical ingress pipeline table. */
         put_resubmit(OFTABLE_LOG_INGRESS_PIPELINE, ofpacts_p);
         ofctrl_add_flow(flow_table, OFTABLE_PHY_TO_LOG,
@@ -1219,6 +1224,24 @@ consider_port_binding(struct ovsdb_idl_index *sbrec_port_binding_by_name,
                                               ofport, flow_table);
         }
 
+        /* Table 39, priority 160.
+         * =======================
+         *
+         * Do not forward local traffic from a localport to a localnet port.
+         */
+        if (!strcmp(binding->type, "localnet")) {
+            /* do not forward traffic from localport to localnet port */
+            match_init_catchall(&match);
+            ofpbuf_clear(ofpacts_p);
+            match_set_metadata(&match, htonll(dp_key));
+            match_set_reg(&match, MFF_LOG_OUTPORT - MFF_REG0, port_key);
+            match_set_reg_masked(&match, MFF_LOG_FLAGS - MFF_REG0,
+                                 MLF_LOCALPORT, MLF_LOCALPORT);
+            ofctrl_add_flow(flow_table, OFTABLE_CHECK_LOOPBACK, 160,
+                            binding->header_.uuid.parts[0], &match,
+                            ofpacts_p, &binding->header_.uuid);
+        }
+
     } else if (!tun && !is_ha_remote) {
         /* Remote port connected by localnet port */
         /* Table 33, priority 100.
@@ -1839,20 +1862,29 @@ physical_handle_ovs_iface_changes(struct physical_ctx *p_ctx,
             continue;
         }
 
-        const struct local_binding *lb =
-            local_binding_find(p_ctx->local_bindings, iface_id);
-
-        if (!lb || !lb->pb) {
-            continue;
+        const struct sbrec_port_binding *lb_pb =
+            local_binding_get_primary_pb(p_ctx->local_bindings, iface_id);
+        if (!lb_pb) {
+            /* For regular VIFs (e.g. lsp) the upcoming port-binding update
+             * will remove lfows related to the unclaimed ovs port.
+             * Localport is a special case and it needs to be managed here
+             * since the port is not binded and otherwise the related lfows
+             * will not be cleared removing the ovs port.
+             */
+            lb_pb = lport_lookup_by_name(p_ctx->sbrec_port_binding_by_name,
+                                         iface_id);
+            if (!lb_pb || strcmp(lb_pb->type, "localport")) {
+                continue;
+            }
         }
 
         int64_t ofport = iface_rec->n_ofport ? *iface_rec->ofport : 0;
         if (ovsrec_interface_is_deleted(iface_rec)) {
-            ofctrl_remove_flows(flow_table, &lb->pb->header_.uuid);
+            ofctrl_remove_flows(flow_table, &lb_pb->header_.uuid);
             simap_find_and_delete(&localvif_to_ofport, iface_id);
         } else {
             if (!ovsrec_interface_is_new(iface_rec)) {
-                ofctrl_remove_flows(flow_table, &lb->pb->header_.uuid);
+                ofctrl_remove_flows(flow_table, &lb_pb->header_.uuid);
             }
 
             simap_put(&localvif_to_ofport, iface_id, ofport);
@@ -1860,7 +1892,7 @@ physical_handle_ovs_iface_changes(struct physical_ctx *p_ctx,
                                   p_ctx->mff_ovn_geneve, p_ctx->ct_zones,
                                   p_ctx->active_tunnels,
                                   p_ctx->local_datapaths,
-                                  lb->pb, p_ctx->chassis,
+                                  lb_pb, p_ctx->chassis,
                                   flow_table, &ofpacts);
         }
     }
diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index b42288ea5..523a45b9a 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -4240,6 +4240,12 @@ send_garp_rarp_update(struct ovsdb_idl_txn *ovnsb_idl_txn,
                       struct shash *nat_addresses)
 {
     volatile struct garp_rarp_data *garp_rarp = NULL;
+
+    /* Skip localports as they don't need to be announced */
+    if (!strcmp(binding_rec->type, "localport")) {
+        return;
+    }
+
     /* Update GARP for NAT IP if it exists.  Consider port bindings with type
      * "l3gateway" for logical switch ports attached to gateway routers, and
      * port bindings with type "patch" for logical switch ports attached to
diff --git a/debian/changelog b/debian/changelog
index 51f9bcc91..25a04f8ae 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ovn (21.03.1-1) unstable; urgency=low
+
+   * New upstream version
+
+ -- OVN team <dev@openvswitch.org>  Fri, 12 Mar 2021 12:00:00 -0500
+
 ovn (21.03.0-1) unstable; urgency=low
 
    * New upstream version
diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
index 017176f98..ef97117b9 100644
--- a/include/ovn/logical-fields.h
+++ b/include/ovn/logical-fields.h
@@ -66,6 +66,8 @@ enum mff_log_flags_bits {
     MLF_LOOKUP_MAC_BIT = 6,
     MLF_LOOKUP_LB_HAIRPIN_BIT = 7,
     MLF_LOOKUP_FDB_BIT = 8,
+    MLF_SKIP_SNAT_FOR_LB_BIT = 9,
+    MLF_LOCALPORT_BIT = 10,
 };
 
 /* MFF_LOG_FLAGS_REG flag assignments */
@@ -102,6 +104,13 @@ enum mff_log_flags {
 
     /* Indicate that the lookup in the fdb table was successful. */
     MLF_LOOKUP_FDB = (1 << MLF_LOOKUP_FDB_BIT),
+
+    /* Indicate that a packet must not SNAT in the gateway router when
+     * load-balancing has taken place. */
+    MLF_SKIP_SNAT_FOR_LB = (1 << MLF_SKIP_SNAT_FOR_LB_BIT),
+
+    /* Indicate the packet has been received from a localport */
+    MLF_LOCALPORT = (1 << MLF_LOCALPORT_BIT),
 };
 
 /* OVN logical fields
diff --git a/lib/expr.c b/lib/expr.c
index f061a8fbe..7b3d3ddb3 100644
--- a/lib/expr.c
+++ b/lib/expr.c
@@ -2452,7 +2452,7 @@ crush_and_numeric(struct expr *expr, const struct expr_symbol *symbol)
             free(or);
             return cmp;
         } else {
-            return or;
+            return crush_cmps(or, symbol);
         }
     } else {
         /* Transform "x && (a0 || a1) && (b0 || b1) && ..." into
diff --git a/lib/inc-proc-eng.c b/lib/inc-proc-eng.c
index 916dbbe39..a6337a1d9 100644
--- a/lib/inc-proc-eng.c
+++ b/lib/inc-proc-eng.c
@@ -27,6 +27,7 @@
 #include "openvswitch/hmap.h"
 #include "openvswitch/vlog.h"
 #include "inc-proc-eng.h"
+#include "unixctl.h"
 
 VLOG_DEFINE_THIS_MODULE(inc_proc_eng);
 
@@ -102,6 +103,40 @@ engine_get_nodes(struct engine_node *node, size_t *n_count)
     return engine_topo_sort(node, NULL, n_count, &n_size);
 }
 
+static void
+engine_clear_stats(struct unixctl_conn *conn, int argc OVS_UNUSED,
+                   const char *argv[] OVS_UNUSED, void *arg OVS_UNUSED)
+{
+    for (size_t i = 0; i < engine_n_nodes; i++) {
+        struct engine_node *node = engine_nodes[i];
+
+        memset(&node->stats, 0, sizeof node->stats);
+    }
+    unixctl_command_reply(conn, NULL);
+}
+
+static void
+engine_dump_stats(struct unixctl_conn *conn, int argc OVS_UNUSED,
+                  const char *argv[] OVS_UNUSED, void *arg OVS_UNUSED)
+{
+    struct ds dump = DS_EMPTY_INITIALIZER;
+
+    for (size_t i = 0; i < engine_n_nodes; i++) {
+        struct engine_node *node = engine_nodes[i];
+
+        ds_put_format(&dump,
+                      "Node: %s\n"
+                      "- recompute: %12"PRIu64"\n"
+                      "- compute:   %12"PRIu64"\n"
+                      "- abort:     %12"PRIu64"\n",
+                      node->name, node->stats.recompute,
+                      node->stats.compute, node->stats.abort);
+    }
+    unixctl_command_reply(conn, ds_cstr(&dump));
+
+    ds_destroy(&dump);
+}
+
 void
 engine_init(struct engine_node *node, struct engine_arg *arg)
 {
@@ -115,6 +150,11 @@ engine_init(struct engine_node *node, struct engine_arg *arg)
             engine_nodes[i]->data = NULL;
         }
     }
+
+    unixctl_command_register("inc-engine/show-stats", "", 0, 0,
+                             engine_dump_stats, NULL);
+    unixctl_command_register("inc-engine/clear-stats", "", 0, 0,
+                             engine_clear_stats, NULL);
 }
 
 void
@@ -288,6 +328,7 @@ engine_recompute(struct engine_node *node, bool forced, bool allowed)
 
     /* Run the node handler which might change state. */
     node->run(node, node->data);
+    node->stats.recompute++;
 }
 
 /* Return true if the node could be computed, false otherwise. */
@@ -312,6 +353,8 @@ engine_compute(struct engine_node *node, bool recompute_allowed)
             }
         }
     }
+    node->stats.compute++;
+
     return true;
 }
 
@@ -321,6 +364,7 @@ engine_run_node(struct engine_node *node, bool recompute_allowed)
     if (!node->n_inputs) {
         /* Run the node handler which might change state. */
         node->run(node, node->data);
+        node->stats.recompute++;
         return;
     }
 
@@ -377,6 +421,7 @@ engine_run(bool recompute_allowed)
         engine_run_node(engine_nodes[i], recompute_allowed);
 
         if (engine_nodes[i]->state == EN_ABORTED) {
+            engine_nodes[i]->stats.abort++;
             engine_run_aborted = true;
             return;
         }
@@ -393,6 +438,7 @@ engine_need_run(void)
         }
 
         engine_nodes[i]->run(engine_nodes[i], engine_nodes[i]->data);
+        engine_nodes[i]->stats.recompute++;
         VLOG_DBG("input node: %s, state: %s", engine_nodes[i]->name,
                  engine_node_state_name[engine_nodes[i]->state]);
         if (engine_nodes[i]->state == EN_UPDATED) {
diff --git a/lib/inc-proc-eng.h b/lib/inc-proc-eng.h
index 857234677..7e9f5bb70 100644
--- a/lib/inc-proc-eng.h
+++ b/lib/inc-proc-eng.h
@@ -107,6 +107,12 @@ enum engine_node_state {
     EN_STATE_MAX,
 };
 
+struct engine_stats {
+    uint64_t recompute;
+    uint64_t compute;
+    uint64_t abort;
+};
+
 struct engine_node {
     /* A unique name for each node. */
     char *name;
@@ -154,6 +160,9 @@ struct engine_node {
     /* Method to clear up tracked data maintained by the engine node in the
      * engine 'data'. It may be NULL. */
     void (*clear_tracked_data)(void *tracked_data);
+
+    /* Engine stats. */
+    struct engine_stats stats;
 };
 
 /* Initialize the data for the engine nodes. It calls each node's
diff --git a/lib/logical-fields.c b/lib/logical-fields.c
index 9d08b44c2..72853013e 100644
--- a/lib/logical-fields.c
+++ b/lib/logical-fields.c
@@ -121,6 +121,10 @@ ovn_init_symtab(struct shash *symtab)
              MLF_FORCE_SNAT_FOR_LB_BIT);
     expr_symtab_add_subfield(symtab, "flags.force_snat_for_lb", NULL,
                              flags_str);
+    snprintf(flags_str, sizeof flags_str, "flags[%d]",
+             MLF_SKIP_SNAT_FOR_LB_BIT);
+    expr_symtab_add_subfield(symtab, "flags.skip_snat_for_lb", NULL,
+                             flags_str);
 
     /* Connection tracking state. */
     expr_symtab_add_field_scoped(symtab, "ct_mark", MFF_CT_MARK, NULL, false,
diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
index c272cc922..37d1728b8 100644
--- a/northd/ovn-northd.8.xml
+++ b/northd/ovn-northd.8.xml
@@ -407,12 +407,13 @@
       it contains a priority-110 flow to move IPv6 Neighbor Discovery and MLD
       traffic to the next table. If load balancing rules with virtual IP
       addresses (and ports) are configured in <code>OVN_Northbound</code>
-      database for alogical switch datapath, a priority-100 flow is added
+      database for a logical switch datapath, a priority-100 flow is added
       with the match <code>ip</code> to match on IP packets and sets the action
-      <code>reg0[0] = 1; next;</code> to act as a hint for table
+      <code>reg0[2] = 1; next;</code> to act as a hint for table
       <code>Pre-stateful</code> to send IP packets to the connection tracker
-      for packet de-fragmentation before eventually advancing to ingress
-      table <code>LB</code>.
+      for packet de-fragmentation (and to possibly do DNAT for already
+      established load balanced traffic) before eventually advancing to ingress
+      table <code>Stateful</code>.
       If controller_event has been enabled and load balancing rules with
       empty backends have been added in <code>OVN_Northbound</code>, a 130 flow
       is added to trigger ovn-controller events whenever the chassis receives a
@@ -470,11 +471,38 @@
     <p>
       This table prepares flows for all possible stateful processing
       in next tables.  It contains a priority-0 flow that simply moves
-      traffic to the next table.  A priority-100 flow sends the packets to
-      connection tracker based on a hint provided by the previous tables
-      (with a match for <code>reg0[0] == 1</code>) by using the
-      <code>ct_next;</code> action.
+      traffic to the next table.
     </p>
+    <ul>
+      <li>
+        Priority-120 flows that send the packets to connection tracker using
+        <code>ct_lb;</code> as the action so that the already established
+        traffic destined to the load balancer VIP gets DNATted based on a hint
+        provided by the previous tables (with a match
+        for <code>reg0[2] == 1</code> and on supported load balancer protocols
+        and address families).  For IPv4 traffic the flows also load the
+        original destination IP and transport port in registers
+        <code>reg1</code> and <code>reg2</code>.  For IPv6 traffic the flows
+        also load the original destination IP and transport port in
+        registers <code>xxreg1</code> and <code>reg2</code>.
+      </li>
+
+      <li>
+         A priority-110 flow sends the packets to connection tracker based
+         on a hint provided by the previous tables
+         (with a match for <code>reg0[2] == 1</code>) by using the
+         <code>ct_lb;</code> action.  This flow is added to handle
+         the traffic for load balancer VIPs whose protocol is not defined
+         (mainly for ICMP traffic).
+      </li>
+
+      <li>
+         A priority-100 flow sends the packets to connection tracker based
+         on a hint provided by the previous tables
+         (with a match for <code>reg0[0] == 1</code>) by using the
+         <code>ct_next;</code> action.
+      </li>
+    </ul>
 
     <h3>Ingress Table 8: <code>from-lport</code> ACL hints</h3>
 
@@ -511,6 +539,14 @@
     <p>
       The table contains the following flows:
     </p>
+    <ul>
+      <li>
+        A priority-65535 flow to advance to the next table if the logical
+        switch has <code>no</code> ACLs configured, otherwise a
+        priority-0 flow to advance to the next table.
+      </li>
+    </ul>
+
     <ul>
       <li>
         A priority-7 flow that matches on packets that initiate a new session.
@@ -551,9 +587,6 @@
         This flow sets <code>reg0[10]</code> and then advances to the next
         table.
       </li>
-      <li>
-        A priority-0 flow to advance to the next table.
-      </li>
     </ul>
 
     <h3>Ingress table 9: <code>from-lport</code> ACLs</h3>
@@ -599,9 +632,14 @@
     </ul>
 
     <p>
-      This table also contains a priority 0 flow with action
-      <code>next;</code>, so that ACLs allow packets by default.  If the
-      logical datapath has a stateful ACL or a load balancer with VIP
+      This table contains a priority-65535 flow to advance to the next table
+      if the logical switch has <code>no</code> ACLs configured, otherwise a
+        priority-0 flow to advance to the next table so that ACLs allow
+        packets by default.
+    </p>
+
+    <p>
+      If the logical datapath has a stateful ACL or a load balancer with VIP
       configured, the following flows will also be added:
     </p>
 
@@ -615,7 +653,7 @@
       </li>
 
       <li>
-        A priority-65535 flow that allows any traffic in the reply
+        A priority-65532 flow that allows any traffic in the reply
         direction for a connection that has been committed to the
         connection tracker (i.e., established flows), as long as
         the committed flow does not have <code>ct_label.blocked</code> set.
@@ -628,19 +666,19 @@
       </li>
 
       <li>
-        A priority-65535 flow that allows any traffic that is considered
+        A priority-65532 flow that allows any traffic that is considered
         related to a committed flow in the connection tracker (e.g., an
         ICMP Port Unreachable from a non-listening UDP port), as long
         as the committed flow does not have <code>ct_label.blocked</code> set.
       </li>
 
       <li>
-        A priority-65535 flow that drops all traffic marked by the
+        A priority-65532 flow that drops all traffic marked by the
         connection tracker as invalid.
       </li>
 
       <li>
-        A priority-65535 flow that drops all traffic in the reply direction
+        A priority-65532 flow that drops all traffic in the reply direction
         with <code>ct_label.blocked</code> set meaning that the connection
         should no longer be allowed due to a policy change.  Packets
         in the request direction are skipped here to let a newly created
@@ -648,11 +686,18 @@
       </li>
 
       <li>
-        A priority-65535 flow that allows IPv6 Neighbor solicitation,
+        A priority-65532 flow that allows IPv6 Neighbor solicitation,
         Neighbor discover, Router solicitation, Router advertisement and MLD
         packets.
       </li>
+    </ul>
 
+    <p>
+      If the logical datapath has any ACL or a load balancer with VIP
+      configured, the following flow will also be added:
+    </p>
+
+    <ul>
       <li>
         A priority 34000 logical flow is added for each logical switch datapath
         with the match <code>eth.dst = <var>E</var></code> to allow the service
@@ -709,33 +754,7 @@
       </li>
     </ul>
 
-    <h3>Ingress Table 12: LB</h3>
-
-    <p>
-      It contains a priority-0 flow that simply moves traffic to the next
-      table.
-    </p>
-
-    <p>
-      A priority-65535 flow with the match
-      <code>inport == <var>I</var></code> for all logical switch
-      datapaths to move traffic to the next table. Where <var>I</var>
-      is the peer of a logical router port. This flow is added to
-      skip the connection tracking of packets which enter from
-      logical router datapath to logical switch datapath.
-    </p>
-
-    <p>
-      For established connections a priority 65534 flow matches on
-      <code>ct.est &amp;&amp; !ct.rel &amp;&amp; !ct.new &amp;&amp;
-      !ct.inv</code> and sets an action <code>reg0[2] = 1; next;</code> to act
-      as a hint for table <code>Stateful</code> to send packets through
-      connection tracker to NAT the packets.  (The packet will automatically
-      get DNATed to the same IP address as the first packet in that
-      connection.)
-    </p>
-
-    <h3>Ingress Table 13: Stateful</h3>
+    <h3>Ingress Table 12: Stateful</h3>
 
     <ul>
       <li>
@@ -792,23 +811,12 @@
         <code>ct_commit; next;</code> action based on a hint provided by
         the previous tables (with a match for <code>reg0[1] == 1</code>).
       </li>
-      <li>
-        Priority-100 flows that send the packets to connection tracker using
-        <code>ct_lb;</code> as the action based on a hint provided by the
-        previous tables (with a match for <code>reg0[2] == 1</code> and
-        on supported load balancer protocols and address families).
-        For IPv4 traffic the flows also load the original destination
-        IP and transport port in registers <code>reg1</code> and
-        <code>reg2</code>.  For IPv6 traffic the flows also load the original
-        destination IP and transport port in registers <code>xxreg1</code> and
-        <code>reg2</code>.
-      </li>
       <li>
         A priority-0 flow that simply moves traffic to the next table.
       </li>
     </ul>
 
-    <h3>Ingress Table 14: Pre-Hairpin</h3>
+    <h3>Ingress Table 13: Pre-Hairpin</h3>
     <ul>
       <li>
         If the logical switch has load balancer(s) configured, then a
@@ -826,7 +834,7 @@
       </li>
     </ul>
 
-    <h3>Ingress Table 15: Nat-Hairpin</h3>
+    <h3>Ingress Table 14: Nat-Hairpin</h3>
     <ul>
       <li>
          If the logical switch has load balancer(s) configured, then a
@@ -861,7 +869,7 @@
       </li>
     </ul>
 
-    <h3>Ingress Table 16: Hairpin</h3>
+    <h3>Ingress Table 15: Hairpin</h3>
     <ul>
       <li>
         A priority-1 flow that hairpins traffic matched by non-default
@@ -874,7 +882,7 @@
       </li>
     </ul>
 
-    <h3>Ingress Table 17: ARP/ND responder</h3>
+    <h3>Ingress Table 16: ARP/ND responder</h3>
 
     <p>
       This table implements ARP/ND responder in a logical switch for known
@@ -1164,7 +1172,7 @@ output;
       </li>
     </ul>
 
-    <h3>Ingress Table 18: DHCP option processing</h3>
+    <h3>Ingress Table 17: DHCP option processing</h3>
 
     <p>
       This table adds the DHCPv4 options to a DHCPv4 packet from the
@@ -1225,7 +1233,7 @@ next;
       </li>
     </ul>
 
-    <h3>Ingress Table 19: DHCP responses</h3>
+    <h3>Ingress Table 18: DHCP responses</h3>
 
     <p>
       This table implements DHCP responder for the DHCP replies generated by
@@ -1306,7 +1314,7 @@ output;
       </li>
     </ul>
 
-    <h3>Ingress Table 20 DNS Lookup</h3>
+    <h3>Ingress Table 19 DNS Lookup</h3>
 
     <p>
       This table looks up and resolves the DNS names to the corresponding
@@ -1335,7 +1343,7 @@ reg0[4] = dns_lookup(); next;
       </li>
     </ul>
 
-    <h3>Ingress Table 21 DNS Responses</h3>
+    <h3>Ingress Table 20 DNS Responses</h3>
 
     <p>
       This table implements DNS responder for the DNS replies generated by
@@ -1370,7 +1378,7 @@ output;
       </li>
     </ul>
 
-    <h3>Ingress table 22 External ports</h3>
+    <h3>Ingress table 21 External ports</h3>
 
     <p>
       Traffic from the <code>external</code> logical ports enter the ingress
@@ -1413,7 +1421,7 @@ output;
       </li>
     </ul>
 
-    <h3>Ingress Table 23 Destination Lookup</h3>
+    <h3>Ingress Table 22 Destination Lookup</h3>
 
     <p>
       This table implements switching behavior.  It contains these logical
@@ -1639,9 +1647,11 @@ output;
       Moreover it contains a priority-110 flow to move IPv6 Neighbor Discovery
       traffic to the next table. If any load balancing rules exist for the
       datapath, a priority-100 flow is added with a match of <code>ip</code>
-      and action of <code>reg0[0] = 1; next;</code> to act as a hint for
+      and action of <code>reg0[2] = 1; next;</code> to act as a hint for
       table <code>Pre-stateful</code> to send IP packets to the connection
-      tracker for packet de-fragmentation.
+      tracker for packet de-fragmentation and possibly DNAT the destination
+      VIP to one of the selected backend for already commited load balanced
+      traffic.
     </p>
 
     <p>
@@ -1683,20 +1693,39 @@ output;
     <h3>Egress Table 2: Pre-stateful</h3>
 
     <p>
-      This is similar to ingress table <code>Pre-stateful</code>.
+      This is similar to ingress table <code>Pre-stateful</code>.  This table
+      adds the below 3 logical flows.
     </p>
 
-    <h3>Egress Table 3: LB</h3>
-    <p>
-      This is similar to ingress table <code>LB</code>.
-    </p>
+    <ul>
+      <li>
+        A Priority-120 flow that send the packets to connection tracker using
+        <code>ct_lb;</code> as the action so that the already established
+        traffic gets unDNATted from the backend IP to the load balancer VIP
+        based on a hint provided by the previous tables with a match
+        for <code>reg0[2] == 1</code>.  If the packet was not DNATted earlier,
+        then <code>ct_lb</code> functions like <code>ct_next</code>.
+      </li>
 
-    <h3>Egress Table 4: <code>from-lport</code> ACL hints</h3>
+      <li>
+        A priority-100 flow sends the packets to connection tracker based
+        on a hint provided by the previous tables
+        (with a match for <code>reg0[0] == 1</code>) by using the
+        <code>ct_next;</code> action.
+      </li>
+
+      <li>
+        A priority-0 flow that matches all packets to advance to the next
+        table.
+      </li>
+    </ul>
+
+    <h3>Egress Table 3: <code>from-lport</code> ACL hints</h3>
     <p>
       This is similar to ingress table <code>ACL hints</code>.
     </p>
 
-    <h3>Egress Table 5: <code>to-lport</code> ACLs</h3>
+    <h3>Egress Table 4: <code>to-lport</code> ACLs</h3>
 
     <p>
       This is similar to ingress table <code>ACLs</code> except for
@@ -1733,28 +1762,28 @@ output;
       </li>
     </ul>
 
-    <h3>Egress Table 6: <code>to-lport</code> QoS Marking</h3>
+    <h3>Egress Table 5: <code>to-lport</code> QoS Marking</h3>
 
     <p>
       This is similar to ingress table <code>QoS marking</code> except
       they apply to <code>to-lport</code> QoS rules.
     </p>
 
-    <h3>Egress Table 7: <code>to-lport</code> QoS Meter</h3>
+    <h3>Egress Table 6: <code>to-lport</code> QoS Meter</h3>
 
     <p>
       This is similar to ingress table <code>QoS meter</code> except
       they apply to <code>to-lport</code> QoS rules.
     </p>
 
-    <h3>Egress Table 8: Stateful</h3>
+    <h3>Egress Table 7: Stateful</h3>
 
     <p>
       This is similar to ingress table <code>Stateful</code> except that
       there are no rules added for load balancing new connections.
     </p>
 
-    <h3>Egress Table 9: Egress Port Security - IP</h3>
+    <h3>Egress Table 8: Egress Port Security - IP</h3>
 
     <p>
       This is similar to the port security logic in table
@@ -1764,7 +1793,7 @@ output;
       <code>ip4.src</code> and <code>ip6.src</code>
     </p>
 
-    <h3>Egress Table 10: Egress Port Security - L2</h3>
+    <h3>Egress Table 9: Egress Port Security - L2</h3>
 
     <p>
       This is similar to the ingress port security logic in ingress table
@@ -2283,8 +2312,7 @@ eth.src = xreg0[0..47];
 arp.op = 2; /* ARP reply. */
 arp.tha = arp.sha;
 arp.sha = xreg0[0..47];
-arp.tpa = arp.spa;
-arp.spa = <var>A</var>;
+arp.tpa &lt;-&gt; arp.spa;
 outport = inport;
 flags.loopback = 1;
 output;
@@ -2720,7 +2748,11 @@ icmp6 {
         (and optional port numbers) to load balance to.  If the router is
         configured to force SNAT any load-balanced packets, the above action
         will be replaced by <code>flags.force_snat_for_lb = 1;
-        ct_lb(<var>args</var>);</code>. If health check is enabled, then
+        ct_lb(<var>args</var>);</code>.
+        If the load balancing rule is configured with <code>skip_snat</code>
+        set to true, the above action will be replaced by
+        <code>flags.skip_snat_for_lb = 1; ct_lb(<var>args</var>);</code>.
+        If health check is enabled, then
         <var>args</var> will only contain those endpoints whose service
         monitor status entry in <code>OVN_Southbound</code> db is
         either <code>online</code> or empty.
@@ -2737,6 +2769,9 @@ icmp6 {
         with an action of <code>ct_dnat;</code>. If the router is
         configured to force SNAT any load-balanced packets, the above action
         will be replaced by <code>flags.force_snat_for_lb = 1; ct_dnat;</code>.
+        If the load balancing rule is configured with <code>skip_snat</code>
+        set to true, the above action will be replaced by
+        <code>flags.skip_snat_for_lb = 1; ct_dnat;</code>.
       </li>
 
       <li>
@@ -2751,6 +2786,9 @@ icmp6 {
         to force SNAT any load-balanced packets, the above action will be
         replaced by <code>flags.force_snat_for_lb = 1;
         ct_lb(<var>args</var>);</code>.
+        If the load balancing rule is configured with <code>skip_snat</code>
+        set to true, the above action will be replaced by
+        <code>flags.skip_snat_for_lb = 1; ct_lb(<var>args</var>);</code>.
       </li>
 
       <li>
@@ -2763,6 +2801,9 @@ icmp6 {
         If the router is configured to force SNAT any load-balanced
         packets, the above action will be replaced by
         <code>flags.force_snat_for_lb = 1; ct_dnat;</code>.
+        If the load balancing rule is configured with <code>skip_snat</code>
+        set to true, the above action will be replaced by
+        <code>flags.skip_snat_for_lb = 1; ct_dnat;</code>.
       </li>
 
       <li>
@@ -3795,6 +3836,15 @@ nd_ns {
         </p>
       </li>
 
+      <li>
+        <p>
+          If a load balancer configured to skip snat has been applied to
+          the Gateway router pipeline, a priority-120 flow matches
+          <code>flags.skip_snat_for_lb == 1 &amp;&amp; ip</code> with an
+          action <code>next;</code>.
+        </p>
+      </li>
+
       <li>
         <p>
           If the Gateway router in the OVN Northbound database has been
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 5a2018c2e..a478d3324 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -97,6 +97,10 @@ static bool check_lsp_is_up;
 static char svc_monitor_mac[ETH_ADDR_STRLEN + 1];
 static struct eth_addr svc_monitor_mac_ea;
 
+/* If this option is 'true' northd will make use of ct.inv match fields.
+ * Otherwise, it will avoid using it.  The default is true. */
+static bool use_ct_inv_match = true;
+
 /* Default probe interval for NB and SB DB connections. */
 #define DEFAULT_PROBE_INTERVAL_MSEC 5000
 static int northd_probe_interval_nb = 0;
@@ -147,32 +151,30 @@ enum ovn_stage {
     PIPELINE_STAGE(SWITCH, IN,  ACL,            9, "ls_in_acl")           \
     PIPELINE_STAGE(SWITCH, IN,  QOS_MARK,      10, "ls_in_qos_mark")      \
     PIPELINE_STAGE(SWITCH, IN,  QOS_METER,     11, "ls_in_qos_meter")     \
-    PIPELINE_STAGE(SWITCH, IN,  LB,            12, "ls_in_lb")            \
-    PIPELINE_STAGE(SWITCH, IN,  STATEFUL,      13, "ls_in_stateful")      \
-    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,  ARP_ND_RSP,    17, "ls_in_arp_rsp")       \
-    PIPELINE_STAGE(SWITCH, IN,  DHCP_OPTIONS,  18, "ls_in_dhcp_options")  \
-    PIPELINE_STAGE(SWITCH, IN,  DHCP_RESPONSE, 19, "ls_in_dhcp_response") \
-    PIPELINE_STAGE(SWITCH, IN,  DNS_LOOKUP,    20, "ls_in_dns_lookup")    \
-    PIPELINE_STAGE(SWITCH, IN,  DNS_RESPONSE,  21, "ls_in_dns_response")  \
-    PIPELINE_STAGE(SWITCH, IN,  EXTERNAL_PORT, 22, "ls_in_external_port") \
-    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,       23, "ls_in_l2_lkup")       \
-    PIPELINE_STAGE(SWITCH, IN,  L2_UNKNOWN,    24, "ls_in_l2_unknown")    \
+    PIPELINE_STAGE(SWITCH, IN,  STATEFUL,      12, "ls_in_stateful")      \
+    PIPELINE_STAGE(SWITCH, IN,  PRE_HAIRPIN,   13, "ls_in_pre_hairpin")   \
+    PIPELINE_STAGE(SWITCH, IN,  NAT_HAIRPIN,   14, "ls_in_nat_hairpin")   \
+    PIPELINE_STAGE(SWITCH, IN,  HAIRPIN,       15, "ls_in_hairpin")       \
+    PIPELINE_STAGE(SWITCH, IN,  ARP_ND_RSP,    16, "ls_in_arp_rsp")       \
+    PIPELINE_STAGE(SWITCH, IN,  DHCP_OPTIONS,  17, "ls_in_dhcp_options")  \
+    PIPELINE_STAGE(SWITCH, IN,  DHCP_RESPONSE, 18, "ls_in_dhcp_response") \
+    PIPELINE_STAGE(SWITCH, IN,  DNS_LOOKUP,    19, "ls_in_dns_lookup")    \
+    PIPELINE_STAGE(SWITCH, IN,  DNS_RESPONSE,  20, "ls_in_dns_response")  \
+    PIPELINE_STAGE(SWITCH, IN,  EXTERNAL_PORT, 21, "ls_in_external_port") \
+    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,       22, "ls_in_l2_lkup")       \
+    PIPELINE_STAGE(SWITCH, IN,  L2_UNKNOWN,    23, "ls_in_l2_unknown")    \
                                                                           \
     /* Logical switch egress stages. */                                   \
     PIPELINE_STAGE(SWITCH, OUT, PRE_LB,       0, "ls_out_pre_lb")         \
     PIPELINE_STAGE(SWITCH, OUT, PRE_ACL,      1, "ls_out_pre_acl")        \
     PIPELINE_STAGE(SWITCH, OUT, PRE_STATEFUL, 2, "ls_out_pre_stateful")   \
-    PIPELINE_STAGE(SWITCH, OUT, LB,           3, "ls_out_lb")             \
-    PIPELINE_STAGE(SWITCH, OUT, ACL_HINT,     4, "ls_out_acl_hint")       \
-    PIPELINE_STAGE(SWITCH, OUT, ACL,          5, "ls_out_acl")            \
-    PIPELINE_STAGE(SWITCH, OUT, QOS_MARK,     6, "ls_out_qos_mark")       \
-    PIPELINE_STAGE(SWITCH, OUT, QOS_METER,    7, "ls_out_qos_meter")      \
-    PIPELINE_STAGE(SWITCH, OUT, STATEFUL,     8, "ls_out_stateful")       \
-    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_IP,  9, "ls_out_port_sec_ip")    \
-    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_L2, 10, "ls_out_port_sec_l2")    \
+    PIPELINE_STAGE(SWITCH, OUT, ACL_HINT,     3, "ls_out_acl_hint")       \
+    PIPELINE_STAGE(SWITCH, OUT, ACL,          4, "ls_out_acl")            \
+    PIPELINE_STAGE(SWITCH, OUT, QOS_MARK,     5, "ls_out_qos_mark")       \
+    PIPELINE_STAGE(SWITCH, OUT, QOS_METER,    6, "ls_out_qos_meter")      \
+    PIPELINE_STAGE(SWITCH, OUT, STATEFUL,     7, "ls_out_stateful")       \
+    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_IP,  8, "ls_out_port_sec_ip")    \
+    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_L2,  9, "ls_out_port_sec_l2")    \
                                                                       \
     /* Logical router ingress stages. */                              \
     PIPELINE_STAGE(ROUTER, IN,  ADMISSION,       0, "lr_in_admission")    \
@@ -626,6 +628,7 @@ struct ovn_datapath {
     bool has_stateful_acl;
     bool has_lb_vip;
     bool has_unknown;
+    bool has_acls;
 
     /* IPAM data. */
     struct ipam_info ipam_info;
@@ -664,9 +667,6 @@ struct ovn_datapath {
     struct hmap nb_pgs;
 };
 
-static bool ls_has_stateful_acl(struct ovn_datapath *od);
-static bool ls_has_lb_vip(struct ovn_datapath *od);
-
 /* Contains a NAT entry with the external addresses pre-parsed. */
 struct ovn_nat {
     const struct nbrec_nat *nb;
@@ -4729,27 +4729,38 @@ ovn_ls_port_group_destroy(struct hmap *nb_pgs)
     hmap_destroy(nb_pgs);
 }
 
-static bool
-ls_has_stateful_acl(struct ovn_datapath *od)
+static void
+ls_get_acl_flags(struct ovn_datapath *od)
 {
-    for (size_t i = 0; i < od->nbs->n_acls; i++) {
-        struct nbrec_acl *acl = od->nbs->acls[i];
-        if (!strcmp(acl->action, "allow-related")) {
-            return true;
+    od->has_acls = false;
+    od->has_stateful_acl = false;
+
+    if (od->nbs->n_acls) {
+        od->has_acls = true;
+
+        for (size_t i = 0; i < od->nbs->n_acls; i++) {
+            struct nbrec_acl *acl = od->nbs->acls[i];
+            if (!strcmp(acl->action, "allow-related")) {
+                od->has_stateful_acl = true;
+                return;
+            }
         }
     }
 
     struct ovn_ls_port_group *ls_pg;
     HMAP_FOR_EACH (ls_pg, key_node, &od->nb_pgs) {
-        for (size_t i = 0; i < ls_pg->nb_pg->n_acls; i++) {
-            struct nbrec_acl *acl = ls_pg->nb_pg->acls[i];
-            if (!strcmp(acl->action, "allow-related")) {
-                return true;
+        if (ls_pg->nb_pg->n_acls) {
+            od->has_acls = true;
+
+            for (size_t i = 0; i < ls_pg->nb_pg->n_acls; i++) {
+                struct nbrec_acl *acl = ls_pg->nb_pg->acls[i];
+                if (!strcmp(acl->action, "allow-related")) {
+                    od->has_stateful_acl = true;
+                    return;
+                }
             }
         }
     }
-
-    return false;
 }
 
 /* Logical switch ingress table 0: Ingress port security - L2
@@ -5128,8 +5139,8 @@ build_pre_lb(struct ovn_datapath *od, struct hmap *lflows,
         vip_configured = (vip_configured || lb->n_vips);
     }
 
-    /* 'REGBIT_CONNTRACK_DEFRAG' is set to let the pre-stateful table send
-     * packet to conntrack for defragmentation.
+    /* 'REGBIT_CONNTRACK_NAT' is set to let the pre-stateful table send
+     * packet to conntrack for defragmentation and possibly for unNATting.
      *
      * Send all the packets to conntrack in the ingress pipeline if the
      * logical switch has a load balancer with VIP configured. Earlier
@@ -5159,9 +5170,9 @@ build_pre_lb(struct ovn_datapath *od, struct hmap *lflows,
      */
     if (vip_configured) {
         ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_LB,
-                      100, "ip", REGBIT_CONNTRACK_DEFRAG" = 1; next;");
+                      100, "ip", REGBIT_CONNTRACK_NAT" = 1; next;");
         ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_LB,
-                      100, "ip", REGBIT_CONNTRACK_DEFRAG" = 1; next;");
+                      100, "ip", REGBIT_CONNTRACK_NAT" = 1; next;");
     }
 }
 
@@ -5173,10 +5184,46 @@ build_pre_stateful(struct ovn_datapath *od, struct hmap *lflows)
     ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_STATEFUL, 0, "1", "next;");
     ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_STATEFUL, 0, "1", "next;");
 
+    const char *lb_protocols[] = {"tcp", "udp", "sctp"};
+    struct ds actions = DS_EMPTY_INITIALIZER;
+    struct ds match = DS_EMPTY_INITIALIZER;
+
+    for (size_t i = 0; i < ARRAY_SIZE(lb_protocols); i++) {
+        ds_clear(&match);
+        ds_clear(&actions);
+        ds_put_format(&match, REGBIT_CONNTRACK_NAT" == 1 && ip4 && %s",
+                      lb_protocols[i]);
+        ds_put_format(&actions, REG_ORIG_DIP_IPV4 " = ip4.dst; "
+                                REG_ORIG_TP_DPORT " = %s.dst; ct_lb;",
+                      lb_protocols[i]);
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_STATEFUL, 120,
+                      ds_cstr(&match), ds_cstr(&actions));
+
+        ds_clear(&match);
+        ds_clear(&actions);
+        ds_put_format(&match, REGBIT_CONNTRACK_NAT" == 1 && ip6 && %s",
+                      lb_protocols[i]);
+        ds_put_format(&actions, REG_ORIG_DIP_IPV6 " = ip6.dst; "
+                                REG_ORIG_TP_DPORT " = %s.dst; ct_lb;",
+                      lb_protocols[i]);
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_STATEFUL, 120,
+                      ds_cstr(&match), ds_cstr(&actions));
+    }
+
+    ds_destroy(&actions);
+    ds_destroy(&match);
+
+    ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_STATEFUL, 110,
+                  REGBIT_CONNTRACK_NAT" == 1", "ct_lb;");
+
+    ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_STATEFUL, 110,
+                  REGBIT_CONNTRACK_NAT" == 1", "ct_lb;");
+
     /* If REGBIT_CONNTRACK_DEFRAG is set as 1, then the packets should be
      * sent to conntrack for tracking and defragmentation. */
     ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_STATEFUL, 100,
                   REGBIT_CONNTRACK_DEFRAG" == 1", "ct_next;");
+
     ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_STATEFUL, 100,
                   REGBIT_CONNTRACK_DEFRAG" == 1", "ct_next;");
 }
@@ -5206,7 +5253,11 @@ build_acl_hints(struct ovn_datapath *od, struct hmap *lflows)
         enum ovn_stage stage = stages[i];
 
         /* In any case, advance to the next stage. */
-        ovn_lflow_add(lflows, od, stage, 0, "1", "next;");
+        if (!od->has_acls && !od->has_lb_vip) {
+            ovn_lflow_add(lflows, od, stage, UINT16_MAX, "1", "next;");
+        } else {
+            ovn_lflow_add(lflows, od, stage, 0, "1", "next;");
+        }
 
         if (!od->has_stateful_acl && !od->has_lb_vip) {
             continue;
@@ -5606,10 +5657,19 @@ build_acls(struct ovn_datapath *od, struct hmap *lflows,
     bool has_stateful = od->has_stateful_acl || od->has_lb_vip;
 
     /* Ingress and Egress ACL Table (Priority 0): Packets are allowed by
-     * default.  A related rule at priority 1 is added below if there
+     * default.  If the logical switch has no ACLs or no load balancers,
+     * then add 65535-priority flow to advance the packet to next
+     * stage.
+     *
+     * A related rule at priority 1 is added below if there
      * are any stateful ACLs in this datapath. */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, 0, "1", "next;");
+    if (!od->has_acls && !od->has_lb_vip) {
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX, "1", "next;");
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX, "1", "next;");
+    } else {
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, 0, "1", "next;");
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, 0, "1", "next;");
+    }
 
     if (has_stateful) {
         /* Ingress and Egress ACL Table (Priority 1).
@@ -5640,21 +5700,23 @@ build_acls(struct ovn_datapath *od, struct hmap *lflows,
                       "ip && (!ct.est || (ct.est && ct_label.blocked == 1))",
                        REGBIT_CONNTRACK_COMMIT" = 1; next;");
 
-        /* Ingress and Egress ACL Table (Priority 65535).
+        /* Ingress and Egress ACL Table (Priority 65532).
          *
          * Always drop traffic that's in an invalid state.  Also drop
          * reply direction packets for connections that have been marked
          * for deletion (bit 0 of ct_label is set).
          *
          * This is enforced at a higher priority than ACLs can be defined. */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
-                      "ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)",
-                      "drop;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
-                      "ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)",
-                      "drop;");
+        char *match =
+            xasprintf("%s(ct.est && ct.rpl && ct_label.blocked == 1)",
+                      use_ct_inv_match ? "ct.inv || " : "");
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX - 3,
+                      match, "drop;");
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
+                      match, "drop;");
+        free(match);
 
-        /* Ingress and Egress ACL Table (Priority 65535).
+        /* Ingress and Egress ACL Table (Priority 65535 - 3).
          *
          * Allow reply traffic that is part of an established
          * conntrack entry that has not been marked for deletion
@@ -5663,14 +5725,15 @@ build_acls(struct ovn_datapath *od, struct hmap *lflows,
          * direction to hit the currently defined policy from ACLs.
          *
          * This is enforced at a higher priority than ACLs can be defined. */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
-                      "ct.est && !ct.rel && !ct.new && !ct.inv "
-                      "&& ct.rpl && ct_label.blocked == 0",
-                      "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
-                      "ct.est && !ct.rel && !ct.new && !ct.inv "
-                      "&& ct.rpl && ct_label.blocked == 0",
-                      "next;");
+        match = xasprintf("ct.est && !ct.rel && !ct.new%s && "
+                          "ct.rpl && ct_label.blocked == 0",
+                          use_ct_inv_match ? " && !ct.inv" : "");
+
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX - 3,
+                      match, "next;");
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
+                      match, "next;");
+        free(match);
 
         /* Ingress and Egress ACL Table (Priority 65535).
          *
@@ -5683,21 +5746,21 @@ build_acls(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.  */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
-                      "!ct.est && ct.rel && !ct.new && !ct.inv "
-                      "&& ct_label.blocked == 0",
-                      "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
-                      "!ct.est && ct.rel && !ct.new && !ct.inv "
-                      "&& ct_label.blocked == 0",
-                      "next;");
+        match = xasprintf("!ct.est && ct.rel && !ct.new%s && "
+                          "ct_label.blocked == 0",
+                          use_ct_inv_match ? " && !ct.inv" : "");
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX - 3,
+                      match, "next;");
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
+                      match, "next;");
+        free(match);
 
-        /* Ingress and Egress ACL Table (Priority 65535).
+        /* Ingress and Egress ACL Table (Priority 65532).
          *
          * Not to do conntrack on ND packets. */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX,
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, UINT16_MAX - 3,
                       "nd || nd_ra || nd_rs || mldv1 || mldv2", "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX,
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
                       "nd || nd_ra || nd_rs || mldv1 || mldv2", "next;");
     }
 
@@ -5784,15 +5847,18 @@ build_acls(struct ovn_datapath *od, struct hmap *lflows,
             actions);
     }
 
-    /* Add a 34000 priority flow to advance the service monitor reply
-     * packets to skip applying ingress ACLs. */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, 34000,
-                  "eth.dst == $svc_monitor_mac", "next;");
 
-    /* Add a 34000 priority flow to advance the service monitor packets
-     * generated by ovn-controller to skip applying egress ACLs. */
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, 34000,
-                  "eth.src == $svc_monitor_mac", "next;");
+    if (od->has_acls || od->has_lb_vip) {
+        /* Add a 34000 priority flow to advance the service monitor reply
+        * packets to skip applying ingress ACLs. */
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, 34000,
+                    "eth.dst == $svc_monitor_mac", "next;");
+
+        /* Add a 34000 priority flow to advance the service monitor packets
+        * generated by ovn-controller to skip applying egress ACLs. */
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, 34000,
+                    "eth.src == $svc_monitor_mac", "next;");
+    }
 }
 
 static void
@@ -5856,37 +5922,6 @@ build_qos(struct ovn_datapath *od, struct hmap *lflows) {
     }
 }
 
-static void
-build_lb(struct ovn_datapath *od, struct hmap *lflows)
-{
-    /* Ingress and Egress LB Table (Priority 0): Packets are allowed by
-     * default.  */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_LB, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_LB, 0, "1", "next;");
-
-    if (od->nbs->n_load_balancer) {
-        for (size_t i = 0; i < od->n_router_ports; i++) {
-            skip_port_from_conntrack(od, od->router_ports[i],
-                                     S_SWITCH_IN_LB, S_SWITCH_OUT_LB,
-                                     UINT16_MAX, lflows);
-        }
-    }
-
-    if (od->has_lb_vip) {
-        /* Ingress and Egress LB Table (Priority 65534).
-         *
-         * Send established traffic through conntrack for just NAT. */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_LB, UINT16_MAX - 1,
-                      "ct.est && !ct.rel && !ct.new && !ct.inv && "
-                      "ct_label.natted == 1",
-                      REGBIT_CONNTRACK_NAT" = 1; next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_LB, UINT16_MAX - 1,
-                      "ct.est && !ct.rel && !ct.new && !ct.inv && "
-                      "ct_label.natted == 1",
-                      REGBIT_CONNTRACK_NAT" = 1; next;");
-    }
-}
-
 static void
 build_lb_rules(struct ovn_datapath *od, struct hmap *lflows,
                struct ovn_northd_lb *lb)
@@ -5971,48 +6006,6 @@ build_stateful(struct ovn_datapath *od, struct hmap *lflows, struct hmap *lbs)
                   REGBIT_CONNTRACK_COMMIT" == 1",
                   "ct_commit { ct_label.blocked = 0; }; next;");
 
-    /* If REGBIT_CONNTRACK_NAT is set as 1, then packets should just be sent
-     * through nat (without committing).
-     *
-     * REGBIT_CONNTRACK_COMMIT is set for new connections and
-     * REGBIT_CONNTRACK_NAT is set for established connections. So they
-     * don't overlap.
-     *
-     * In the ingress pipeline, also store the original destination IP and
-     * transport port to be used when detecting hairpin packets.
-     */
-    const char *lb_protocols[] = {"tcp", "udp", "sctp"};
-    struct ds actions = DS_EMPTY_INITIALIZER;
-    struct ds match = DS_EMPTY_INITIALIZER;
-
-    for (size_t i = 0; i < ARRAY_SIZE(lb_protocols); i++) {
-        ds_clear(&match);
-        ds_clear(&actions);
-        ds_put_format(&match, REGBIT_CONNTRACK_NAT" == 1 && ip4 && %s",
-                      lb_protocols[i]);
-        ds_put_format(&actions, REG_ORIG_DIP_IPV4 " = ip4.dst; "
-                                REG_ORIG_TP_DPORT " = %s.dst; ct_lb;",
-                      lb_protocols[i]);
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_STATEFUL, 100,
-                      ds_cstr(&match), ds_cstr(&actions));
-
-        ds_clear(&match);
-        ds_clear(&actions);
-        ds_put_format(&match, REGBIT_CONNTRACK_NAT" == 1 && ip6 && %s",
-                      lb_protocols[i]);
-        ds_put_format(&actions, REG_ORIG_DIP_IPV6 " = ip6.dst; "
-                                REG_ORIG_TP_DPORT " = %s.dst; ct_lb;",
-                      lb_protocols[i]);
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_STATEFUL, 100,
-                      ds_cstr(&match), ds_cstr(&actions));
-    }
-
-    ds_destroy(&actions);
-    ds_destroy(&match);
-
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_STATEFUL, 100,
-                  REGBIT_CONNTRACK_NAT" == 1", "ct_lb;");
-
     /* Load balancing rules for new connections get committed to conntrack
      * table.  So even if REGBIT_CONNTRACK_COMMIT is set in a previous table
      * a higher priority rule for load balancing below also commits the
@@ -6759,7 +6752,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *lflows)
     struct ds actions = DS_EMPTY_INITIALIZER;
     struct ovn_datapath *od;
 
-    /* Ingress table 24: Destination lookup for unknown MACs (priority 0). */
+    /* Ingress table 23: Destination lookup for unknown MACs (priority 0). */
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbs) {
             continue;
@@ -6794,8 +6787,8 @@ build_lswitch_lflows_pre_acl_and_acl(struct ovn_datapath *od,
                                      struct hmap *lbs)
 {
     if (od->nbs) {
-        od->has_stateful_acl = ls_has_stateful_acl(od);
         od->has_lb_vip = ls_has_lb_vip(od);
+        ls_get_acl_flags(od);
 
         build_pre_acls(od, lflows);
         build_pre_lb(od, lflows, meter_groups, lbs);
@@ -6803,7 +6796,6 @@ build_lswitch_lflows_pre_acl_and_acl(struct ovn_datapath *od,
         build_acl_hints(od, lflows);
         build_acls(od, lflows, port_groups, meter_groups);
         build_qos(od, lflows);
-        build_lb(od, lflows);
         build_stateful(od, lflows, lbs);
         build_lb_hairpin(od, lflows);
     }
@@ -8573,10 +8565,16 @@ get_force_snat_ip(struct ovn_datapath *od, const char *key_type,
     return true;
 }
 
+enum lb_snat_type {
+    NO_FORCE_SNAT,
+    FORCE_SNAT,
+    SKIP_SNAT,
+};
+
 static void
 add_router_lb_flow(struct hmap *lflows, struct ovn_datapath *od,
                    struct ds *match, struct ds *actions, int priority,
-                   bool force_snat_for_lb, struct ovn_lb_vip *lb_vip,
+                   enum lb_snat_type snat_type, struct ovn_lb_vip *lb_vip,
                    const char *proto, struct nbrec_load_balancer *lb,
                    struct shash *meter_groups, struct sset *nat_entries)
 {
@@ -8585,9 +8583,10 @@ add_router_lb_flow(struct hmap *lflows, struct ovn_datapath *od,
 
     /* A match and actions for new connections. */
     char *new_match = xasprintf("ct.new && %s", ds_cstr(match));
-    if (force_snat_for_lb) {
-        char *new_actions = xasprintf("flags.force_snat_for_lb = 1; %s",
-                                      ds_cstr(actions));
+    if (snat_type == FORCE_SNAT || snat_type == SKIP_SNAT) {
+        char *new_actions = xasprintf("flags.%s_snat_for_lb = 1; %s",
+                snat_type == SKIP_SNAT ? "skip" : "force",
+                ds_cstr(actions));
         ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_DNAT, priority,
                                 new_match, new_actions, &lb->header_);
         free(new_actions);
@@ -8598,11 +8597,12 @@ add_router_lb_flow(struct hmap *lflows, struct ovn_datapath *od,
 
     /* A match and actions for established connections. */
     char *est_match = xasprintf("ct.est && %s", ds_cstr(match));
-    if (force_snat_for_lb) {
+    if (snat_type == FORCE_SNAT || snat_type == SKIP_SNAT) {
+        char *est_actions = xasprintf("flags.%s_snat_for_lb = 1; ct_dnat;",
+                snat_type == SKIP_SNAT ? "skip" : "force");
         ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_DNAT, priority,
-                                est_match,
-                                "flags.force_snat_for_lb = 1; ct_dnat;",
-                                &lb->header_);
+                                est_match, est_actions, &lb->header_);
+        free(est_actions);
     } else {
         ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_DNAT, priority,
                                 est_match, "ct_dnat;", &lb->header_);
@@ -8675,11 +8675,13 @@ add_router_lb_flow(struct hmap *lflows, struct ovn_datapath *od,
     ds_put_format(&undnat_match, ") && outport == %s && "
                  "is_chassis_resident(%s)", od->l3dgw_port->json_key,
                  od->l3redirect_port->json_key);
-    if (force_snat_for_lb) {
+    if (snat_type == FORCE_SNAT || snat_type == SKIP_SNAT) {
+        char *action = xasprintf("flags.%s_snat_for_lb = 1; ct_dnat;",
+                                 snat_type == SKIP_SNAT ? "skip" : "force");
         ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_UNDNAT, 120,
-                                ds_cstr(&undnat_match),
-                                "flags.force_snat_for_lb = 1; ct_dnat;",
+                                ds_cstr(&undnat_match), action,
                                 &lb->header_);
+        free(action);
     } else {
         ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_UNDNAT, 120,
                                 ds_cstr(&undnat_match), "ct_dnat;",
@@ -8689,6 +8691,105 @@ add_router_lb_flow(struct hmap *lflows, struct ovn_datapath *od,
     ds_destroy(&undnat_match);
 }
 
+static void
+build_lrouter_lb_flows(struct hmap *lflows, struct ovn_datapath *od,
+                       struct hmap *lbs, struct shash *meter_groups,
+                       struct sset *nat_entries, struct ds *match,
+                       struct ds *actions)
+{
+    /* A set to hold all ips that need defragmentation and tracking. */
+    struct sset all_ips = SSET_INITIALIZER(&all_ips);
+    bool lb_force_snat_ip =
+        !lport_addresses_is_empty(&od->lb_force_snat_addrs);
+
+    for (int i = 0; i < od->nbr->n_load_balancer; i++) {
+        struct nbrec_load_balancer *nb_lb = od->nbr->load_balancer[i];
+        struct ovn_northd_lb *lb =
+            ovn_northd_lb_find(lbs, &nb_lb->header_.uuid);
+        ovs_assert(lb);
+
+        bool lb_skip_snat = smap_get_bool(&nb_lb->options, "skip_snat", false);
+        if (lb_skip_snat) {
+            ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 120,
+                          "flags.skip_snat_for_lb == 1 && ip", "next;");
+        }
+
+        for (size_t j = 0; j < lb->n_vips; j++) {
+            struct ovn_lb_vip *lb_vip = &lb->vips[j];
+            struct ovn_northd_lb_vip *lb_vip_nb = &lb->vips_nb[j];
+            ds_clear(actions);
+            build_lb_vip_actions(lb_vip, lb_vip_nb, actions,
+                                 lb->selection_fields, false);
+
+            if (!sset_contains(&all_ips, lb_vip->vip_str)) {
+                sset_add(&all_ips, lb_vip->vip_str);
+                /* If there are any load balancing rules, we should send
+                 * the packet to conntrack for defragmentation and
+                 * tracking.  This helps with two things.
+                 *
+                 * 1. With tracking, we can send only new connections to
+                 *    pick a DNAT ip address from a group.
+                 * 2. If there are L4 ports in load balancing rules, we
+                 *    need the defragmentation to match on L4 ports. */
+                ds_clear(match);
+                if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
+                    ds_put_format(match, "ip && ip4.dst == %s",
+                                  lb_vip->vip_str);
+                } else {
+                    ds_put_format(match, "ip && ip6.dst == %s",
+                                  lb_vip->vip_str);
+                }
+                ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_DEFRAG,
+                                        100, ds_cstr(match), "ct_next;",
+                                        &nb_lb->header_);
+            }
+
+            /* Higher priority rules are added for load-balancing in DNAT
+             * table.  For every match (on a VIP[:port]), we add two flows
+             * via add_router_lb_flow().  One flow is for specific matching
+             * on ct.new with an action of "ct_lb($targets);".  The other
+             * flow is for ct.est with an action of "ct_dnat;". */
+            ds_clear(match);
+            if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
+                ds_put_format(match, "ip && ip4.dst == %s",
+                              lb_vip->vip_str);
+            } else {
+                ds_put_format(match, "ip && ip6.dst == %s",
+                              lb_vip->vip_str);
+            }
+
+            int prio = 110;
+            bool is_udp = nullable_string_is_equal(nb_lb->protocol, "udp");
+            bool is_sctp = nullable_string_is_equal(nb_lb->protocol,
+                                                    "sctp");
+            const char *proto = is_udp ? "udp" : is_sctp ? "sctp" : "tcp";
+
+            if (lb_vip->vip_port) {
+                ds_put_format(match, " && %s && %s.dst == %d", proto,
+                              proto, lb_vip->vip_port);
+                prio = 120;
+            }
+
+            if (od->l3redirect_port &&
+                (lb_vip->n_backends || !lb_vip->empty_backend_rej)) {
+                ds_put_format(match, " && is_chassis_resident(%s)",
+                              od->l3redirect_port->json_key);
+            }
+
+            enum lb_snat_type snat_type = NO_FORCE_SNAT;
+            if (lb_skip_snat) {
+                snat_type = SKIP_SNAT;
+            } else if (lb_force_snat_ip || od->lb_force_snat_router_ip) {
+                snat_type = FORCE_SNAT;
+            }
+            add_router_lb_flow(lflows, od, match, actions, prio,
+                               snat_type, lb_vip, proto, nb_lb,
+                               meter_groups, nat_entries);
+        }
+    }
+    sset_destroy(&all_ips);
+}
+
 #define ND_RA_MAX_INTERVAL_MAX 1800
 #define ND_RA_MAX_INTERVAL_MIN 4
 
@@ -8893,14 +8994,12 @@ build_lrouter_arp_flow(struct ovn_datapath *od, struct ovn_port *op,
                       "arp.op = 2; /* ARP reply */ "
                       "arp.tha = arp.sha; "
                       "arp.sha = %s; "
-                      "arp.tpa = arp.spa; "
-                      "arp.spa = %s; "
+                      "arp.tpa <-> arp.spa; "
                       "outport = inport; "
                       "flags.loopback = 1; "
                       "output;",
                       eth_addr,
-                      eth_addr,
-                      ip_address);
+                      eth_addr);
     }
 
     ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_IP_INPUT, priority,
@@ -10855,16 +10954,24 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
         get_router_load_balancer_ips(op->od, &all_ips_v4, &all_ips_v6);
 
         const char *ip_address;
-        SSET_FOR_EACH (ip_address, &all_ips_v4) {
+        if (sset_count(&all_ips_v4)) {
             ds_clear(match);
             if (op == op->od->l3dgw_port) {
                 ds_put_format(match, "is_chassis_resident(%s)",
                               op->od->l3redirect_port->json_key);
             }
 
-            build_lrouter_arp_flow(op->od, op,
-                                   ip_address, REG_INPORT_ETH_ADDR,
+            struct ds load_balancer_ips_v4 = DS_EMPTY_INITIALIZER;
+
+            /* For IPv4 we can just create one rule with all required IPs. */
+            ds_put_cstr(&load_balancer_ips_v4, "{ ");
+            ds_put_and_free_cstr(&load_balancer_ips_v4,
+                                 sset_join(&all_ips_v4, ", ", " }"));
+
+            build_lrouter_arp_flow(op->od, op, ds_cstr(&load_balancer_ips_v4),
+                                   REG_INPORT_ETH_ADDR,
                                    match, false, 90, NULL, lflows);
+            ds_destroy(&load_balancer_ips_v4);
         }
 
         SSET_FOR_EACH (ip_address, &all_ips_v6) {
@@ -11002,668 +11109,643 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
     }
 }
 
-/* NAT, Defrag and load balancing. */
 static void
-build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od,
-                                struct hmap *lflows,
-                                struct shash *meter_groups,
-                                struct hmap *lbs,
-                                struct ds *match, struct ds *actions)
+build_lrouter_in_unsnat_flow(struct hmap *lflows, struct ovn_datapath *od,
+                             const struct nbrec_nat *nat, struct ds *match,
+                             struct ds *actions, bool distributed, bool is_v6)
 {
-    if (od->nbr) {
+    /* Ingress UNSNAT table: It is for already established connections'
+    * reverse traffic. i.e., SNAT has already been done in egress
+    * pipeline and now the packet has entered the ingress pipeline as
+    * part of a reply. We undo the SNAT here.
+    *
+    * Undoing SNAT has to happen before DNAT processing.  This is
+    * because when the packet was DNATed in ingress pipeline, it did
+    * not know about the possibility of eventual additional SNAT in
+    * egress pipeline. */
+    if (strcmp(nat->type, "snat") && strcmp(nat->type, "dnat_and_snat")) {
+        return;
+    }
 
-        /* Packets are allowed by default. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_DEFRAG, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_OUT_UNDNAT, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_OUT_EGR_LOOP, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ECMP_STATEFUL, 0, "1", "next;");
-
-        /* Send the IPv6 NS packets to next table. When ovn-controller
-         * generates IPv6 NS (for the action - nd_ns{}), the injected
-         * packet would go through conntrack - which is not required. */
-        ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 120, "nd_ns", "next;");
-
-        /* NAT rules are only valid on Gateway routers and routers with
-         * l3dgw_port (router has a port with gateway chassis
-         * specified). */
-        if (!smap_get(&od->nbr->options, "chassis") && !od->l3dgw_port) {
-            return;
+    bool stateless = lrouter_nat_is_stateless(nat);
+    if (!od->l3dgw_port) {
+        /* Gateway router. */
+        ds_clear(match);
+        ds_clear(actions);
+        ds_put_format(match, "ip && ip%s.dst == %s",
+                      is_v6 ? "6" : "4", nat->external_ip);
+        if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
+            ds_put_format(actions, "ip%s.dst=%s; next;",
+                          is_v6 ? "6" : "4", nat->logical_ip);
+        } else {
+            ds_put_cstr(actions, "ct_snat;");
         }
 
-        struct sset nat_entries = SSET_INITIALIZER(&nat_entries);
+        ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_UNSNAT,
+                                90, ds_cstr(match), ds_cstr(actions),
+                                &nat->header_);
+    } else {
+        /* Distributed router. */
 
-        bool dnat_force_snat_ip =
-            !lport_addresses_is_empty(&od->dnat_force_snat_addrs);
-        bool lb_force_snat_ip =
-            !lport_addresses_is_empty(&od->lb_force_snat_addrs);
+        /* Traffic received on l3dgw_port is subject to NAT. */
+        ds_clear(match);
+        ds_clear(actions);
+        ds_put_format(match, "ip && ip%s.dst == %s && inport == %s",
+                      is_v6 ? "6" : "4", nat->external_ip,
+                      od->l3dgw_port->json_key);
+        if (!distributed && od->l3redirect_port) {
+            /* Flows for NAT rules that are centralized are only
+            * programmed on the gateway chassis. */
+            ds_put_format(match, " && is_chassis_resident(%s)",
+                          od->l3redirect_port->json_key);
+        }
 
-        for (int i = 0; i < od->nbr->n_nat; i++) {
-            const struct nbrec_nat *nat;
+        if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
+            ds_put_format(actions, "ip%s.dst=%s; next;",
+                          is_v6 ? "6" : "4", nat->logical_ip);
+        } else {
+            ds_put_cstr(actions, "ct_snat;");
+        }
 
-            nat = od->nbr->nat[i];
+        ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_UNSNAT,
+                                100, ds_cstr(match), ds_cstr(actions),
+                                &nat->header_);
+    }
+}
 
-            ovs_be32 ip, mask;
-            struct in6_addr ipv6, mask_v6, v6_exact = IN6ADDR_EXACT_INIT;
-            bool is_v6 = false;
-            bool stateless = lrouter_nat_is_stateless(nat);
-            struct nbrec_address_set *allowed_ext_ips =
-                                      nat->allowed_ext_ips;
-            struct nbrec_address_set *exempted_ext_ips =
-                                      nat->exempted_ext_ips;
+static void
+build_lrouter_in_dnat_flow(struct hmap *lflows, struct ovn_datapath *od,
+                           const struct nbrec_nat *nat, struct ds *match,
+                           struct ds *actions, bool distributed,
+                           ovs_be32 mask, bool is_v6)
+{
+    /* Ingress DNAT table: Packets enter the pipeline with destination
+    * IP address that needs to be DNATted from a external IP address
+    * to a logical IP address. */
+    if (!strcmp(nat->type, "dnat") || !strcmp(nat->type, "dnat_and_snat")) {
+        bool stateless = lrouter_nat_is_stateless(nat);
 
-            if (allowed_ext_ips && exempted_ext_ips) {
-                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-                VLOG_WARN_RL(&rl, "NAT rule: "UUID_FMT" not applied, since "
-                             "both allowed and exempt external ips set",
-                             UUID_ARGS(&(nat->header_.uuid)));
-                continue;
+        if (!od->l3dgw_port) {
+            /* Gateway router. */
+            /* Packet when it goes from the initiator to destination.
+            * We need to set flags.loopback because the router can
+            * send the packet back through the same interface. */
+            ds_clear(match);
+            ds_put_format(match, "ip && ip%s.dst == %s",
+                          is_v6 ? "6" : "4", nat->external_ip);
+            ds_clear(actions);
+            if (nat->allowed_ext_ips || nat->exempted_ext_ips) {
+                lrouter_nat_add_ext_ip_match(od, lflows, match, nat,
+                                             is_v6, true, mask);
             }
 
-            char *error = ip_parse_masked(nat->external_ip, &ip, &mask);
-            if (error || mask != OVS_BE32_MAX) {
-                free(error);
-                error = ipv6_parse_masked(nat->external_ip, &ipv6, &mask_v6);
-                if (error || memcmp(&mask_v6, &v6_exact, sizeof(mask_v6))) {
-                    /* Invalid for both IPv4 and IPv6 */
-                    static struct vlog_rate_limit rl =
-                        VLOG_RATE_LIMIT_INIT(5, 1);
-                    VLOG_WARN_RL(&rl, "bad external ip %s for nat",
-                                 nat->external_ip);
-                    free(error);
-                    continue;
-                }
-                /* It was an invalid IPv4 address, but valid IPv6.
-                 * Treat the rest of the handling of this NAT rule
-                 * as IPv6. */
-                is_v6 = true;
-            }
-
-            /* Check the validity of nat->logical_ip. 'logical_ip' can
-             * be a subnet when the type is "snat". */
-            int cidr_bits;
-            if (is_v6) {
-                error = ipv6_parse_masked(nat->logical_ip, &ipv6, &mask_v6);
-                cidr_bits = ipv6_count_cidr_bits(&mask_v6);
-            } else {
-                error = ip_parse_masked(nat->logical_ip, &ip, &mask);
-                cidr_bits = ip_count_cidr_bits(mask);
+            if (!lport_addresses_is_empty(&od->dnat_force_snat_addrs)) {
+                /* Indicate to the future tables that a DNAT has taken
+                * place and a force SNAT needs to be done in the
+                * Egress SNAT table. */
+                ds_put_format(actions, "flags.force_snat_for_dnat = 1; ");
             }
-            if (!strcmp(nat->type, "snat")) {
-                if (error) {
-                    /* Invalid for both IPv4 and IPv6 */
-                    static struct vlog_rate_limit rl =
-                        VLOG_RATE_LIMIT_INIT(5, 1);
-                    VLOG_WARN_RL(&rl, "bad ip network or ip %s for snat "
-                                 "in router "UUID_FMT"",
-                                 nat->logical_ip, UUID_ARGS(&od->key));
-                    free(error);
-                    continue;
-                }
+
+            if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
+                ds_put_format(actions, "flags.loopback = 1; "
+                              "ip%s.dst=%s; next;",
+                              is_v6 ? "6" : "4", nat->logical_ip);
             } else {
-                if (error || (!is_v6 && mask != OVS_BE32_MAX)
-                    || (is_v6 && memcmp(&mask_v6, &v6_exact,
-                                        sizeof mask_v6))) {
-                    /* Invalid for both IPv4 and IPv6 */
-                    static struct vlog_rate_limit rl =
-                        VLOG_RATE_LIMIT_INIT(5, 1);
-                    VLOG_WARN_RL(&rl, "bad ip %s for dnat in router "
-                        ""UUID_FMT"", nat->logical_ip, UUID_ARGS(&od->key));
-                    free(error);
-                    continue;
+                ds_put_format(actions, "flags.loopback = 1; ct_dnat(%s",
+                              nat->logical_ip);
+
+                if (nat->external_port_range[0]) {
+                    ds_put_format(actions, ",%s", nat->external_port_range);
                 }
+                ds_put_format(actions, ");");
             }
 
-            /* For distributed router NAT, determine whether this NAT rule
-             * satisfies the conditions for distributed NAT processing. */
-            bool distributed = false;
-            struct eth_addr mac;
-            if (od->l3dgw_port && !strcmp(nat->type, "dnat_and_snat") &&
-                nat->logical_port && nat->external_mac) {
-                if (eth_addr_from_string(nat->external_mac, &mac)) {
-                    distributed = true;
-                } else {
-                    static struct vlog_rate_limit rl =
-                        VLOG_RATE_LIMIT_INIT(5, 1);
-                    VLOG_WARN_RL(&rl, "bad mac %s for dnat in router "
-                        ""UUID_FMT"", nat->external_mac, UUID_ARGS(&od->key));
-                    continue;
+            ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_DNAT, 100,
+                                    ds_cstr(match), ds_cstr(actions),
+                                    &nat->header_);
+        } else {
+            /* Distributed router. */
+
+            /* Traffic received on l3dgw_port is subject to NAT. */
+            ds_clear(match);
+            ds_put_format(match, "ip && ip%s.dst == %s && inport == %s",
+                          is_v6 ? "6" : "4", nat->external_ip,
+                          od->l3dgw_port->json_key);
+            if (!distributed && od->l3redirect_port) {
+                /* Flows for NAT rules that are centralized are only
+                * programmed on the gateway chassis. */
+                ds_put_format(match, " && is_chassis_resident(%s)",
+                              od->l3redirect_port->json_key);
+            }
+            ds_clear(actions);
+            if (nat->allowed_ext_ips || nat->exempted_ext_ips) {
+                lrouter_nat_add_ext_ip_match(od, lflows, match, nat,
+                                             is_v6, true, mask);
+            }
+
+            if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
+                ds_put_format(actions, "ip%s.dst=%s; next;",
+                              is_v6 ? "6" : "4", nat->logical_ip);
+            } else {
+                ds_put_format(actions, "ct_dnat(%s", nat->logical_ip);
+                if (nat->external_port_range[0]) {
+                    ds_put_format(actions, ",%s", nat->external_port_range);
                 }
+                ds_put_format(actions, ");");
             }
 
-            /* Ingress UNSNAT table: It is for already established connections'
-             * reverse traffic. i.e., SNAT has already been done in egress
-             * pipeline and now the packet has entered the ingress pipeline as
-             * part of a reply. We undo the SNAT here.
-             *
-             * Undoing SNAT has to happen before DNAT processing.  This is
-             * because when the packet was DNATed in ingress pipeline, it did
-             * not know about the possibility of eventual additional SNAT in
-             * egress pipeline. */
-            if (!strcmp(nat->type, "snat")
-                || !strcmp(nat->type, "dnat_and_snat")) {
-                if (!od->l3dgw_port) {
-                    /* Gateway router. */
-                    ds_clear(match);
-                    ds_clear(actions);
-                    ds_put_format(match, "ip && ip%s.dst == %s",
-                                  is_v6 ? "6" : "4",
-                                  nat->external_ip);
-                    if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
-                       ds_put_format(actions, "ip%s.dst=%s; next;",
-                                     is_v6 ? "6" : "4", nat->logical_ip);
-                    } else {
-                       ds_put_cstr(actions, "ct_snat;");
-                    }
+            ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_DNAT, 100,
+                                    ds_cstr(match), ds_cstr(actions),
+                                    &nat->header_);
+        }
+    }
+}
 
-                    ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_UNSNAT,
-                                            90, ds_cstr(match),
-                                            ds_cstr(actions),
-                                            &nat->header_);
-                } else {
-                    /* Distributed router. */
+static void
+build_lrouter_out_undnat_flow(struct hmap *lflows, struct ovn_datapath *od,
+                              const struct nbrec_nat *nat, struct ds *match,
+                              struct ds *actions, bool distributed,
+                              struct eth_addr mac, bool is_v6)
+{
+    /* Egress UNDNAT table: It is for already established connections'
+    * reverse traffic. i.e., DNAT has already been done in ingress
+    * pipeline and now the packet has entered the egress pipeline as
+    * part of a reply. We undo the DNAT here.
+    *
+    * Note that this only applies for NAT on a distributed router.
+    * Undo DNAT on a gateway router is done in the ingress DNAT
+    * pipeline stage. */
+    if (!od->l3dgw_port ||
+        (strcmp(nat->type, "dnat") && strcmp(nat->type, "dnat_and_snat"))) {
+        return;
+    }
 
-                    /* Traffic received on l3dgw_port is subject to NAT. */
-                    ds_clear(match);
-                    ds_clear(actions);
-                    ds_put_format(match, "ip && ip%s.dst == %s"
-                                          " && inport == %s",
-                                  is_v6 ? "6" : "4",
-                                  nat->external_ip,
-                                  od->l3dgw_port->json_key);
-                    if (!distributed && od->l3redirect_port) {
-                        /* Flows for NAT rules that are centralized are only
-                         * programmed on the gateway chassis. */
-                        ds_put_format(match, " && is_chassis_resident(%s)",
-                                      od->l3redirect_port->json_key);
-                    }
+    ds_clear(match);
+    ds_put_format(match, "ip && ip%s.src == %s && outport == %s",
+                  is_v6 ? "6" : "4", nat->logical_ip,
+                  od->l3dgw_port->json_key);
+    if (!distributed && od->l3redirect_port) {
+        /* Flows for NAT rules that are centralized are only
+        * programmed on the gateway chassis. */
+        ds_put_format(match, " && is_chassis_resident(%s)",
+                      od->l3redirect_port->json_key);
+    }
+    ds_clear(actions);
+    if (distributed) {
+        ds_put_format(actions, "eth.src = "ETH_ADDR_FMT"; ",
+                      ETH_ADDR_ARGS(mac));
+    }
 
-                    if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
-                        ds_put_format(actions, "ip%s.dst=%s; next;",
-                                      is_v6 ? "6" : "4", nat->logical_ip);
-                    } else {
-                        ds_put_cstr(actions, "ct_snat;");
-                    }
+    if (!strcmp(nat->type, "dnat_and_snat") &&
+        lrouter_nat_is_stateless(nat)) {
+        ds_put_format(actions, "ip%s.src=%s; next;",
+                      is_v6 ? "6" : "4", nat->external_ip);
+    } else {
+        ds_put_format(actions, "ct_dnat;");
+    }
 
-                    ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_UNSNAT,
-                                            100,
-                                            ds_cstr(match), ds_cstr(actions),
-                                            &nat->header_);
-                }
-            }
+    ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_UNDNAT, 100,
+                            ds_cstr(match), ds_cstr(actions),
+                            &nat->header_);
+}
 
-            /* Ingress DNAT table: Packets enter the pipeline with destination
-             * IP address that needs to be DNATted from a external IP address
-             * to a logical IP address. */
-            if (!strcmp(nat->type, "dnat")
-                || !strcmp(nat->type, "dnat_and_snat")) {
-                if (!od->l3dgw_port) {
-                    /* Gateway router. */
-                    /* Packet when it goes from the initiator to destination.
-                     * We need to set flags.loopback because the router can
-                     * send the packet back through the same interface. */
-                    ds_clear(match);
-                    ds_put_format(match, "ip && ip%s.dst == %s",
-                                  is_v6 ? "6" : "4",
-                                  nat->external_ip);
-                    ds_clear(actions);
-                    if (allowed_ext_ips || exempted_ext_ips) {
-                        lrouter_nat_add_ext_ip_match(od, lflows, match, nat,
-                                                     is_v6, true, mask);
-                    }
+static void
+build_lrouter_out_snat_flow(struct hmap *lflows, struct ovn_datapath *od,
+                            const struct nbrec_nat *nat, struct ds *match,
+                            struct ds *actions, bool distributed,
+                            struct eth_addr mac, ovs_be32 mask,
+                            int cidr_bits, bool is_v6)
+{
+    /* Egress SNAT table: Packets enter the egress pipeline with
+    * source ip address that needs to be SNATted to a external ip
+    * address. */
+    if (strcmp(nat->type, "snat") && strcmp(nat->type, "dnat_and_snat")) {
+        return;
+    }
 
-                    if (dnat_force_snat_ip) {
-                        /* Indicate to the future tables that a DNAT has taken
-                         * place and a force SNAT needs to be done in the
-                         * Egress SNAT table. */
-                        ds_put_format(actions,
-                                      "flags.force_snat_for_dnat = 1; ");
-                    }
+    bool stateless = lrouter_nat_is_stateless(nat);
+    if (!od->l3dgw_port) {
+        /* Gateway router. */
+        ds_clear(match);
+        ds_put_format(match, "ip && ip%s.src == %s",
+                      is_v6 ? "6" : "4", nat->logical_ip);
+        ds_clear(actions);
 
-                    if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
-                        ds_put_format(actions, "flags.loopback = 1; "
-                                      "ip%s.dst=%s; next;",
-                                      is_v6 ? "6" : "4", nat->logical_ip);
-                    } else {
-                        ds_put_format(actions, "flags.loopback = 1; "
-                                      "ct_dnat(%s", nat->logical_ip);
+        if (nat->allowed_ext_ips || nat->exempted_ext_ips) {
+            lrouter_nat_add_ext_ip_match(od, lflows, match, nat,
+                                         is_v6, false, mask);
+        }
 
-                        if (nat->external_port_range[0]) {
-                            ds_put_format(actions, ",%s",
-                                          nat->external_port_range);
-                        }
-                        ds_put_format(actions, ");");
-                    }
+        if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
+            ds_put_format(actions, "ip%s.src=%s; next;",
+                          is_v6 ? "6" : "4", nat->external_ip);
+        } else {
+            ds_put_format(actions, "ct_snat(%s", nat->external_ip);
 
-                    ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_DNAT, 100,
-                                            ds_cstr(match), ds_cstr(actions),
-                                            &nat->header_);
-                } else {
-                    /* Distributed router. */
+            if (nat->external_port_range[0]) {
+                ds_put_format(actions, ",%s",
+                              nat->external_port_range);
+            }
+            ds_put_format(actions, ");");
+        }
 
-                    /* Traffic received on l3dgw_port is subject to NAT. */
-                    ds_clear(match);
-                    ds_put_format(match, "ip && ip%s.dst == %s"
-                                          " && inport == %s",
-                                  is_v6 ? "6" : "4",
-                                  nat->external_ip,
-                                  od->l3dgw_port->json_key);
-                    if (!distributed && od->l3redirect_port) {
-                        /* Flows for NAT rules that are centralized are only
-                         * programmed on the gateway chassis. */
-                        ds_put_format(match, " && is_chassis_resident(%s)",
-                                      od->l3redirect_port->json_key);
-                    }
-                    ds_clear(actions);
-                    if (allowed_ext_ips || exempted_ext_ips) {
-                        lrouter_nat_add_ext_ip_match(od, lflows, match, nat,
-                                                     is_v6, true, mask);
-                    }
+        /* The priority here is calculated such that the
+        * nat->logical_ip with the longest mask gets a higher
+        * priority. */
+        ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_SNAT,
+                                cidr_bits + 1, ds_cstr(match),
+                                ds_cstr(actions), &nat->header_);
+    } else {
+        uint16_t priority = cidr_bits + 1;
 
-                    if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
-                        ds_put_format(actions, "ip%s.dst=%s; next;",
-                                      is_v6 ? "6" : "4", nat->logical_ip);
-                    } else {
-                        ds_put_format(actions, "ct_dnat(%s", nat->logical_ip);
-                        if (nat->external_port_range[0]) {
-                            ds_put_format(actions, ",%s",
-                                          nat->external_port_range);
-                        }
-                        ds_put_format(actions, ");");
-                    }
+        /* Distributed router. */
+        ds_clear(match);
+        ds_put_format(match, "ip && ip%s.src == %s && outport == %s",
+                      is_v6 ? "6" : "4", nat->logical_ip,
+                      od->l3dgw_port->json_key);
+        if (!distributed && od->l3redirect_port) {
+            /* Flows for NAT rules that are centralized are only
+            * programmed on the gateway chassis. */
+            priority += 128;
+            ds_put_format(match, " && is_chassis_resident(%s)",
+                          od->l3redirect_port->json_key);
+        }
+        ds_clear(actions);
 
-                    ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_DNAT, 100,
-                                            ds_cstr(match), ds_cstr(actions),
-                                            &nat->header_);
-                }
-            }
+        if (nat->allowed_ext_ips || nat->exempted_ext_ips) {
+            lrouter_nat_add_ext_ip_match(od, lflows, match, nat,
+                                         is_v6, false, mask);
+        }
 
-            /* ARP resolve for NAT IPs. */
-            if (od->l3dgw_port) {
-                if (!strcmp(nat->type, "snat")) {
-                    ds_clear(match);
-                    ds_put_format(
-                        match, "inport == %s && %s == %s",
-                        od->l3dgw_port->json_key,
-                        is_v6 ? "ip6.src" : "ip4.src", nat->external_ip);
-                    ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_IP_INPUT,
-                                            120, ds_cstr(match), "next;",
-                                            &nat->header_);
-                }
+        if (distributed) {
+            ds_put_format(actions, "eth.src = "ETH_ADDR_FMT"; ",
+                          ETH_ADDR_ARGS(mac));
+        }
 
-                if (!sset_contains(&nat_entries, nat->external_ip)) {
-                    ds_clear(match);
-                    ds_put_format(
-                        match, "outport == %s && %s == %s",
-                        od->l3dgw_port->json_key,
-                        is_v6 ? REG_NEXT_HOP_IPV6 : REG_NEXT_HOP_IPV4,
+        if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
+            ds_put_format(actions, "ip%s.src=%s; next;",
+                          is_v6 ? "6" : "4", nat->external_ip);
+        } else {
+            ds_put_format(actions, "ct_snat(%s",
                         nat->external_ip);
-                    ds_clear(actions);
-                    ds_put_format(
-                        actions, "eth.dst = %s; next;",
-                        distributed ? nat->external_mac :
-                        od->l3dgw_port->lrp_networks.ea_s);
-                    ovn_lflow_add_with_hint(lflows, od,
-                                            S_ROUTER_IN_ARP_RESOLVE,
-                                            100, ds_cstr(match),
-                                            ds_cstr(actions),
-                                            &nat->header_);
-                    sset_add(&nat_entries, nat->external_ip);
-                }
-            } else {
-                /* Add the NAT external_ip to the nat_entries even for
-                 * gateway routers. This is required for adding load balancer
-                 * flows.*/
-                sset_add(&nat_entries, nat->external_ip);
+            if (nat->external_port_range[0]) {
+                ds_put_format(actions, ",%s", nat->external_port_range);
             }
+            ds_put_format(actions, ");");
+        }
 
-            /* Egress UNDNAT table: It is for already established connections'
-             * reverse traffic. i.e., DNAT has already been done in ingress
-             * pipeline and now the packet has entered the egress pipeline as
-             * part of a reply. We undo the DNAT here.
-             *
-             * Note that this only applies for NAT on a distributed router.
-             * Undo DNAT on a gateway router is done in the ingress DNAT
-             * pipeline stage. */
-            if (od->l3dgw_port && (!strcmp(nat->type, "dnat")
-                || !strcmp(nat->type, "dnat_and_snat"))) {
-                ds_clear(match);
-                ds_put_format(match, "ip && ip%s.src == %s"
-                                      " && outport == %s",
-                              is_v6 ? "6" : "4",
-                              nat->logical_ip,
-                              od->l3dgw_port->json_key);
-                if (!distributed && od->l3redirect_port) {
-                    /* Flows for NAT rules that are centralized are only
-                     * programmed on the gateway chassis. */
-                    ds_put_format(match, " && is_chassis_resident(%s)",
-                                  od->l3redirect_port->json_key);
-                }
-                ds_clear(actions);
-                if (distributed) {
-                    ds_put_format(actions, "eth.src = "ETH_ADDR_FMT"; ",
-                                  ETH_ADDR_ARGS(mac));
-                }
+        /* The priority here is calculated such that the
+        * nat->logical_ip with the longest mask gets a higher
+        * priority. */
+        ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_SNAT,
+                                priority, ds_cstr(match),
+                                ds_cstr(actions), &nat->header_);
+    }
+}
 
-                if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
-                    ds_put_format(actions, "ip%s.src=%s; next;",
-                                  is_v6 ? "6" : "4", nat->external_ip);
-                } else {
-                    ds_put_format(actions, "ct_dnat;");
-                }
+static void
+build_lrouter_ingress_flow(struct hmap *lflows, struct ovn_datapath *od,
+                           const struct nbrec_nat *nat, struct ds *match,
+                           struct ds *actions, struct eth_addr mac,
+                           bool distributed, bool is_v6)
+{
+    if (od->l3dgw_port && !strcmp(nat->type, "snat")) {
+        ds_clear(match);
+        ds_put_format(
+            match, "inport == %s && %s == %s",
+            od->l3dgw_port->json_key,
+            is_v6 ? "ip6.src" : "ip4.src", nat->external_ip);
+        ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_IP_INPUT,
+                                120, ds_cstr(match), "next;",
+                                &nat->header_);
+    }
+    /* Logical router ingress table 0:
+    * For NAT on a distributed router, add rules allowing
+    * ingress traffic with eth.dst matching nat->external_mac
+    * on the l3dgw_port instance where nat->logical_port is
+    * resident. */
+    if (distributed) {
+        /* Store the ethernet address of the port receiving the packet.
+        * This will save us from having to match on inport further
+        * down in the pipeline.
+        */
+        ds_clear(actions);
+        ds_put_format(actions, REG_INPORT_ETH_ADDR " = %s; next;",
+                    od->l3dgw_port->lrp_networks.ea_s);
 
-                ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_UNDNAT, 100,
-                                        ds_cstr(match), ds_cstr(actions),
-                                        &nat->header_);
-            }
+        ds_clear(match);
+        ds_put_format(match,
+                    "eth.dst == "ETH_ADDR_FMT" && inport == %s"
+                    " && is_chassis_resident(\"%s\")",
+                    ETH_ADDR_ARGS(mac),
+                    od->l3dgw_port->json_key,
+                    nat->logical_port);
+        ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_ADMISSION, 50,
+                                ds_cstr(match), ds_cstr(actions),
+                                &nat->header_);
+    }
+}
 
-            /* Egress SNAT table: Packets enter the egress pipeline with
-             * source ip address that needs to be SNATted to a external ip
-             * address. */
-            if (!strcmp(nat->type, "snat")
-                || !strcmp(nat->type, "dnat_and_snat")) {
-                if (!od->l3dgw_port) {
-                    /* Gateway router. */
-                    ds_clear(match);
-                    ds_put_format(match, "ip && ip%s.src == %s",
-                                  is_v6 ? "6" : "4",
-                                  nat->logical_ip);
-                    ds_clear(actions);
+static int
+lrouter_check_nat_entry(struct ovn_datapath *od, const struct nbrec_nat *nat,
+                        ovs_be32 *mask, bool *is_v6, int *cidr_bits,
+                        struct eth_addr *mac, bool *distributed)
+{
+    struct in6_addr ipv6, mask_v6, v6_exact = IN6ADDR_EXACT_INIT;
+    ovs_be32 ip;
 
-                    if (allowed_ext_ips || exempted_ext_ips) {
-                        lrouter_nat_add_ext_ip_match(od, lflows, match, nat,
-                                                     is_v6, false, mask);
-                    }
+    if (nat->allowed_ext_ips && nat->exempted_ext_ips) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+        VLOG_WARN_RL(&rl, "NAT rule: "UUID_FMT" not applied, since "
+                    "both allowed and exempt external ips set",
+                    UUID_ARGS(&(nat->header_.uuid)));
+        return -EINVAL;
+    }
 
-                    if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
-                        ds_put_format(actions, "ip%s.src=%s; next;",
-                                      is_v6 ? "6" : "4", nat->external_ip);
-                    } else {
-                        ds_put_format(actions, "ct_snat(%s",
-                                      nat->external_ip);
+    char *error = ip_parse_masked(nat->external_ip, &ip, mask);
+    *is_v6 = false;
 
-                        if (nat->external_port_range[0]) {
-                            ds_put_format(actions, ",%s",
-                                          nat->external_port_range);
-                        }
-                        ds_put_format(actions, ");");
-                    }
+    if (error || *mask != OVS_BE32_MAX) {
+        free(error);
+        error = ipv6_parse_masked(nat->external_ip, &ipv6, &mask_v6);
+        if (error || memcmp(&mask_v6, &v6_exact, sizeof(mask_v6))) {
+            /* Invalid for both IPv4 and IPv6 */
+            static struct vlog_rate_limit rl =
+                VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad external ip %s for nat",
+                        nat->external_ip);
+            free(error);
+            return -EINVAL;
+        }
+        /* It was an invalid IPv4 address, but valid IPv6.
+        * Treat the rest of the handling of this NAT rule
+        * as IPv6. */
+        *is_v6 = true;
+    }
 
-                    /* The priority here is calculated such that the
-                     * nat->logical_ip with the longest mask gets a higher
-                     * priority. */
-                    ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_SNAT,
-                                            cidr_bits + 1,
-                                            ds_cstr(match), ds_cstr(actions),
-                                            &nat->header_);
-                } else {
-                    uint16_t priority = cidr_bits + 1;
+    /* Check the validity of nat->logical_ip. 'logical_ip' can
+    * be a subnet when the type is "snat". */
+    if (*is_v6) {
+        error = ipv6_parse_masked(nat->logical_ip, &ipv6, &mask_v6);
+        *cidr_bits = ipv6_count_cidr_bits(&mask_v6);
+    } else {
+        error = ip_parse_masked(nat->logical_ip, &ip, mask);
+        *cidr_bits = ip_count_cidr_bits(*mask);
+    }
+    if (!strcmp(nat->type, "snat")) {
+        if (error) {
+            /* Invalid for both IPv4 and IPv6 */
+            static struct vlog_rate_limit rl =
+                VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad ip network or ip %s for snat "
+                        "in router "UUID_FMT"",
+                        nat->logical_ip, UUID_ARGS(&od->key));
+            free(error);
+            return -EINVAL;
+        }
+    } else {
+        if (error || (*is_v6 == false && *mask != OVS_BE32_MAX)
+            || (*is_v6 && memcmp(&mask_v6, &v6_exact,
+                                sizeof mask_v6))) {
+            /* Invalid for both IPv4 and IPv6 */
+            static struct vlog_rate_limit rl =
+                VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad ip %s for dnat in router "
+                ""UUID_FMT"", nat->logical_ip, UUID_ARGS(&od->key));
+            free(error);
+            return -EINVAL;
+        }
+    }
 
-                    /* Distributed router. */
-                    ds_clear(match);
-                    ds_put_format(match, "ip && ip%s.src == %s"
-                                          " && outport == %s",
-                                  is_v6 ? "6" : "4",
-                                  nat->logical_ip,
-                                  od->l3dgw_port->json_key);
-                    if (!distributed && od->l3redirect_port) {
-                        /* Flows for NAT rules that are centralized are only
-                         * programmed on the gateway chassis. */
-                        priority += 128;
-                        ds_put_format(match, " && is_chassis_resident(%s)",
-                                      od->l3redirect_port->json_key);
-                    }
-                    ds_clear(actions);
+    /* For distributed router NAT, determine whether this NAT rule
+     * satisfies the conditions for distributed NAT processing. */
+    *distributed = false;
+    if (od->l3dgw_port && !strcmp(nat->type, "dnat_and_snat") &&
+        nat->logical_port && nat->external_mac) {
+        if (eth_addr_from_string(nat->external_mac, mac)) {
+            *distributed = true;
+        } else {
+            static struct vlog_rate_limit rl =
+                VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad mac %s for dnat in router "
+                ""UUID_FMT"", nat->external_mac, UUID_ARGS(&od->key));
+            return -EINVAL;
+        }
+    }
 
-                    if (allowed_ext_ips || exempted_ext_ips) {
-                        lrouter_nat_add_ext_ip_match(od, lflows, match, nat,
-                                                     is_v6, false, mask);
-                    }
+    return 0;
+}
 
-                    if (distributed) {
-                        ds_put_format(actions, "eth.src = "ETH_ADDR_FMT"; ",
-                                      ETH_ADDR_ARGS(mac));
-                    }
+/* NAT, Defrag and load balancing. */
+static void
+build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od,
+                                struct hmap *lflows,
+                                struct shash *meter_groups,
+                                struct hmap *lbs,
+                                struct ds *match, struct ds *actions)
+{
+    if (!od->nbr) {
+        return;
+    }
 
-                    if (!strcmp(nat->type, "dnat_and_snat") && stateless) {
-                        ds_put_format(actions, "ip%s.src=%s; next;",
-                                      is_v6 ? "6" : "4", nat->external_ip);
-                    } else {
-                        ds_put_format(actions, "ct_snat(%s",
-                                      nat->external_ip);
-                        if (nat->external_port_range[0]) {
-                            ds_put_format(actions, ",%s",
-                                          nat->external_port_range);
-                        }
-                        ds_put_format(actions, ");");
-                    }
+    /* Packets are allowed by default. */
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_DEFRAG, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_UNSNAT, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_ROUTER_OUT_UNDNAT, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_ROUTER_OUT_EGR_LOOP, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_ECMP_STATEFUL, 0, "1", "next;");
+
+    /* Send the IPv6 NS packets to next table. When ovn-controller
+     * generates IPv6 NS (for the action - nd_ns{}), the injected
+     * packet would go through conntrack - which is not required. */
+    ovn_lflow_add(lflows, od, S_ROUTER_OUT_SNAT, 120, "nd_ns", "next;");
+
+    /* NAT rules are only valid on Gateway routers and routers with
+     * l3dgw_port (router has a port with gateway chassis
+     * specified). */
+    if (!smap_get(&od->nbr->options, "chassis") && !od->l3dgw_port) {
+        return;
+    }
 
-                    /* The priority here is calculated such that the
-                     * nat->logical_ip with the longest mask gets a higher
-                     * priority. */
-                    ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_SNAT,
-                                            priority, ds_cstr(match),
-                                            ds_cstr(actions),
-                                            &nat->header_);
-                }
-            }
+    struct sset nat_entries = SSET_INITIALIZER(&nat_entries);
 
-            /* Logical router ingress table 0:
-             * For NAT on a distributed router, add rules allowing
-             * ingress traffic with eth.dst matching nat->external_mac
-             * on the l3dgw_port instance where nat->logical_port is
-             * resident. */
-            if (distributed) {
-                /* Store the ethernet address of the port receiving the packet.
-                 * This will save us from having to match on inport further
-                 * down in the pipeline.
-                 */
-                ds_clear(actions);
-                ds_put_format(actions, REG_INPORT_ETH_ADDR " = %s; next;",
-                              od->l3dgw_port->lrp_networks.ea_s);
+    bool dnat_force_snat_ip =
+        !lport_addresses_is_empty(&od->dnat_force_snat_addrs);
+    bool lb_force_snat_ip =
+        !lport_addresses_is_empty(&od->lb_force_snat_addrs);
 
-                ds_clear(match);
-                ds_put_format(match,
-                              "eth.dst == "ETH_ADDR_FMT" && inport == %s"
-                              " && is_chassis_resident(\"%s\")",
-                              ETH_ADDR_ARGS(mac),
-                              od->l3dgw_port->json_key,
-                              nat->logical_port);
-                ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_ADMISSION, 50,
-                                        ds_cstr(match), ds_cstr(actions),
-                                        &nat->header_);
-            }
+    for (int i = 0; i < od->nbr->n_nat; i++) {
+        const struct nbrec_nat *nat = nat = od->nbr->nat[i];
+        struct eth_addr mac = eth_addr_broadcast;
+        bool is_v6, distributed;
+        ovs_be32 mask;
+        int cidr_bits;
 
-            /* Ingress Gateway Redirect Table: For NAT on a distributed
-             * router, add flows that are specific to a NAT rule.  These
-             * flows indicate the presence of an applicable NAT rule that
-             * can be applied in a distributed manner.
-             * In particulr REG_SRC_IPV4/REG_SRC_IPV6 and eth.src are set to
-             * NAT external IP and NAT external mac so the ARP request
-             * generated in the following stage is sent out with proper IP/MAC
-             * src addresses.
-             */
-            if (distributed) {
-                ds_clear(match);
-                ds_clear(actions);
-                ds_put_format(match,
-                              "ip%s.src == %s && outport == %s && "
-                              "is_chassis_resident(\"%s\")",
-                              is_v6 ? "6" : "4", nat->logical_ip,
-                              od->l3dgw_port->json_key, nat->logical_port);
-                ds_put_format(actions, "eth.src = %s; %s = %s; next;",
-                              nat->external_mac,
-                              is_v6 ? REG_SRC_IPV6 : REG_SRC_IPV4,
-                              nat->external_ip);
-                ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_GW_REDIRECT,
-                                        100, ds_cstr(match),
-                                        ds_cstr(actions), &nat->header_);
-            }
+        if (lrouter_check_nat_entry(od, nat, &mask, &is_v6, &cidr_bits,
+                                    &mac, &distributed) < 0) {
+            continue;
+        }
 
-            /* Egress Loopback table: For NAT on a distributed router.
-             * If packets in the egress pipeline on the distributed
-             * gateway port have ip.dst matching a NAT external IP, then
-             * loop a clone of the packet back to the beginning of the
-             * ingress pipeline with inport = outport. */
-            if (od->l3dgw_port) {
-                /* Distributed router. */
-                ds_clear(match);
-                ds_put_format(match, "ip%s.dst == %s && outport == %s",
-                              is_v6 ? "6" : "4",
-                              nat->external_ip,
-                              od->l3dgw_port->json_key);
-                if (!distributed) {
-                    ds_put_format(match, " && is_chassis_resident(%s)",
-                                  od->l3redirect_port->json_key);
-                } else {
-                    ds_put_format(match, " && is_chassis_resident(\"%s\")",
-                                  nat->logical_port);
-                }
+        /* S_ROUTER_IN_UNSNAT */
+        build_lrouter_in_unsnat_flow(lflows, od, nat, match, actions, distributed,
+                                     is_v6);
+        /* S_ROUTER_IN_DNAT */
+        build_lrouter_in_dnat_flow(lflows, od, nat, match, actions, distributed,
+                                   mask, is_v6);
 
+        /* ARP resolve for NAT IPs. */
+        if (od->l3dgw_port) {
+            if (!sset_contains(&nat_entries, nat->external_ip)) {
+                ds_clear(match);
+                ds_put_format(
+                    match, "outport == %s && %s == %s",
+                    od->l3dgw_port->json_key,
+                    is_v6 ? REG_NEXT_HOP_IPV6 : REG_NEXT_HOP_IPV4,
+                    nat->external_ip);
                 ds_clear(actions);
-                ds_put_format(actions,
-                              "clone { ct_clear; "
-                              "inport = outport; outport = \"\"; "
-                              "flags = 0; flags.loopback = 1; ");
-                for (int j = 0; j < MFF_N_LOG_REGS; j++) {
-                    ds_put_format(actions, "reg%d = 0; ", j);
-                }
-                ds_put_format(actions, REGBIT_EGRESS_LOOPBACK" = 1; "
-                              "next(pipeline=ingress, table=%d); };",
-                              ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
-                ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_EGR_LOOP, 100,
-                                        ds_cstr(match), ds_cstr(actions),
+                ds_put_format(
+                    actions, "eth.dst = %s; next;",
+                    distributed ? nat->external_mac :
+                    od->l3dgw_port->lrp_networks.ea_s);
+                ovn_lflow_add_with_hint(lflows, od,
+                                        S_ROUTER_IN_ARP_RESOLVE,
+                                        100, ds_cstr(match),
+                                        ds_cstr(actions),
                                         &nat->header_);
+                sset_add(&nat_entries, nat->external_ip);
             }
-        }
-
-        /* Handle force SNAT options set in the gateway router. */
-        if (!od->l3dgw_port) {
-            if (dnat_force_snat_ip) {
-                if (od->dnat_force_snat_addrs.n_ipv4_addrs) {
-                    build_lrouter_force_snat_flows(lflows, od, "4",
-                        od->dnat_force_snat_addrs.ipv4_addrs[0].addr_s,
-                        "dnat");
-                }
-                if (od->dnat_force_snat_addrs.n_ipv6_addrs) {
-                    build_lrouter_force_snat_flows(lflows, od, "6",
-                        od->dnat_force_snat_addrs.ipv6_addrs[0].addr_s,
-                        "dnat");
-                }
-            }
-            if (lb_force_snat_ip) {
-                if (od->lb_force_snat_addrs.n_ipv4_addrs) {
-                    build_lrouter_force_snat_flows(lflows, od, "4",
-                        od->lb_force_snat_addrs.ipv4_addrs[0].addr_s, "lb");
-                }
-                if (od->lb_force_snat_addrs.n_ipv6_addrs) {
-                    build_lrouter_force_snat_flows(lflows, od, "6",
-                        od->lb_force_snat_addrs.ipv6_addrs[0].addr_s, "lb");
-                }
+        } else {
+            /* Add the NAT external_ip to the nat_entries even for
+             * gateway routers. This is required for adding load balancer
+             * flows.*/
+            sset_add(&nat_entries, nat->external_ip);
+        }
+
+        /* S_ROUTER_OUT_UNDNAT */
+        build_lrouter_out_undnat_flow(lflows, od, nat, match, actions, distributed,
+                                      mac, is_v6);
+        /* S_ROUTER_OUT_SNAT */
+        build_lrouter_out_snat_flow(lflows, od, nat, match, actions, distributed,
+                                    mac, mask, cidr_bits, is_v6);
+
+        /* S_ROUTER_IN_ADMISSION - S_ROUTER_IN_IP_INPUT */
+        build_lrouter_ingress_flow(lflows, od, nat, match, actions,
+                                   mac, distributed, is_v6);
+
+        /* Ingress Gateway Redirect Table: For NAT on a distributed
+         * router, add flows that are specific to a NAT rule.  These
+         * flows indicate the presence of an applicable NAT rule that
+         * can be applied in a distributed manner.
+         * In particulr REG_SRC_IPV4/REG_SRC_IPV6 and eth.src are set to
+         * NAT external IP and NAT external mac so the ARP request
+         * generated in the following stage is sent out with proper IP/MAC
+         * src addresses.
+         */
+        if (distributed) {
+            ds_clear(match);
+            ds_clear(actions);
+            ds_put_format(match,
+                          "ip%s.src == %s && outport == %s && "
+                          "is_chassis_resident(\"%s\")",
+                          is_v6 ? "6" : "4", nat->logical_ip,
+                          od->l3dgw_port->json_key, nat->logical_port);
+            ds_put_format(actions, "eth.src = %s; %s = %s; next;",
+                          nat->external_mac,
+                          is_v6 ? REG_SRC_IPV6 : REG_SRC_IPV4,
+                          nat->external_ip);
+            ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_GW_REDIRECT,
+                                    100, ds_cstr(match),
+                                    ds_cstr(actions), &nat->header_);
+        }
+
+        /* Egress Loopback table: For NAT on a distributed router.
+         * If packets in the egress pipeline on the distributed
+         * gateway port have ip.dst matching a NAT external IP, then
+         * loop a clone of the packet back to the beginning of the
+         * ingress pipeline with inport = outport. */
+        if (od->l3dgw_port) {
+            /* Distributed router. */
+            ds_clear(match);
+            ds_put_format(match, "ip%s.dst == %s && outport == %s",
+                          is_v6 ? "6" : "4",
+                          nat->external_ip,
+                          od->l3dgw_port->json_key);
+            if (!distributed) {
+                ds_put_format(match, " && is_chassis_resident(%s)",
+                              od->l3redirect_port->json_key);
+            } else {
+                ds_put_format(match, " && is_chassis_resident(\"%s\")",
+                              nat->logical_port);
             }
 
-            /* For gateway router, re-circulate every packet through
-            * the DNAT zone.  This helps with the following.
-            *
-            * Any packet that needs to be unDNATed in the reverse
-            * direction gets unDNATed. Ideally this could be done in
-            * the egress pipeline. But since the gateway router
-            * does not have any feature that depends on the source
-            * ip address being external IP address for IP routing,
-            * we can do it here, saving a future re-circulation. */
-            ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 50,
-                          "ip", "flags.loopback = 1; ct_dnat;");
+            ds_clear(actions);
+            ds_put_format(actions,
+                          "clone { ct_clear; "
+                          "inport = outport; outport = \"\"; "
+                          "flags = 0; flags.loopback = 1; ");
+            for (int j = 0; j < MFF_N_LOG_REGS; j++) {
+                ds_put_format(actions, "reg%d = 0; ", j);
+            }
+            ds_put_format(actions, REGBIT_EGRESS_LOOPBACK" = 1; "
+                          "next(pipeline=ingress, table=%d); };",
+                          ovn_stage_get_table(S_ROUTER_IN_ADMISSION));
+            ovn_lflow_add_with_hint(lflows, od, S_ROUTER_OUT_EGR_LOOP, 100,
+                                    ds_cstr(match), ds_cstr(actions),
+                                    &nat->header_);
         }
+    }
 
-        /* Load balancing and packet defrag are only valid on
-         * Gateway routers or router with gateway port. */
-        if (!smap_get(&od->nbr->options, "chassis") && !od->l3dgw_port) {
-            sset_destroy(&nat_entries);
-            return;
+    /* Handle force SNAT options set in the gateway router. */
+    if (!od->l3dgw_port) {
+        if (dnat_force_snat_ip) {
+            if (od->dnat_force_snat_addrs.n_ipv4_addrs) {
+                build_lrouter_force_snat_flows(lflows, od, "4",
+                    od->dnat_force_snat_addrs.ipv4_addrs[0].addr_s,
+                    "dnat");
+            }
+            if (od->dnat_force_snat_addrs.n_ipv6_addrs) {
+                build_lrouter_force_snat_flows(lflows, od, "6",
+                    od->dnat_force_snat_addrs.ipv6_addrs[0].addr_s,
+                    "dnat");
+            }
         }
-
-        /* A set to hold all ips that need defragmentation and tracking. */
-        struct sset all_ips = SSET_INITIALIZER(&all_ips);
-
-        for (int i = 0; i < od->nbr->n_load_balancer; i++) {
-            struct nbrec_load_balancer *nb_lb = od->nbr->load_balancer[i];
-            struct ovn_northd_lb *lb =
-                ovn_northd_lb_find(lbs, &nb_lb->header_.uuid);
-            ovs_assert(lb);
-
-            for (size_t j = 0; j < lb->n_vips; j++) {
-                struct ovn_lb_vip *lb_vip = &lb->vips[j];
-                struct ovn_northd_lb_vip *lb_vip_nb = &lb->vips_nb[j];
-                ds_clear(actions);
-                build_lb_vip_actions(lb_vip, lb_vip_nb, actions,
-                                     lb->selection_fields, false);
-
-                if (!sset_contains(&all_ips, lb_vip->vip_str)) {
-                    sset_add(&all_ips, lb_vip->vip_str);
-                    /* If there are any load balancing rules, we should send
-                     * the packet to conntrack for defragmentation and
-                     * tracking.  This helps with two things.
-                     *
-                     * 1. With tracking, we can send only new connections to
-                     *    pick a DNAT ip address from a group.
-                     * 2. If there are L4 ports in load balancing rules, we
-                     *    need the defragmentation to match on L4 ports. */
-                    ds_clear(match);
-                    if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
-                        ds_put_format(match, "ip && ip4.dst == %s",
-                                      lb_vip->vip_str);
-                    } else {
-                        ds_put_format(match, "ip && ip6.dst == %s",
-                                      lb_vip->vip_str);
-                    }
-                    ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_DEFRAG,
-                                            100, ds_cstr(match), "ct_next;",
-                                            &nb_lb->header_);
-                }
-
-                /* Higher priority rules are added for load-balancing in DNAT
-                 * table.  For every match (on a VIP[:port]), we add two flows
-                 * via add_router_lb_flow().  One flow is for specific matching
-                 * on ct.new with an action of "ct_lb($targets);".  The other
-                 * flow is for ct.est with an action of "ct_dnat;". */
-                ds_clear(match);
-                if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
-                    ds_put_format(match, "ip && ip4.dst == %s",
-                                  lb_vip->vip_str);
-                } else {
-                    ds_put_format(match, "ip && ip6.dst == %s",
-                                  lb_vip->vip_str);
-                }
-
-                int prio = 110;
-                bool is_udp = nullable_string_is_equal(nb_lb->protocol, "udp");
-                bool is_sctp = nullable_string_is_equal(nb_lb->protocol,
-                                                        "sctp");
-                const char *proto = is_udp ? "udp" : is_sctp ? "sctp" : "tcp";
-
-                if (lb_vip->vip_port) {
-                    ds_put_format(match, " && %s && %s.dst == %d", proto,
-                                  proto, lb_vip->vip_port);
-                    prio = 120;
-                }
-
-                if (od->l3redirect_port &&
-                    (lb_vip->n_backends || !lb_vip->empty_backend_rej)) {
-                    ds_put_format(match, " && is_chassis_resident(%s)",
-                                  od->l3redirect_port->json_key);
-                }
-                bool force_snat_for_lb =
-                    lb_force_snat_ip || od->lb_force_snat_router_ip;
-                add_router_lb_flow(lflows, od, match, actions, prio,
-                                   force_snat_for_lb, lb_vip, proto,
-                                   nb_lb, meter_groups, &nat_entries);
+        if (lb_force_snat_ip) {
+            if (od->lb_force_snat_addrs.n_ipv4_addrs) {
+                build_lrouter_force_snat_flows(lflows, od, "4",
+                    od->lb_force_snat_addrs.ipv4_addrs[0].addr_s, "lb");
+            }
+            if (od->lb_force_snat_addrs.n_ipv6_addrs) {
+                build_lrouter_force_snat_flows(lflows, od, "6",
+                    od->lb_force_snat_addrs.ipv6_addrs[0].addr_s, "lb");
             }
         }
-        sset_destroy(&all_ips);
+
+        /* For gateway router, re-circulate every packet through
+         * the DNAT zone.  This helps with the following.
+         *
+         * Any packet that needs to be unDNATed in the reverse
+         * direction gets unDNATed. Ideally this could be done in
+         * the egress pipeline. But since the gateway router
+         * does not have any feature that depends on the source
+         * ip address being external IP address for IP routing,
+         * we can do it here, saving a future re-circulation. */
+        ovn_lflow_add(lflows, od, S_ROUTER_IN_DNAT, 50,
+                      "ip", "flags.loopback = 1; ct_dnat;");
+    }
+
+    /* Load balancing and packet defrag are only valid on
+     * Gateway routers or router with gateway port. */
+    if (!smap_get(&od->nbr->options, "chassis") && !od->l3dgw_port) {
         sset_destroy(&nat_entries);
+        return;
     }
+
+    build_lrouter_lb_flows(lflows, od, lbs, meter_groups, &nat_entries,
+                           match, actions);
+
+    sset_destroy(&nat_entries);
 }
 
 
@@ -12909,6 +12991,9 @@ ovnnb_db_run(struct northd_context *ctx,
 
     use_logical_dp_groups = smap_get_bool(&nb->options,
                                           "use_logical_dp_groups", false);
+    use_ct_inv_match = smap_get_bool(&nb->options,
+                                     "use_ct_inv_match", true);
+
     /* deprecated, use --event instead */
     controller_event_en = smap_get_bool(&nb->options,
                                         "controller_event", false);
diff --git a/ovn-nb.xml b/ovn-nb.xml
index b0a4adffe..046d053e9 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -227,6 +227,21 @@
         </p>
       </column>
 
+      <column name="options" key="use_ct_inv_match">
+        <p>
+          If set to false, <code>ovn-northd</code> will not use the
+          <code>ct.inv</code> field in any of the logical flow matches.
+          The default value is true.  If the NIC supports offloading
+          OVS datapath flows but doesn't support offloading ct_state
+          <code>inv</code> flag, then the datapath flows matching on this flag
+          (either <code>+inv</code> or <code>-inv</code>) will not be
+          offloaded.  CMS should consider setting <code>use_ct_inv_match</code>
+          to <code>false</code> in such cases.  This results in a side effect
+          of the invalid packets getting delivered to the destination VIF,
+          which otherwise would have been dropped by <code>OVN</code>.
+        </p>
+      </column>
+
       <group title="Options for configuring interconnection route advertisement">
         <p>
           These options control how routes are advertised between OVN
@@ -1653,6 +1668,12 @@
         exactly one IPv4 and/or one IPv6 address on it, separated by a space
         character.
       </column>
+
+      <column name="options" key="skip_snat">
+        If the load balancing rule is configured with <code>skip_snat</code>
+        option, the force_snat_for_lb option configured for the router
+        pipeline will not be applied for this load balancer.
+      </column>
     </group>
   </table>
 
diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
index 2cd3e261f..5c64fff12 100644
--- a/tests/ovn-controller.at
+++ b/tests/ovn-controller.at
@@ -431,3 +431,83 @@ OVS_WAIT_UNTIL([
 
 OVN_CLEANUP([hv1])
 AT_CLEANUP
+
+# Test that changes of a port binding from one type to another doesn'that
+# result in any ovn-controller asserts or crashes.
+AT_SETUP([ovn-controller - port binding type change handling])
+AT_KEYWORDS([ovn])
+ovn_start
+
+net_add n1
+sim_add hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+
+check ovn-nbctl ls-add ls1 -- lsp-add ls1 lsp1
+
+as hv1
+check ovs-vsctl \
+    -- add-port br-int vif1 \
+    -- set Interface vif1 external_ids:iface-id=lsp1
+
+# ovn-controller should bind the interface.
+wait_for_ports_up
+hv_uuid=$(fetch_column Chassis _uuid name=hv1)
+check_column "$hv_uuid" Port_Binding chassis logical_port=lsp1
+
+AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[lsp1]], OVS interface name : [[vif1]], num binding lports : [[1]]
+primary lport : [[lsp1]]
+----------------------------------------
+])
+
+# pause ovn-northd
+check as northd ovn-appctl -t ovn-northd pause
+check as northd-backup ovn-appctl -t ovn-northd pause
+
+as northd ovn-appctl -t ovn-northd status
+as northd-backup ovn-appctl -t ovn-northd status
+
+pb_types=(patch chassisredirect l3gateway localnet localport l2gateway
+          virtual external remote vtep)
+for type in ${pb_types[[@]]}
+do
+    for update_type in ${pb_types[[@]]}
+    do
+        check ovn-sbctl set port_binding lsp1 type=$type
+        check as hv1 ovs-vsctl set open . external_ids:ovn-cms-options=$type
+        OVS_WAIT_UNTIL([test $type = $(ovn-sbctl get chassis . other_config:ovn-cms-options)])
+
+        AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[lsp1]], OVS interface name : [[vif1]], num binding lports : [[0]]
+----------------------------------------
+])
+
+        echo "Updating to $update_type from $type"
+        check ovn-sbctl set port_binding lsp1 type=$update_type
+        check as hv1 ovs-vsctl set open . external_ids:ovn-cms-options=$update_type
+        OVS_WAIT_UNTIL([test $update_type = $(ovn-sbctl get chassis . other_config:ovn-cms-options)])
+
+        AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[lsp1]], OVS interface name : [[vif1]], num binding lports : [[0]]
+----------------------------------------
+])
+        # Set the port binding type back to VIF.
+        check ovn-sbctl set port_binding lsp1 type=\"\"
+        check as hv1 ovs-vsctl set open . external_ids:ovn-cms-options=foo
+        OVS_WAIT_UNTIL([test foo = $(ovn-sbctl get chassis . other_config:ovn-cms-options)])
+
+        AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[lsp1]], OVS interface name : [[vif1]], num binding lports : [[1]]
+primary lport : [[lsp1]]
+----------------------------------------
+])
+    done
+done
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
index 2ba29a960..4cf14b1f2 100644
--- a/tests/ovn-macros.at
+++ b/tests/ovn-macros.at
@@ -433,6 +433,24 @@ wait_for_ports_up() {
         done
     fi
 }
+
+# reset_pcap_file iface pcap_file
+# Resets the pcap file associates with OVS interface.  should be used
+# with dummy datapath.
+reset_iface_pcap_file() {
+    local iface=$1
+    local pcap_file=$2
+    check rm -f dummy-*.pcap
+    check ovs-vsctl -- set Interface $iface options:tx_pcap=dummy-tx.pcap \
+options:rxq_pcap=dummy-rx.pcap
+    OVS_WAIT_WHILE([test 24 = $(wc -c dummy-tx.pcap | cut -d " " -f1)])
+    check rm -f ${pcap_file}*.pcap
+    check ovs-vsctl -- set Interface $iface options:tx_pcap=${pcap_file}-tx.pcap \
+options:rxq_pcap=${pcap_file}-rx.pcap
+
+    OVS_WAIT_WHILE([test 24 = $(wc -c ${pcap_file}-tx.pcap | cut -d " " -f1)])
+}
+
 OVS_END_SHELL_HELPERS
 
 m4_define([OVN_POPULATE_ARP], [AT_CHECK(ovn_populate_arp__, [0], [ignore])])
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index 6d91aa4c5..8af55161f 100644
--- a/tests/ovn-nbctl.at
+++ b/tests/ovn-nbctl.at
@@ -1551,6 +1551,7 @@ IPv4 Routes
 AT_CHECK([ovn-nbctl --ecmp lr-route-add lr0 10.0.0.0/24 11.0.0.2], [1], [],
   [ovn-nbctl: duplicate nexthop for the same ECMP route
 ])
+AT_CHECK([ovn-nbctl --may-exist --ecmp lr-route-add lr0 10.0.0.0/24 11.0.0.2])
 
 dnl Delete ecmp routes
 AT_CHECK([ovn-nbctl lr-route-del lr0 10.0.0.0/24 11.0.0.1])
@@ -1614,6 +1615,7 @@ AT_CHECK([ovn-nbctl --ecmp-symmetric-reply lr-route-add lr0 2003:0db8:1::/64 200
 AT_CHECK([ovn-nbctl --ecmp-symmetric-reply lr-route-add lr0 2003:0db8:1::/64 2001:0db8:0:f103::6], [1], [],
   [ovn-nbctl: duplicate nexthop for the same ECMP route
 ])
+AT_CHECK([ovn-nbctl --may-exist --ecmp-symmetric-reply lr-route-add lr0 2003:0db8:1::/64 2001:0db8:0:f103::6])
 
 AT_CHECK([ovn-nbctl lr-route-list lr0], [0], [dnl
 IPv4 Routes
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index b78baa708..8ca915302 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -1077,7 +1077,7 @@ check ovn-nbctl --wait=sb ls-lb-add sw0 lb1
 
 AT_CAPTURE_FILE([sbflows])
 OVS_WAIT_FOR_OUTPUT(
-  [ovn-sbctl dump-flows sw0 | tee sbflows | grep 'priority=120.*ct_lb' | sed 's/table=..//'], 0, [dnl
+  [ovn-sbctl dump-flows sw0 | tee sbflows | grep 'priority=120.*backends' | sed 's/table=..//'], 0, [dnl
   (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 
@@ -1087,7 +1087,7 @@ wait_row_count Service_Monitor 0
 
 AT_CAPTURE_FILE([sbflows2])
 OVS_WAIT_FOR_OUTPUT(
-  [ovn-sbctl dump-flows sw0 | tee sbflows2 | grep 'priority=120.*ct_lb' | sed 's/table=..//'], [0],
+  [ovn-sbctl dump-flows sw0 | tee sbflows2 | grep 'priority=120.*backends' | sed 's/table=..//'], [0],
 [  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 
@@ -1098,7 +1098,7 @@ health_check @hc
 wait_row_count Service_Monitor 2
 check ovn-nbctl --wait=sb sync
 
-ovn-sbctl dump-flows sw0 | grep ct_lb | grep priority=120 > lflows.txt
+ovn-sbctl dump-flows sw0 | grep backends | grep priority=120 > lflows.txt
 AT_CHECK([cat lflows.txt | sed 's/table=..//'], [0], [dnl
   (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
@@ -1109,7 +1109,7 @@ sm_sw1_p1=$(fetch_column Service_Monitor _uuid logical_port=sw1-p1)
 
 AT_CAPTURE_FILE([sbflows3])
 OVS_WAIT_FOR_OUTPUT(
-  [ovn-sbctl dump-flows sw0 | tee sbflows 3 | grep 'priority=120.*ct_lb' | sed 's/table=..//'], [0],
+  [ovn-sbctl dump-flows sw0 | tee sbflows 3 | grep 'priority=120.*backends' | sed 's/table=..//'], [0],
 [  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 
@@ -1120,7 +1120,7 @@ check ovn-nbctl --wait=sb sync
 
 AT_CAPTURE_FILE([sbflows4])
 OVS_WAIT_FOR_OUTPUT(
-  [ovn-sbctl dump-flows sw0 | tee sbflows4 | grep 'priority=120.*ct_lb' | sed 's/table=..//'], [0],
+  [ovn-sbctl dump-flows sw0 | tee sbflows4 | grep 'priority=120.*backends' | sed 's/table=..//'], [0],
 [  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80);)
 ])
 
@@ -1132,7 +1132,7 @@ check ovn-nbctl --wait=sb sync
 
 AT_CAPTURE_FILE([sbflows5])
 OVS_WAIT_FOR_OUTPUT(
-  [ovn-sbctl dump-flows sw0 | tee sbflows5 | grep 'priority=120.*ct_lb'], 1)
+  [ovn-sbctl dump-flows sw0 | tee sbflows5 | grep 'priority=120.*backends'], 1)
 
 AT_CAPTURE_FILE([sbflows6])
 OVS_WAIT_FOR_OUTPUT(
@@ -1149,7 +1149,7 @@ check ovn-nbctl --wait=sb sync
 
 AT_CAPTURE_FILE([sbflows7])
 OVS_WAIT_FOR_OUTPUT(
-  [ovn-sbctl dump-flows sw0 | tee sbflows7 | grep ct_lb | grep priority=120 | sed 's/table=..//'], 0,
+  [ovn-sbctl dump-flows sw0 | tee sbflows7 | grep backends | grep priority=120 | sed 's/table=..//'], 0,
 [  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80,20.0.0.3:80);)
 ])
 
@@ -1185,7 +1185,7 @@ wait_row_count Service_Monitor 1 port=1000
 
 AT_CAPTURE_FILE([sbflows9])
 OVS_WAIT_FOR_OUTPUT(
-  [ovn-sbctl dump-flows sw0 | tee sbflows9 | grep ct_lb | grep priority=120 | sed 's/table=..//' | sort],
+  [ovn-sbctl dump-flows sw0 | tee sbflows9 | grep backends | grep priority=120 | sed 's/table=..//' | sort],
   0,
 [  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80);)
   (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.40 && tcp.dst == 1000), action=(reg1 = 10.0.0.40; reg2[[0..15]] = 1000; ct_lb(backends=10.0.0.3:1000);)
@@ -1199,7 +1199,7 @@ check ovn-nbctl --wait=sb sync
 
 AT_CAPTURE_FILE([sbflows10])
 OVS_WAIT_FOR_OUTPUT(
-  [ovn-sbctl dump-flows sw0 | tee sbflows10 | grep ct_lb | grep priority=120 | sed 's/table=..//' | sort],
+  [ovn-sbctl dump-flows sw0 | tee sbflows10 | grep backends | grep priority=120 | sed 's/table=..//' | sort],
   0,
 [  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80,20.0.0.3:80);)
   (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.40 && tcp.dst == 1000), action=(reg1 = 10.0.0.40; reg2[[0..15]] = 1000; ct_lb(backends=10.0.0.3:1000,20.0.0.3:80);)
@@ -1209,7 +1209,7 @@ AS_BOX([Associate lb1 to sw1])
 check ovn-nbctl --wait=sb ls-lb-add sw1 lb1
 AT_CAPTURE_FILE([sbflows11])
 OVS_WAIT_FOR_OUTPUT(
-  [ovn-sbctl dump-flows sw1 | tee sbflows11 | grep ct_lb | grep priority=120 | sed 's/table=..//' | sort],
+  [ovn-sbctl dump-flows sw1 | tee sbflows11 | grep backends | grep priority=120 | sed 's/table=..//' | sort],
   0, [dnl
   (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80,20.0.0.3:80);)
   (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.40 && tcp.dst == 1000), action=(reg1 = 10.0.0.40; reg2[[0..15]] = 1000; ct_lb(backends=10.0.0.3:1000,20.0.0.3:80);)
@@ -1269,7 +1269,7 @@ ovn-sbctl set service_monitor $sm_sw1_p1 status=offline
 AT_CAPTURE_FILE([sbflows12])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows sw0 | tee sbflows12 | grep "ip4.dst == 10.0.0.10 && tcp.dst == 80" | grep priority=120 | sed 's/table=..//'], [0], [dnl
-  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg0 = 0; reject { outport <-> inport; next(pipeline=egress,table=6);};)
+  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg0 = 0; reject { outport <-> inport; next(pipeline=egress,table=5);};)
 ])
 
 AT_CLEANUP
@@ -1504,6 +1504,19 @@ ovn-nbctl lr-nat-add lr dnat_and_snat 43.43.43.4 42.42.42.4 ls-vm 00:00:00:00:00
 ovn-nbctl lr-nat-add lr snat 43.43.43.150 43.43.43.50
 ovn-nbctl lr-nat-add lr snat 43.43.43.150 43.43.43.51
 
+ovn-nbctl lb-add lb1 "192.168.2.1:8080" "10.0.0.4:8080"
+ovn-nbctl lb-add lb2 "192.168.2.4:8080" "10.0.0.5:8080" udp
+ovn-nbctl lb-add lb3 "192.168.2.5:8080" "10.0.0.6:8080"
+ovn-nbctl lb-add lb4 "192.168.2.6:8080" "10.0.0.7:8080"
+ovn-nbctl lb-add lb5 "fe80::200:ff:fe00:101:8080" "fe02::200:ff:fe00:101:8080"
+ovn-nbctl lb-add lb5 "fe80::200:ff:fe00:102:8080" "fe02::200:ff:fe00:102:8080"
+
+ovn-nbctl lr-lb-add lr lb1
+ovn-nbctl lr-lb-add lr lb2
+ovn-nbctl lr-lb-add lr lb3
+ovn-nbctl lr-lb-add lr lb4
+ovn-nbctl lr-lb-add lr lb5
+
 ovn-nbctl --wait=sb sync
 
 # Ingress router port ETH address is stored in lr_in_admission.
@@ -1526,28 +1539,46 @@ action=(xreg0[[0..47]] = 00:00:00:00:01:00; next;)
 AT_CHECK([ovn-sbctl lflow-list | grep -E "lr_in_ip_input.*priority=90" | grep "arp\|nd" | sort], [0], [dnl
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(arp.op == 1 && arp.tpa == 43.43.43.150), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.150; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(arp.op == 1 && arp.tpa == 43.43.43.2), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.2; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(arp.op == 1 && arp.tpa == 43.43.43.3), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.3; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(arp.op == 1 && arp.tpa == 43.43.43.4), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.4; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(inport == "lrp" && arp.op == 1 && arp.tpa == 42.42.42.1 && arp.spa == 42.42.42.0/24), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 42.42.42.1; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp" && arp.op == 1 && arp.tpa == { 192.168.2.1, 192.168.2.4, 192.168.2.5, 192.168.2.6 }), dnl
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(inport == "lrp" && ip6.dst == {fe80::200:ff:fe00:1, ff02::1:ff00:1} && nd_ns && nd.target == fe80::200:ff:fe00:1), dnl
 action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:1; nd.target = fe80::200:ff:fe00:1; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp" && nd_ns && nd.target == fe80::200:ff:fe00:101:8080), dnl
+action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:101:8080; nd.target = fe80::200:ff:fe00:101:8080; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp" && nd_ns && nd.target == fe80::200:ff:fe00:102:8080), dnl
+action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:102:8080; nd.target = fe80::200:ff:fe00:102:8080; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.1 && arp.spa == 43.43.43.0/24), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.1; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == { 192.168.2.1, 192.168.2.4, 192.168.2.5, 192.168.2.6 }), dnl
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(inport == "lrp-public" && ip6.dst == {fe80::200:ff:fe00:100, ff02::1:ff00:100} && nd_ns && nd.target == fe80::200:ff:fe00:100), dnl
 action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:100; nd.target = fe80::200:ff:fe00:100; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp-public" && nd_ns && nd.target == fe80::200:ff:fe00:101:8080), dnl
+action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:101:8080; nd.target = fe80::200:ff:fe00:101:8080; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp-public" && nd_ns && nd.target == fe80::200:ff:fe00:102:8080), dnl
+action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:102:8080; nd.target = fe80::200:ff:fe00:102:8080; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
 ])
 
 # xreg0[0..47] isn't used anywhere else.
@@ -1583,28 +1614,46 @@ action=(xreg0[[0..47]] = 00:00:00:00:01:00; next;)
 AT_CHECK([ovn-sbctl lflow-list | grep -E "lr_in_ip_input.*priority=90" | grep "arp\|nd" | sort], [0], [dnl
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(arp.op == 1 && arp.tpa == 43.43.43.150), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.150; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(arp.op == 1 && arp.tpa == 43.43.43.2), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.2; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(arp.op == 1 && arp.tpa == 43.43.43.3), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.3; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(arp.op == 1 && arp.tpa == 43.43.43.4), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.4; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(inport == "lrp" && arp.op == 1 && arp.tpa == 42.42.42.1 && arp.spa == 42.42.42.0/24), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 42.42.42.1; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp" && arp.op == 1 && arp.tpa == { 192.168.2.1, 192.168.2.4, 192.168.2.5, 192.168.2.6 }), dnl
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(inport == "lrp" && ip6.dst == {fe80::200:ff:fe00:1, ff02::1:ff00:1} && nd_ns && nd.target == fe80::200:ff:fe00:1), dnl
 action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:1; nd.target = fe80::200:ff:fe00:1; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp" && nd_ns && nd.target == fe80::200:ff:fe00:101:8080), dnl
+action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:101:8080; nd.target = fe80::200:ff:fe00:101:8080; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp" && nd_ns && nd.target == fe80::200:ff:fe00:102:8080), dnl
+action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:102:8080; nd.target = fe80::200:ff:fe00:102:8080; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.1 && arp.spa == 43.43.43.0/24), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.1; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == { 192.168.2.1, 192.168.2.4, 192.168.2.5, 192.168.2.6 } && is_chassis_resident("cr-lrp-public")), dnl
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=90   , dnl
 match=(inport == "lrp-public" && ip6.dst == {fe80::200:ff:fe00:100, ff02::1:ff00:100} && nd_ns && nd.target == fe80::200:ff:fe00:100 && is_chassis_resident("cr-lrp-public")), dnl
 action=(nd_na_router { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:100; nd.target = fe80::200:ff:fe00:100; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp-public" && nd_ns && nd.target == fe80::200:ff:fe00:101:8080 && is_chassis_resident("cr-lrp-public")), dnl
+action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:101:8080; nd.target = fe80::200:ff:fe00:101:8080; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
+  table=3 (lr_in_ip_input     ), priority=90   , dnl
+match=(inport == "lrp-public" && nd_ns && nd.target == fe80::200:ff:fe00:102:8080 && is_chassis_resident("cr-lrp-public")), dnl
+action=(nd_na { eth.src = xreg0[[0..47]]; ip6.src = fe80::200:ff:fe00:102:8080; nd.target = fe80::200:ff:fe00:102:8080; nd.tll = xreg0[[0..47]]; outport = inport; flags.loopback = 1; output; };)
 ])
 
 # Priority 91 drop flows (per distributed gw port), if port is not resident.
@@ -1626,16 +1675,16 @@ action=(drop;)
 AT_CHECK([ovn-sbctl lflow-list | grep -E "lr_in_ip_input.*priority=92" | grep "arp\|nd" | sort], [0], [dnl
   table=3 (lr_in_ip_input     ), priority=92   , dnl
 match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.150 && is_chassis_resident("cr-lrp-public")), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.150; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=92   , dnl
 match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.2 && is_chassis_resident("cr-lrp-public")), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.2; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=92   , dnl
 match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.3 && is_chassis_resident("cr-lrp-public")), dnl
-action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa = arp.spa; arp.spa = 43.43.43.3; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
   table=3 (lr_in_ip_input     ), priority=92   , dnl
 match=(inport == "lrp-public" && arp.op == 1 && arp.tpa == 43.43.43.4 && is_chassis_resident("ls-vm")), dnl
-action=(eth.dst = eth.src; eth.src = 00:00:00:00:00:02; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 00:00:00:00:00:02; arp.tpa = arp.spa; arp.spa = 43.43.43.4; outport = inport; flags.loopback = 1; output;)
+action=(eth.dst = eth.src; eth.src = 00:00:00:00:00:02; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 00:00:00:00:00:02; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;)
 ])
 
 # xreg0[0..47] isn't used anywhere else.
@@ -1671,13 +1720,13 @@ AT_CHECK([ovn-sbctl lflow-list | grep "ls_out_pre_lb.*priority=100" | grep reg0
 ovn-nbctl ls-lb-add sw0 lb1
 ovn-nbctl --wait=sb sync
 AT_CHECK([ovn-sbctl lflow-list | grep "ls_out_pre_lb.*priority=100" | grep reg0 | sort], [0], [dnl
-  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
+  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[2]] = 1; next;)
 ])
 
 ovn-nbctl ls-lb-add sw0 lb2
 ovn-nbctl --wait=sb sync
 AT_CHECK([ovn-sbctl lflow-list | grep "ls_out_pre_lb.*priority=100" | grep reg0 | sort], [0], [dnl
-  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
+  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[2]] = 1; next;)
 ])
 
 lb1_uuid=$(ovn-nbctl --bare --columns _uuid find load_balancer name=lb1)
@@ -1686,7 +1735,7 @@ lb2_uuid=$(ovn-nbctl --bare --columns _uuid find load_balancer name=lb2)
 ovn-nbctl clear load_balancer $lb1_uuid vips
 ovn-nbctl --wait=sb sync
 AT_CHECK([ovn-sbctl lflow-list | grep "ls_out_pre_lb.*priority=100" | grep reg0 | sort], [0], [dnl
-  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
+  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[2]] = 1; next;)
 ])
 
 ovn-nbctl clear load_balancer $lb2_uuid vips
@@ -1699,14 +1748,14 @@ ovn-nbctl set load_balancer $lb2_uuid vips:"10.0.0.11"="10.0.0.4"
 
 ovn-nbctl --wait=sb sync
 AT_CHECK([ovn-sbctl lflow-list | grep "ls_out_pre_lb.*priority=100" | grep reg0 | sort], [0], [dnl
-  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
+  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[2]] = 1; next;)
 ])
 
 # Now reverse the order of clearing the vip.
 ovn-nbctl clear load_balancer $lb2_uuid vips
 ovn-nbctl --wait=sb sync
 AT_CHECK([ovn-sbctl lflow-list | grep "ls_out_pre_lb.*priority=100" | grep reg0 | sort], [0], [dnl
-  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[0]] = 1; next;)
+  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[2]] = 1; next;)
 ])
 
 ovn-nbctl clear load_balancer $lb1_uuid vips
@@ -1754,10 +1803,10 @@ AT_CAPTURE_FILE([sw1flows])
 
 AT_CHECK(
   [grep -E 'ls_(in|out)_acl' sw0flows sw1flows | grep pg0 | sort], [0], [dnl
-sw0flows:  table=5 (ls_out_acl         ), priority=2003 , match=(outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw0flows:  table=9 (ls_in_acl          ), priority=2002 , match=(inport == @pg0 && ip4 && tcp && tcp.dst == 80), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=6); };)
-sw1flows:  table=5 (ls_out_acl         ), priority=2003 , match=(outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw1flows:  table=9 (ls_in_acl          ), priority=2002 , match=(inport == @pg0 && ip4 && tcp && tcp.dst == 80), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=6); };)
+sw0flows:  table=4 (ls_out_acl         ), priority=2003 , match=(outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw0flows:  table=9 (ls_in_acl          ), priority=2002 , match=(inport == @pg0 && ip4 && tcp && tcp.dst == 80), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=5); };)
+sw1flows:  table=4 (ls_out_acl         ), priority=2003 , match=(outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw1flows:  table=9 (ls_in_acl          ), priority=2002 , match=(inport == @pg0 && ip4 && tcp && tcp.dst == 80), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=egress,table=5); };)
 ])
 
 AS_BOX([2])
@@ -1770,10 +1819,10 @@ ovn-sbctl dump-flows sw1 > sw1flows2
 AT_CAPTURE_FILE([sw1flows2])
 
 AT_CHECK([grep "ls_out_acl" sw0flows2 sw1flows2 | grep pg0 | sort], [0], [dnl
-sw0flows2:  table=5 (ls_out_acl         ), priority=2002 , match=(outport == @pg0 && ip4 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw0flows2:  table=5 (ls_out_acl         ), priority=2003 , match=(outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw1flows2:  table=5 (ls_out_acl         ), priority=2002 , match=(outport == @pg0 && ip4 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw1flows2:  table=5 (ls_out_acl         ), priority=2003 , match=(outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
+sw0flows2:  table=4 (ls_out_acl         ), priority=2002 , match=(outport == @pg0 && ip4 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw0flows2:  table=4 (ls_out_acl         ), priority=2003 , match=(outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw1flows2:  table=4 (ls_out_acl         ), priority=2002 , match=(outport == @pg0 && ip4 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw1flows2:  table=4 (ls_out_acl         ), priority=2003 , match=(outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
 ])
 
 AS_BOX([3])
@@ -1786,18 +1835,18 @@ ovn-sbctl dump-flows sw1 > sw1flows3
 AT_CAPTURE_FILE([sw1flows3])
 
 AT_CHECK([grep "ls_out_acl" sw0flows3 sw1flows3 | grep pg0 | sort], [0], [dnl
-sw0flows3:  table=5 (ls_out_acl         ), priority=2001 , match=(reg0[[7]] == 1 && (outport == @pg0 && ip)), action=(reg0[[1]] = 1; next;)
-sw0flows3:  table=5 (ls_out_acl         ), priority=2001 , match=(reg0[[8]] == 1 && (outport == @pg0 && ip)), action=(next;)
-sw0flows3:  table=5 (ls_out_acl         ), priority=2002 , match=((reg0[[10]] == 1) && outport == @pg0 && ip4 && udp), action=(ct_commit { ct_label.blocked = 1; };  reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw0flows3:  table=5 (ls_out_acl         ), priority=2002 , match=((reg0[[9]] == 1) && outport == @pg0 && ip4 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw0flows3:  table=5 (ls_out_acl         ), priority=2003 , match=((reg0[[10]] == 1) && outport == @pg0 && ip6 && udp), action=(ct_commit { ct_label.blocked = 1; };  reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw0flows3:  table=5 (ls_out_acl         ), priority=2003 , match=((reg0[[9]] == 1) && outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw1flows3:  table=5 (ls_out_acl         ), priority=2001 , match=(reg0[[7]] == 1 && (outport == @pg0 && ip)), action=(reg0[[1]] = 1; next;)
-sw1flows3:  table=5 (ls_out_acl         ), priority=2001 , match=(reg0[[8]] == 1 && (outport == @pg0 && ip)), action=(next;)
-sw1flows3:  table=5 (ls_out_acl         ), priority=2002 , match=((reg0[[10]] == 1) && outport == @pg0 && ip4 && udp), action=(ct_commit { ct_label.blocked = 1; };  reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw1flows3:  table=5 (ls_out_acl         ), priority=2002 , match=((reg0[[9]] == 1) && outport == @pg0 && ip4 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw1flows3:  table=5 (ls_out_acl         ), priority=2003 , match=((reg0[[10]] == 1) && outport == @pg0 && ip6 && udp), action=(ct_commit { ct_label.blocked = 1; };  reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
-sw1flows3:  table=5 (ls_out_acl         ), priority=2003 , match=((reg0[[9]] == 1) && outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=23); };)
+sw0flows3:  table=4 (ls_out_acl         ), priority=2001 , match=(reg0[[7]] == 1 && (outport == @pg0 && ip)), action=(reg0[[1]] = 1; next;)
+sw0flows3:  table=4 (ls_out_acl         ), priority=2001 , match=(reg0[[8]] == 1 && (outport == @pg0 && ip)), action=(next;)
+sw0flows3:  table=4 (ls_out_acl         ), priority=2002 , match=((reg0[[10]] == 1) && outport == @pg0 && ip4 && udp), action=(ct_commit { ct_label.blocked = 1; };  reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw0flows3:  table=4 (ls_out_acl         ), priority=2002 , match=((reg0[[9]] == 1) && outport == @pg0 && ip4 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw0flows3:  table=4 (ls_out_acl         ), priority=2003 , match=((reg0[[10]] == 1) && outport == @pg0 && ip6 && udp), action=(ct_commit { ct_label.blocked = 1; };  reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw0flows3:  table=4 (ls_out_acl         ), priority=2003 , match=((reg0[[9]] == 1) && outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw1flows3:  table=4 (ls_out_acl         ), priority=2001 , match=(reg0[[7]] == 1 && (outport == @pg0 && ip)), action=(reg0[[1]] = 1; next;)
+sw1flows3:  table=4 (ls_out_acl         ), priority=2001 , match=(reg0[[8]] == 1 && (outport == @pg0 && ip)), action=(next;)
+sw1flows3:  table=4 (ls_out_acl         ), priority=2002 , match=((reg0[[10]] == 1) && outport == @pg0 && ip4 && udp), action=(ct_commit { ct_label.blocked = 1; };  reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw1flows3:  table=4 (ls_out_acl         ), priority=2002 , match=((reg0[[9]] == 1) && outport == @pg0 && ip4 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw1flows3:  table=4 (ls_out_acl         ), priority=2003 , match=((reg0[[10]] == 1) && outport == @pg0 && ip6 && udp), action=(ct_commit { ct_label.blocked = 1; };  reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
+sw1flows3:  table=4 (ls_out_acl         ), priority=2003 , match=((reg0[[9]] == 1) && outport == @pg0 && ip6 && udp), action=(reg0 = 0; reject { /* eth.dst <-> eth.src; ip.dst <-> ip.src; is implicit. */ outport <-> inport; next(pipeline=ingress,table=22); };)
 ])
 
 AT_CLEANUP
@@ -1932,17 +1981,17 @@ check ovn-nbctl --wait=sb \
     -- acl-add ls from-lport 2 "udp" allow-related \
     -- acl-add ls to-lport 2 "udp" allow-related
 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 | grep 'ct\.' | sort], [0], [dnl
-  table=4 (ls_out_acl_hint    ), priority=1    , match=(ct.est && ct_label.blocked == 0), action=(reg0[[10]] = 1; next;)
-  table=4 (ls_out_acl_hint    ), priority=2    , match=(ct.est && ct_label.blocked == 1), action=(reg0[[9]] = 1; next;)
-  table=4 (ls_out_acl_hint    ), priority=3    , match=(!ct.est), action=(reg0[[9]] = 1; next;)
-  table=4 (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=4 (ls_out_acl_hint    ), priority=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 1; next;)
-  table=4 (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=4 (ls_out_acl_hint    ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
-  table=5 (ls_out_acl         ), priority=1    , match=(ip && (!ct.est || (ct.est && ct_label.blocked == 1))), action=(reg0[[1]] = 1; next;)
-  table=5 (ls_out_acl         ), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
-  table=5 (ls_out_acl         ), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
-  table=5 (ls_out_acl         ), priority=65535, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=3 (ls_out_acl_hint    ), priority=1    , match=(ct.est && ct_label.blocked == 0), action=(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=3    , match=(!ct.est), action=(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=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 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=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
+  table=4 (ls_out_acl         ), priority=1    , match=(ip && (!ct.est || (ct.est && ct_label.blocked == 1))), action=(reg0[[1]] = 1; next;)
+  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=8 (ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_label.blocked == 0), action=(reg0[[10]] = 1; next;)
   table=8 (ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_label.blocked == 1), action=(reg0[[9]] = 1; next;)
   table=8 (ls_in_acl_hint     ), priority=3    , match=(!ct.est), action=(reg0[[9]] = 1; next;)
@@ -1951,9 +2000,9 @@ AT_CHECK([ovn-sbctl lflow-list ls | grep -e ls_in_acl_hint -e ls_out_acl_hint -e
   table=8 (ls_in_acl_hint     ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_label.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=8 (ls_in_acl_hint     ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=9 (ls_in_acl          ), priority=1    , match=(ip && (!ct.est || (ct.est && ct_label.blocked == 1))), action=(reg0[[1]] = 1; next;)
-  table=9 (ls_in_acl          ), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
-  table=9 (ls_in_acl          ), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
-  table=9 (ls_in_acl          ), priority=65535, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=9 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
 ])
 
 AS_BOX([Check match ct_state with load balancer])
@@ -1963,18 +2012,25 @@ check ovn-nbctl --wait=sb \
     -- lb-add lb "10.0.0.1" "10.0.0.2" \
     -- 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 | grep 'ct\.' | sort], [0], [dnl
-  table=4 (ls_out_acl_hint    ), priority=1    , match=(ct.est && ct_label.blocked == 0), action=(reg0[[10]] = 1; next;)
-  table=4 (ls_out_acl_hint    ), priority=2    , match=(ct.est && ct_label.blocked == 1), action=(reg0[[9]] = 1; next;)
-  table=4 (ls_out_acl_hint    ), priority=3    , match=(!ct.est), action=(reg0[[9]] = 1; next;)
-  table=4 (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=4 (ls_out_acl_hint    ), priority=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 1; next;)
-  table=4 (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=4 (ls_out_acl_hint    ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
-  table=5 (ls_out_acl         ), priority=1    , match=(ip && (!ct.est || (ct.est && ct_label.blocked == 1))), action=(reg0[[1]] = 1; next;)
-  table=5 (ls_out_acl         ), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
-  table=5 (ls_out_acl         ), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
-  table=5 (ls_out_acl         ), priority=65535, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+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=3 (ls_out_acl_hint    ), priority=0    , match=(1), action=(next;)
+  table=3 (ls_out_acl_hint    ), priority=1    , match=(ct.est && ct_label.blocked == 0), action=(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=3    , match=(!ct.est), action=(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=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 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=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
+  table=4 (ls_out_acl         ), priority=0    , match=(1), action=(next;)
+  table=4 (ls_out_acl         ), priority=1    , match=(ip && (!ct.est || (ct.est && ct_label.blocked == 1))), action=(reg0[[1]] = 1; next;)
+  table=4 (ls_out_acl         ), priority=1001 , match=(reg0[[7]] == 1 && (ip)), action=(reg0[[1]] = 1; next;)
+  table=4 (ls_out_acl         ), priority=1001 , match=(reg0[[8]] == 1 && (ip)), action=(next;)
+  table=4 (ls_out_acl         ), priority=34000, match=(eth.src == $svc_monitor_mac), action=(next;)
+  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=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+  table=8 (ls_in_acl_hint     ), priority=0    , match=(1), action=(next;)
   table=8 (ls_in_acl_hint     ), priority=1    , match=(ct.est && ct_label.blocked == 0), action=(reg0[[10]] = 1; next;)
   table=8 (ls_in_acl_hint     ), priority=2    , match=(ct.est && ct_label.blocked == 1), action=(reg0[[9]] = 1; next;)
   table=8 (ls_in_acl_hint     ), priority=3    , match=(!ct.est), action=(reg0[[9]] = 1; next;)
@@ -1982,12 +2038,28 @@ AT_CHECK([ovn-sbctl lflow-list ls | grep -e ls_in_acl_hint -e ls_out_acl_hint -e
   table=8 (ls_in_acl_hint     ), priority=5    , match=(!ct.trk), action=(reg0[[8]] = 1; reg0[[9]] = 1; next;)
   table=8 (ls_in_acl_hint     ), priority=6    , match=(!ct.new && ct.est && !ct.rpl && ct_label.blocked == 1), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
   table=8 (ls_in_acl_hint     ), priority=7    , match=(ct.new && !ct.est), action=(reg0[[7]] = 1; reg0[[9]] = 1; next;)
+  table=9 (ls_in_acl          ), priority=0    , match=(1), action=(next;)
   table=9 (ls_in_acl          ), priority=1    , match=(ip && (!ct.est || (ct.est && ct_label.blocked == 1))), action=(reg0[[1]] = 1; next;)
-  table=9 (ls_in_acl          ), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
-  table=9 (ls_in_acl          ), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
-  table=9 (ls_in_acl          ), priority=65535, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=9 (ls_in_acl          ), priority=1001 , match=(reg0[[7]] == 1 && (ip)), action=(reg0[[1]] = 1; next;)
+  table=9 (ls_in_acl          ), priority=1001 , match=(reg0[[8]] == 1 && (ip)), action=(next;)
+  table=9 (ls_in_acl          ), priority=34000, match=(eth.dst == $svc_monitor_mac), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=9 (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+])
+
+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=3 (ls_out_acl_hint    ), priority=65535, match=(1), action=(next;)
+  table=4 (ls_out_acl         ), priority=65535, match=(1), action=(next;)
+  table=8 (ls_in_acl_hint     ), priority=65535, match=(1), action=(next;)
+  table=9 (ls_in_acl          ), priority=65535, match=(1), action=(next;)
 ])
 
+
 AT_CLEANUP
 
 AT_SETUP([datapath requested-tnl-key])
@@ -2197,20 +2269,20 @@ check ovn-nbctl \
 check ovn-nbctl --wait=sb sync
 
 AT_CHECK([ovn-sbctl lflow-list sw0 | grep ls_in_pre_hairpin | sort], [0], [dnl
-  table=14(ls_in_pre_hairpin  ), priority=0    , match=(1), action=(next;)
-  table=14(ls_in_pre_hairpin  ), priority=100  , match=(ip && ct.trk), action=(reg0[[6]] = chk_lb_hairpin(); reg0[[12]] = chk_lb_hairpin_reply(); next;)
+  table=13(ls_in_pre_hairpin  ), priority=0    , match=(1), action=(next;)
+  table=13(ls_in_pre_hairpin  ), priority=100  , match=(ip && ct.trk), action=(reg0[[6]] = chk_lb_hairpin(); reg0[[12]] = chk_lb_hairpin_reply(); next;)
 ])
 
 AT_CHECK([ovn-sbctl lflow-list sw0 | grep ls_in_nat_hairpin | sort], [0], [dnl
-  table=15(ls_in_nat_hairpin  ), priority=0    , match=(1), action=(next;)
-  table=15(ls_in_nat_hairpin  ), priority=100  , match=(ip && ct.est && ct.trk && reg0[[6]] == 1), action=(ct_snat;)
-  table=15(ls_in_nat_hairpin  ), priority=100  , match=(ip && ct.new && ct.trk && reg0[[6]] == 1), action=(ct_snat_to_vip; next;)
-  table=15(ls_in_nat_hairpin  ), priority=90   , match=(ip && reg0[[12]] == 1), action=(ct_snat;)
+  table=14(ls_in_nat_hairpin  ), priority=0    , match=(1), action=(next;)
+  table=14(ls_in_nat_hairpin  ), priority=100  , match=(ip && ct.est && ct.trk && reg0[[6]] == 1), action=(ct_snat;)
+  table=14(ls_in_nat_hairpin  ), priority=100  , match=(ip && ct.new && ct.trk && reg0[[6]] == 1), action=(ct_snat_to_vip; next;)
+  table=14(ls_in_nat_hairpin  ), priority=90   , match=(ip && reg0[[12]] == 1), action=(ct_snat;)
 ])
 
 AT_CHECK([ovn-sbctl lflow-list sw0 | grep ls_in_hairpin | sort], [0], [dnl
-  table=16(ls_in_hairpin      ), priority=0    , match=(1), action=(next;)
-  table=16(ls_in_hairpin      ), priority=1    , match=((reg0[[6]] == 1 || reg0[[12]] == 1)), action=(eth.dst <-> eth.src; outport = inport; flags.loopback = 1; output;)
+  table=15(ls_in_hairpin      ), priority=0    , match=(1), action=(next;)
+  table=15(ls_in_hairpin      ), priority=1    , match=((reg0[[6]] == 1 || reg0[[12]] == 1)), action=(eth.dst <-> eth.src; outport = inport; flags.loopback = 1; output;)
 ])
 
 AT_CLEANUP
@@ -2324,6 +2396,13 @@ check ovn-nbctl lsp-set-options public-lr0 router-port=lr0-public
 
 check ovn-nbctl --wait=sb lr-policy-add lr0  10 "ip4.src == 10.0.0.3" reroute 172.168.0.101,172.168.0.102
 
+ovn-nbctl lr-policy-list lr0 > policy-list
+AT_CAPTURE_FILE([policy-list])
+AT_CHECK([cat policy-list], [0], [dnl
+Routing Policies
+        10                                ip4.src == 10.0.0.3         reroute             172.168.0.101, 172.168.0.102
+])
+
 ovn-sbctl dump-flows lr0 > lr0flows3
 AT_CAPTURE_FILE([lr0flows3])
 
@@ -2551,7 +2630,7 @@ wait_row_count nb:Logical_Switch_Port 1 up=false name=lsp1
 
 AT_CLEANUP
 
-AT_SETUP([ovn -- lb_force_snat_ip for Gateway Routers])
+AT_SETUP([ovn -- Load Balancers and lb_force_snat_ip for Gateway Routers])
 ovn_start
 
 check ovn-nbctl ls-add sw0
@@ -2589,11 +2668,11 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | sort], [0], [dnl
   table=5 (lr_in_unsnat       ), priority=0    , match=(1), action=(next;)
 ])
 
-AT_CHECK([grep "lr_in_dnat" lr0flows | grep force_snat_for_lb | sort], [0], [dnl
-])
-
-
-AT_CHECK([grep "lr_out_snat" lr0flows | grep force_snat_for_lb | sort], [0], [dnl
+AT_CHECK([grep "lr_in_dnat" lr0flows | sort], [0], [dnl
+  table=6 (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
+  table=6 (lr_in_dnat         ), priority=120  , match=(ct.est && ip && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(ct_dnat;)
+  table=6 (lr_in_dnat         ), priority=120  , match=(ct.new && ip && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(ct_lb(backends=10.0.0.4:8080);)
+  table=6 (lr_in_dnat         ), priority=50   , match=(ip), action=(flags.loopback = 1; ct_dnat;)
 ])
 
 check ovn-nbctl --wait=sb set logical_router lr0 options:lb_force_snat_ip="20.0.0.4 aef0::4"
@@ -2608,14 +2687,18 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | sort], [0], [dnl
   table=5 (lr_in_unsnat       ), priority=110  , match=(ip6 && ip6.dst == aef0::4), action=(ct_snat;)
 ])
 
-AT_CHECK([grep "lr_in_dnat" lr0flows | grep force_snat_for_lb | sort], [0], [dnl
+AT_CHECK([grep "lr_in_dnat" lr0flows | sort], [0], [dnl
+  table=6 (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
   table=6 (lr_in_dnat         ), priority=120  , match=(ct.est && ip && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_dnat;)
   table=6 (lr_in_dnat         ), priority=120  , match=(ct.new && ip && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb(backends=10.0.0.4:8080);)
+  table=6 (lr_in_dnat         ), priority=50   , match=(ip), action=(flags.loopback = 1; ct_dnat;)
 ])
 
-AT_CHECK([grep "lr_out_snat" lr0flows | grep force_snat_for_lb | sort], [0], [dnl
+AT_CHECK([grep "lr_out_snat" lr0flows | sort], [0], [dnl
+  table=1 (lr_out_snat        ), priority=0    , match=(1), action=(next;)
   table=1 (lr_out_snat        ), priority=100  , match=(flags.force_snat_for_lb == 1 && ip4), action=(ct_snat(20.0.0.4);)
   table=1 (lr_out_snat        ), priority=100  , match=(flags.force_snat_for_lb == 1 && ip6), action=(ct_snat(aef0::4);)
+  table=1 (lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
 ])
 
 check ovn-nbctl --wait=sb set logical_router lr0 options:lb_force_snat_ip="router_ip"
@@ -2633,15 +2716,19 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | sort], [0], [dnl
   table=5 (lr_in_unsnat       ), priority=110  , match=(inport == "lr0-sw1" && ip4.dst == 20.0.0.1), action=(ct_snat;)
 ])
 
-AT_CHECK([grep "lr_in_dnat" lr0flows | grep force_snat_for_lb | sort], [0], [dnl
+AT_CHECK([grep "lr_in_dnat" lr0flows | sort], [0], [dnl
+  table=6 (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
   table=6 (lr_in_dnat         ), priority=120  , match=(ct.est && ip && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_dnat;)
   table=6 (lr_in_dnat         ), priority=120  , match=(ct.new && ip && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb(backends=10.0.0.4:8080);)
+  table=6 (lr_in_dnat         ), priority=50   , match=(ip), action=(flags.loopback = 1; ct_dnat;)
 ])
 
-AT_CHECK([grep "lr_out_snat" lr0flows | grep force_snat_for_lb | sort], [0], [dnl
+AT_CHECK([grep "lr_out_snat" lr0flows | sort], [0], [dnl
+  table=1 (lr_out_snat        ), priority=0    , match=(1), action=(next;)
   table=1 (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.100);)
   table=1 (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
   table=1 (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), action=(ct_snat(20.0.0.1);)
+  table=1 (lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
 ])
 
 check ovn-nbctl --wait=sb remove logical_router lr0 options chassis
@@ -2653,7 +2740,9 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | sort], [0], [dnl
   table=5 (lr_in_unsnat       ), priority=0    , match=(1), action=(next;)
 ])
 
-AT_CHECK([grep "lr_out_snat" lr0flows | grep force_snat_for_lb | sort], [0], [dnl
+AT_CHECK([grep "lr_out_snat" lr0flows | sort], [0], [dnl
+  table=1 (lr_out_snat        ), priority=0    , match=(1), action=(next;)
+  table=1 (lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
 ])
 
 check ovn-nbctl set logical_router lr0 options:chassis=ch1
@@ -2670,16 +2759,43 @@ AT_CHECK([grep "lr_in_unsnat" lr0flows | sort], [0], [dnl
   table=5 (lr_in_unsnat       ), priority=110  , match=(inport == "lr0-sw1" && ip6.dst == bef0::1), action=(ct_snat;)
 ])
 
-AT_CHECK([grep "lr_in_dnat" lr0flows | grep force_snat_for_lb | sort], [0], [dnl
+AT_CHECK([grep "lr_in_dnat" lr0flows | sort], [0], [dnl
+  table=6 (lr_in_dnat         ), priority=0    , match=(1), action=(next;)
   table=6 (lr_in_dnat         ), priority=120  , match=(ct.est && ip && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_dnat;)
   table=6 (lr_in_dnat         ), priority=120  , match=(ct.new && ip && ip4.dst == 10.0.0.10 && tcp && tcp.dst == 80), action=(flags.force_snat_for_lb = 1; ct_lb(backends=10.0.0.4:8080);)
+  table=6 (lr_in_dnat         ), priority=50   , match=(ip), action=(flags.loopback = 1; ct_dnat;)
 ])
 
-AT_CHECK([grep "lr_out_snat" lr0flows | grep force_snat_for_lb | sort], [0], [dnl
+AT_CHECK([grep "lr_out_snat" lr0flows | sort], [0], [dnl
+  table=1 (lr_out_snat        ), priority=0    , match=(1), action=(next;)
   table=1 (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), action=(ct_snat(172.168.0.100);)
   table=1 (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
   table=1 (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), action=(ct_snat(20.0.0.1);)
   table=1 (lr_out_snat        ), priority=110  , match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-sw1"), action=(ct_snat(bef0::1);)
+  table=1 (lr_out_snat        ), priority=120  , match=(nd_ns), action=(next;)
+])
+
+check ovn-nbctl --wait=sb lb-add lb2 10.0.0.20:80 10.0.0.40:8080
+check ovn-nbctl --wait=sb set load_balancer lb2 options:skip_snat=true
+check ovn-nbctl lr-lb-add lr0 lb2
+check ovn-nbctl --wait=sb lb-del lb1
+ovn-sbctl dump-flows lr0 > lr0flows
+
+AT_CHECK([grep "lr_in_unsnat" lr0flows | sort], [0], [dnl
+  table=5 (lr_in_unsnat       ), priority=0    , match=(1), action=(next;)
+  table=5 (lr_in_unsnat       ), priority=110  , match=(inport == "lr0-public" && ip4.dst == 172.168.0.100), action=(ct_snat;)
+  table=5 (lr_in_unsnat       ), priority=110  , match=(inport == "lr0-sw0" && ip4.dst == 10.0.0.1), action=(ct_snat;)
+  table=5 (lr_in_unsnat       ), priority=110  , match=(inport == "lr0-sw1" && ip4.dst == 20.0.0.1), action=(ct_snat;)
+  table=5 (lr_in_unsnat       ), priority=110  , match=(inport == "lr0-sw1" && ip6.dst == bef0::1), action=(ct_snat;)
+])
+
+AT_CHECK([grep "lr_in_dnat" lr0flows | grep skip_snat_for_lb | sort], [0], [dnl
+  table=6 (lr_in_dnat         ), priority=120  , match=(ct.est && ip && ip4.dst == 10.0.0.20 && tcp && tcp.dst == 80), action=(flags.skip_snat_for_lb = 1; ct_dnat;)
+  table=6 (lr_in_dnat         ), priority=120  , match=(ct.new && ip && ip4.dst == 10.0.0.20 && tcp && tcp.dst == 80), action=(flags.skip_snat_for_lb = 1; ct_lb(backends=10.0.0.40:8080);)
+])
+
+AT_CHECK([grep "lr_out_snat" lr0flows | grep skip_snat_for_lb | sort], [0], [dnl
+  table=1 (lr_out_snat        ), priority=120  , match=(flags.skip_snat_for_lb == 1 && ip), action=(next;)
 ])
 
 AT_CLEANUP
@@ -2783,3 +2899,206 @@ wait_row_count FDB 0
 ovn-sbctl list FDB
 
 AT_CLEANUP
+
+AT_SETUP([ovn -- LS load balancer logical flows])
+ovn_start
+
+check ovn-nbctl \
+    -- ls-add sw0 \
+    -- lb-add lb0 10.0.0.10:80 10.0.0.4:8080 \
+    -- ls-lb-add sw0 lb0
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24
+check ovn-nbctl lsp-add sw0 sw0-lr0
+check ovn-nbctl lsp-set-type sw0-lr0 router
+check ovn-nbctl lsp-set-addresses sw0-lr0 00:00:00:00:ff:01
+check ovn-nbctl lsp-set-options sw0-lr0 router-port=lr0-sw0
+
+check ovn-nbctl --wait=sb sync
+
+check_stateful_flows() {
+    ovn-sbctl dump-flows sw0 > sw0flows
+    AT_CAPTURE_FILE([sw0flows])
+
+    AT_CHECK([grep "ls_in_pre_lb" sw0flows | sort], [0], [dnl
+  table=6 (ls_in_pre_lb       ), priority=0    , match=(1), action=(next;)
+  table=6 (ls_in_pre_lb       ), priority=100  , match=(ip), action=(reg0[[2]] = 1; next;)
+  table=6 (ls_in_pre_lb       ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
+  table=6 (ls_in_pre_lb       ), priority=110  , match=(ip && inport == "sw0-lr0"), action=(next;)
+  table=6 (ls_in_pre_lb       ), priority=110  , match=(nd || nd_rs || nd_ra || mldv1 || mldv2), action=(next;)
+])
+
+    AT_CHECK([grep "ls_in_pre_stateful" sw0flows | sort], [0], [dnl
+  table=7 (ls_in_pre_stateful ), priority=0    , match=(1), action=(next;)
+  table=7 (ls_in_pre_stateful ), priority=100  , match=(reg0[[0]] == 1), action=(ct_next;)
+  table=7 (ls_in_pre_stateful ), priority=110  , match=(reg0[[2]] == 1), action=(ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4 && sctp), action=(reg1 = ip4.dst; reg2[[0..15]] = sctp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4 && tcp), action=(reg1 = ip4.dst; reg2[[0..15]] = tcp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4 && udp), action=(reg1 = ip4.dst; reg2[[0..15]] = udp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6 && sctp), action=(xxreg1 = ip6.dst; reg2[[0..15]] = sctp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6 && tcp), action=(xxreg1 = ip6.dst; reg2[[0..15]] = tcp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6 && udp), action=(xxreg1 = ip6.dst; reg2[[0..15]] = udp.dst; ct_lb;)
+])
+
+    AT_CHECK([grep "ls_in_stateful" sw0flows | sort], [0], [dnl
+  table=12(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
+  table=12(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1), action=(ct_commit { ct_label.blocked = 0; }; next;)
+  table=12(ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.4:8080);)
+])
+
+    AT_CHECK([grep "ls_out_pre_lb" sw0flows | sort], [0], [dnl
+  table=0 (ls_out_pre_lb      ), priority=0    , match=(1), action=(next;)
+  table=0 (ls_out_pre_lb      ), priority=100  , match=(ip), action=(reg0[[2]] = 1; next;)
+  table=0 (ls_out_pre_lb      ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
+  table=0 (ls_out_pre_lb      ), priority=110  , match=(ip && outport == "sw0-lr0"), action=(next;)
+  table=0 (ls_out_pre_lb      ), priority=110  , match=(nd || nd_rs || nd_ra || mldv1 || mldv2), action=(next;)
+])
+
+    AT_CHECK([grep "ls_out_pre_stateful" sw0flows | sort], [0], [dnl
+  table=2 (ls_out_pre_stateful), priority=0    , match=(1), action=(next;)
+  table=2 (ls_out_pre_stateful), priority=100  , match=(reg0[[0]] == 1), action=(ct_next;)
+  table=2 (ls_out_pre_stateful), priority=110  , match=(reg0[[2]] == 1), action=(ct_lb;)
+])
+
+    AT_CHECK([grep "ls_out_lb" sw0flows | sort], [0], [])
+
+    AT_CHECK([grep "ls_out_stateful" sw0flows | sort], [0], [dnl
+  table=7 (ls_out_stateful    ), priority=0    , match=(1), action=(next;)
+  table=7 (ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1), action=(ct_commit { ct_label.blocked = 0; }; next;)
+])
+}
+
+check_stateful_flows
+
+# Add few ACLs
+check ovn-nbctl --wait=sb acl-add sw0 from-lport 1002 "ip4 && tcp && tcp.dst == 80" allow-related
+check ovn-nbctl --wait=sb acl-add sw0 to-lport 1002 "ip4 && tcp && tcp.src == 80" drop
+
+check_stateful_flows
+
+# Remove load balancer from sw0
+check ovn-nbctl --wait=sb ls-lb-del sw0 lb0
+
+ovn-sbctl dump-flows sw0 > sw0flows
+AT_CAPTURE_FILE([sw0flows])
+
+AT_CHECK([grep "ls_in_pre_lb" sw0flows | sort], [0], [dnl
+  table=6 (ls_in_pre_lb       ), priority=0    , match=(1), action=(next;)
+  table=6 (ls_in_pre_lb       ), priority=110  , match=(eth.dst == $svc_monitor_mac), action=(next;)
+  table=6 (ls_in_pre_lb       ), priority=110  , match=(ip && inport == "sw0-lr0"), action=(next;)
+  table=6 (ls_in_pre_lb       ), priority=110  , match=(nd || nd_rs || nd_ra || mldv1 || mldv2), action=(next;)
+])
+
+AT_CHECK([grep "ls_in_pre_stateful" sw0flows | sort], [0], [dnl
+  table=7 (ls_in_pre_stateful ), priority=0    , match=(1), action=(next;)
+  table=7 (ls_in_pre_stateful ), priority=100  , match=(reg0[[0]] == 1), action=(ct_next;)
+  table=7 (ls_in_pre_stateful ), priority=110  , match=(reg0[[2]] == 1), action=(ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4 && sctp), action=(reg1 = ip4.dst; reg2[[0..15]] = sctp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4 && tcp), action=(reg1 = ip4.dst; reg2[[0..15]] = tcp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4 && udp), action=(reg1 = ip4.dst; reg2[[0..15]] = udp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6 && sctp), action=(xxreg1 = ip6.dst; reg2[[0..15]] = sctp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6 && tcp), action=(xxreg1 = ip6.dst; reg2[[0..15]] = tcp.dst; ct_lb;)
+  table=7 (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6 && udp), action=(xxreg1 = ip6.dst; reg2[[0..15]] = udp.dst; ct_lb;)
+])
+
+AT_CHECK([grep "ls_in_stateful" sw0flows | sort], [0], [dnl
+  table=12(ls_in_stateful     ), priority=0    , match=(1), action=(next;)
+  table=12(ls_in_stateful     ), priority=100  , match=(reg0[[1]] == 1), action=(ct_commit { ct_label.blocked = 0; }; next;)
+])
+
+AT_CHECK([grep "ls_out_pre_lb" sw0flows | sort], [0], [dnl
+  table=0 (ls_out_pre_lb      ), priority=0    , match=(1), action=(next;)
+  table=0 (ls_out_pre_lb      ), priority=110  , match=(eth.src == $svc_monitor_mac), action=(next;)
+  table=0 (ls_out_pre_lb      ), priority=110  , match=(ip && outport == "sw0-lr0"), action=(next;)
+  table=0 (ls_out_pre_lb      ), priority=110  , match=(nd || nd_rs || nd_ra || mldv1 || mldv2), action=(next;)
+])
+
+AT_CHECK([grep "ls_out_pre_stateful" sw0flows | sort], [0], [dnl
+  table=2 (ls_out_pre_stateful), priority=0    , match=(1), action=(next;)
+  table=2 (ls_out_pre_stateful), priority=100  , match=(reg0[[0]] == 1), action=(ct_next;)
+  table=2 (ls_out_pre_stateful), priority=110  , match=(reg0[[2]] == 1), action=(ct_lb;)
+])
+
+AT_CHECK([grep "ls_out_stateful" sw0flows | sort], [0], [dnl
+  table=7 (ls_out_stateful    ), priority=0    , match=(1), action=(next;)
+  table=7 (ls_out_stateful    ), priority=100  , match=(reg0[[1]] == 1), action=(ct_commit { ct_label.blocked = 0; }; next;)
+])
+
+AT_CLEANUP
+])
+
+AT_SETUP([ovn -- ct.inv usage])
+ovn_start
+
+check ovn-nbctl ls-add sw0
+check ovn-nbctl lsp-add sw0 sw0p1
+
+check ovn-nbctl --wait=sb acl-add sw0 to-lport 1002 ip allow-related
+
+ovn-sbctl dump-flows sw0 > sw0flows
+AT_CAPTURE_FILE([sw0flows])
+
+AT_CHECK([grep -w "ls_in_acl" sw0flows | grep 6553 | sort], [0], [dnl
+  table=9 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=9 (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+])
+
+AT_CHECK([grep -w "ls_out_acl" sw0flows | grep 6553 | sort], [0], [dnl
+  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=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+])
+
+# Disable ct.inv usage.
+check ovn-nbctl --wait=sb set NB_Global . options:use_ct_inv_match=false
+
+ovn-sbctl dump-flows sw0 > sw0flows
+AT_CAPTURE_FILE([sw0flows])
+
+AT_CHECK([grep -w "ls_in_acl" sw0flows | grep 6553 | sort], [0], [dnl
+  table=9 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=((ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=9 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && ct.rpl && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+])
+
+AT_CHECK([grep -w "ls_out_acl" sw0flows | grep 6553 | sort], [0], [dnl
+  table=4 (ls_out_acl         ), priority=65532, match=(!ct.est && ct.rel && !ct.new && ct_label.blocked == 0), action=(next;)
+  table=4 (ls_out_acl         ), priority=65532, match=((ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=4 (ls_out_acl         ), priority=65532, match=(ct.est && !ct.rel && !ct.new && ct.rpl && ct_label.blocked == 0), action=(next;)
+  table=4 (ls_out_acl         ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+])
+
+AT_CHECK([grep -c "ct.inv" sw0flows], [1], [dnl
+0
+])
+
+# Enable ct.inv usage.
+check ovn-nbctl --wait=sb set NB_Global . options:use_ct_inv_match=true
+
+ovn-sbctl dump-flows sw0 > sw0flows
+AT_CAPTURE_FILE([sw0flows])
+
+AT_CHECK([grep -w "ls_in_acl" sw0flows | grep 6553 | sort], [0], [dnl
+  table=9 (ls_in_acl          ), priority=65532, match=(!ct.est && ct.rel && !ct.new && !ct.inv && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(ct.est && !ct.rel && !ct.new && !ct.inv && ct.rpl && ct_label.blocked == 0), action=(next;)
+  table=9 (ls_in_acl          ), priority=65532, match=(ct.inv || (ct.est && ct.rpl && ct_label.blocked == 1)), action=(drop;)
+  table=9 (ls_in_acl          ), priority=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+])
+
+AT_CHECK([grep -w "ls_out_acl" sw0flows | grep 6553 | sort], [0], [dnl
+  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=65532, match=(nd || nd_ra || nd_rs || mldv1 || mldv2), action=(next;)
+])
+
+AT_CHECK([grep -c "ct.inv" sw0flows], [0], [dnl
+6
+])
+
+AT_CLEANUP
diff --git a/tests/ovn.at b/tests/ovn.at
index b465784cd..0377b75c3 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -693,6 +693,11 @@ ip,nw_src=4.0.0.0/4.0.0.0
 ip,nw_src=64.0.0.0/64.0.0.0
 ip,nw_src=8.0.0.0/8.0.0.0
 ])
+AT_CHECK([expr_to_flow 'ip4.dst == 172.27.0.65 && ip4.src == $set1 && ip4.dst != 10.128.0.0/14'], [0], [dnl
+ip,nw_src=10.0.0.1,nw_dst=172.27.0.65
+ip,nw_src=10.0.0.2,nw_dst=172.27.0.65
+ip,nw_src=10.0.0.3,nw_dst=172.27.0.65
+])
 AT_CLEANUP
 
 AT_SETUP([ovn -- converting expressions to flows -- port groups])
@@ -9878,15 +9883,12 @@ AT_CHECK([ovn-nbctl --wait=sb sync], [0], [ignore])
 ovn-sbctl dump-flows > sbflows
 AT_CAPTURE_FILE([sbflows])
 
-reset_pcap_file() {
-    local iface=$1
-    local pcap_file=$2
-    check ovs-vsctl -- set Interface $iface options:tx_pcap=dummy-tx.pcap \
-options:rxq_pcap=dummy-rx.pcap
-    rm -f ${pcap_file}*.pcap
-    check ovs-vsctl -- set Interface $iface options:tx_pcap=${pcap_file}-tx.pcap \
-options:rxq_pcap=${pcap_file}-rx.pcap
-}
+hv1_gw1_ofport=$(as hv1 ovs-vsctl --bare --columns ofport find Interface name=ovn-gw1-0)
+hv1_gw2_ofport=$(as hv1 ovs-vsctl --bare --columns ofport find Interface name=ovn-gw2-0)
+
+OVS_WAIT_UNTIL([
+    test 1 = $(as hv1 ovs-ofctl dump-flows br-int table=37 | grep -c "active_backup,ofport,members:$hv1_gw1_ofport,$hv1_gw2_ofport")
+])
 
 test_ip_packet()
 {
@@ -9932,13 +9934,13 @@ test_ip_packet()
     echo $expected > ext1-vif1.expected
     exp_gw_ip_garp=ffffffffffff00000201020308060001080006040001000002010203ac100101000000000000ac100101
     echo $exp_gw_ip_garp >> ext1-vif1.expected
-    as $active_gw reset_pcap_file br-phys_n1 $active_gw/br-phys_n1
+    as $active_gw reset_iface_pcap_file br-phys_n1 $active_gw/br-phys_n1
 
     if test $backup_vswitchd_dead != 1; then
         # Reset the file only if vswitchd in backup gw is alive
-        as $backup_gw reset_pcap_file br-phys_n1 $backup_gw/br-phys_n1
+        as $backup_gw reset_iface_pcap_file br-phys_n1 $backup_gw/br-phys_n1
     fi
-    as ext1 reset_pcap_file ext1-vif1 ext1/vif1
+    as ext1 reset_iface_pcap_file ext1-vif1 ext1/vif1
 
     # Resend packet from foo1 to outside1
     check as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 $packet
@@ -9990,6 +9992,10 @@ AT_CHECK(
 <1>
 ])
 
+OVS_WAIT_UNTIL([
+    test 1 = $(as hv1 ovs-ofctl dump-flows br-int table=37 | grep -c "active_backup,ofport,members:$hv1_gw2_ofport,$hv1_gw1_ofport")
+])
+
 test_ip_packet gw2 gw1 0
 
 # Get the claim count of both gw1 and gw2.
@@ -10010,6 +10016,12 @@ OVS_WAIT_UNTIL([test $gw1_claim_ct = `cat gw1/ovn-controller.log \
 AT_CHECK([test $gw2_claim_ct = `cat gw2/ovn-controller.log | \
 grep -c "cr-alice: Claiming"`])
 
+OVS_WAIT_UNTIL([
+    bfd_status=$(as hv1 ovs-vsctl get interface ovn-gw2-0 bfd_status:state)
+    echo "bfd status = $bfd_status"
+    test "$bfd_status" = "down"
+])
+
 test_ip_packet gw1 gw2 1
 
 as gw2
@@ -11490,10 +11502,100 @@ for i in 1 2; do
     done
 done
 
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int |awk '/table=65/{print substr($8, 16, length($8))}' |sort -n], [0], [dnl
+10
+11
+])
+
+# remove the localport from br-int and re-create it
+as hv1
+check ovs-vsctl del-port vif01
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int |awk '/table=65/{print substr($8, 16, length($8))}' |sort -n], [0], [dnl
+11
+])
+
+as hv1
+check ovs-vsctl add-port br-int vif01 \
+    -- set Interface vif01 external-ids:iface-id=lp01
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int |awk '/table=65/{print substr($8, 16, length($8))}' |sort -n], [0], [dnl
+2
+11
+])
+
 OVN_CLEANUP([hv1],[hv2])
 
 AT_CLEANUP
 
+AT_SETUP([ovn -- localport suppress gARP])
+ovn_start
+
+send_garp() {
+    local inport=$1 eth_src=$2 eth_dst=$3 spa=$4 tpa=$5
+    local request=${eth_dst}${eth_src}08060001080006040001${eth_src}${spa}${eth_dst}${tpa}
+    as hv1 ovs-appctl netdev-dummy/receive vif$inport $request
+}
+
+net_add n1
+sim_add hv1
+as hv1
+check ovs-vsctl add-br br-phys
+ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
+ovn_attach n1 br-phys 192.168.0.1
+
+check ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
+
+check ovn-nbctl ls-add ls \
+    -- lsp-add ls lp \
+    -- lsp-set-type lp localport \
+    -- lsp-set-addresses lp "00:00:00:00:00:01 10.0.0.1" \
+    -- lsp-add ls ln \
+    -- lsp-set-type ln localnet \
+    -- lsp-set-addresses ln unknown \
+    -- lsp-set-options ln network_name=phys \
+    -- lsp-add ls lsp \
+    -- lsp-set-addresses lsp "00:00:00:00:00:02 10.0.0.2"
+
+dnl First bind the localport.
+check ovs-vsctl add-port br-int vif1 \
+    -- set Interface vif1 external-ids:iface-id=lp
+check ovn-nbctl --wait=hv sync
+
+dnl Then bind the regular vif.
+check ovs-vsctl add-port br-int vif2 \
+    -- set Interface vif2 external-ids:iface-id=lsp \
+        options:tx_pcap=hv1/vif2-tx.pcap \
+        options:rxq_pcap=hv1/vif2-rx.pcap
+
+wait_for_ports_up lsp
+check ovn-nbctl --wait=hv sync
+
+dnl Wait for at least two gARPs from lsp (10.0.0.2).
+lsp_garp=ffffffffffff000000000002080600010800060400010000000000020a0000020000000000000a000002
+OVS_WAIT_UNTIL([
+    garps=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/br-phys-tx.pcap | grep ${lsp_garp} -c`
+    test $garps -ge 2
+])
+
+dnl At this point it's safe to assume that ovn-controller skipped sending gARP
+dnl for the localport.  Check that there are no other packets than the gARPs
+dnl for the regular vif.
+AT_CHECK([
+    pkts=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/br-phys-tx.pcap | grep -v ${lsp_garp} -c`
+    test 0 -eq $pkts
+])
+
+spa=$(ip_to_hex 10 0 0 1)
+tpa=$(ip_to_hex 10 0 0 100)
+send_garp 1 000000000001 ffffffffffff $spa $tpa
+
+dnl traffic from localport should not be sent to localnet
+AT_CHECK([tcpdump -r hv1/br-phys_n1-tx.pcap arp[[24:4]]=0x0a000064 | wc -l],[0],[dnl
+0
+],[ignore])
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+
 AT_SETUP([ovn -- 1 LR with HA distributed router gateway port])
 ovn_start
 
@@ -13901,16 +14003,16 @@ check ovn-nbctl acl-add ls1 to-lport 3 'ip4.src==10.0.0.1' allow
 check ovn-nbctl --wait=hv sync
 
 # Check OVS flows, the less restrictive flows should have been installed.
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=45 | ofctl_strip_all | \
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=44 | ofctl_strip_all | \
     grep "priority=1003" | \
     sed 's/conjunction([[^)]]*)/conjunction()/g' | sort], [0], [dnl
- table=45, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=resubmit(,46)
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
+ table=44, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=resubmit(,45)
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
 ])
 
 # Traffic 10.0.0.1, 10.0.0.2 -> 10.0.0.3, 10.0.0.4 should be allowed.
@@ -13945,16 +14047,16 @@ check ovn-nbctl acl-del ls1 to-lport 3 'ip4.src==10.0.0.1 || ip4.src==10.0.0.1'
 check ovn-nbctl --wait=hv sync
 
 # Check OVS flows, the second less restrictive allow ACL should have been installed.
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=45 | ofctl_strip_all | \
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=44 | ofctl_strip_all | \
     grep "priority=1003" | \
     sed 's/conjunction([[^)]]*)/conjunction()/g' | sort], [0], [dnl
- table=45, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=resubmit(,46)
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
+ table=44, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=resubmit(,45)
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
 ])
 
 # Remove the less restrictive allow ACL.
@@ -13962,16 +14064,16 @@ check ovn-nbctl acl-del ls1 to-lport 3 'ip4.src==10.0.0.1'
 check ovn-nbctl --wait=hv sync
 
 # Check OVS flows, the 10.0.0.1 conjunction should have been reinstalled.
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=45 | ofctl_strip_all | \
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=44 | ofctl_strip_all | \
     grep "priority=1003" | \
     sed 's/conjunction([[^)]]*)/conjunction()/g' | sort], [0], [dnl
- table=45, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
+ table=44, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
 ])
 
 # Traffic 10.0.0.1, 10.0.0.2 -> 10.0.0.3, 10.0.0.4 should be allowed.
@@ -14001,16 +14103,16 @@ check ovn-nbctl acl-add ls1 to-lport 3 'ip4.src==10.0.0.1' allow
 check ovn-nbctl --wait=hv sync
 
 # Check OVS flows, the less restrictive flows should have been installed.
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=45 | ofctl_strip_all | \
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=44 | ofctl_strip_all | \
    grep "priority=1003" | \
    sed 's/conjunction([[^)]]*)/conjunction()/g' | sort], [0], [dnl
- table=45, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=resubmit(,46)
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
+ table=44, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=resubmit(,45)
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
 ])
 
 # Add another ACL that overlaps with the existing less restrictive ones.
@@ -14021,19 +14123,19 @@ check ovn-nbctl --wait=hv sync
 # with an additional conjunction action.
 #
 # New non-conjunctive flows should be added to match on 'udp'.
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=45 | ofctl_strip_all | \
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=44 | ofctl_strip_all | \
    grep "priority=1003" | \
    sed 's/conjunction([[^)]]*)/conjunction()/g' | sort], [0], [dnl
- table=45, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,conj_id=4,ip,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=resubmit(,46)
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction(),conjunction()
- table=45, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
- table=45, priority=1003,udp,metadata=0x1 actions=resubmit(,46)
- table=45, priority=1003,udp6,metadata=0x1 actions=resubmit(,46)
+ table=44, priority=1003,conj_id=2,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,conj_id=3,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,conj_id=4,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.3 actions=conjunction(),conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_dst=10.0.0.4 actions=conjunction(),conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.1 actions=resubmit(,45)
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.2 actions=conjunction(),conjunction()
+ table=44, priority=1003,ip,metadata=0x1,nw_src=10.0.0.42 actions=conjunction()
+ table=44, priority=1003,udp,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=1003,udp6,metadata=0x1 actions=resubmit(,45)
 ])
 
 OVN_CLEANUP([hv1])
@@ -15375,7 +15477,7 @@ wait_for_ports_up ls1-lp_ext1
 # There should be a flow in hv2 to drop traffic from ls1-lp_ext1 destined
 # to router mac.
 AT_CHECK([as hv2 ovs-ofctl dump-flows br-int \
-table=30,dl_src=f0:00:00:00:00:03,dl_dst=a0:10:00:00:00:01 | \
+table=29,dl_src=f0:00:00:00:00:03,dl_dst=a0:10:00:00:00:01 | \
 grep -c "actions=drop"], [0], [1
 ])
 
@@ -16647,56 +16749,67 @@ ovs-vsctl -- add-port br-int hv2-vif2 -- \
 
 ovn-nbctl ls-add sw0
 
-ovn-nbctl lsp-add sw0 sw0-vir
-ovn-nbctl lsp-set-addresses sw0-vir "50:54:00:00:00:10 10.0.0.10"
-ovn-nbctl lsp-set-port-security sw0-vir "50:54:00:00:00:10 10.0.0.10"
-ovn-nbctl lsp-set-type sw0-vir virtual
-ovn-nbctl set logical_switch_port sw0-vir options:virtual-ip=10.0.0.10
-ovn-nbctl set logical_switch_port sw0-vir options:virtual-parents=sw0-p1,sw0-p2,sw0-p3
+check ovn-nbctl lsp-add sw0 sw0-vir
+check ovn-nbctl lsp-set-addresses sw0-vir "50:54:00:00:00:10 10.0.0.10"
+check ovn-nbctl lsp-set-port-security sw0-vir "50:54:00:00:00:10 10.0.0.10"
+check ovn-nbctl lsp-set-type sw0-vir virtual
+check ovn-nbctl set logical_switch_port sw0-vir options:virtual-ip=10.0.0.10
+check ovn-nbctl set logical_switch_port sw0-vir options:virtual-parents=sw0-p1,sw0-p2,sw0-p3
 
-ovn-nbctl lsp-add sw0 sw0-p1
-ovn-nbctl lsp-set-addresses sw0-p1 "50:54:00:00:00:03 10.0.0.3"
-ovn-nbctl lsp-set-port-security sw0-p1 "50:54:00:00:00:03 10.0.0.3 10.0.0.10"
+check ovn-nbctl lsp-add sw0 sw0-p1
+check ovn-nbctl lsp-set-addresses sw0-p1 "50:54:00:00:00:03 10.0.0.3"
+check ovn-nbctl lsp-set-port-security sw0-p1 "50:54:00:00:00:03 10.0.0.3 10.0.0.10"
 
-ovn-nbctl lsp-add sw0 sw0-p2
-ovn-nbctl lsp-set-addresses sw0-p2 "50:54:00:00:00:04 10.0.0.4"
-ovn-nbctl lsp-set-port-security sw0-p2 "50:54:00:00:00:04 10.0.0.4 10.0.0.10"
+check ovn-nbctl lsp-add sw0 sw0-p2
+check ovn-nbctl lsp-set-addresses sw0-p2 "50:54:00:00:00:04 10.0.0.4"
+check ovn-nbctl lsp-set-port-security sw0-p2 "50:54:00:00:00:04 10.0.0.4 10.0.0.10"
 
-ovn-nbctl lsp-add sw0 sw0-p3
-ovn-nbctl lsp-set-addresses sw0-p3 "50:54:00:00:00:05 10.0.0.5"
-ovn-nbctl lsp-set-port-security sw0-p3 "50:54:00:00:00:05 10.0.0.5 10.0.0.10"
+check ovn-nbctl lsp-add sw0 sw0-p3
+check ovn-nbctl lsp-set-addresses sw0-p3 "50:54:00:00:00:05 10.0.0.5"
+check ovn-nbctl lsp-set-port-security sw0-p3 "50:54:00:00:00:05 10.0.0.5 10.0.0.10"
 
 # Create the second logical switch with one port
-ovn-nbctl ls-add sw1
-ovn-nbctl lsp-add sw1 sw1-p1
-ovn-nbctl lsp-set-addresses sw1-p1 "40:54:00:00:00:03 20.0.0.3"
-ovn-nbctl lsp-set-port-security sw1-p1 "40:54:00:00:00:03 20.0.0.3"
+check ovn-nbctl ls-add sw1
+check ovn-nbctl lsp-add sw1 sw1-p1
+check ovn-nbctl lsp-set-addresses sw1-p1 "40:54:00:00:00:03 20.0.0.3"
+check ovn-nbctl lsp-set-port-security sw1-p1 "40:54:00:00:00:03 20.0.0.3"
 
 # Create a logical router and attach both logical switches
-ovn-nbctl lr-add lr0
-ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24
-ovn-nbctl lsp-add sw0 sw0-lr0
-ovn-nbctl lsp-set-type sw0-lr0 router
-ovn-nbctl lsp-set-addresses sw0-lr0 00:00:00:00:ff:01
-ovn-nbctl lsp-set-options sw0-lr0 router-port=lr0-sw0
+check ovn-nbctl lr-add lr0
+check ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24
+check ovn-nbctl lsp-add sw0 sw0-lr0
+check ovn-nbctl lsp-set-type sw0-lr0 router
+check ovn-nbctl lsp-set-addresses sw0-lr0 00:00:00:00:ff:01
+check ovn-nbctl lsp-set-options sw0-lr0 router-port=lr0-sw0
 
-ovn-nbctl lrp-add lr0 lr0-sw1 00:00:00:00:ff:02 20.0.0.1/24
-ovn-nbctl lsp-add sw1 sw1-lr0
-ovn-nbctl lsp-set-type sw1-lr0 router
-ovn-nbctl lsp-set-addresses sw1-lr0 00:00:00:00:ff:02
-ovn-nbctl lsp-set-options sw1-lr0 router-port=lr0-sw1
+check ovn-nbctl lrp-add lr0 lr0-sw1 00:00:00:00:ff:02 20.0.0.1/24
+check ovn-nbctl lsp-add sw1 sw1-lr0
+check ovn-nbctl lsp-set-type sw1-lr0 router
+check ovn-nbctl lsp-set-addresses sw1-lr0 00:00:00:00:ff:02
+check ovn-nbctl lsp-set-options sw1-lr0 router-port=lr0-sw1
 
-OVN_POPULATE_ARP
+# Add an ACL that matches on sw0-vir being bound locally.
+check ovn-nbctl acl-add sw0 to-lport 1000 'is_chassis_resident("sw0-vir") && ip' allow
 
-# Delete sw0-vir and add again.
-ovn-nbctl lsp-del sw0-vir
+check ovn-nbctl ls-add public
+check ovn-nbctl lrp-add lr0 lr0-public 00:00:20:20:12:13 172.168.0.100/24
+check ovn-nbctl lsp-add public public-lr0
+check ovn-nbctl lsp-set-type public-lr0 router
+check ovn-nbctl lsp-set-addresses public-lr0 router
+check ovn-nbctl lsp-set-options public-lr0 router-port=lr0-public
 
-ovn-nbctl lsp-add sw0 sw0-vir
-ovn-nbctl lsp-set-addresses sw0-vir "50:54:00:00:00:10 10.0.0.10"
-ovn-nbctl lsp-set-port-security sw0-vir "50:54:00:00:00:10 10.0.0.10"
-ovn-nbctl lsp-set-type sw0-vir virtual
-ovn-nbctl set logical_switch_port sw0-vir options:virtual-ip=10.0.0.10
-ovn-nbctl set logical_switch_port sw0-vir options:virtual-parents=sw0-p1,sw0-p2,sw0-p3
+# localnet port
+check ovn-nbctl lsp-add public ln-public
+check ovn-nbctl lsp-set-type ln-public localnet
+check ovn-nbctl lsp-set-addresses ln-public unknown
+check ovn-nbctl lsp-set-options ln-public network_name=public
+
+# schedule the gw router port to a chassis. Change the name of the chassis
+check ovn-nbctl --wait=hv lrp-set-gateway-chassis lr0-public hv1 20
+
+check ovn-nbctl lr-nat-add lr0 dnat_and_snat 172.168.0.50 10.0.0.10 sw0-vir 10:54:00:00:00:10
+
+OVN_POPULATE_ARP
 
 wait_for_ports_up
 ovn-nbctl --wait=hv sync
@@ -16746,6 +16859,30 @@ ovs-vsctl del-port hv1-vif3
 AT_CHECK([test x$(ovn-sbctl --bare --columns chassis find port_binding \
 logical_port=sw0-vir) = x], [0], [])
 
+check_virtual_offlows_present() {
+    hv=$1
+
+    AT_CHECK([as $hv ovs-ofctl dump-flows br-int table=44 | ofctl_strip_all | grep "priority=2000"], [0], [dnl
+ table=44, priority=2000,ip,metadata=0x1 actions=resubmit(,45)
+ table=44, priority=2000,ipv6,metadata=0x1 actions=resubmit(,45)
+])
+
+    AT_CHECK([as $hv ovs-ofctl dump-flows br-int table=11 | ofctl_strip_all | \
+    grep "priority=92" | grep 172.168.0.50], [0], [dnl
+ table=11, priority=92,arp,reg14=0x3,metadata=0x3,arp_tpa=172.168.0.50,arp_op=1 actions=move:NXM_OF_ETH_SRC[[]]->NXM_OF_ETH_DST[[]],mod_dl_src:10:54:00:00:00:10,load:0x2->NXM_OF_ARP_OP[[]],move:NXM_NX_ARP_SHA[[]]->NXM_NX_ARP_THA[[]],load:0x105400000010->NXM_NX_ARP_SHA[[]],push:NXM_OF_ARP_SPA[[]],push:NXM_OF_ARP_TPA[[]],pop:NXM_OF_ARP_SPA[[]],pop:NXM_OF_ARP_TPA[[]],move:NXM_NX_REG14[[]]->NXM_NX_REG15[[]],load:0x1->NXM_NX_REG10[[0]],resubmit(,37)
+])
+}
+
+check_virtual_offlows_not_present() {
+    hv=$1
+    AT_CHECK([as $hv ovs-ofctl dump-flows br-int table=45 | ofctl_strip_all | grep "priority=2000"], [1], [dnl
+])
+
+    AT_CHECK([as $hv ovs-ofctl dump-flows br-int table=11 | ofctl_strip_all | \
+    grep "priority=92" | grep 172.168.0.50], [1], [dnl
+])
+}
+
 # From sw0-p0 send GARP for 10.0.0.10. hv1 should claim sw0-vir
 # and sw0-p1 should be its virtual_parent.
 eth_src=505400000003
@@ -16767,6 +16904,13 @@ AT_CHECK([grep lr_in_arp_resolve lr0-flows2 | grep "reg0 == 10.0.0.10" | sed 's/
   table=??(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:03; next;)
 ])
 
+# hv1 should add the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_present hv1
+
+# hv2 should not have the above flows.
+check_virtual_offlows_not_present hv2
+
 # Forcibly clear virtual_parent. ovn-controller should release the binding
 # gracefully.
 pb_uuid=$(ovn-sbctl --bare --columns _uuid find port_binding logical_port=sw0-vir)
@@ -16777,6 +16921,13 @@ logical_port=sw0-vir) = x])
 
 wait_row_count nb:Logical_Switch_Port 1 up=false name=sw0-vir
 
+check ovn-nbctl --wait=hv sync
+# hv1 should remove the flow for the ACL with is_chassis_redirect check for sw0-vir.
+check_virtual_offlows_not_present hv1
+
+# hv2 should not have the flow for ACL.
+check_virtual_offlows_not_present hv2
+
 # From sw0-p0 resend GARP for 10.0.0.10. hv1 should reclaim sw0-vir
 # and sw0-p1 should be its virtual_parent.
 send_garp 1 1 $eth_src $eth_dst $spa $tpa
@@ -16789,6 +16940,58 @@ logical_port=sw0-vir) = xsw0-p1])
 
 wait_for_ports_up sw0-vir
 
+check ovn-nbctl --wait=hv sync
+# hv1 should add the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_present hv1
+
+# hv2 should not have the above flows.
+check_virtual_offlows_not_present hv2
+
+# Release sw0-p1.
+as hv1 ovs-vsctl set interface hv1-vif1 external-ids:iface-id=sw0-px
+wait_column "false" nb:Logical_Switch_Port up name=sw0-p1
+wait_column "false" nb:Logical_Switch_Port up name=sw0-vir
+
+check ovn-nbctl --wait=hv sync
+# hv1 should remove the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_not_present hv1
+
+# hv2 should not have the above flows.
+check_virtual_offlows_not_present hv2
+
+# Claim sw0-p1 again.
+as hv1 ovs-vsctl set interface hv1-vif1 external-ids:iface-id=sw0-p1
+wait_for_ports_up sw0-p1
+
+# hv1 should not have the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_not_present hv1
+
+# hv2 should not have the above flows.
+check_virtual_offlows_not_present hv2
+
+# From sw0-p0 send GARP for 10.0.0.10. hv1 should claim sw0-vir
+# and sw0-p1 should be its virtual_parent.
+eth_src=505400000003
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 10)
+tpa=$(ip_to_hex 10 0 0 10)
+send_garp 1 1 $eth_src $eth_dst $spa $tpa
+
+wait_row_count Port_Binding 1 logical_port=sw0-vir chassis=$hv1_ch_uuid
+check_row_count Port_Binding 1 logical_port=sw0-vir virtual_parent=sw0-p1
+wait_for_ports_up sw0-vir
+check ovn-nbctl --wait=hv sync
+
+# hv1 should add the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_present hv1
+
+# hv2 should not have the above flows.
+check_virtual_offlows_not_present hv2
+
 # From sw0-p3 send GARP for 10.0.0.10. hv1 should claim sw0-vir
 # and sw0-p3 should be its virtual_parent.
 eth_src=505400000005
@@ -16806,8 +17009,8 @@ logical_port=sw0-vir) = xsw0-p3])
 wait_for_ports_up sw0-vir
 
 # There should be an arp resolve flow to resolve the virtual_ip with the
-# sw0-p2's MAC.
-sleep 1
+# sw0-p3's MAC.
+check ovn-nbctl --wait=hv sync
 ovn-sbctl dump-flows lr0 > lr0-flows3
 AT_CAPTURE_FILE([lr0-flows3])
 cp ovn-sb/ovn-sb.db lr0-flows3.db
@@ -16815,6 +17018,13 @@ AT_CHECK([grep lr_in_arp_resolve lr0-flows3 | grep "reg0 == 10.0.0.10"  | sed 's
   table=??(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:05; next;)
 ])
 
+# hv1 should add the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_present hv1
+
+# hv2 should not have the above flows.
+check_virtual_offlows_not_present hv2
+
 # send the garp from sw0-p2 (in hv2). hv2 should claim sw0-vir
 # and sw0-p2 shpuld be its virtual_parent.
 eth_src=505400000004
@@ -16832,14 +17042,21 @@ logical_port=sw0-vir) = xsw0-p2])
 wait_for_ports_up sw0-vir
 
 # There should be an arp resolve flow to resolve the virtual_ip with the
-# sw0-p3's MAC.
-sleep 1
+# sw0-p2's MAC.
+check ovn-nbctl --wait=hv sync
 ovn-sbctl dump-flows lr0 > lr0-flows4
 AT_CAPTURE_FILE([lr0-flows4])
 AT_CHECK([grep lr_in_arp_resolve lr0-flows4 | grep "reg0 == 10.0.0.10" | sed 's/table=../table=??/'], [0], [dnl
   table=??(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:04; next;)
 ])
 
+# hv2 should add the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_present hv2
+
+# hv1 should not have the above flows.
+check_virtual_offlows_not_present hv1
+
 # Now send arp reply from sw0-p1. hv1 should claim sw0-vir
 # and sw0-p1 shpuld be its virtual_parent.
 eth_src=505400000003
@@ -16863,6 +17080,14 @@ AT_CHECK([grep lr_in_arp_resolve lr0-flows5 | grep "reg0 == 10.0.0.10" | sed 's/
   table=??(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:03; next;)
 ])
 
+check ovn-nbctl --wait=hv sync
+# hv1 should add the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_present hv1
+
+# hv2 should not have the above flows.
+check_virtual_offlows_not_present hv2
+
 # Delete hv1-vif1 port. hv1 should release sw0-vir
 as hv1 ovs-vsctl del-port hv1-vif1
 
@@ -16883,6 +17108,15 @@ AT_CHECK([grep lr_in_arp_resolve lr0-flows6 | grep "reg0 == 10.0.0.10" | sed 's/
   table=??(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 00:00:00:00:00:00; next;)
 ])
 
+check ovn-nbctl --wait=hv sync
+# hv1 should remove the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_not_present hv1
+
+# hv2 should not have the above flows.
+check_virtual_offlows_not_present hv2
+
+
 # Now send arp reply from sw0-p2. hv2 should claim sw0-vir
 # and sw0-p2 should be its virtual_parent.
 eth_src=505400000004
@@ -16906,6 +17140,14 @@ AT_CHECK([grep lr_in_arp_resolve lr0-flows7 | grep "reg0 == 10.0.0.10" | sed 's/
   table=??(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:04; next;)
 ])
 
+check ovn-nbctl --wait=hv sync
+# hv2 should add the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_present hv2
+
+# hv1 should not have the above flows.
+check_virtual_offlows_not_present hv1
+
 # Delete sw0-p2 logical port
 ovn-nbctl lsp-del sw0-p2
 
@@ -16933,6 +17175,14 @@ AT_CHECK([grep ls_in_arp_rsp sw0-flows3 | grep bind_vport | sed 's/table=../tabl
   table=??(ls_in_arp_rsp      ), priority=100  , match=(inport == "sw0-p3" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
 ])
 
+check ovn-nbctl --wait=hv sync
+# hv2 should remove the flow for the ACL with is_chassis_redirect check for sw0-vir and
+# arp responder flow in lr0 pipeline.
+check_virtual_offlows_not_present hv2
+
+# hv1 should not have the above flows.
+check_virtual_offlows_not_present hv2
+
 ovn-nbctl --wait=hv remove logical_switch_port sw0-vir options virtual-parents
 ovn-sbctl dump-flows sw0 > sw0-flows4
 AT_CAPTURE_FILE([sw0-flows4])
@@ -16942,6 +17192,38 @@ ovn-sbctl dump-flows lr0 > lr0-flows8
 AT_CAPTURE_FILE([lr0-flows8])
 AT_CHECK([grep lr_in_arp_resolve lr0-flows8 | grep "reg0 == 10.0.0.10"], [1])
 
+# Delete sw0-vir and add again.
+ovn-nbctl lsp-del sw0-vir
+
+ovn-nbctl lsp-add sw0 sw0-vir
+ovn-nbctl lsp-set-addresses sw0-vir "50:54:00:00:00:10 10.0.0.10"
+ovn-nbctl lsp-set-port-security sw0-vir "50:54:00:00:00:10 10.0.0.10"
+ovn-nbctl lsp-set-type sw0-vir virtual
+ovn-nbctl set logical_switch_port sw0-vir options:virtual-ip=10.0.0.10
+ovn-nbctl set logical_switch_port sw0-vir options:virtual-parents=sw0-p1,sw0-p2,sw0-p3
+
+ovn-nbctl --wait=hv sync
+
+# Check that logical flows are added for sw0-vir in lsp_in_arp_rsp pipeline
+# with bind_vport action.
+
+ovn-sbctl dump-flows sw0 > sw0-flows
+AT_CAPTURE_FILE([sw0-flows])
+
+AT_CHECK([grep ls_in_arp_rsp sw0-flows | grep bind_vport | sed 's/table=../table=??/' | sort], [0], [dnl
+  table=??(ls_in_arp_rsp      ), priority=100  , match=(inport == "sw0-p1" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+  table=??(ls_in_arp_rsp      ), priority=100  , match=(inport == "sw0-p3" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+])
+
+ovn-sbctl dump-flows lr0 > lr0-flows
+AT_CAPTURE_FILE([lr0-flows])
+
+# Since the sw0-vir is not claimed by any chassis, eth.dst should be set to
+# zero if the ip4.dst is the virtual ip in the router pipeline.
+AT_CHECK([grep lr_in_arp_resolve lr0-flows | grep "reg0 == 10.0.0.10" | sed 's/table=../table=??/'], [0], [dnl
+  table=??(lr_in_arp_resolve  ), priority=100  , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 00:00:00:00:00:00; next;)
+])
+
 OVN_CLEANUP([hv1], [hv2])
 AT_CLEANUP
 
@@ -17321,6 +17603,27 @@ check ovs-vsctl -- add-port br-int hv2-vif4 -- \
     ofport-request=1
 ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
 
+AT_CAPTURE_FILE([exp])
+AT_CAPTURE_FILE([rcv])
+check_packets() {
+    > exp
+    > rcv
+    if test "$1" = --uniq; then
+        sort="sort -u"; shift
+    else
+        sort=sort
+    fi
+    for tuple in "$@"; do
+        set $tuple; pcap=$1 type=$2
+        echo "--- $pcap" | tee -a exp >> rcv
+        $sort "$type" >> exp
+        $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $pcap | $sort >> rcv
+        echo | tee -a exp >> rcv
+    done
+
+    $at_diff exp rcv >/dev/null
+}
+
 OVN_POPULATE_ARP
 
 # Enable IGMP snooping on sw1.
@@ -17337,21 +17640,16 @@ ovn-sbctl dump-flows > sbflows
 AT_CAPTURE_FILE([expected])
 AT_CAPTURE_FILE([received])
 > expected
-> received
-for i in 1 2; do
-    for j in 1 2; do
-        pcap=hv$i/vif$j-tx.pcap
-        echo "--- $pcap" | tee -a expected >> received
-        $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $pcap | sort >> received
-        echo | tee -a expected >> received
-    done
-done
-check $at_diff -F'^---' expected received
+OVS_WAIT_UNTIL(
+  [check_packets 'hv1/vif1-tx.pcap expected' \
+                 'hv1/vif2-tx.pcap expected' \
+                 'hv2/vif1-tx.pcap expected' \
+                 'hv2/vif2-tx.pcap expected'],
+  [$at_diff -F'^---' exp rcv])
 
 check ovn-nbctl --wait=hv sync
 
 AT_CAPTURE_FILE([sbflows2])
-cp ovn-sb/ovn-sb.db ovn-sb2.db
 ovn-sbctl dump-flows > sbflows2
 
 # Inject IGMP Join for 239.0.1.68 on sw1-p11.
@@ -17369,7 +17667,6 @@ wait_row_count IGMP_Group 2 address=239.0.1.68
 check ovn-nbctl --wait=hv sync
 
 AT_CAPTURE_FILE([sbflows3])
-cp ovn-sb/ovn-sb.db ovn-sb3.db
 ovn-sbctl dump-flows > sbflows3
 
 AS_BOX([IGMP traffic test 1])
@@ -17386,22 +17683,6 @@ store_ip_multicast_pkt \
     $(ip_to_hex 10 0 0 42) $(ip_to_hex 239 0 1 68) 1e 20 ca70 11 \
     e518e518000a3b3a0000 expected
 
-AT_CAPTURE_FILE([exp])
-AT_CAPTURE_FILE([rcv])
-check_packets() {
-    > exp
-    > rcv
-    for tuple in "$@"; do
-        set $tuple; pcap=$1 type=$2
-        echo "--- $pcap" | tee -a exp >> rcv
-        sort "$type" >> exp
-        $PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" $pcap | sort >> rcv
-        echo | tee -a exp >> rcv
-    done
-
-    $at_diff exp rcv >/dev/null
-}
-
 OVS_WAIT_UNTIL(
   [check_packets 'hv1/vif1-tx.pcap expected' \
                  'hv2/vif1-tx.pcap expected' \
@@ -17492,15 +17773,26 @@ check ovn-nbctl set Logical_Switch sw2 \
     other_config:mcast_ip4_src="20.0.0.254"
 
 AS_BOX([IGMP traffic test 4])
-# Wait for 1 query interval (1 sec) and check that two queries are generated.
+# Check that multiple queries are generated over time.
 > expected
 store_igmp_v3_query 0000000002fe $(ip_to_hex 20 0 0 254) 84dd expected
 store_igmp_v3_query 0000000002fe $(ip_to_hex 20 0 0 254) 84dd expected
 
-OVS_WAIT_UNTIL(
-  [check_packets 'hv1/vif3-tx.pcap expected' \
-                 'hv2/vif3-tx.pcap expected'],
-  [$at_diff -F'^---' exp rcv])
+for count in 1 2 3; do
+    as hv1 reset_pcap_file hv1-vif1 hv1/vif1
+    as hv1 reset_pcap_file hv1-vif2 hv1/vif2
+    as hv1 reset_pcap_file hv1-vif3 hv1/vif3
+    as hv1 reset_pcap_file hv1-vif4 hv1/vif4
+    as hv2 reset_pcap_file hv2-vif1 hv2/vif1
+    as hv2 reset_pcap_file hv2-vif2 hv2/vif2
+    as hv2 reset_pcap_file hv2-vif3 hv2/vif3
+    as hv2 reset_pcap_file hv2-vif4 hv2/vif4
+    OVS_WAIT_UNTIL(
+      [check_packets --uniq \
+                     'hv1/vif3-tx.pcap expected' \
+                     'hv2/vif3-tx.pcap expected'],
+      [$at_diff -F'^---' exp rcv])
+done
 
 # Disable IGMP querier on sw2.
 check ovn-nbctl set Logical_Switch sw2 \
@@ -19776,7 +20068,14 @@ AT_CAPTURE_FILE([sbflows])
 OVS_WAIT_FOR_OUTPUT(
   [ovn-sbctl dump-flows > sbflows
    ovn-sbctl dump-flows sw0 | grep ct_lb | grep priority=120 | sed 's/table=..//'], 0,
-  [  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80,20.0.0.3:80; hash_fields="ip_dst,ip_src,tcp_dst,tcp_src");)
+  [dnl
+  (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4 && sctp), action=(reg1 = ip4.dst; reg2[[0..15]] = sctp.dst; ct_lb;)
+  (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4 && tcp), action=(reg1 = ip4.dst; reg2[[0..15]] = tcp.dst; ct_lb;)
+  (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip4 && udp), action=(reg1 = ip4.dst; reg2[[0..15]] = udp.dst; ct_lb;)
+  (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6 && sctp), action=(xxreg1 = ip6.dst; reg2[[0..15]] = sctp.dst; ct_lb;)
+  (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6 && tcp), action=(xxreg1 = ip6.dst; reg2[[0..15]] = tcp.dst; ct_lb;)
+  (ls_in_pre_stateful ), priority=120  , match=(reg0[[2]] == 1 && ip6 && udp), action=(xxreg1 = ip6.dst; reg2[[0..15]] = udp.dst; ct_lb;)
+  (ls_in_stateful     ), priority=120  , match=(ct.new && ip4.dst == 10.0.0.10 && tcp.dst == 80), action=(reg1 = 10.0.0.10; reg2[[0..15]] = 80; ct_lb(backends=10.0.0.3:80,20.0.0.3:80; hash_fields="ip_dst,ip_src,tcp_dst,tcp_src");)
 ])
 
 AT_CAPTURE_FILE([sbflows2])
@@ -22463,7 +22762,7 @@ check ovn-nbctl --wait=hv sync
 # wait_conj_id_count COUNT ["ID COUNT [MATCH]"]...
 #
 # Waits until COUNT flows matching against conj_id appear in the
-# table 45 on hv1's br-int bridge.  Makes the flows available in
+# table 44 on hv1's br-int bridge.  Makes the flows available in
 # "hv1flows", which will be logged on error.
 #
 # In addition, for each quoted "ID COUNT" or "ID COUNT MATCH",
@@ -22480,7 +22779,7 @@ wait_conj_id_count() {
   echo "waiting for $1 conj_id flows..."
   OVS_WAIT_FOR_OUTPUT_UNQUOTED(
     [ovs-ofctl dump-flows br-int > hv1flows
-     grep table=45 hv1flows | grep -c conj_id],
+     grep table=44 hv1flows | grep -c conj_id],
     [$retval], [$1
 ])
 
@@ -22489,7 +22788,7 @@ wait_conj_id_count() {
     set -- $arg; id=$1 count=$2 match=$3
     echo "checking that there are $count ${match:+$match }flows with conj_id=$id..."
     AT_CHECK_UNQUOTED(
-      [grep table=45 hv1flows | grep "$match" | grep -c conj_id=$id],
+      [grep table=44 hv1flows | grep "$match" | grep -c conj_id=$id],
       [0], [$count
 ])
   done
@@ -22514,8 +22813,8 @@ wait_conj_id_count 1 "3 1 udp"
 AS_BOX([Add back the tcp ACL.])
 check ovn-nbctl --wait=hv acl-add pg0 to-lport 1002 "outport == @pg0 && ip4 && tcp.dst >= 80 && tcp.dst <= 82" allow
 wait_conj_id_count 2 "3 1 udp" "4 1 tcp"
-AT_CHECK([test 1 = $(as hv1 ovs-ofctl dump-flows br-int table=45 | grep udp | grep -c "conj_id=3")])
-AT_CHECK([test 1 = $(as hv1 ovs-ofctl dump-flows br-int table=45 | grep tcp | grep -c "conj_id=4")])
+AT_CHECK([test 1 = $(as hv1 ovs-ofctl dump-flows br-int table=44 | grep udp | grep -c "conj_id=3")])
+AT_CHECK([test 1 = $(as hv1 ovs-ofctl dump-flows br-int table=44 | grep tcp | grep -c "conj_id=4")])
 
 AS_BOX([Add another tcp ACL.])
 check ovn-nbctl --wait=hv acl-add pg0 to-lport 1002 "outport == @pg0 && inport == @pg0 && ip4 && tcp.dst >= 84 && tcp.dst <= 86" allow
@@ -24317,6 +24616,14 @@ as hv1 ovn-appctl -t ovn-controller debug/resume
 wait_column "true" Port_Binding up logical_port=lsp1
 wait_column "true" nb:Logical_Switch_Port up name=lsp1
 
+AS_BOX([ovn-controller should set Port_Binding.up - to false when OVS port is released])
+check ovs-vsctl remove Interface lsp1 external_ids iface-id
+check ovs-vsctl remove Interface lsp2 external_ids iface-id
+wait_column "false" Port_Binding up logical_port=lsp1
+wait_column "false" Port_Binding up logical_port=lsp2
+wait_column "false" Port_Binding up logical_port=lsp1
+wait_column "false" nb:Logical_Switch_Port up name=lsp1
+
 OVN_CLEANUP([hv1])
 AT_CLEANUP
 
@@ -24454,43 +24761,43 @@ AT_CHECK([kill -0 $(cat hv1/ovn-controller.pid)])
 check ovn-nbctl --wait=hv sync
 
 # Check OVS flows are installed properly.
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=45 | ofctl_strip_all | \
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=44 | ofctl_strip_all | \
     grep "priority=2002" | grep conjunction | \
     sed 's/conjunction([[^)]]*)/conjunction()/g' | sort], [0], [dnl
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x10/0xfff0 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x100/0xff00 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x1000/0xf000 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x2/0xfffe actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x20/0xffe0 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x200/0xfe00 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x2000/0xe000 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x4/0xfffc actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x40/0xffc0 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x400/0xfc00 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x4000/0xc000 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x8/0xfff8 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x80/0xff80 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x800/0xf800 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x8000/0x8000 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=1 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x100/0x100,reg15=0x3,metadata=0x1,nw_src=192.168.47.3 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x10/0xfff0 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x100/0xff00 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x1000/0xf000 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x2/0xfffe actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x20/0xffe0 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x200/0xfe00 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x2000/0xe000 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x4/0xfffc actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x40/0xffc0 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x400/0xfc00 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x4000/0xc000 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x8/0xfff8 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x80/0xff80 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x800/0xf800 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x8000/0x8000 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=1 actions=conjunction()
- table=45, priority=2002,udp,reg0=0x80/0x80,reg15=0x3,metadata=0x1,nw_src=192.168.47.3 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x10/0xfff0 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x100/0xff00 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x1000/0xf000 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x2/0xfffe actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x20/0xffe0 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x200/0xfe00 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x2000/0xe000 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x4/0xfffc actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x40/0xffc0 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x400/0xfc00 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x4000/0xc000 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x8/0xfff8 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x80/0xff80 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x800/0xf800 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x8000/0x8000 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,metadata=0x1,nw_src=192.168.47.3,tp_dst=1 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x100/0x100,reg15=0x3,metadata=0x1,nw_src=192.168.47.3 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x10/0xfff0 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x100/0xff00 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x1000/0xf000 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x2/0xfffe actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x20/0xffe0 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x200/0xfe00 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x2000/0xe000 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x4/0xfffc actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x40/0xffc0 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x400/0xfc00 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x4000/0xc000 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x8/0xfff8 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x80/0xff80 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x800/0xf800 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=0x8000/0x8000 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,metadata=0x1,nw_src=192.168.47.3,tp_dst=1 actions=conjunction()
+ table=44, priority=2002,udp,reg0=0x80/0x80,reg15=0x3,metadata=0x1,nw_src=192.168.47.3 actions=conjunction()
 ])
 
 OVN_CLEANUP([hv1])
@@ -24918,3 +25225,633 @@ AT_CHECK([cat hv2_offlows_table72.txt | grep -v NXST], [1], [dnl
 
 OVN_CLEANUP([hv1], [hv2])
 AT_CLEANUP
+
+AT_SETUP([ovn -- container port changed to normal port and then deleted])
+ovn_start
+
+net_add n1
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovs-vsctl -- add-port br-int vm1
+
+check ovn-nbctl ls-add ls
+check ovn-nbctl lsp-add ls vm1
+check ovn-nbctl lsp-add ls vm-cont vm1 1
+check as hv1 ovs-vsctl set Interface vm1 external_ids:iface-id=vm1
+
+wait_for_ports_up
+
+check as hv1 ovn-appctl -t ovn-controller debug/pause
+check ovn-nbctl clear logical_switch_port vm-cont parent_name
+check as hv1 ovs-vsctl set Interface vm1 external_ids:iface-id=foo
+check ovn-nbctl lsp-del vm-cont
+check as hv1 ovn-appctl -t ovn-controller debug/resume
+
+ovn-nbctl --wait=hv sync
+
+# Make sure that ovn-controller has not asserted.
+AT_CHECK([kill -0 $(cat hv1/ovn-controller.pid)])
+
+wait_column "false" nb:Logical_Switch_Port up name=vm1
+
+check ovn-nbctl lsp-add ls vm-cont1 vm1 1
+check ovn-nbctl lsp-add ls vm-cont2 vm1 2
+
+check ovn-nbctl --wait=sb lsp-del vm1
+
+check as hv1 ovn-appctl -t ovn-controller debug/pause
+check ovn-nbctl clear logical_switch_port vm-cont1 parent_name
+check ovn-nbctl clear logical_switch_port vm-cont2 parent_name
+
+check as hv1 ovn-appctl -t ovn-controller debug/resume
+
+check ovn-nbctl --wait=hv sync
+
+# Make sure that ovn-controller has not crashed.
+AT_CHECK([kill -0 $(cat hv1/ovn-controller.pid)])
+
+check ovn-nbctl lsp-add ls vm1
+check ovn-nbctl set logical_switch_port vm-cont1 parent_name=vm1
+check ovn-nbctl --wait=sb set logical_switch_port vm-cont2 parent_name=vm1
+check as hv1 ovs-vsctl set Interface vm1 external_ids:iface-id=vm1
+
+wait_for_ports_up
+
+check as hv1 ovn-appctl -t ovn-controller debug/pause
+check ovn-nbctl --wait=sb lsp-del vm1
+check ovn-nbctl clear logical_switch_port vm-cont1 parent_name
+check ovn-nbctl --wait=sb clear logical_switch_port vm-cont2 parent_name
+check ovn-nbctl lsp-del vm-cont1
+check ovn-nbctl --wait=sb lsp-del vm-cont2
+check as hv1 ovn-appctl -t ovn-controller debug/resume
+
+check ovn-nbctl --wait=hv sync
+
+# Make sure that ovn-controller has not crashed.
+AT_CHECK([kill -0 $(cat hv1/ovn-controller.pid)])
+
+check ovn-nbctl lsp-add ls vm1
+check ovn-nbctl lsp-add ls vm-cont1 vm1 1
+check ovn-nbctl lsp-add ls vm-cont2 vm1 2
+
+wait_for_ports_up
+
+check as hv1 ovn-appctl -t ovn-controller debug/pause
+check ovn-nbctl clear logical_switch_port vm-cont1 parent_name
+check ovn-nbctl --wait=sb clear logical_switch_port vm-cont2 parent_name
+check ovn-nbctl lsp-del vm-cont1
+check ovn-nbctl lsp-del vm-cont2
+check as hv1 ovn-appctl -t ovn-controller debug/resume
+
+check ovn-nbctl --wait=hv sync
+
+# Make sure that ovn-controller has not crashed.
+AT_CHECK([kill -0 $(cat hv1/ovn-controller.pid)])
+
+check ovn-nbctl lsp-add ls vm-cont1 vm1 1
+check ovn-nbctl lsp-add ls vm-cont2 vm1 2
+
+wait_for_ports_up
+
+check as hv1 ovn-appctl -t ovn-controller debug/pause
+check ovn-nbctl clear logical_switch_port vm-cont1 parent_name
+check ovn-nbctl --wait=sb clear logical_switch_port vm-cont2 parent_name
+
+check as hv1 ovs-vsctl set Interface vm1 external_ids:iface-id=foo
+
+check as hv1 ovn-appctl -t ovn-controller debug/resume
+
+wait_column "false" nb:Logical_Switch_Port up name=vm1
+wait_column "false" nb:Logical_Switch_Port up name=vm-cont1
+wait_column "false" nb:Logical_Switch_Port up name=vm-cont2
+
+check ovn-nbctl set logical_switch_port vm-cont1 parent_name=vm1
+check as hv1 ovs-vsctl set Interface vm1 external_ids:iface-id=vm1
+check ovn-nbctl --wait=sb set logical_switch_port vm-cont2 parent_name=vm1
+
+wait_for_ports_up
+
+check as hv1 ovn-appctl -t ovn-controller debug/pause
+check ovn-nbctl clear logical_switch_port vm-cont1 parent_name
+check as hv1 ovs-vsctl set Interface vm1 external_ids:iface-id=vm-cont1
+check as hv1 ovn-appctl -t ovn-controller debug/resume
+
+wait_column "false" nb:Logical_Switch_Port up name=vm1
+wait_column "true" nb:Logical_Switch_Port up name=vm-cont1
+wait_column "false" nb:Logical_Switch_Port up name=vm-cont2
+
+check ovn-nbctl --wait=sb set logical_switch_port vm-cont2 parent_name=vm-cont1
+check ovn-nbctl --wait=sb set logical_switch_port vm1 parent_name=vm-cont1
+
+wait_for_ports_up
+
+# Delete vm1, vm-cont1 and vm-cont2 and recreate again.
+check ovn-nbctl lsp-del vm1
+check ovn-nbctl lsp-del vm-cont1
+check ovn-nbctl --wait=hv lsp-del vm-cont2
+
+check as hv1 ovs-vsctl set Interface vm1 external_ids:iface-id=vm1
+check ovn-nbctl lsp-add ls vm1
+check ovn-nbctl lsp-add ls vm-cont1 vm1 1
+check ovn-nbctl lsp-add ls vm-cont2 vm1 2
+
+wait_for_ports_up
+
+# Make vm1 as a child port of some non existent lport - foo. vm1, vm1-cont1 and
+# vm1-cont2 should be released.
+check ovn-nbctl --wait=sb set logical_switch_port vm1 parent_name=bar
+wait_column "false" nb:Logical_Switch_Port up name=vm1
+wait_column "false" nb:Logical_Switch_Port up name=vm-cont1
+wait_column "false" nb:Logical_Switch_Port up name=vm-cont2
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+
+AT_SETUP([ovn -- container port changed from one parent to another])
+ovn_start
+
+net_add n1
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovs-vsctl -- add-port br-int vm1 -- set interface vm1 ofport-request=1
+ovs-vsctl -- add-port br-int vm2 -- set interface vm1 ofport-request=2
+
+check ovn-nbctl ls-add ls
+check ovn-nbctl lsp-add ls vm1
+check ovn-nbctl lsp-add ls vm1-cont vm1 1
+check ovn-nbctl lsp-add ls vm2
+check ovn-nbctl lsp-add ls vm2-cont vm2 2
+
+check as hv1 ovs-vsctl set Interface vm1 external_ids:iface-id=vm1
+check as hv1 ovs-vsctl set Interface vm2 external_ids:iface-id=vm2
+
+wait_for_ports_up
+
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=0 | grep -c dl_vlan=1], [0], [dnl
+1
+])
+
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=0 | grep -c dl_vlan=2], [0], [dnl
+1
+])
+
+# change the parent of vm1-cont to vm2.
+as hv1 ovn-appctl -t ovn-controller vlog/set dbg
+check ovn-nbctl --wait=sb set logical_switch_port vm1-cont parent_name=vm2 \
+-- set logical_switch_port vm1-cont tag_request=3
+
+wait_for_ports_up
+
+check ovn-nbctl --wait=hv sync
+
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=0 | grep -c dl_vlan=1], [1], [dnl
+0
+])
+
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=0 | grep -c dl_vlan=2], [0], [dnl
+1
+])
+
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int table=0 | grep -c dl_vlan=3], [0], [dnl
+1
+])
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+
+AT_SETUP([ovn -- container port use-after-free test])
+ovn_start
+
+net_add n1
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovs-vsctl -- add-port br-int vm1
+
+check ovn-nbctl ls-add ls
+check ovn-nbctl lsp-add ls vm1
+check ovn-nbctl lsp-add ls vm-cont vm1 1
+check ovs-vsctl set Interface vm1 external_ids:iface-id=vm1
+check ovn-nbctl clear logical_switch_port vm-cont parent_name
+check ovs-vsctl set Interface vm1 external_ids:iface-id=foo
+check ovn-nbctl lsp-del vm-cont
+check ovn-nbctl ls-del ls
+check ovn-nbctl ls-add ls
+check ovn-nbctl lsp-add ls vm1
+check ovn-nbctl lsp-add ls vm-cont vm1 1
+check ovs-vsctl set Interface vm1 external_ids:iface-id=vm1
+check as hv1 ovn-appctl -t ovn-controller debug/pause
+check ovn-nbctl clear logical_switch_port vm-cont parent_name
+check ovn-nbctl lsp-del vm-cont
+check as hv1 ovn-appctl -t ovn-controller debug/resume
+check as hv1 ovs-vsctl set Interface vm1 external_ids:iface-id=foo
+
+ovn-nbctl --wait=hv sync
+
+# Make sure that ovn-controller has not asserted.
+AT_CHECK([kill -0 $(cat hv1/ovn-controller.pid)])
+
+wait_column "false" nb:Logical_Switch_Port up name=vm1
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+
+# Test that OVS.external_ids:iface-id doesn't affect non-VIF port bindings.
+AT_SETUP([ovn -- Non-VIF ports incremental processing])
+ovn_start
+
+net_add n1
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.10
+
+check ovn-nbctl ls-add ls1 -- lsp-add ls1 lsp1
+
+as hv1
+check ovs-vsctl \
+    -- add-port br-int vif1 \
+    -- set Interface vif1 external_ids:iface-id=lsp1
+
+# ovn-controller should bind the interface.
+wait_for_ports_up
+hv_uuid=$(fetch_column Chassis _uuid name=hv1)
+check_column "$hv_uuid" Port_Binding chassis logical_port=lsp1
+
+# Change the port type to router, ovn-controller should release it.
+check ovn-nbctl --wait=hv lsp-set-type lsp1 router
+check_column "" Port_Binding chassis logical_port=lsp1
+
+# Clear port type, ovn-controller should rebind it.
+check ovn-nbctl --wait=hv lsp-set-type lsp1 ''
+check_column "$hv_uuid" Port_Binding chassis logical_port=lsp1
+
+# Change the port type to localnet, ovn-controller should release it.
+check ovn-nbctl --wait=hv lsp-set-type lsp1 localnet
+check_column "" Port_Binding chassis logical_port=lsp1
+
+# Clear port type, ovn-controller should rebind it.
+check ovn-nbctl --wait=hv lsp-set-type lsp1 ''
+check_column "$hv_uuid" Port_Binding chassis logical_port=lsp1
+
+# Change the port type to localport, ovn-controller should release it.
+check ovn-nbctl --wait=hv lsp-set-type lsp1 localport
+check_column "" Port_Binding chassis logical_port=lsp1
+
+# Clear port type, ovn-controller should rebind it.
+check ovn-nbctl --wait=hv lsp-set-type lsp1 ''
+check_column "$hv_uuid" Port_Binding chassis logical_port=lsp1
+
+# Change the port type to localnet and then delete it.
+# ovn-controller should handle this properly.
+check as hv1 ovn-appctl -t ovn-controller debug/pause
+check ovn-nbctl --wait=sb lsp-set-type lsp1 localport
+check ovn-nbctl --wait=sb lsp-del lsp1
+check as hv1 ovn-appctl -t ovn-controller debug/resume
+
+check ovn-nbctl --wait=hv sync
+
+# Make sure that ovn-controller has not asserted.
+AT_CHECK([kill -0 $(cat hv1/ovn-controller.pid)])
+
+check ovn-nbctl lsp-add ls1 lsp1
+wait_for_ports_up
+
+# Change the port type to virtual and then delete it.
+# ovn-controller should handle this properly.
+check as hv1 ovn-appctl -t ovn-controller debug/pause
+check ovn-nbctl --wait=sb lsp-set-type lsp1 virtual
+check ovn-nbctl --wait=sb lsp-del lsp1
+check as hv1 ovn-appctl -t ovn-controller debug/resume
+
+check ovn-nbctl --wait=hv sync
+
+# Make sure that ovn-controller has not asserted.
+AT_CHECK([kill -0 $(cat hv1/ovn-controller.pid)])
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+
+# Tests that ovn-controller creates local bindings correctly by running
+# ovn-appctl -t ovn-controller debug/dump-local-bindings.
+# Ideally this test case should have been a unit test case.
+AT_SETUP([ovn -- ovn-controller local bindings])
+ovn_start
+
+net_add n1
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovs-vsctl -- add-port br-int hv1-vm1
+
+sim_add hv2
+as hv2
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+ovs-vsctl -- add-port br-int hv2-vm1
+
+check ovn-nbctl ls-add sw0
+check ovn-nbctl lsp-add sw0 sw0p1
+check ovn-nbctl lsp-add sw0 sw0p2
+
+check as hv1 ovs-vsctl set interface hv1-vm1 external_ids:iface-id=sw0p1
+check as hv2 ovs-vsctl set interface hv2-vm1 external_ids:iface-id=sw0p2
+
+wait_for_ports_up
+
+AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[hv1-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p1]]
+----------------------------------------
+])
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p2]], OVS interface name : [[hv2-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p2]]
+----------------------------------------
+])
+
+# Create an ovs interface in hv1
+check as hv1 ovs-vsctl add-port br-int hv1-vm2 -- set interface hv1-vm2 external_ids:iface-id=sw1p1
+check ovn-nbctl --wait=hv sync
+AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[hv1-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p1]]
+----------------------------------------
+name: [[sw1p1]], OVS interface name : [[hv1-vm2]], num binding lports : [[0]]
+----------------------------------------
+])
+
+# Create lport sw1p1
+check ovn-nbctl ls-add sw1 -- lsp-add sw1 sw1p1
+
+wait_for_ports_up
+
+AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[hv1-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p1]]
+----------------------------------------
+name: [[sw1p1]], OVS interface name : [[hv1-vm2]], num binding lports : [[1]]
+primary lport : [[sw1p1]]
+----------------------------------------
+])
+
+# Swap sw0p1 and sw0p2.
+check as hv1 ovs-vsctl set interface hv1-vm1 external_ids:iface-id=sw0p2
+check as hv2 ovs-vsctl set interface hv2-vm1 external_ids:iface-id=sw0p1
+
+check ovn-nbctl --wait=hv sync
+
+AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p2]], OVS interface name : [[hv1-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p2]]
+----------------------------------------
+name: [[sw1p1]], OVS interface name : [[hv1-vm2]], num binding lports : [[1]]
+primary lport : [[sw1p1]]
+----------------------------------------
+])
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[hv2-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p1]]
+----------------------------------------
+])
+
+# Create child port for sw0p1
+check ovn-nbctl --wait=hv lsp-add sw0 sw0p1-c1 sw0p1 1
+AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[NULL]], num binding lports : [[2]]
+primary lport : [[sw0p1]]
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p2]], OVS interface name : [[hv1-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p2]]
+----------------------------------------
+name: [[sw1p1]], OVS interface name : [[hv1-vm2]], num binding lports : [[1]]
+primary lport : [[sw1p1]]
+----------------------------------------
+])
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[hv2-vm1]], num binding lports : [[2]]
+primary lport : [[sw0p1]]
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+----------------------------------------
+])
+
+# Create another child port for sw0p1
+check ovn-nbctl --wait=hv lsp-add sw0 sw0p1-c2 sw0p1 2
+
+wait_for_ports_up
+
+AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[NULL]], num binding lports : [[3]]
+primary lport : [[sw0p1]]
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p2]], OVS interface name : [[hv1-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p2]]
+----------------------------------------
+name: [[sw1p1]], OVS interface name : [[hv1-vm2]], num binding lports : [[1]]
+primary lport : [[sw1p1]]
+----------------------------------------
+])
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[hv2-vm1]], num binding lports : [[3]]
+primary lport : [[sw0p1]]
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+])
+
+# Swap sw0p1 and sw0p2 again.
+check as hv1 ovs-vsctl set interface hv1-vm1 external_ids:iface-id=sw0p1
+check as hv2 ovs-vsctl set interface hv2-vm1 external_ids:iface-id=sw0p2
+
+check ovn-nbctl --wait=hv sync
+
+AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[hv1-vm1]], num binding lports : [[3]]
+primary lport : [[sw0p1]]
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw1p1]], OVS interface name : [[hv1-vm2]], num binding lports : [[1]]
+primary lport : [[sw1p1]]
+----------------------------------------
+])
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[sw0p1]], OVS interface name : [[NULL]], num binding lports : [[3]]
+primary lport : [[sw0p1]]
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p2]], OVS interface name : [[hv2-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p2]]
+----------------------------------------
+])
+
+# Make sw0p1 as child port of non existent lport - foo
+check ovn-nbctl --wait=hv set logical_switch_port sw0p1 parent_name=foo
+
+AT_CHECK([as hv1 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[foo]], OVS interface name : [[NULL]], num binding lports : [[1]]
+no primary lport
+child lport[[1]] : [[sw0p1]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p1]], OVS interface name : [[hv1-vm1]], num binding lports : [[2]]
+no primary lport
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw1p1]], OVS interface name : [[hv1-vm2]], num binding lports : [[1]]
+primary lport : [[sw1p1]]
+----------------------------------------
+])
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[foo]], OVS interface name : [[NULL]], num binding lports : [[1]]
+no primary lport
+child lport[[1]] : [[sw0p1]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p1]], OVS interface name : [[NULL]], num binding lports : [[2]]
+no primary lport
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p2]], OVS interface name : [[hv2-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p2]]
+----------------------------------------
+])
+
+# Change the lport type of sw0p2 to different types and make sure that
+# local bindings are correct.
+
+hv2_uuid=$(fetch_column Chassis _uuid name=hv2)
+check_column "$hv2_uuid" Port_Binding chassis logical_port=sw0p2
+
+# Change the port type to router, ovn-controller should release it.
+check ovn-nbctl --wait=hv lsp-set-type sw0p2 router
+check_column "" Port_Binding chassis logical_port=sw0p2
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[foo]], OVS interface name : [[NULL]], num binding lports : [[1]]
+no primary lport
+child lport[[1]] : [[sw0p1]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p1]], OVS interface name : [[NULL]], num binding lports : [[2]]
+no primary lport
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p2]], OVS interface name : [[hv2-vm1]], num binding lports : [[0]]
+----------------------------------------
+])
+
+# change the port type to external from router.
+check ovn-nbctl --wait=hv lsp-set-type sw0p2 external
+check_column "" Port_Binding chassis logical_port=sw0p2
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[foo]], OVS interface name : [[NULL]], num binding lports : [[1]]
+no primary lport
+child lport[[1]] : [[sw0p1]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p1]], OVS interface name : [[NULL]], num binding lports : [[2]]
+no primary lport
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p2]], OVS interface name : [[hv2-vm1]], num binding lports : [[0]]
+----------------------------------------
+])
+
+# change the port type to localnet from external.
+check ovn-nbctl --wait=hv lsp-set-type sw0p2 localnet
+check_column "" Port_Binding chassis logical_port=sw0p2
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[foo]], OVS interface name : [[NULL]], num binding lports : [[1]]
+no primary lport
+child lport[[1]] : [[sw0p1]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p1]], OVS interface name : [[NULL]], num binding lports : [[2]]
+no primary lport
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p2]], OVS interface name : [[hv2-vm1]], num binding lports : [[0]]
+----------------------------------------
+])
+
+# change the port type to localport from localnet.
+check ovn-nbctl --wait=hv lsp-set-type sw0p2 localnet
+check_column "" Port_Binding chassis logical_port=sw0p2
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[foo]], OVS interface name : [[NULL]], num binding lports : [[1]]
+no primary lport
+child lport[[1]] : [[sw0p1]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p1]], OVS interface name : [[NULL]], num binding lports : [[2]]
+no primary lport
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p2]], OVS interface name : [[hv2-vm1]], num binding lports : [[0]]
+----------------------------------------
+])
+
+# change the port type back to vif.
+check ovn-nbctl --wait=hv lsp-set-type sw0p2 ""
+wait_column "$hv2_uuid" Port_Binding chassis logical_port=sw0p2
+
+AT_CHECK([as hv2 ovn-appctl -t ovn-controller debug/dump-local-bindings], [0], [dnl
+Local bindings:
+name: [[foo]], OVS interface name : [[NULL]], num binding lports : [[1]]
+no primary lport
+child lport[[1]] : [[sw0p1]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p1]], OVS interface name : [[NULL]], num binding lports : [[2]]
+no primary lport
+child lport[[1]] : [[sw0p1-c1]], type : [[CONTAINER]]
+child lport[[2]] : [[sw0p1-c2]], type : [[CONTAINER]]
+----------------------------------------
+name: [[sw0p2]], OVS interface name : [[hv2-vm1]], num binding lports : [[1]]
+primary lport : [[sw0p2]]
+----------------------------------------
+])
+
+OVN_CLEANUP([hv1], [hv2])
+AT_CLEANUP
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 9819573bb..bd27b01a0 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -4722,7 +4722,7 @@ OVS_WAIT_UNTIL([
 ])
 
 OVS_WAIT_UNTIL([
-    n_pkt=$(ovs-ofctl dump-flows br-int table=45 | grep -v n_packets=0 | \
+    n_pkt=$(ovs-ofctl dump-flows br-int table=44 | grep -v n_packets=0 | \
 grep controller | grep tp_dst=84 -c)
     test $n_pkt -eq 1
 ])
@@ -5831,3 +5831,131 @@ as
 OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
 /.*terminating with signal 15.*/d"])
 AT_CLEANUP
+
+AT_SETUP([ovn -- No ct_state matches in dp flows when no ACLs in an LS])
+AT_KEYWORDS([no ct_state match])
+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
+
+check ovn-nbctl ls-add sw0
+
+check ovn-nbctl lsp-add sw0 sw0-p1
+check ovn-nbctl lsp-set-addresses sw0-p1 "50:54:00:00:00:03"
+check ovn-nbctl lsp-set-port-security sw0-p1 "50:54:00:00:00:03"
+
+check ovn-nbctl lsp-add sw0 sw0-p2
+check ovn-nbctl lsp-set-addresses sw0-p2 "50:54:00:00:00:04 10.0.0.4"
+check ovn-nbctl lsp-set-port-security sw0-p2 "50:54:00:00:00:04 10.0.0.4"
+
+
+# Create the second logical switch with one port and configure some ACLs.
+check ovn-nbctl ls-add sw1
+check ovn-nbctl lsp-add sw1 sw1-p1
+
+# Create port group and ACLs for sw1 ports.
+check ovn-nbctl pg-add pg1 sw1-p1
+check ovn-nbctl acl-add pg1 from-lport 1002 "ip" allow-related
+check ovn-nbctl acl-add pg1 to-lport 1002 "ip" allow-related
+
+
+OVN_POPULATE_ARP
+ovn-nbctl --wait=hv sync
+
+ADD_NAMESPACES(sw0-p1)
+ADD_VETH(sw0-p1, sw0-p1, br-int, "10.0.0.3/24", "50:54:00:00:00:03", \
+         "10.0.0.1")
+
+
+ADD_NAMESPACES(sw0-p2)
+ADD_VETH(sw0-p2, sw0-p2, br-int, "10.0.0.4/24", "50:54:00:00:00:04", \
+         "10.0.0.1")
+
+ADD_NAMESPACES(sw1-p1)
+ADD_VETH(sw1-p1, sw1-p1, br-int, "20.0.0.4/24", "30:54:00:00:00:04", \
+         "20.0.0.1")
+
+wait_for_ports_up
+
+NS_CHECK_EXEC([sw0-p1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.4 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+ovs-appctl dpctl/dump-flows
+
+# sw1-p1 may send IPv6 traffic.  So filter this out.  Since sw1-p1 has
+# ACLs configured, the datapath flows for the packets from sw1-p1 will have
+# matches on ct_state and ct_label fields.
+# Since sw0 doesn't have any ACLs, there should be no match on ct fields.
+AT_CHECK([ovs-appctl dpctl/dump-flows | grep ct_state | grep -v ipv6 -c], [1], [dnl
+0
+])
+
+AT_CHECK([ovs-appctl dpctl/dump-flows | grep ct_label | grep -v ipv6 -c], [1], [dnl
+0
+])
+
+# Add an ACL to sw0.
+check ovn-nbctl --wait=hv acl-add sw0 to-lport 1002 ip allow-related
+
+NS_CHECK_EXEC([sw0-p1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.4 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+ovs-appctl dpctl/dump-flows
+
+AT_CHECK([ovs-appctl dpctl/dump-flows | grep ct_state | grep -v ipv6 -c], [0], [ignore])
+
+AT_CHECK([ovs-appctl dpctl/dump-flows | grep ct_label | grep -v ipv6 -c], [0], [ignore])
+
+# Clear ACL for sw0
+check ovn-nbctl --wait=hv clear logical_switch sw0 acls
+
+check ovs-appctl dpctl/del-flows
+
+check ovn-nbctl --wait=hv sync
+
+NS_CHECK_EXEC([sw0-p1], [ping -q -c 3 -i 0.3 -w 2 10.0.0.4 | FORMAT_PING], \
+[0], [dnl
+3 packets transmitted, 3 received, 0% packet loss, time 0ms
+])
+
+ovs-appctl dpctl/dump-flows
+
+AT_CHECK([ovs-appctl dpctl/dump-flows | grep ct_state | grep -v ipv6 -c], [1], [dnl
+0
+])
+
+AT_CHECK([ovs-appctl dpctl/dump-flows | grep ct_label | grep -v ipv6 -c], [1], [dnl
+0
+])
+
+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-ctl b/utilities/ovn-ctl
index 967db6d6c..c52c17ee0 100755
--- a/utilities/ovn-ctl
+++ b/utilities/ovn-ctl
@@ -45,18 +45,12 @@ pidfile_is_running () {
     test -e "$pidfile" && [ -s "$pidfile" ] && pid=`cat "$pidfile"` && pid_exists "$pid"
 } >/dev/null 2>&1
 
-stop_xx_ovsdb() {
-    if pidfile_is_running $1; then
-        ovn-appctl -t $OVN_RUNDIR/$2 exit
-    fi
-}
-
 stop_nb_ovsdb() {
-    stop_xx_ovsdb $DB_NB_PID ovnnb_db.ctl
+    OVS_RUNDIR=${OVS_RUNDIR} stop_ovn_daemon ovnnb_db $DB_NB_PID $OVN_RUNDIR/ovnnb_db.ctl
 }
 
 stop_sb_ovsdb() {
-    stop_xx_ovsdb $DB_SB_PID ovnsb_db.ctl
+    OVS_RUNDIR=${OVS_RUNDIR} stop_ovn_daemon ovnsb_db $DB_SB_PID $OVN_RUNDIR/ovnsb_db.ctl
 }
 
 stop_ovsdb () {
@@ -65,11 +59,11 @@ stop_ovsdb () {
 }
 
 stop_ic_nb_ovsdb() {
-    stop_xx_ovsdb $DB_IC_NB_PID ovn_ic_nb_db.ctl
+    OVS_RUNDIR=${OVS_RUNDIR} stop_ovn_daemon ovn_ic_nb_db $DB_IC_NB_PID $OVN_RUNDIR/ovn_ic_nb_db.ctl
 }
 
 stop_ic_sb_ovsdb() {
-    stop_xx_ovsdb $DB_IC_SB_PID ovn_ic_sb_db.ctl
+    OVS_RUNDIR=${OVS_RUNDIR} stop_ovn_daemon ovn_ic_sb_db $DB_IC_SB_PID $OVN_RUNDIR/ovn_ic_sb_db.ctl
 }
 
 stop_ic_ovsdb () {
@@ -590,7 +584,7 @@ stop_ic () {
 }
 
 stop_controller () {
-    OVS_RUNDIR=${OVS_RUNDIR} stop_ovn_daemon ovn-controller "$@"
+    OVS_RUNDIR=${OVS_RUNDIR} stop_ovn_daemon ovn-controller "" "" "$@"
 }
 
 stop_controller_vtep () {
diff --git a/utilities/ovn-lib.in b/utilities/ovn-lib.in
index 016815626..301cc5712 100644
--- a/utilities/ovn-lib.in
+++ b/utilities/ovn-lib.in
@@ -137,10 +137,22 @@ start_ovn_daemon () {
 }
 
 stop_ovn_daemon () {
-    if test -e "$ovn_rundir/$1.pid"; then
-        if pid=`cat "$ovn_rundir/$1.pid"`; then
+    local pid_file=$2
+    local ctl_file=$3
+    local other_args=$4
+
+    if [ -z "$pid_file" ]; then
+        pid_file="$ovn_rundir/$1.pid"
+    fi
+
+    if test -e "$pid_file"; then
+        if pid=`cat "$pid_file"`; then
+            if [ -z "$ctl_file" ]; then
+                ctl_file="$ovn_rundir/$1.$pid.ctl"
+            fi
+
             if pid_exists "$pid" >/dev/null 2>&1; then :; else
-                rm -f $ovn_rundir/$1.$pid.ctl $ovn_rundir/$1.$pid
+                rm -f $ctl_file $pid_file
                 return 0
             fi
 
@@ -148,7 +160,7 @@ stop_ovn_daemon () {
             actions="TERM .1 .25 .65 1 1 1 1 \
                      KILL 1 1 1 2 10 15 30 \
                      FAIL"
-            version=`ovs-appctl -T 1 -t $ovn_rundir/$1.$pid.ctl version \
+            version=`ovs-appctl -T 1 -t $ctl_file version \
                      | awk 'NR==1{print $NF}'`
 
             # Use `ovs-appctl exit` only if the running daemon version
@@ -159,20 +171,36 @@ stop_ovn_daemon () {
             if version_geq "$version" "2.5.90"; then
                 actions="$graceful $actions"
             fi
+            actiontype=""
             for action in $actions; do
                 if pid_exists "$pid" >/dev/null 2>&1; then :; else
-                    return 0
+                    # pid does not exist.
+                    if [ -n "$actiontype" ]; then
+                        return 0
+                    fi
+                    # But, does the file exist? We may have had a daemon
+                    # segfault with `ovs-appctl exit`. Check one more time
+                    # before deciding that the daemon is dead.
+                    [ -e "$pid_file" ] && sleep 2 && pid=`cat "$pid_file"` 2>/dev/null
+                    if pid_exists "$pid" >/dev/null 2>&1; then :; else
+                        return 0
+                    fi
                 fi
                 case $action in
                     EXIT)
                         action "Exiting $1 ($pid)" \
-                            ${bindir}/ovs-appctl -T 1 -t $ovn_rundir/$1.$pid.ctl exit $2
+                            ${bindir}/ovs-appctl -T 1 -t $ctl_file exit $other_args
+                        # The above command could have resulted in delayed
+                        # daemon segfault. And if a monitor is running, it
+                        # would restart the daemon giving it a new pid.
                         ;;
                     TERM)
                         action "Killing $1 ($pid)" kill $pid
+                        actiontype="force"
                         ;;
                     KILL)
                         action "Killing $1 ($pid) with SIGKILL" kill -9 $pid
+                        actiontype="force"
                         ;;
                     FAIL)
                         log_failure_msg "Killing $1 ($pid) failed"
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index 2c77f4ba7..51af138c6 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -3866,11 +3866,15 @@ static void
 print_routing_policy(const struct nbrec_logical_router_policy *policy,
                      struct ds *s)
 {
-    if (policy->nexthop != NULL) {
-        char *next_hop = normalize_prefix_str(policy->nexthop);
-        ds_put_format(s, "%10"PRId64" %50s %15s %25s", policy->priority,
-                      policy->match, policy->action, next_hop);
-        free(next_hop);
+    if (policy->n_nexthops) {
+        ds_put_format(s, "%10"PRId64" %50s %15s", policy->priority,
+                      policy->match, policy->action);
+        for (int i = 0; i < policy->n_nexthops; i++) {
+            char *next_hop = normalize_prefix_str(policy->nexthops[i]);
+            char *fmt = i ? ", %s" : " %25s";
+            ds_put_format(s, fmt, next_hop);
+            free(next_hop);
+        }
     } else {
         ds_put_format(s, "%10"PRId64" %50s %15s", policy->priority,
                       policy->match, policy->action);
@@ -4068,7 +4072,9 @@ nbctl_lr_route_add(struct ctl_context *ctx)
             goto cleanup;
         }
     } else if (route) {
-        ctl_error(ctx, "duplicate nexthop for the same ECMP route");
+        if (!may_exist) {
+            ctl_error(ctx, "duplicate nexthop for the same ECMP route");
+        }
         goto cleanup;
     }
 
diff --git a/utilities/ovndb-servers.ocf b/utilities/ovndb-servers.ocf
index 7351c7d64..eba9c97a1 100755
--- a/utilities/ovndb-servers.ocf
+++ b/utilities/ovndb-servers.ocf
@@ -259,6 +259,9 @@ ovsdb_server_notify() {
             ovn-nbctl -- --id=@conn_uuid create Connection \
 target="p${NB_MASTER_PROTO}\:${NB_MASTER_PORT}\:${LISTEN_ON_IP}" \
 inactivity_probe=$INACTIVE_PROBE -- set NB_Global . connections=@conn_uuid
+        else
+            CONN_UID=$(sed -e 's/^\[//' -e 's/\]$//' <<< ${conn})
+            ovn-nbctl set connection "${CONN_UID}" target="p${NB_MASTER_PROTO}\:${NB_MASTER_PORT}\:${LISTEN_ON_IP}"
         fi
 
         conn=`ovn-sbctl get SB_global . connections`
@@ -267,6 +270,9 @@ inactivity_probe=$INACTIVE_PROBE -- set NB_Global . connections=@conn_uuid
             ovn-sbctl -- --id=@conn_uuid create Connection \
 target="p${SB_MASTER_PROTO}\:${SB_MASTER_PORT}\:${LISTEN_ON_IP}" \
 inactivity_probe=$INACTIVE_PROBE -- set SB_Global . connections=@conn_uuid
+        else
+            CONN_UID=$(sed -e 's/^\[//' -e 's/\]$//' <<< ${conn})
+            ovn-sbctl set connection "${CONN_UID}" target="p${SB_MASTER_PROTO}\:${SB_MASTER_PORT}\:${LISTEN_ON_IP}"
         fi
 
     else