diff --git a/controller/automake.mk b/controller/automake.mk
index 2f6c508907..7b410c93ff 100644
--- a/controller/automake.mk
+++ b/controller/automake.mk
@@ -33,7 +33,9 @@ controller_ovn_controller_SOURCES = \
 	controller/physical.c \
 	controller/physical.h \
 	controller/mac-learn.c \
-	controller/mac-learn.h
+	controller/mac-learn.h \
+	controller/lflow-generate.c \
+	controller/lflow-generate.h
 
 controller_ovn_controller_LDADD = lib/libovn.la $(OVS_LIBDIR)/libopenvswitch.la
 man_MANS += controller/ovn-controller.8
diff --git a/controller/binding.c b/controller/binding.c
index 7fde0fdbb9..0fd1cc4a47 100644
--- a/controller/binding.c
+++ b/controller/binding.c
@@ -14,13 +14,8 @@
  */
 
 #include <config.h>
-#include "binding.h"
-#include "ha-chassis.h"
-#include "if-status.h"
-#include "lflow.h"
-#include "lport.h"
-#include "patch.h"
 
+/* OVS includes. */
 #include "lib/bitmap.h"
 #include "openvswitch/poll-loop.h"
 #include "lib/sset.h"
@@ -29,9 +24,20 @@
 #include "lib/vswitch-idl.h"
 #include "openvswitch/hmap.h"
 #include "openvswitch/vlog.h"
-#include "lib/chassis-index.h"
 #include "lib/ovn-sb-idl.h"
+
+/* OVN includes. */
+#include "binding.h"
+#include "ha-chassis.h"
+#include "if-status.h"
+#include "lflow.h"
+#include "lib/chassis-index.h"
+#include "lib/ldata.h"
+#include "lib/ovn-util.h"
+#include "lport.h"
 #include "ovn-controller.h"
+#include "patch.h"
+
 
 VLOG_DEFINE_THIS_MODULE(binding);
 
@@ -76,105 +82,26 @@ binding_register_ovs_idl(struct ovsdb_idl *ovs_idl)
     ovsdb_idl_add_column(ovs_idl, &ovsrec_qos_col_type);
 }
 
-static struct tracked_binding_datapath *tracked_binding_datapath_create(
-    const struct sbrec_datapath_binding *,
-    bool is_new, struct hmap *tracked_dps);
-static struct tracked_binding_datapath *tracked_binding_datapath_find(
-    struct hmap *, const struct sbrec_datapath_binding *);
-static void tracked_binding_datapath_lport_add(
-    const struct sbrec_port_binding *, struct hmap *tracked_datapaths);
 static void update_lport_tracking(const struct sbrec_port_binding *pb,
                                   struct hmap *tracked_dp_bindings);
 
-static void
-add_local_datapath__(struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
-                     struct ovsdb_idl_index *sbrec_port_binding_by_datapath,
-                     struct ovsdb_idl_index *sbrec_port_binding_by_name,
-                     const struct sbrec_datapath_binding *datapath,
-                     bool has_local_l3gateway, int depth,
-                     struct hmap *local_datapaths,
-                     struct hmap *tracked_datapaths)
-{
-    uint32_t dp_key = datapath->tunnel_key;
-    struct local_datapath *ld = get_local_datapath(local_datapaths, dp_key);
-    if (ld) {
-        if (has_local_l3gateway) {
-            ld->has_local_l3gateway = true;
-        }
-        return;
-    }
-
-    ld = xzalloc(sizeof *ld);
-    hmap_insert(local_datapaths, &ld->hmap_node, dp_key);
-    ld->datapath = datapath;
-    ld->localnet_port = NULL;
-    ld->has_local_l3gateway = has_local_l3gateway;
-
-    if (tracked_datapaths) {
-        struct tracked_binding_datapath *tdp =
-            tracked_binding_datapath_find(tracked_datapaths, datapath);
-        if (!tdp) {
-            tracked_binding_datapath_create(datapath, true, tracked_datapaths);
-        } else {
-            /* Its possible that there is already an entry in tracked datapaths
-             * for this 'datapath'. tracked_binding_datapath_lport_add() may
-             * have created it. Since the 'datapath' is added to the
-             * local datapaths, set 'tdp->is_new' to true so that the flows
-             * for this datapath are programmed properly.
-             * */
-            tdp->is_new = true;
-        }
-    }
+struct local_datpath_added_aux {
+    bool has_local_l3gateway;
+    struct hmap *tracked_datapaths;
+};
 
-    if (depth >= 100) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-        VLOG_WARN_RL(&rl, "datapaths nested too deep");
-        return;
+/* This function is called by local_datapath_add() if a new local_datapath
+ * is created. */
+static void
+local_datapath_added(struct local_datapath *ld, void *aux)
+{
+    struct local_datpath_added_aux *aux_ = aux;
+    if (aux_->tracked_datapaths) {
+        tracked_datapath_add(ld->datapath, TRACKED_RESOURCE_NEW,
+                             aux_->tracked_datapaths);
     }
 
-    struct sbrec_port_binding *target =
-        sbrec_port_binding_index_init_row(sbrec_port_binding_by_datapath);
-    sbrec_port_binding_index_set_datapath(target, datapath);
-
-    const struct sbrec_port_binding *pb;
-    SBREC_PORT_BINDING_FOR_EACH_EQUAL (pb, target,
-                                       sbrec_port_binding_by_datapath) {
-        if (!strcmp(pb->type, "patch") || !strcmp(pb->type, "l3gateway")) {
-            const char *peer_name = smap_get(&pb->options, "peer");
-            if (peer_name) {
-                const struct sbrec_port_binding *peer;
-
-                peer = lport_lookup_by_name(sbrec_port_binding_by_name,
-                                            peer_name);
-
-                if (peer && peer->datapath) {
-                    if (!strcmp(pb->type, "patch")) {
-                        /* Add the datapath to local datapath only for patch
-                         * ports. For l3gateway ports, since gateway router
-                         * resides on one chassis, we don't need to add.
-                         * Otherwise, all other chassis might create patch
-                         * ports between br-int and the provider bridge. */
-                        add_local_datapath__(sbrec_datapath_binding_by_key,
-                                             sbrec_port_binding_by_datapath,
-                                             sbrec_port_binding_by_name,
-                                             peer->datapath, false,
-                                             depth + 1, local_datapaths,
-                                             tracked_datapaths);
-                    }
-                    ld->n_peer_ports++;
-                    if (ld->n_peer_ports > ld->n_allocated_peer_ports) {
-                        ld->peer_ports =
-                            x2nrealloc(ld->peer_ports,
-                                       &ld->n_allocated_peer_ports,
-                                       sizeof *ld->peer_ports);
-                    }
-                    ld->peer_ports[ld->n_peer_ports - 1].local = pb;
-                    ld->peer_ports[ld->n_peer_ports - 1].remote = peer;
-                }
-            }
-        }
-    }
-    sbrec_port_binding_index_destroy_row(target);
+    ld->has_local_l3gateway = aux_->has_local_l3gateway;
 }
 
 static void
@@ -185,11 +112,17 @@ add_local_datapath(struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
                    bool has_local_l3gateway, struct hmap *local_datapaths,
                    struct hmap *tracked_datapaths)
 {
-    add_local_datapath__(sbrec_datapath_binding_by_key,
-                         sbrec_port_binding_by_datapath,
-                         sbrec_port_binding_by_name,
-                         datapath, has_local_l3gateway, 0, local_datapaths,
-                         tracked_datapaths);
+    struct local_datpath_added_aux aux = {
+        .has_local_l3gateway = has_local_l3gateway,
+        .tracked_datapaths = tracked_datapaths,
+    };
+
+    local_datapath_add(local_datapaths, datapath,
+                       sbrec_datapath_binding_by_key,
+                       sbrec_port_binding_by_datapath,
+                       sbrec_port_binding_by_name,
+                       local_datapath_added,
+                       &aux);
 }
 
 static void
@@ -546,7 +479,8 @@ update_local_lport_ids(const struct sbrec_port_binding *pb,
 
         if (b_ctx->tracked_dp_bindings) {
             /* Add the 'pb' to the tracked_datapaths. */
-            tracked_binding_datapath_lport_add(pb, b_ctx->tracked_dp_bindings);
+            tracked_datapath_lport_add(pb, TRACKED_RESOURCE_NEW,
+                                       b_ctx->tracked_dp_bindings);
         }
     }
 }
@@ -566,27 +500,11 @@ remove_local_lport_ids(const struct sbrec_port_binding *pb,
 
         if (b_ctx->tracked_dp_bindings) {
             /* Add the 'pb' to the tracked_datapaths. */
-            tracked_binding_datapath_lport_add(pb, b_ctx->tracked_dp_bindings);
-        }
-    }
-}
-
-/* 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
-};
+            tracked_datapath_lport_add(pb, TRACKED_RESOURCE_REMOVED,
+                                       b_ctx->tracked_dp_bindings);
+        }
+    }
+}
 
 /* Local bindings. binding.c module binds the logical port (represented by
  * Port_Binding rows) and sets the 'chassis' column when it sees the
@@ -865,113 +783,6 @@ binding_dump_local_bindings(struct local_binding_data *lbinding_data,
     free(nodes);
 }
 
-static bool
-is_lport_vif(const struct sbrec_port_binding *pb)
-{
-    return !pb->type[0];
-}
-
-static struct tracked_binding_datapath *
-tracked_binding_datapath_create(const struct sbrec_datapath_binding *dp,
-                                bool is_new,
-                                struct hmap *tracked_datapaths)
-{
-    struct tracked_binding_datapath *t_dp = xzalloc(sizeof *t_dp);
-    t_dp->dp = dp;
-    t_dp->is_new = is_new;
-    shash_init(&t_dp->lports);
-    hmap_insert(tracked_datapaths, &t_dp->node, uuid_hash(&dp->header_.uuid));
-    return t_dp;
-}
-
-static struct tracked_binding_datapath *
-tracked_binding_datapath_find(struct hmap *tracked_datapaths,
-                              const struct sbrec_datapath_binding *dp)
-{
-    struct tracked_binding_datapath *t_dp;
-    size_t hash = uuid_hash(&dp->header_.uuid);
-    HMAP_FOR_EACH_WITH_HASH (t_dp, node, hash, tracked_datapaths) {
-        if (uuid_equals(&t_dp->dp->header_.uuid, &dp->header_.uuid)) {
-            return t_dp;
-        }
-    }
-
-    return NULL;
-}
-
-static void
-tracked_binding_datapath_lport_add(const struct sbrec_port_binding *pb,
-                                   struct hmap *tracked_datapaths)
-{
-    if (!tracked_datapaths) {
-        return;
-    }
-
-    struct tracked_binding_datapath *tracked_dp =
-        tracked_binding_datapath_find(tracked_datapaths, pb->datapath);
-    if (!tracked_dp) {
-        tracked_dp = tracked_binding_datapath_create(pb->datapath, false,
-                                                     tracked_datapaths);
-    }
-
-    /* Check if the lport is already present or not.
-     * If it is already present, then just update the 'pb' field. */
-    struct tracked_binding_lport *lport =
-        shash_find_data(&tracked_dp->lports, pb->logical_port);
-
-    if (!lport) {
-        lport = xmalloc(sizeof *lport);
-        shash_add(&tracked_dp->lports, pb->logical_port, lport);
-    }
-
-    lport->pb = pb;
-}
-
-void
-binding_tracked_dp_destroy(struct hmap *tracked_datapaths)
-{
-    struct tracked_binding_datapath *t_dp;
-    HMAP_FOR_EACH_POP (t_dp, node, tracked_datapaths) {
-        shash_destroy_free_data(&t_dp->lports);
-        free(t_dp);
-    }
-
-    hmap_destroy(tracked_datapaths);
-}
-
-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;
-    } else if (!strcmp(pb->type, "chassisredirect")) {
-        return LP_CHASSISREDIRECT;
-    } else if (!strcmp(pb->type, "l3gateway")) {
-        return LP_L3GATEWAY;
-    } else if (!strcmp(pb->type, "localnet")) {
-        return LP_LOCALNET;
-    } else if (!strcmp(pb->type, "localport")) {
-        return LP_LOCALPORT;
-    } else if (!strcmp(pb->type, "l2gateway")) {
-        return LP_L2GATEWAY;
-    } else if (!strcmp(pb->type, "virtual")) {
-        return LP_VIRTUAL;
-    } else if (!strcmp(pb->type, "external")) {
-        return LP_EXTERNAL;
-    } else if (!strcmp(pb->type, "remote")) {
-        return LP_REMOTE;
-    } else if (!strcmp(pb->type, "vtep")) {
-        return LP_VTEP;
-    }
-
-    return LP_UNKNOWN;
-}
-
 static char *
 get_lport_type_str(enum en_lport_type lport_type)
 {
@@ -1797,61 +1608,17 @@ add_local_datapath_peer_port(const struct sbrec_port_binding *pb,
                              struct binding_ctx_out *b_ctx_out,
                              struct local_datapath *ld)
 {
-    const struct sbrec_port_binding *peer;
-    peer = get_peer_lport(pb, b_ctx_in);
-
-    if (!peer) {
-        return;
-    }
-
-    bool present = false;
-    for (size_t i = 0; i < ld->n_peer_ports; i++) {
-        if (ld->peer_ports[i].local == pb) {
-            present = true;
-            break;
-        }
-    }
-
-    if (!present) {
-        ld->n_peer_ports++;
-        if (ld->n_peer_ports > ld->n_allocated_peer_ports) {
-            ld->peer_ports =
-                x2nrealloc(ld->peer_ports,
-                           &ld->n_allocated_peer_ports,
-                           sizeof *ld->peer_ports);
-        }
-        ld->peer_ports[ld->n_peer_ports - 1].local = pb;
-        ld->peer_ports[ld->n_peer_ports - 1].remote = peer;
-    }
-
-    struct local_datapath *peer_ld =
-        get_local_datapath(b_ctx_out->local_datapaths,
-                           peer->datapath->tunnel_key);
-    if (!peer_ld) {
-        add_local_datapath__(b_ctx_in->sbrec_datapath_binding_by_key,
-                             b_ctx_in->sbrec_port_binding_by_datapath,
-                             b_ctx_in->sbrec_port_binding_by_name,
-                             peer->datapath, false,
-                             1, b_ctx_out->local_datapaths,
-                             b_ctx_out->tracked_dp_bindings);
-        return;
-    }
-
-    for (size_t i = 0; i < peer_ld->n_peer_ports; i++) {
-        if (peer_ld->peer_ports[i].local == peer) {
-            return;
-        }
-    }
+    struct local_datpath_added_aux aux = {
+        .has_local_l3gateway = false,
+        .tracked_datapaths = b_ctx_out->tracked_dp_bindings,
+    };
 
-    peer_ld->n_peer_ports++;
-    if (peer_ld->n_peer_ports > peer_ld->n_allocated_peer_ports) {
-        peer_ld->peer_ports =
-            x2nrealloc(peer_ld->peer_ports,
-                        &peer_ld->n_allocated_peer_ports,
-                        sizeof *peer_ld->peer_ports);
-    }
-    peer_ld->peer_ports[peer_ld->n_peer_ports - 1].local = peer;
-    peer_ld->peer_ports[peer_ld->n_peer_ports - 1].remote = pb;
+    local_datapath_add_or_update_peer_port(
+        pb, b_ctx_in->sbrec_datapath_binding_by_key,
+        b_ctx_in->sbrec_port_binding_by_datapath,
+        b_ctx_in->sbrec_port_binding_by_name,
+        ld, b_ctx_out->local_datapaths,
+        local_datapath_added, &aux);
 }
 
 static void
@@ -1859,34 +1626,7 @@ remove_local_datapath_peer_port(const struct sbrec_port_binding *pb,
                                 struct local_datapath *ld,
                                 struct hmap *local_datapaths)
 {
-    size_t i = 0;
-    for (i = 0; i < ld->n_peer_ports; i++) {
-        if (ld->peer_ports[i].local == pb) {
-            break;
-        }
-    }
-
-    if (i == ld->n_peer_ports) {
-        return;
-    }
-
-    const struct sbrec_port_binding *peer = ld->peer_ports[i].remote;
-
-    /* Possible improvement: We can shrink the allocated peer ports
-     * if (ld->n_peer_ports < ld->n_allocated_peer_ports / 2).
-     */
-    ld->peer_ports[i].local = ld->peer_ports[ld->n_peer_ports - 1].local;
-    ld->peer_ports[i].remote = ld->peer_ports[ld->n_peer_ports - 1].remote;
-    ld->n_peer_ports--;
-
-    struct local_datapath *peer_ld =
-        get_local_datapath(local_datapaths, peer->datapath->tunnel_key);
-    if (peer_ld) {
-        /* Remove the peer port from the peer datapath. The peer
-         * datapath also tries to remove its peer lport, but that would
-         * be no-op. */
-        remove_local_datapath_peer_port(peer, peer_ld, local_datapaths);
-    }
+   local_datapath_remove_peer_port(pb, ld, local_datapaths);
 }
 
 static void
@@ -1923,7 +1663,8 @@ update_lport_tracking(const struct sbrec_port_binding *pb,
         return;
     }
 
-    tracked_binding_datapath_lport_add(pb, tracked_dp_bindings);
+    tracked_datapath_lport_add(pb, TRACKED_RESOURCE_NEW,
+                               tracked_dp_bindings);
 }
 
 /* Considers the ovs iface 'iface_rec' for claiming.
@@ -2491,6 +2232,10 @@ delete_done:
             get_local_datapath(b_ctx_out->local_datapaths,
                                pb->datapath->tunnel_key);
 
+        if (ld) {
+            local_datapath_add_lport(ld, pb->logical_port, pb);
+        }
+
         switch (lport_type) {
         case LP_VIF:
         case LP_CONTAINER:
diff --git a/controller/binding.h b/controller/binding.h
index 8f32894769..e88d335374 100644
--- a/controller/binding.h
+++ b/controller/binding.h
@@ -107,19 +107,6 @@ void local_binding_set_up(struct shash *local_bindings, const char *pb_name,
 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 {
-    const struct sbrec_port_binding *pb;
-};
-
-/* Represent a tracked binding datapath. */
-struct tracked_binding_datapath {
-    struct hmap_node node;
-    const struct sbrec_datapath_binding *dp;
-    bool is_new;
-    struct shash lports; /* shash of struct tracked_binding_lport. */
-};
-
 void binding_register_ovs_idl(struct ovsdb_idl *);
 void binding_run(struct binding_ctx_in *, struct binding_ctx_out *);
 bool binding_cleanup(struct ovsdb_idl_txn *ovnsb_idl_txn,
diff --git a/controller/lflow-generate.c b/controller/lflow-generate.c
new file mode 100644
index 0000000000..77d81ea330
--- /dev/null
+++ b/controller/lflow-generate.c
@@ -0,0 +1,179 @@
+/*
+ * 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>
+
+/* OVS includes. */
+#include "lib/hmapx.h"
+#include "lib/util.h"
+#include "openvswitch/vlog.h"
+
+/* OVN includes. */
+#include "ldata.h"
+#include "lflow-generate.h"
+#include "lib/lflow.h"
+#include "lib/lb.h"
+#include "lib/ovn-sb-idl.h"
+#include "lib/ovn-util.h"
+
+VLOG_DEFINE_THIS_MODULE(lflow_gen);
+
+static void generate_lflows_for_lport__(struct local_lport *dp_lport);
+
+void
+lflow_generate_run(struct hmap *local_datapaths, struct hmap *local_lbs)
+{
+    struct local_datapath *ldp;
+    HMAP_FOR_EACH (ldp, hmap_node, local_datapaths) {
+        ovn_ctrl_lflows_build_dp_lflows(ldp->active_lflows, ldp);
+
+        struct shash_node *node;
+        SHASH_FOR_EACH (node, &ldp->lports) {
+            generate_lflows_for_lport__(node->data);
+        }
+    }
+
+    struct local_load_balancer *local_lb;
+    HMAP_FOR_EACH (local_lb, hmap_node, local_lbs) {
+        lflow_generate_load_balancer_lflows(local_lb);
+    }
+}
+
+void
+lflow_generate_datapath_flows(struct local_datapath *ldp,
+                              bool build_lport_flows)
+{
+    local_datapath_switch_lflow_map(ldp);
+    ovn_ctrl_lflows_build_dp_lflows(ldp->active_lflows, ldp);
+
+    if (build_lport_flows) {
+        struct shash_node *node;
+        SHASH_FOR_EACH (node, &ldp->lports) {
+            generate_lflows_for_lport__(node->data);
+        }
+    }
+}
+
+void
+lflow_generate_lport_flows(const struct sbrec_port_binding *pb,
+                           struct local_datapath *ldp)
+{
+    struct local_lport *lport =
+        local_datapath_get_lport(ldp, pb->logical_port);
+    if (lport) {
+        generate_lflows_for_lport__(lport);
+    } else {
+        lport = local_datapath_add_lport(ldp, pb->logical_port, pb);
+        local_lport_update_cache(lport);
+        ovn_ctrl_build_lport_lflows(lport->active_lflows, lport);
+    }
+}
+
+void
+lflow_delete_generated_lport_lflows(const struct sbrec_port_binding *pb,
+                                    struct local_datapath *ldp)
+{
+    struct local_lport *lport =
+        local_datapath_get_lport(ldp, pb->logical_port);
+    if (lport) {
+        local_lport_switch_lflow_map(lport);
+    }
+}
+
+void
+lflow_delete_generated_lflows(struct hmap *local_datapaths,
+                              struct hmap *local_lbs)
+{
+    struct local_datapath *ldp;
+    HMAP_FOR_EACH (ldp, hmap_node, local_datapaths) {
+        ovn_ctrl_lflows_clear(&ldp->ctrl_lflows[0]);
+        ovn_ctrl_lflows_clear(&ldp->ctrl_lflows[1]);
+
+        struct local_lport *lport;
+        struct shash_node *node;
+        SHASH_FOR_EACH (node, &ldp->lports) {
+            lport = node->data;
+            ovn_ctrl_lflows_clear(&lport->ctrl_lflows[0]);
+            ovn_ctrl_lflows_clear(&lport->ctrl_lflows[1]);
+        }
+    }
+
+    struct local_load_balancer *local_lb;
+    HMAP_FOR_EACH (local_lb, hmap_node, local_lbs) {
+        ovn_ctrl_lflows_clear(&local_lb->lswitch_lflows[0]);
+        ovn_ctrl_lflows_clear(&local_lb->lswitch_lflows[1]);
+        ovn_ctrl_lflows_clear(&local_lb->lrouter_lflows[0]);
+        ovn_ctrl_lflows_clear(&local_lb->lrouter_lflows[1]);
+    }
+}
+
+
+/* Returns true if the local datapath 'ldp' needs logical flow
+ * generation.  False otherwise.
+ */
+bool
+lflow_datapath_needs_generation(struct local_datapath *ldp)
+{
+    ovs_assert(ldp->datapath);
+
+    /* Right now we check if the datapath options have changed
+     * from the locally stored value. */
+    return !smap_equal(&ldp->dp_options, &ldp->datapath->options);
+}
+
+bool
+lflow_lport_needs_generation(struct local_datapath *ldp,
+                             const struct sbrec_port_binding *pb)
+{
+    struct local_lport *dp_lport = local_datapath_get_lport(
+        ldp, pb->logical_port);
+
+    if (!dp_lport) {
+        return true;
+    }
+
+    return local_lport_update_cache(dp_lport);
+}
+
+void
+lflow_generate_load_balancer_lflows(struct local_load_balancer *local_lb)
+{
+    ovn_ctrl_build_lb_lflows(local_lb->active_lswitch_lflows,
+                             local_lb->active_lrouter_lflows,
+                             local_lb->ovn_lb);
+}
+
+bool
+lflow_load_balancer_needs_gen(struct local_load_balancer *local_lb)
+{
+    local_load_balancer_update(local_lb);
+    return true;
+}
+
+
+void
+lflow_clear_generated_lb_lflows(struct local_load_balancer *local_lb)
+{
+    local_load_balancer_switch_lflow_map(local_lb);
+}
+
+static void
+generate_lflows_for_lport__(struct local_lport *dp_lport)
+{
+    local_lport_switch_lflow_map(dp_lport);
+    local_lport_update_cache(dp_lport);
+    ovn_ctrl_build_lport_lflows(dp_lport->active_lflows, dp_lport);
+}
diff --git a/controller/lflow-generate.h b/controller/lflow-generate.h
new file mode 100644
index 0000000000..baaecbf854
--- /dev/null
+++ b/controller/lflow-generate.h
@@ -0,0 +1,49 @@
+/*
+ * 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 OVN_LFLOW_GENERATE_H
+#define OVN_LFLOW_GENERATE_H 1
+
+struct hmap;
+struct sbrec_port_binding_table;
+struct sbrec_datapath_binding_table;
+struct sbrec_port_binding;
+
+void lflow_generate_run(struct hmap *local_datapaths, struct hmap *local_lbs);
+void lflow_generate_datapath_flows(struct local_datapath *ldp,
+                                   bool build_lport_flows);
+void lflow_generate_lport_flows(const struct sbrec_port_binding *pb,
+                                struct local_datapath *ldp);
+
+void lflow_delete_generated_lport_lflows(const struct sbrec_port_binding *,
+                                         struct local_datapath *);
+
+void lflow_delete_generated_lflows(struct hmap *local_datapaths,
+                                   struct hmap *local_lbs);
+
+bool lflow_datapath_needs_generation(struct local_datapath *ldp);
+bool lflow_lport_needs_generation(struct local_datapath *ldp,
+                                  const struct sbrec_port_binding *);
+
+void lflow_delete_generated_lport_lflows(const struct sbrec_port_binding *,
+                                         struct local_datapath *);
+
+void lflow_generate_load_balancer_lflows(struct local_load_balancer *local_lb);
+bool lflow_load_balancer_needs_gen(struct local_load_balancer *local_lb);
+void lflow_clear_generated_lb_lflows(struct local_load_balancer *local_lb);
+
+
+#endif /* controller/lflow-generate.h */
diff --git a/controller/lflow.c b/controller/lflow.c
index 34eca135ae..ff511a11f5 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -14,27 +14,33 @@
  */
 
 #include <config.h>
-#include "lflow.h"
+
+/* OVS includes. */
 #include "coverage.h"
-#include "ha-chassis.h"
-#include "lflow-cache.h"
-#include "lport.h"
-#include "ofctrl.h"
 #include "openvswitch/dynamic-string.h"
 #include "openvswitch/ofp-actions.h"
 #include "openvswitch/ofpbuf.h"
 #include "openvswitch/vlog.h"
-#include "ovn-controller.h"
-#include "ovn/actions.h"
-#include "ovn/expr.h"
+#include "lib/ovn-sb-idl.h"
+#include "lib/packets.h"
+#include "lib/simap.h"
+#include "lib/sset.h"
+
+/* OVN includes. */
+#include "include/ovn/actions.h"
+#include "include/ovn/expr.h"
+#include "ha-chassis.h"
+#include "ldata.h"
+#include "lflow.h"
+#include "lflow-cache.h"
 #include "lib/lb.h"
+#include "lib/lflow.h"
 #include "lib/ovn-l7.h"
-#include "lib/ovn-sb-idl.h"
 #include "lib/extend-table.h"
-#include "packets.h"
+#include "lport.h"
+#include "ofctrl.h"
+#include "ovn-controller.h"
 #include "physical.h"
-#include "simap.h"
-#include "sset.h"
 
 VLOG_DEFINE_THIS_MODULE(lflow);
 
@@ -55,7 +61,7 @@ struct lookup_port_aux {
     struct ovsdb_idl_index *sbrec_multicast_group_by_name_datapath;
     struct ovsdb_idl_index *sbrec_port_binding_by_name;
     const struct sbrec_datapath_binding *dp;
-    const struct sbrec_logical_flow *lflow;
+    const struct uuid *lflow_uuid;
     struct lflow_resource_ref *lfrr;
 };
 
@@ -63,19 +69,19 @@ struct condition_aux {
     struct ovsdb_idl_index *sbrec_port_binding_by_name;
     const struct sbrec_chassis *chassis;
     const struct sset *active_tunnels;
-    const struct sbrec_logical_flow *lflow;
+    const struct uuid *lflow_uuid;
     /* Resource reference to store the port name referenced
      * in is_chassis_resident() to the logical flow. */
     struct lflow_resource_ref *lfrr;
 };
 
 static bool
-consider_logical_flow(const struct sbrec_logical_flow *lflow,
-                      struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
-                      struct hmap *nd_ra_opts,
-                      struct controller_event_options *controller_event_opts,
-                      struct lflow_ctx_in *l_ctx_in,
-                      struct lflow_ctx_out *l_ctx_out);
+consider_sb_logical_flow(const struct sbrec_logical_flow *,
+                         struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
+                         struct hmap *nd_ra_opts,
+                         struct controller_event_options *,
+                         struct lflow_ctx_in *,
+                        struct lflow_ctx_out *);
 static void lflow_resource_add(struct lflow_resource_ref *, enum ref_type,
                                const char *ref_name, const struct uuid *);
 static struct ref_lflow_node *ref_lflow_lookup(struct hmap *ref_lflow_table,
@@ -98,6 +104,10 @@ lookup_port_cb(const void *aux_, const char *port_name, unsigned int *portp)
 
     const struct lookup_port_aux *aux = aux_;
 
+    if (!aux) {
+        return false;
+    }
+
     const struct sbrec_port_binding *pb
         = lport_lookup_by_name(aux->sbrec_port_binding_by_name, port_name);
     if (pb && pb->datapath == aux->dp) {
@@ -105,15 +115,17 @@ lookup_port_cb(const void *aux_, const char *port_name, unsigned int *portp)
         return true;
     }
 
-    /* Store the key (DP + name) that used to lookup the multicast group to
-     * lflow reference, so that in the future when the multicast group's
-     * existance (found/not found) changes, the logical flow that references
-     * this multicast group can be reprocessed. */
-    struct ds mg_key = DS_EMPTY_INITIALIZER;
-    get_mc_group_key(port_name, aux->dp->tunnel_key, &mg_key);
-    lflow_resource_add(aux->lfrr, REF_TYPE_MC_GROUP, ds_cstr(&mg_key),
-                       &aux->lflow->header_.uuid);
-    ds_destroy(&mg_key);
+    if (aux->lfrr) {
+        /* Store the key (DP + name) that used to lookup the multicast group to
+        * lflow reference, so that in the future when the multicast group's
+        * existance (found/not found) changes, the logical flow that references
+        * this multicast group can be reprocessed. */
+        struct ds mg_key = DS_EMPTY_INITIALIZER;
+        get_mc_group_key(port_name, aux->dp->tunnel_key, &mg_key);
+        lflow_resource_add(aux->lfrr, REF_TYPE_MC_GROUP, ds_cstr(&mg_key),
+                           aux->lflow_uuid);
+        ds_destroy(&mg_key);
+    }
 
     const struct sbrec_multicast_group *mg = mcgroup_lookup_by_dp_name(
         aux->sbrec_multicast_group_by_name_datapath, aux->dp, port_name);
@@ -131,6 +143,10 @@ tunnel_ofport_cb(const void *aux_, const char *port_name, ofp_port_t *ofport)
 {
     const struct lookup_port_aux *aux = aux_;
 
+    if (!aux) {
+        return false;
+    }
+
     const struct sbrec_port_binding *pb
         = lport_lookup_by_name(aux->sbrec_port_binding_by_name, port_name);
     if (!pb || (pb->datapath != aux->dp) || !pb->chassis) {
@@ -155,12 +171,14 @@ is_chassis_resident_cb(const void *c_aux_, const char *port_name)
         return false;
     }
 
-    /* Store the port_name to lflow reference. */
-    int64_t dp_id = pb->datapath->tunnel_key;
-    char buf[16];
-    get_unique_lport_key(dp_id, pb->tunnel_key, buf, sizeof(buf));
-    lflow_resource_add(c_aux->lfrr, REF_TYPE_PORTBINDING, buf,
-                       &c_aux->lflow->header_.uuid);
+    if (c_aux->lfrr) {
+        /* Store the port_name to lflow reference. */
+        int64_t dp_id = pb->datapath->tunnel_key;
+        char buf[16];
+        get_unique_lport_key(dp_id, pb->tunnel_key, buf, sizeof(buf));
+        lflow_resource_add(c_aux->lfrr, REF_TYPE_PORTBINDING, buf,
+                           c_aux->lflow_uuid);
+    }
 
     if (strcmp(pb->type, "chassisredirect")) {
         /* for non-chassisredirect ports */
@@ -355,9 +373,9 @@ add_logical_flows(struct lflow_ctx_in *l_ctx_in,
     controller_event_opts_init(&controller_event_opts);
 
     SBREC_LOGICAL_FLOW_TABLE_FOR_EACH (lflow, l_ctx_in->logical_flow_table) {
-        if (!consider_logical_flow(lflow, &dhcp_opts, &dhcpv6_opts,
-                                   &nd_ra_opts, &controller_event_opts,
-                                   l_ctx_in, l_ctx_out)) {
+        if (!consider_sb_logical_flow(lflow, &dhcp_opts, &dhcpv6_opts,
+                                      &nd_ra_opts, &controller_event_opts,
+                                      l_ctx_in, l_ctx_out)) {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
             VLOG_ERR_RL(&rl, "Conjunction id overflow when processing lflow "
                         UUID_FMT, UUID_ARGS(&lflow->header_.uuid));
@@ -430,9 +448,9 @@ lflow_handle_changed_flows(struct lflow_ctx_in *l_ctx_in,
         if (lflow) {
             VLOG_DBG("re-add lflow "UUID_FMT,
                      UUID_ARGS(&lflow->header_.uuid));
-            if (!consider_logical_flow(lflow, &dhcp_opts, &dhcpv6_opts,
-                                       &nd_ra_opts, &controller_event_opts,
-                                       l_ctx_in, l_ctx_out)) {
+            if (!consider_sb_logical_flow(lflow, &dhcp_opts, &dhcpv6_opts,
+                                          &nd_ra_opts, &controller_event_opts,
+                                          l_ctx_in, l_ctx_out)) {
                 ret = false;
                 break;
             }
@@ -530,9 +548,9 @@ lflow_handle_changed_ref(enum ref_type ref_type, const char *ref_name,
             continue;
         }
 
-        if (!consider_logical_flow(lflow, &dhcp_opts, &dhcpv6_opts,
-                                   &nd_ra_opts, &controller_event_opts,
-                                   l_ctx_in, l_ctx_out)) {
+        if (!consider_sb_logical_flow(lflow, &dhcp_opts, &dhcpv6_opts,
+                                      &nd_ra_opts, &controller_event_opts,
+                                      l_ctx_in, l_ctx_out)) {
             ret = false;
             l_ctx_out->conj_id_overflow = true;
             break;
@@ -570,48 +588,15 @@ update_conj_id_ofs(uint32_t *conj_id_ofs, uint32_t n_conjs)
 }
 
 static void
-add_matches_to_flow_table(const struct sbrec_logical_flow *lflow,
-                          const struct sbrec_datapath_binding *dp,
-                          struct hmap *matches, uint8_t ptable,
-                          uint8_t output_ptable, struct ofpbuf *ovnacts,
-                          bool ingress, struct lflow_ctx_in *l_ctx_in,
-                          struct lflow_ctx_out *l_ctx_out)
+add_matches_to_flow_table__(struct hmap *matches, uint8_t ptable, bool ingress,
+                            const struct sbrec_datapath_binding *dp,
+                            const struct uuid *lflow_uuid,
+                            uint16_t lflow_priority,
+                            const struct sset *local_lport_ids,
+                            struct ofpbuf *ofpacts,
+                            struct lflow_resource_ref *lfrr,
+                            struct ovn_desired_flow_table *flow_table)
 {
-    struct lookup_port_aux aux = {
-        .sbrec_multicast_group_by_name_datapath
-            = l_ctx_in->sbrec_multicast_group_by_name_datapath,
-        .sbrec_port_binding_by_name = l_ctx_in->sbrec_port_binding_by_name,
-        .dp = dp,
-        .lflow = lflow,
-        .lfrr = l_ctx_out->lfrr,
-    };
-
-    /* Encode OVN logical actions into OpenFlow. */
-    uint64_t ofpacts_stub[1024 / 8];
-    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
-    struct ovnact_encode_params ep = {
-        .lookup_port = lookup_port_cb,
-        .tunnel_ofport = tunnel_ofport_cb,
-        .aux = &aux,
-        .is_switch = datapath_is_switch(dp),
-        .group_table = l_ctx_out->group_table,
-        .meter_table = l_ctx_out->meter_table,
-        .lflow_uuid = lflow->header_.uuid,
-
-        .pipeline = ingress ? OVNACT_P_INGRESS : OVNACT_P_EGRESS,
-        .ingress_ptable = OFTABLE_LOG_INGRESS_PIPELINE,
-        .egress_ptable = OFTABLE_LOG_EGRESS_PIPELINE,
-        .output_ptable = output_ptable,
-        .mac_bind_ptable = OFTABLE_MAC_BINDING,
-        .mac_lookup_ptable = OFTABLE_MAC_LOOKUP,
-        .lb_hairpin_ptable = OFTABLE_CHK_LB_HAIRPIN,
-        .lb_hairpin_reply_ptable = OFTABLE_CHK_LB_HAIRPIN_REPLY,
-        .ct_snat_vip_ptable = OFTABLE_CT_SNAT_FOR_VIP,
-        .fdb_ptable = OFTABLE_GET_FDB,
-        .fdb_lookup_ptable = OFTABLE_LOOKUP_FDB,
-    };
-    ovnacts_encode(ovnacts->data, ovnacts->size, &ep, &ofpacts);
-
     struct expr_match *m;
     HMAP_FOR_EACH (m, hmap_node, matches) {
         match_set_metadata(&m->match, htonll(dp->tunnel_key));
@@ -623,21 +608,23 @@ add_matches_to_flow_table(const struct sbrec_logical_flow *lflow,
                 int64_t dp_id = dp->tunnel_key;
                 char buf[16];
                 get_unique_lport_key(dp_id, port_id, buf, sizeof(buf));
-                lflow_resource_add(l_ctx_out->lfrr, REF_TYPE_PORTBINDING, buf,
-                                   &lflow->header_.uuid);
-                if (!sset_contains(l_ctx_in->local_lport_ids, buf)) {
+                if (lfrr) {
+                    lflow_resource_add(lfrr, REF_TYPE_PORTBINDING, buf,
+                                       lflow_uuid);
+                }
+                if (!sset_contains(local_lport_ids, buf)) {
                     VLOG_DBG("lflow "UUID_FMT
                              " port %s in match is not local, skip",
-                             UUID_ARGS(&lflow->header_.uuid),
+                             UUID_ARGS(lflow_uuid),
                              buf);
                     continue;
                 }
             }
         }
         if (!m->n) {
-            ofctrl_add_flow(l_ctx_out->flow_table, ptable, lflow->priority,
-                            lflow->header_.uuid.parts[0], &m->match, &ofpacts,
-                            &lflow->header_.uuid);
+            ofctrl_add_flow(flow_table, ptable, lflow_priority,
+                            lflow_uuid->parts[0], &m->match, ofpacts,
+                            lflow_uuid);
         } else {
             uint64_t conj_stubs[64 / 8];
             struct ofpbuf conj;
@@ -653,13 +640,60 @@ add_matches_to_flow_table(const struct sbrec_logical_flow *lflow,
                 dst->n_clauses = src->n_clauses;
             }
 
-            ofctrl_add_or_append_flow(l_ctx_out->flow_table, ptable,
-                                      lflow->priority, 0,
-                                      &m->match, &conj, &lflow->header_.uuid);
+            ofctrl_add_or_append_flow(flow_table, ptable,
+                                      lflow_priority, 0,
+                                      &m->match, &conj, lflow_uuid);
             ofpbuf_uninit(&conj);
         }
     }
+}
 
+static void
+add_matches_to_flow_table(const struct ovn_ctrl_lflow *lflow,
+                          const struct sbrec_datapath_binding *dp,
+                          struct hmap *matches,
+                          uint8_t ptable, uint8_t output_ptable,
+                          struct ofpbuf *ovnacts,
+                          bool ingress, struct lflow_ctx_in *l_ctx_in,
+                          struct lflow_ctx_out *l_ctx_out)
+{
+    struct lookup_port_aux aux = {
+        .sbrec_multicast_group_by_name_datapath
+            = l_ctx_in->sbrec_multicast_group_by_name_datapath,
+        .sbrec_port_binding_by_name = l_ctx_in->sbrec_port_binding_by_name,
+        .dp = dp,
+    };
+
+    /* Encode OVN logical actions into OpenFlow. */
+    uint64_t ofpacts_stub[1024 / 8];
+    struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(ofpacts_stub);
+    struct ovnact_encode_params ep = {
+        .lookup_port = lookup_port_cb,
+        .tunnel_ofport = tunnel_ofport_cb,
+        .aux = &aux,
+        .is_switch = datapath_is_switch(dp),
+        .group_table = l_ctx_out->group_table,
+        .meter_table = l_ctx_out->meter_table,
+        .lflow_uuid = lflow->uuid_,
+
+        .pipeline = ingress ? OVNACT_P_INGRESS : OVNACT_P_EGRESS,
+        .ingress_ptable = OFTABLE_LOG_INGRESS_PIPELINE,
+        .egress_ptable = OFTABLE_LOG_EGRESS_PIPELINE,
+        .output_ptable = output_ptable,
+        .mac_bind_ptable = OFTABLE_MAC_BINDING,
+        .mac_lookup_ptable = OFTABLE_MAC_LOOKUP,
+        .lb_hairpin_ptable = OFTABLE_CHK_LB_HAIRPIN,
+        .lb_hairpin_reply_ptable = OFTABLE_CHK_LB_HAIRPIN_REPLY,
+        .ct_snat_vip_ptable = OFTABLE_CT_SNAT_FOR_VIP,
+        .fdb_ptable = OFTABLE_GET_FDB,
+        .fdb_lookup_ptable = OFTABLE_LOOKUP_FDB,
+    };
+    ovnacts_encode(ovnacts->data, ovnacts->size, &ep, &ofpacts);
+
+    add_matches_to_flow_table__(matches, ptable, ingress, dp,
+                                &lflow->uuid_, lflow->priority,
+                                l_ctx_in->local_lport_ids, &ofpacts,
+                                l_ctx_out->lfrr, l_ctx_out->flow_table);
     ofpbuf_uninit(&ofpacts);
 }
 
@@ -669,8 +703,9 @@ add_matches_to_flow_table(const struct sbrec_logical_flow *lflow,
  * If parsing is successful, '*prereqs' is also consumed.
  */
 static struct expr *
-convert_match_to_expr(const struct sbrec_logical_flow *lflow,
-                      const struct sbrec_datapath_binding *dp,
+convert_match_to_expr(char *lflow_match,
+                      const struct uuid *lflow_uuid,
+                      int64_t dp_id,
                       struct expr **prereqs,
                       const struct shash *addr_sets,
                       const struct shash *port_groups,
@@ -681,19 +716,22 @@ convert_match_to_expr(const struct sbrec_logical_flow *lflow,
     struct sset port_groups_ref = SSET_INITIALIZER(&port_groups_ref);
     char *error = NULL;
 
-    struct expr *e = expr_parse_string(lflow->match, &symtab, addr_sets,
+    struct expr *e = expr_parse_string(lflow_match, &symtab, addr_sets,
                                        port_groups, &addr_sets_ref,
-                                       &port_groups_ref, dp->tunnel_key,
+                                       &port_groups_ref, dp_id,
                                        &error);
-    const char *addr_set_name;
-    SSET_FOR_EACH (addr_set_name, &addr_sets_ref) {
-        lflow_resource_add(lfrr, REF_TYPE_ADDRSET, addr_set_name,
-                           &lflow->header_.uuid);
-    }
-    const char *port_group_name;
-    SSET_FOR_EACH (port_group_name, &port_groups_ref) {
-        lflow_resource_add(lfrr, REF_TYPE_PORTGROUP, port_group_name,
-                           &lflow->header_.uuid);
+
+    if (lflow_uuid) {
+        const char *addr_set_name;
+        SSET_FOR_EACH (addr_set_name, &addr_sets_ref) {
+            lflow_resource_add(lfrr, REF_TYPE_ADDRSET, addr_set_name,
+                               lflow_uuid);
+        }
+        const char *port_group_name;
+        SSET_FOR_EACH (port_group_name, &port_groups_ref) {
+            lflow_resource_add(lfrr, REF_TYPE_PORTGROUP, port_group_name,
+                               lflow_uuid);
+        }
     }
 
     if (pg_addr_set_ref) {
@@ -713,7 +751,7 @@ convert_match_to_expr(const struct sbrec_logical_flow *lflow,
     if (error) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
         VLOG_WARN_RL(&rl, "error parsing match \"%s\": %s",
-                    lflow->match, error);
+                     lflow_match, error);
         free(error);
         return NULL;
     }
@@ -722,28 +760,30 @@ convert_match_to_expr(const struct sbrec_logical_flow *lflow,
 }
 
 static bool
-consider_logical_flow__(const struct sbrec_logical_flow *lflow,
-                        const struct sbrec_datapath_binding *dp,
+consider_logical_flow__(const struct ovn_ctrl_lflow *lflow,
+                        bool ingress, uint8_t table_id,
+                        uint32_t dp_key,
                         struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
                         struct hmap *nd_ra_opts,
                         struct controller_event_options *controller_event_opts,
                         struct lflow_ctx_in *l_ctx_in,
                         struct lflow_ctx_out *l_ctx_out)
 {
-    if (!get_local_datapath(l_ctx_in->local_datapaths, dp->tunnel_key)) {
+    struct local_datapath *ldp = get_local_datapath(l_ctx_in->local_datapaths,
+                                                    dp_key);
+    if (!ldp) {
         VLOG_DBG("lflow "UUID_FMT" is not for local datapath, skip",
-                 UUID_ARGS(&lflow->header_.uuid));
+                 UUID_ARGS(&lflow->uuid_));
         return true;
     }
 
-    /* Determine translation of logical table IDs to physical table IDs. */
-    bool ingress = !strcmp(lflow->pipeline, "ingress");
+    const struct sbrec_datapath_binding *dp = ldp->datapath;
 
     /* Determine translation of logical table IDs to physical table IDs. */
     uint8_t first_ptable = (ingress
                             ? OFTABLE_LOG_INGRESS_PIPELINE
                             : OFTABLE_LOG_EGRESS_PIPELINE);
-    uint8_t ptable = first_ptable + lflow->table_id;
+    uint8_t ptable = first_ptable + table_id;
     uint8_t output_ptable = (ingress
                              ? OFTABLE_REMOTE_OUTPUT
                              : OFTABLE_SAVE_INPORT);
@@ -762,7 +802,7 @@ consider_logical_flow__(const struct sbrec_logical_flow *lflow,
 
         .pipeline = ingress ? OVNACT_P_INGRESS : OVNACT_P_EGRESS,
         .n_tables = LOG_PIPELINE_LEN,
-        .cur_ltable = lflow->table_id,
+        .cur_ltable = table_id,
     };
     struct expr *prereqs = NULL;
     char *error;
@@ -783,19 +823,19 @@ consider_logical_flow__(const struct sbrec_logical_flow *lflow,
             = l_ctx_in->sbrec_multicast_group_by_name_datapath,
         .sbrec_port_binding_by_name = l_ctx_in->sbrec_port_binding_by_name,
         .dp = dp,
-        .lflow = lflow,
+        .lflow_uuid = &lflow->uuid_,
         .lfrr = l_ctx_out->lfrr,
     };
     struct condition_aux cond_aux = {
         .sbrec_port_binding_by_name = l_ctx_in->sbrec_port_binding_by_name,
         .chassis = l_ctx_in->chassis,
         .active_tunnels = l_ctx_in->active_tunnels,
-        .lflow = lflow,
+        .lflow_uuid = &lflow->uuid_,
         .lfrr = l_ctx_out->lfrr,
     };
 
     struct lflow_cache_value *lcv =
-        lflow_cache_get(l_ctx_out->lflow_cache, &lflow->header_.uuid);
+        lflow_cache_get(l_ctx_out->lflow_cache, &lflow->uuid_);
     uint32_t conj_id_ofs =
         lcv ? lcv->conj_id_ofs : *l_ctx_out->conj_id_ofs;
     enum lflow_cache_type lcv_type =
@@ -815,7 +855,9 @@ consider_logical_flow__(const struct sbrec_logical_flow *lflow,
     switch (lcv_type) {
     case LCACHE_T_NONE:
     case LCACHE_T_CONJ_ID:
-        expr = convert_match_to_expr(lflow, dp, &prereqs, l_ctx_in->addr_sets,
+        expr = convert_match_to_expr(lflow->match, &lflow->uuid_,
+                                     dp->tunnel_key, &prereqs,
+                                     l_ctx_in->addr_sets,
                                      l_ctx_in->port_groups, l_ctx_out->lfrr,
                                      &pg_addr_set_ref);
         if (!expr) {
@@ -861,7 +903,7 @@ consider_logical_flow__(const struct sbrec_logical_flow *lflow,
         matches_size = expr_matches_prepare(matches, conj_id_ofs);
         if (hmap_is_empty(matches)) {
             VLOG_DBG("lflow "UUID_FMT" matches are empty, skip",
-                    UUID_ARGS(&lflow->header_.uuid));
+                    UUID_ARGS(&lflow->uuid_));
             goto done;
         }
         break;
@@ -885,17 +927,17 @@ consider_logical_flow__(const struct sbrec_logical_flow *lflow,
         if (lflow_cache_is_enabled(l_ctx_out->lflow_cache)) {
             if (cached_expr && !is_cr_cond_present) {
                 lflow_cache_add_matches(l_ctx_out->lflow_cache,
-                                        &lflow->header_.uuid, matches,
+                                        &lflow->uuid_, matches,
                                         matches_size);
                 matches = NULL;
             } else if (cached_expr) {
                 lflow_cache_add_expr(l_ctx_out->lflow_cache,
-                                     &lflow->header_.uuid, conj_id_ofs,
+                                     &lflow->uuid_, conj_id_ofs,
                                      cached_expr, expr_size(cached_expr));
                 cached_expr = NULL;
             } else if (n_conjs) {
                 lflow_cache_add_conj_id(l_ctx_out->lflow_cache,
-                                        &lflow->header_.uuid, conj_id_ofs);
+                                        &lflow->uuid_, conj_id_ofs);
             }
         }
         break;
@@ -920,12 +962,33 @@ done:
 }
 
 static bool
-consider_logical_flow(const struct sbrec_logical_flow *lflow,
-                      struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
-                      struct hmap *nd_ra_opts,
-                      struct controller_event_options *controller_event_opts,
-                      struct lflow_ctx_in *l_ctx_in,
-                      struct lflow_ctx_out *l_ctx_out)
+consider_sb_logical_flow__(const struct sbrec_logical_flow *lflow,
+                           const struct sbrec_datapath_binding *dp,
+                           struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
+                           struct hmap *nd_ra_opts,
+                           struct controller_event_options *event_opts,
+                           struct lflow_ctx_in *l_ctx_in,
+                           struct lflow_ctx_out *l_ctx_out)
+{
+    struct ovn_ctrl_lflow ctrl_lflow;
+    ctrl_lflow.uuid_ = lflow->header_.uuid;
+    ctrl_lflow.match = lflow->match;
+    ctrl_lflow.actions = lflow->actions;
+    ctrl_lflow.priority = lflow->priority;
+
+    bool ingress = !strcmp(lflow->pipeline, "ingress");
+    return consider_logical_flow__(&ctrl_lflow, ingress, lflow->table_id,
+                                   dp->tunnel_key, dhcp_opts, dhcpv6_opts, nd_ra_opts,
+                                   event_opts, l_ctx_in, l_ctx_out);
+}
+
+static bool
+consider_sb_logical_flow(const struct sbrec_logical_flow *lflow,
+                         struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
+                         struct hmap *nd_ra_opts,
+                         struct controller_event_options *event_opts,
+                         struct lflow_ctx_in *l_ctx_in,
+                         struct lflow_ctx_out *l_ctx_out)
 {
     const struct sbrec_logical_dp_group *dp_group = lflow->logical_dp_group;
     const struct sbrec_datapath_binding *dp = lflow->logical_datapath;
@@ -938,17 +1001,15 @@ consider_logical_flow(const struct sbrec_logical_flow *lflow,
     }
     ovs_assert(!dp_group || !dp);
 
-    if (dp && !consider_logical_flow__(lflow, dp,
-                                       dhcp_opts, dhcpv6_opts, nd_ra_opts,
-                                       controller_event_opts,
-                                       l_ctx_in, l_ctx_out)) {
+    if (dp && !consider_sb_logical_flow__(lflow, dp,
+                                          dhcp_opts, dhcpv6_opts, nd_ra_opts,
+                                          event_opts, l_ctx_in, l_ctx_out)) {
         ret = false;
     }
     for (size_t i = 0; dp_group && i < dp_group->n_datapaths; i++) {
-        if (!consider_logical_flow__(lflow, dp_group->datapaths[i],
-                                     dhcp_opts,  dhcpv6_opts, nd_ra_opts,
-                                     controller_event_opts,
-                                     l_ctx_in, l_ctx_out)) {
+        if (!consider_sb_logical_flow__(lflow, dp_group->datapaths[i],
+                                        dhcp_opts,  dhcpv6_opts, nd_ra_opts,
+                                        event_opts, l_ctx_in, l_ctx_out)) {
             ret = false;
         }
     }
@@ -1688,9 +1749,9 @@ lflow_add_flows_for_datapath(const struct sbrec_datapath_binding *dp,
     const struct sbrec_logical_flow *lflow;
     SBREC_LOGICAL_FLOW_FOR_EACH_EQUAL (
         lflow, lf_row, l_ctx_in->sbrec_logical_flow_by_logical_datapath) {
-        if (!consider_logical_flow__(lflow, dp, &dhcp_opts, &dhcpv6_opts,
-                                     &nd_ra_opts, &controller_event_opts,
-                                     l_ctx_in, l_ctx_out)) {
+        if (!consider_sb_logical_flow__(lflow, dp, &dhcp_opts, &dhcpv6_opts,
+                                        &nd_ra_opts, &controller_event_opts,
+                                        l_ctx_in, l_ctx_out)) {
             handled = false;
             l_ctx_out->conj_id_overflow = true;
             goto lflow_processing_end;
@@ -1718,9 +1779,10 @@ lflow_add_flows_for_datapath(const struct sbrec_datapath_binding *dp,
         sbrec_logical_flow_index_set_logical_dp_group(lf_row, ldpg);
         SBREC_LOGICAL_FLOW_FOR_EACH_EQUAL (
             lflow, lf_row, l_ctx_in->sbrec_logical_flow_by_logical_dp_group) {
-            if (!consider_logical_flow__(lflow, dp, &dhcp_opts, &dhcpv6_opts,
-                                         &nd_ra_opts, &controller_event_opts,
-                                         l_ctx_in, l_ctx_out)) {
+            if (!consider_sb_logical_flow__(lflow, dp, &dhcp_opts,
+                                            &dhcpv6_opts, &nd_ra_opts,
+                                            &controller_event_opts,
+                                            l_ctx_in, l_ctx_out)) {
                 handled = false;
                 l_ctx_out->conj_id_overflow = true;
                 goto lflow_processing_end;
@@ -1853,3 +1915,72 @@ lflow_handle_changed_fdbs(struct lflow_ctx_in *l_ctx_in,
 
     return true;
 }
+
+void
+lflow_process_ctrl_lflows(struct hmap *ctrl_lflows,
+                          const struct sbrec_datapath_binding *dp,
+                          struct lflow_ctx_in *l_ctx_in,
+                          struct lflow_ctx_out *l_ctx_out)
+{
+    struct hmap dhcp_opts = HMAP_INITIALIZER(&dhcp_opts);
+    struct hmap dhcpv6_opts = HMAP_INITIALIZER(&dhcpv6_opts);
+    const struct sbrec_dhcp_options *dhcp_opt_row;
+    SBREC_DHCP_OPTIONS_TABLE_FOR_EACH (dhcp_opt_row,
+                                       l_ctx_in->dhcp_options_table) {
+        dhcp_opt_add(&dhcp_opts, dhcp_opt_row->name, dhcp_opt_row->code,
+                     dhcp_opt_row->type);
+    }
+
+
+    const struct sbrec_dhcpv6_options *dhcpv6_opt_row;
+    SBREC_DHCPV6_OPTIONS_TABLE_FOR_EACH (dhcpv6_opt_row,
+                                         l_ctx_in->dhcpv6_options_table) {
+       dhcp_opt_add(&dhcpv6_opts, dhcpv6_opt_row->name, dhcpv6_opt_row->code,
+                    dhcpv6_opt_row->type);
+    }
+
+    struct hmap nd_ra_opts = HMAP_INITIALIZER(&nd_ra_opts);
+    nd_ra_opts_init(&nd_ra_opts);
+
+    struct controller_event_options controller_event_opts;
+    controller_event_opts_init(&controller_event_opts);
+
+
+    struct ovn_ctrl_lflow *ctrl_lflow;
+
+    HMAP_FOR_EACH (ctrl_lflow, hmap_node, ctrl_lflows) {
+        bool ingress = (ovn_stage_get_pipeline(ctrl_lflow->stage) == P_IN);
+        uint8_t table_id = ovn_stage_get_table(ctrl_lflow->stage);
+        uint32_t dp_key =
+            ctrl_lflow->dp_key ? ctrl_lflow->dp_key : dp->tunnel_key;
+        consider_logical_flow__(ctrl_lflow, ingress, table_id, dp_key,
+                                 &dhcp_opts, &dhcpv6_opts,
+                                 &nd_ra_opts, &controller_event_opts,
+                                 l_ctx_in, l_ctx_out);
+    }
+
+    dhcp_opts_destroy(&dhcp_opts);
+    dhcp_opts_destroy(&dhcpv6_opts);
+    nd_ra_opts_destroy(&nd_ra_opts);
+    controller_event_opts_destroy(&controller_event_opts);
+}
+
+void
+lflow_remove_ctrl_lflows(struct hmap *ctrl_lflows,
+                         struct ovn_desired_flow_table *flow_table)
+{
+    struct hmap flood_remove_nodes = HMAP_INITIALIZER(&flood_remove_nodes);
+
+    struct ovn_ctrl_lflow *ctrl_lflow;
+    HMAP_FOR_EACH (ctrl_lflow, hmap_node, ctrl_lflows) {
+        ofctrl_flood_remove_add_node(&flood_remove_nodes, &ctrl_lflow->uuid_);
+    }
+
+    ofctrl_flood_remove_flows(flow_table, &flood_remove_nodes);
+
+    struct ofctrl_flood_remove_node *ofrn;
+    HMAP_FOR_EACH_POP (ofrn, hmap_node, &flood_remove_nodes) {
+        free(ofrn);
+    }
+    hmap_destroy(&flood_remove_nodes);
+}
diff --git a/controller/lflow.h b/controller/lflow.h
index e98edf81df..0c73d3e6d0 100644
--- a/controller/lflow.h
+++ b/controller/lflow.h
@@ -54,6 +54,7 @@ struct sbrec_port_binding;
 struct simap;
 struct sset;
 struct uuid;
+struct ovn_ctrl_lflow;
 
 /* OpenFlow table numbers.
  *
@@ -182,4 +183,11 @@ bool lflow_handle_flows_for_lport(const struct sbrec_port_binding *,
                                   struct lflow_ctx_out *);
 bool lflow_handle_changed_mc_groups(struct lflow_ctx_in *,
                                     struct lflow_ctx_out *);
+
+void lflow_process_ctrl_lflows(struct hmap *ctrl_lflows,
+                               const struct sbrec_datapath_binding *,
+                               struct lflow_ctx_in *,
+                               struct lflow_ctx_out *);
+void lflow_remove_ctrl_lflows(struct hmap *ctrl_lflows,
+                              struct ovn_desired_flow_table *);
 #endif /* controller/lflow.h */
diff --git a/controller/lport.c b/controller/lport.c
index 478fcfd829..962f07ebb4 100644
--- a/controller/lport.c
+++ b/controller/lport.c
@@ -23,22 +23,6 @@
 #include "lib/ovn-sb-idl.h"
 VLOG_DEFINE_THIS_MODULE(lport);
 
-const struct sbrec_port_binding *
-lport_lookup_by_name(struct ovsdb_idl_index *sbrec_port_binding_by_name,
-                     const char *name)
-{
-    struct sbrec_port_binding *pb = sbrec_port_binding_index_init_row(
-        sbrec_port_binding_by_name);
-    sbrec_port_binding_index_set_logical_port(pb, name);
-
-    const struct sbrec_port_binding *retval = sbrec_port_binding_index_find(
-        sbrec_port_binding_by_name, pb);
-
-    sbrec_port_binding_index_destroy_row(pb);
-
-    return retval;
-}
-
 const struct sbrec_port_binding *
 lport_lookup_by_key(struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
                     struct ovsdb_idl_index *sbrec_port_binding_by_key,
diff --git a/controller/lport.h b/controller/lport.h
index 345efc1840..76e9c42460 100644
--- a/controller/lport.h
+++ b/controller/lport.h
@@ -34,10 +34,6 @@ struct sset;
  * instead we define our own indexes.
  */
 
-const struct sbrec_port_binding *lport_lookup_by_name(
-    struct ovsdb_idl_index *sbrec_port_binding_by_name,
-    const char *name);
-
 const struct sbrec_port_binding *lport_lookup_by_key(
     struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
     struct ovsdb_idl_index *sbrec_port_binding_by_key,
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index 3968ef0597..b5478f3ded 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -36,6 +36,7 @@
 #include "if-status.h"
 #include "ip-mcast.h"
 #include "openvswitch/hmap.h"
+#include "ldata.h"
 #include "lflow.h"
 #include "lflow-cache.h"
 #include "lib/vswitch-idl.h"
@@ -47,9 +48,12 @@
 #include "openvswitch/vlog.h"
 #include "ovn/actions.h"
 #include "ovn/features.h"
+#include "lflow-generate.h"
 #include "lib/chassis-index.h"
 #include "lib/extend-table.h"
 #include "lib/ip-mcast-index.h"
+#include "lib/lb.h"
+#include "lib/lflow.h"
 #include "lib/mcast-group-index.h"
 #include "lib/ovn-sb-idl.h"
 #include "lib/ovn-util.h"
@@ -124,15 +128,6 @@ struct pending_pkt {
 /* Registered ofctrl seqno type for nb_cfg propagation. */
 static size_t ofctrl_seq_type_nb_cfg;
 
-struct local_datapath *
-get_local_datapath(const struct hmap *local_datapaths, uint32_t tunnel_key)
-{
-    struct hmap_node *node = hmap_first_with_hash(local_datapaths, tunnel_key);
-    return (node
-            ? CONTAINER_OF(node, struct local_datapath, hmap_node)
-            : NULL);
-}
-
 uint32_t
 get_tunnel_type(const char *name)
 {
@@ -1020,6 +1015,9 @@ struct ed_type_runtime_data {
     struct sset egress_ifaces;
     struct smap local_iface_ids;
 
+    /* Load balancer data - hmap of 'struct local_load_balancer'. */
+    struct hmap local_load_balancers;
+
     /* Tracked data. See below for more details and comments. */
     bool tracked;
     bool local_lports_changed;
@@ -1033,14 +1031,14 @@ struct ed_type_runtime_data {
  *
  *  ------------------------------------------------------------------------
  * |                      | This is a hmap of                               |
- * |                      | 'struct tracked_binding_datapath' defined in    |
+ * |                      | 'struct tracked_datapath' defined in            |
  * |                      | binding.h. Runtime data handlers for OVS        |
  * |                      | 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 'lbinding_data').    |
+ * |                      | (added/updated/deleted in 'lbinding_data').     |
  * |                      | So any changes to the runtime data -            |
- * |                      | local_datapaths and lbinding_data is captured  |
+ * |                      | local_datapaths and lbinding_data is captured   |
  * |                      | here.                                           |
  *  ------------------------------------------------------------------------
  * |                      | This is a bool which represents if the runtime  |
@@ -1067,7 +1065,7 @@ struct ed_type_runtime_data {
  *
  *  ---------------------------------------------------------------------
  * | local_datapaths  | The changes to these runtime data is captured in |
- * | lbinding_data   | 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  |
@@ -1092,7 +1090,7 @@ en_runtime_data_clear_tracked_data(void *data_)
 {
     struct ed_type_runtime_data *data = data_;
 
-    binding_tracked_dp_destroy(&data->tracked_dp_bindings);
+    tracked_datapaths_destroy(&data->tracked_dp_bindings);
     hmap_init(&data->tracked_dp_bindings);
     data->local_lports_changed = false;
     data->tracked = false;
@@ -1111,6 +1109,7 @@ en_runtime_data_init(struct engine_node *node OVS_UNUSED,
     sset_init(&data->egress_ifaces);
     smap_init(&data->local_iface_ids);
     local_binding_data_init(&data->lbinding_data);
+    hmap_init(&data->local_load_balancers);
 
     /* Init the tracked data. */
     hmap_init(&data->tracked_dp_bindings);
@@ -1128,15 +1127,9 @@ en_runtime_data_cleanup(void *data)
     sset_destroy(&rt_data->active_tunnels);
     sset_destroy(&rt_data->egress_ifaces);
     smap_destroy(&rt_data->local_iface_ids);
-    struct local_datapath *cur_node, *next_node;
-    HMAP_FOR_EACH_SAFE (cur_node, next_node, hmap_node,
-                        &rt_data->local_datapaths) {
-        free(cur_node->peer_ports);
-        hmap_remove(&rt_data->local_datapaths, &cur_node->hmap_node);
-        free(cur_node);
-    }
-    hmap_destroy(&rt_data->local_datapaths);
+    local_datapaths_destroy(&rt_data->local_datapaths);
     local_binding_data_destroy(&rt_data->lbinding_data);
+    local_load_balancers_destroy(&rt_data->local_load_balancers);
 }
 
 static void
@@ -1240,19 +1233,16 @@ en_runtime_data_run(struct engine_node *node, void *data)
         /* don't cleanup since there is no data yet */
         first_run = false;
     } else {
-        struct local_datapath *cur_node, *next_node;
-        HMAP_FOR_EACH_SAFE (cur_node, next_node, hmap_node, local_datapaths) {
-            free(cur_node->peer_ports);
-            hmap_remove(local_datapaths, &cur_node->hmap_node);
-            free(cur_node);
-        }
-        hmap_clear(local_datapaths);
+        local_datapaths_destroy(local_datapaths);
         local_binding_data_destroy(&rt_data->lbinding_data);
+        local_load_balancers_destroy(&rt_data->local_load_balancers);
         sset_destroy(local_lports);
         sset_destroy(local_lport_ids);
         sset_destroy(active_tunnels);
         sset_destroy(&rt_data->egress_ifaces);
         smap_destroy(&rt_data->local_iface_ids);
+        hmap_init(local_datapaths);
+        hmap_init(&rt_data->local_load_balancers);
         sset_init(local_lports);
         sset_init(local_lport_ids);
         sset_init(active_tunnels);
@@ -1703,12 +1693,12 @@ port_groups_runtime_data_handler(struct engine_node *node, void *data)
                                                  pg_sb->name);
         ovs_assert(pg_lports);
 
-        struct tracked_binding_datapath *tdp;
+        struct tracked_datapath *tdp;
         bool need_update = false;
         HMAP_FOR_EACH (tdp, node, &rt_data->tracked_dp_bindings) {
             struct shash_node *shash_node;
             SHASH_FOR_EACH (shash_node, &tdp->lports) {
-                struct tracked_binding_lport *lport = shash_node->data;
+                struct tracked_lport *lport = shash_node->data;
                 if (sset_contains(pg_lports, lport->pb->logical_port)) {
                     /* At least one local port-binding change is related to the
                      * port_group, so the port_group_cs_local needs update. */
@@ -1862,9 +1852,9 @@ ct_zones_runtime_data_handler(struct engine_node *node, void *data OVS_UNUSED)
     }
 
     struct hmap *tracked_dp_bindings = &rt_data->tracked_dp_bindings;
-    struct tracked_binding_datapath *tdp;
+    struct tracked_datapath *tdp;
     HMAP_FOR_EACH (tdp, node, tracked_dp_bindings) {
-        if (tdp->is_new) {
+        if (tdp->tracked_type == TRACKED_RESOURCE_NEW) {
             /* A new datapath has been added. Fall back to full recompute. */
             return false;
         }
@@ -1936,6 +1926,426 @@ struct ed_type_lflow_output {
     struct lflow_output_persistent_data pd;
 };
 
+struct ed_type_lflow_needs_generation {
+    /* Tracked data. */
+    bool tracked;
+    struct hmap tracked_datapaths;
+    struct hmap tracked_lbs;
+};
+
+static void *
+en_lflow_needs_generation_init(struct engine_node *node OVS_UNUSED,
+                               struct engine_arg *arg OVS_UNUSED)
+{
+    struct ed_type_lflow_needs_generation *data = xzalloc(sizeof *data);
+    hmap_init(&data->tracked_datapaths);
+    hmap_init(&data->tracked_lbs);
+    return data;
+}
+
+static void
+en_lflow_needs_generation_cleanup(void *data OVS_UNUSED)
+{
+
+}
+
+static void
+en_lflow_needs_generation_clear_tracked_data(void *data)
+{
+    struct ed_type_lflow_needs_generation *lflow_need_gen = data;
+    lflow_need_gen->tracked = false;
+    tracked_datapaths_destroy(&lflow_need_gen->tracked_datapaths);
+    hmap_init(&lflow_need_gen->tracked_datapaths);
+
+    tracked_lbs_destroy(&lflow_need_gen->tracked_lbs);
+    hmap_init(&lflow_need_gen->tracked_lbs);
+}
+
+static void
+en_lflow_needs_generation_run(struct engine_node *node OVS_UNUSED,
+                              void *data)
+{
+    struct ed_type_lflow_needs_generation *lflow_need_gen = data;
+    lflow_need_gen->tracked = false;
+
+    struct sbrec_load_balancer_table *lb_table =
+        (struct sbrec_load_balancer_table *)EN_OVSDB_GET(
+            engine_get_input("SB_load_balancer", node));
+
+    struct ed_type_runtime_data *rt_data =
+        engine_get_input_data("runtime_data", node);
+
+    const struct sbrec_load_balancer *sb_lb;
+    SBREC_LOAD_BALANCER_TABLE_FOR_EACH (sb_lb, lb_table) {
+        local_load_balancer_add(&rt_data->local_load_balancers,
+                                &rt_data->local_datapaths, sb_lb);
+    }
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+static bool
+lflow_needs_generation_runtime_data_handler(struct engine_node *node,
+                                            void *data)
+{
+    struct ed_type_runtime_data *rt_data =
+        engine_get_input_data("runtime_data", node);
+
+    /* There is no tracked data. Fall back to full recompute of
+     * lflow_needs_generation. */
+    if (!rt_data->tracked) {
+        return false;
+    }
+
+    struct hmap *tracked_dp_bindings = &rt_data->tracked_dp_bindings;
+    if (hmap_is_empty(tracked_dp_bindings)) {
+        return true;
+    }
+
+    struct ed_type_lflow_needs_generation *lflow_need_gen = data;
+    lflow_need_gen->tracked = true;
+    struct tracked_datapath *tdp;
+
+    HMAP_FOR_EACH (tdp, node, tracked_dp_bindings) {
+        if (tdp->tracked_type == TRACKED_RESOURCE_NEW) {
+            struct local_datapath *ld =
+                get_local_datapath(&rt_data->local_datapaths,
+                                   tdp->dp->tunnel_key);
+            ovs_assert(ld);
+            tracked_datapath_add(ld->datapath, TRACKED_RESOURCE_NEW,
+                                 &lflow_need_gen->tracked_datapaths);
+            for (size_t i = 0; i < ld->datapath->n_load_balancers; i++) {
+                const struct sbrec_load_balancer *sb_lb =
+                    ld->datapath->load_balancers[i];
+                struct local_load_balancer *local_lb =
+                    local_load_balancer_get(&rt_data->local_load_balancers,
+                                            &sb_lb->header_.uuid);
+                if (!local_lb) {
+                    local_lb = local_load_balancer_add(
+                        &rt_data->local_load_balancers,
+                        &rt_data->local_datapaths, sb_lb);
+                }
+                tracked_lb_add(local_lb, TRACKED_RESOURCE_NEW,
+                               &lflow_need_gen->tracked_lbs);
+            }
+        } else if (tdp->tracked_type == TRACKED_RESOURCE_UPDATED) {
+            struct local_datapath *ld =
+                get_local_datapath(&rt_data->local_datapaths,
+                                   tdp->dp->tunnel_key);
+            if (!ld) {
+                continue;
+            }
+
+            struct shash_node *shash_node;
+            SHASH_FOR_EACH (shash_node, &tdp->lports) {
+                struct tracked_lport *lport = shash_node->data;
+                if (lport->tracked_type == TRACKED_RESOURCE_REMOVED) {
+                    tracked_datapath_lport_add(
+                        lport->pb, TRACKED_RESOURCE_REMOVED,
+                        &lflow_need_gen->tracked_datapaths);
+                } else {
+                    if (lflow_lport_needs_generation(ld, lport->pb)) {
+                        tracked_datapath_lport_add(
+                            lport->pb, TRACKED_RESOURCE_NEW,
+                            &lflow_need_gen->tracked_datapaths);
+                    }
+                }
+            }
+        }
+    }
+
+    if (!hmap_is_empty(&lflow_need_gen->tracked_datapaths)) {
+        engine_set_node_state(node, EN_UPDATED);
+    }
+
+    return true;
+}
+
+static bool
+lflow_needs_generation_datapath_binding_handler(struct engine_node *node,
+                                                void *data)
+{
+    struct ed_type_runtime_data *rt_data =
+        engine_get_input_data("runtime_data", node);
+    struct sbrec_datapath_binding_table *dp_table =
+        (struct sbrec_datapath_binding_table *)EN_OVSDB_GET(
+            engine_get_input("SB_datapath_binding", node));
+
+    struct ed_type_lflow_needs_generation *lflow_need_gen = data;
+    lflow_need_gen->tracked = true;
+
+    const struct sbrec_datapath_binding *dp;
+    SBREC_DATAPATH_BINDING_TABLE_FOR_EACH_TRACKED (dp, dp_table) {
+        if (sbrec_datapath_binding_is_new(dp) ||
+                sbrec_datapath_binding_is_deleted(dp)) {
+            continue;
+        }
+
+        struct local_datapath *ldp = get_local_datapath(
+            &rt_data->local_datapaths, dp->tunnel_key);
+        if (ldp && lflow_datapath_needs_generation(ldp)) {
+            tracked_datapath_add(ldp->datapath, TRACKED_RESOURCE_NEW,
+                                 &lflow_need_gen->tracked_datapaths);
+        }
+    }
+
+    if (!hmap_is_empty(&lflow_need_gen->tracked_datapaths)) {
+        engine_set_node_state(node, EN_UPDATED);
+    }
+
+    return true;
+}
+
+static bool
+lflow_needs_generation_port_binding_handler(struct engine_node *node,
+                                            void *data)
+{
+    struct ed_type_runtime_data *rt_data =
+        engine_get_input_data("runtime_data", node);
+    struct sbrec_port_binding_table *pb_table =
+        (struct sbrec_port_binding_table *)EN_OVSDB_GET(
+            engine_get_input("SB_port_binding", node));
+
+    struct ed_type_lflow_needs_generation *lflow_need_gen = data;
+    lflow_need_gen->tracked = true;
+
+    const struct sbrec_port_binding *pb;
+    SBREC_PORT_BINDING_TABLE_FOR_EACH_TRACKED (pb, pb_table) {
+        struct local_datapath *ldp;
+        ldp = get_local_datapath(&rt_data->local_datapaths,
+                                 pb->datapath->tunnel_key);
+        if (!ldp) {
+            continue;
+        }
+
+        if (sbrec_port_binding_is_deleted(pb)) {
+            tracked_datapath_lport_add(pb, TRACKED_RESOURCE_REMOVED,
+                                       &lflow_need_gen->tracked_datapaths);
+        } else {
+            if (lflow_lport_needs_generation(ldp, pb)) {
+                tracked_datapath_lport_add(
+                    pb, sbrec_port_binding_is_new(pb) ? TRACKED_RESOURCE_NEW :
+                    TRACKED_RESOURCE_UPDATED,
+                    &lflow_need_gen->tracked_datapaths);
+            }
+        }
+    }
+
+    if (!hmap_is_empty(&lflow_need_gen->tracked_datapaths)) {
+        engine_set_node_state(node, EN_UPDATED);
+    }
+
+    return true;
+}
+
+static bool
+lflow_needs_generation_load_balancer_handler(struct engine_node *node  OVS_UNUSED,
+                                             void *data OVS_UNUSED)
+{
+    struct ed_type_runtime_data *rt_data =
+        engine_get_input_data("runtime_data", node);
+    struct sbrec_load_balancer_table *lb_table =
+        (struct sbrec_load_balancer_table *)EN_OVSDB_GET(
+            engine_get_input("SB_load_balancer", node));
+
+    struct ed_type_lflow_needs_generation *lflow_need_gen = data;
+    lflow_need_gen->tracked = true;
+
+    const struct sbrec_load_balancer *sb_lb;
+    SBREC_LOAD_BALANCER_TABLE_FOR_EACH_TRACKED (sb_lb, lb_table) {
+        struct local_load_balancer *local_lb;
+        if (sbrec_load_balancer_is_deleted(sb_lb)) {
+            local_lb = local_load_balancer_get(&rt_data->local_load_balancers,
+                                               &sb_lb->header_.uuid);
+            if (local_lb) {
+                tracked_lb_add(local_lb, TRACKED_RESOURCE_REMOVED,
+                               &lflow_need_gen->tracked_lbs);
+            }
+        } else {
+            local_lb = local_load_balancer_get(&rt_data->local_load_balancers,
+                                               &sb_lb->header_.uuid);
+            if (!local_lb) {
+                local_lb = local_load_balancer_add(
+                    &rt_data->local_load_balancers,
+                    &rt_data->local_datapaths, sb_lb);
+                if (local_lb) {
+                    tracked_lb_add(local_lb, TRACKED_RESOURCE_NEW,
+                                   &lflow_need_gen->tracked_lbs);
+                }
+            } else {
+                if (lflow_load_balancer_needs_gen(local_lb)) {
+                    tracked_lb_add(local_lb, TRACKED_RESOURCE_UPDATED,
+                                   &lflow_need_gen->tracked_lbs);
+                }
+            }
+        }
+    }
+
+    if (!hmap_is_empty(&lflow_need_gen->tracked_lbs)) {
+        engine_set_node_state(node, EN_UPDATED);
+    }
+
+    return true;
+}
+
+struct ed_type_lflow_generate {
+    struct hmap generic_lswitch_lflows;
+    struct hmap generic_lrouter_lflows;
+
+    /* Tracked data. */
+    bool tracked;
+    struct hmap tracked_datapaths;
+    struct hmap tracked_lbs;
+};
+
+static void *
+en_lflow_generate_init(struct engine_node *node OVS_UNUSED,
+                       struct engine_arg *arg OVS_UNUSED)
+{
+    struct ed_type_lflow_generate *data = xzalloc(sizeof *data);
+
+    hmap_init(&data->generic_lswitch_lflows);
+    hmap_init(&data->generic_lrouter_lflows);
+
+    build_lswitch_generic_lflows(&data->generic_lswitch_lflows);
+    build_lrouter_generic_lflows(&data->generic_lrouter_lflows);
+
+    hmap_init(&data->tracked_datapaths);
+    hmap_init(&data->tracked_lbs);
+    return data;
+}
+
+static void
+en_lflow_generate_cleanup(void *data)
+{
+    struct ed_type_lflow_generate *lflow_generate_data = data;
+
+    ovn_ctrl_lflows_destroy(&lflow_generate_data->generic_lswitch_lflows);
+    ovn_ctrl_lflows_destroy(&lflow_generate_data->generic_lrouter_lflows);
+}
+
+static void
+en_lflow_generate_clear_tracked_data(void *data)
+{
+    struct ed_type_lflow_generate *lflow_gen = data;
+    lflow_gen->tracked = false;
+
+    tracked_datapaths_destroy(&lflow_gen->tracked_datapaths);
+    hmap_init(&lflow_gen->tracked_datapaths);
+
+    tracked_lbs_destroy(&lflow_gen->tracked_lbs);
+    hmap_init(&lflow_gen->tracked_lbs);
+}
+
+static void
+en_lflow_generate_run(struct engine_node *node OVS_UNUSED,
+                      void *data OVS_UNUSED)
+{
+    struct ed_type_runtime_data *rt_data =
+        engine_get_input_data("runtime_data", node);
+
+    struct ed_type_lflow_generate *lflow_gen = data;
+    lflow_gen->tracked = false;
+
+    lflow_delete_generated_lflows(&rt_data->local_datapaths,
+                                  &rt_data->local_load_balancers);
+    lflow_generate_run(&rt_data->local_datapaths,
+                       &rt_data->local_load_balancers);
+
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+static bool
+lflow_generate_lflow_needs_generation_handler(struct engine_node *node,
+                                             void *data)
+{
+    struct ed_type_lflow_needs_generation *need_lflow_gen =
+        engine_get_input_data("lflow_needs_generation", node);
+
+    struct ed_type_runtime_data *rt_data =
+        engine_get_input_data("runtime_data", node);
+
+    /* There is no tracked data. Fall back to full recompute of
+     * flow_output. */
+    if (!need_lflow_gen->tracked) {
+        return false;
+    }
+
+    if (hmap_is_empty(&need_lflow_gen->tracked_datapaths) &&
+        hmap_is_empty(&need_lflow_gen->tracked_lbs)) {
+        return true;
+    }
+
+    struct ed_type_lflow_generate *lflow_gen = data;
+    lflow_gen->tracked = true;
+
+    struct tracked_datapath *tdp;
+    HMAP_FOR_EACH (tdp, node, &need_lflow_gen->tracked_datapaths) {
+        struct local_datapath *ld =
+            get_local_datapath(&rt_data->local_datapaths, tdp->dp->tunnel_key);
+        ovs_assert(ld);
+
+        switch (tdp->tracked_type) {
+        case TRACKED_RESOURCE_NEW:
+            lflow_generate_datapath_flows(ld, true);
+            tracked_datapath_add(ld->datapath, TRACKED_RESOURCE_NEW,
+                                 &lflow_gen->tracked_datapaths);
+            break;
+
+        case TRACKED_RESOURCE_UPDATED: {
+            struct shash_node *shash_node;
+            SHASH_FOR_EACH (shash_node, &tdp->lports) {
+                struct tracked_lport *t_lport = shash_node->data;
+                if (t_lport->tracked_type == TRACKED_RESOURCE_REMOVED) {
+                    lflow_delete_generated_lport_lflows(t_lport->pb, ld);
+                    tracked_datapath_lport_add(
+                        t_lport->pb, TRACKED_RESOURCE_REMOVED,
+                        &lflow_gen->tracked_datapaths);
+                } else {
+                    lflow_generate_lport_flows(t_lport->pb, ld);
+                    tracked_datapath_lport_add(t_lport->pb,
+                                               TRACKED_RESOURCE_NEW,
+                                               &lflow_gen->tracked_datapaths);
+                    }
+            }
+            break;
+        }
+        case TRACKED_RESOURCE_REMOVED:
+            OVS_NOT_REACHED();
+        }
+    }
+
+    struct tracked_lb *tlb;
+    HMAP_FOR_EACH (tlb, node, &need_lflow_gen->tracked_lbs) {
+        switch (tlb->tracked_type) {
+        case TRACKED_RESOURCE_NEW:
+            lflow_generate_load_balancer_lflows(tlb->local_lb);
+            tracked_lb_add(tlb->local_lb, TRACKED_RESOURCE_NEW,
+                           &lflow_gen->tracked_lbs);
+            break;
+
+        case TRACKED_RESOURCE_UPDATED:
+            lflow_clear_generated_lb_lflows(tlb->local_lb);
+            lflow_generate_load_balancer_lflows(tlb->local_lb);
+            tracked_lb_add(tlb->local_lb, TRACKED_RESOURCE_UPDATED,
+                           &lflow_gen->tracked_lbs);
+            break;
+
+        case TRACKED_RESOURCE_REMOVED:
+            lflow_clear_generated_lb_lflows(tlb->local_lb);
+            tracked_lb_add(tlb->local_lb, TRACKED_RESOURCE_REMOVED,
+                           &lflow_gen->tracked_lbs);
+            break;
+        }
+    }
+
+    if (!hmap_is_empty(&lflow_gen->tracked_datapaths) ||
+            !hmap_is_empty(&lflow_gen->tracked_lbs)) {
+        engine_set_node_state(node, EN_UPDATED);
+    }
+
+    return true;
+}
+
 static void
 init_lflow_ctx(struct engine_node *node,
                struct ed_type_runtime_data *rt_data,
@@ -2079,6 +2489,8 @@ en_lflow_output_run(struct engine_node *node, void *data)
 {
     struct ed_type_runtime_data *rt_data =
         engine_get_input_data("runtime_data", node);
+    struct ed_type_lflow_generate *lflow_gen_data =
+        engine_get_input_data("lflow_generate", node);
 
     struct ovsrec_open_vswitch_table *ovs_table =
         (struct ovsrec_open_vswitch_table *)EN_OVSDB_GET(
@@ -2128,6 +2540,7 @@ en_lflow_output_run(struct engine_node *node, void *data)
     struct lflow_ctx_in l_ctx_in;
     struct lflow_ctx_out l_ctx_out;
     init_lflow_ctx(node, rt_data, fo, &l_ctx_in, &l_ctx_out);
+
     lflow_run(&l_ctx_in, &l_ctx_out);
 
     if (l_ctx_out.conj_id_overflow) {
@@ -2146,6 +2559,47 @@ en_lflow_output_run(struct engine_node *node, void *data)
         }
     }
 
+    /* Disable cachin and lfrr for processing controller lflows. */
+    l_ctx_out.lfrr = NULL;
+    l_ctx_out.lflow_cache = NULL;
+
+    struct local_datapath *ld;
+    HMAP_FOR_EACH (ld, hmap_node, &rt_data->local_datapaths) {
+        lflow_process_ctrl_lflows(ld->active_lflows, ld->datapath,
+                                  &l_ctx_in, &l_ctx_out);
+
+        if (ld->is_switch) {
+            lflow_process_ctrl_lflows(&lflow_gen_data->generic_lswitch_lflows,
+                                      ld->datapath,
+                                      &l_ctx_in, &l_ctx_out);
+        } else {
+            lflow_process_ctrl_lflows(&lflow_gen_data->generic_lrouter_lflows,
+                                      ld->datapath,
+                                      &l_ctx_in, &l_ctx_out);
+        }
+
+        struct shash_node *shash_node;
+        SHASH_FOR_EACH (shash_node, &ld->lports) {
+            struct local_lport *lport = shash_node->data;
+            lflow_process_ctrl_lflows(lport->active_lflows,
+                                      lport->pb->datapath,
+                                      &l_ctx_in, &l_ctx_out);
+        }
+
+        for (size_t i = 0; i < ld->datapath->n_load_balancers; i++) {
+            const struct sbrec_load_balancer *slb =
+                ld->datapath->load_balancers[i];
+            struct local_load_balancer *local_lb =
+                local_load_balancer_get(&rt_data->local_load_balancers,
+                                        &slb->header_.uuid);
+            ovs_assert(local_lb);
+            struct hmap *lflows = ld->is_switch ? local_lb->active_lswitch_lflows :
+                                   local_lb->active_lrouter_lflows;
+            lflow_process_ctrl_lflows(lflows, ld->datapath, &l_ctx_in,
+                                      &l_ctx_out);
+        }
+    }
+
     engine_set_node_state(node, EN_UPDATED);
 }
 
@@ -2361,9 +2815,9 @@ lflow_output_runtime_data_handler(struct engine_node *node,
     struct ed_type_lflow_output *fo = data;
     init_lflow_ctx(node, rt_data, fo, &l_ctx_in, &l_ctx_out);
 
-    struct tracked_binding_datapath *tdp;
+    struct tracked_datapath *tdp;
     HMAP_FOR_EACH (tdp, node, tracked_dp_bindings) {
-        if (tdp->is_new) {
+        if (tdp->tracked_type == TRACKED_RESOURCE_NEW) {
             if (!lflow_add_flows_for_datapath(tdp->dp, &l_ctx_in,
                                               &l_ctx_out)) {
                 return false;
@@ -2371,7 +2825,7 @@ lflow_output_runtime_data_handler(struct engine_node *node,
         } else {
             struct shash_node *shash_node;
             SHASH_FOR_EACH (shash_node, &tdp->lports) {
-                struct tracked_binding_lport *lport = shash_node->data;
+                struct tracked_lport *lport = shash_node->data;
                 if (!lflow_handle_flows_for_lport(lport->pb, &l_ctx_in,
                                                   &l_ctx_out)) {
                     return false;
@@ -2418,6 +2872,135 @@ lflow_output_sb_fdb_handler(struct engine_node *node, void *data)
     return handled;
 }
 
+static bool
+lflow_output_lflow_generate_handler(struct engine_node *node, void *data)
+{
+    struct ed_type_lflow_generate *lflow_gen_data =
+        engine_get_input_data("lflow_generate", node);
+
+    if (!lflow_gen_data->tracked) {
+        return false;
+    }
+
+    struct ed_type_runtime_data *rt_data =
+        engine_get_input_data("runtime_data", node);
+    struct ed_type_lflow_output *fo = data;
+
+    struct lflow_ctx_in l_ctx_in;
+    struct lflow_ctx_out l_ctx_out;
+    init_lflow_ctx(node, rt_data, fo, &l_ctx_in, &l_ctx_out);
+
+    /* Disable cachin and lfrr for processing controller lflows. */
+    l_ctx_out.lfrr = NULL;
+    l_ctx_out.lflow_cache = NULL;
+
+    struct tracked_datapath *tdp;
+    HMAP_FOR_EACH (tdp, node, &lflow_gen_data->tracked_datapaths) {
+        struct local_datapath *ldp =
+            get_local_datapath(&rt_data->local_datapaths, tdp->dp->tunnel_key);
+        ovs_assert(ldp);
+
+        /* Right now this cannot happen.  When a local datapath is removed,
+         * it should result in full recompute. */
+        ovs_assert(tdp->tracked_type != TRACKED_RESOURCE_REMOVED);
+
+        if (!hmap_is_empty(ldp->cleared_lflows)) {
+            lflow_remove_ctrl_lflows(ldp->cleared_lflows, &fo->flow_table);
+            ovn_ctrl_lflows_clear(ldp->cleared_lflows);
+        }
+
+        if (tdp->tracked_type == TRACKED_RESOURCE_NEW) {
+            lflow_process_ctrl_lflows(ldp->active_lflows, ldp->datapath,
+                                      &l_ctx_in, &l_ctx_out);
+
+            if (ldp->is_switch) {
+                lflow_process_ctrl_lflows(
+                    &lflow_gen_data->generic_lswitch_lflows,
+                    ldp->datapath, &l_ctx_in, &l_ctx_out);
+            } else {
+                lflow_process_ctrl_lflows(
+                    &lflow_gen_data->generic_lrouter_lflows,
+                    ldp->datapath, &l_ctx_in, &l_ctx_out);
+            }
+
+            struct shash_node *shash_node;
+            SHASH_FOR_EACH (shash_node, &ldp->lports) {
+                struct local_lport *dp_lport = shash_node->data;
+                if (!hmap_is_empty(dp_lport->cleared_lflows)) {
+                    lflow_remove_ctrl_lflows(dp_lport->cleared_lflows,
+                                             &fo->flow_table);
+                    ovn_ctrl_lflows_clear(dp_lport->cleared_lflows);
+                }
+                lflow_process_ctrl_lflows(dp_lport->active_lflows,
+                                          dp_lport->pb->datapath,
+                                          &l_ctx_in, &l_ctx_out);
+            }
+        } else {
+            struct shash_node *shash_node;
+            SHASH_FOR_EACH (shash_node, &tdp->lports) {
+                struct tracked_lport *t_lport = shash_node->data;
+                struct local_lport *dp_lport =
+                    local_datapath_get_lport(ldp, t_lport->pb->logical_port);
+                ovs_assert(dp_lport);
+                if (!hmap_is_empty(dp_lport->cleared_lflows)) {
+                    lflow_remove_ctrl_lflows(dp_lport->cleared_lflows,
+                                             &fo->flow_table);
+                    ovn_ctrl_lflows_clear(dp_lport->cleared_lflows);
+                }
+
+                if (t_lport->tracked_type == TRACKED_RESOURCE_REMOVED) {
+                    local_datapath_remove_lport(ldp,
+                                                dp_lport->pb->logical_port);
+                } else {
+                    lflow_process_ctrl_lflows(dp_lport->active_lflows,
+                                              dp_lport->pb->datapath,
+                                              &l_ctx_in, &l_ctx_out);
+                }
+            }
+        }
+    }
+
+    struct tracked_lb *tlb;
+    HMAP_FOR_EACH (tlb, node, &lflow_gen_data->tracked_lbs) {
+        if (!hmap_is_empty(tlb->local_lb->cleared_lswitch_lflows)) {
+            lflow_remove_ctrl_lflows(
+                tlb->local_lb->cleared_lswitch_lflows, &fo->flow_table);
+            ovn_ctrl_lflows_clear(tlb->local_lb->cleared_lswitch_lflows);
+        }
+
+        if (!hmap_is_empty(tlb->local_lb->cleared_lrouter_lflows)) {
+            lflow_remove_ctrl_lflows(
+                tlb->local_lb->cleared_lrouter_lflows, &fo->flow_table);
+            ovn_ctrl_lflows_clear(tlb->local_lb->cleared_lrouter_lflows);
+        }
+
+        if (tlb->tracked_type == TRACKED_RESOURCE_REMOVED) {
+            local_load_balancer_remove(&rt_data->local_load_balancers,
+                                       tlb->local_lb);
+        } else {
+            const struct sbrec_load_balancer *slb = tlb->local_lb->ovn_lb->slb;
+
+            for (size_t i = 0; i < slb->n_datapaths; i++) {
+                struct local_datapath *ldp =
+                    get_local_datapath(&rt_data->local_datapaths,
+                                       slb->datapaths[i]->tunnel_key);
+                if (!ldp) {
+                    continue;
+                }
+
+                struct hmap *lflows =
+                    ldp->is_switch ? tlb->local_lb->active_lswitch_lflows :
+                    tlb->local_lb->active_lrouter_lflows;
+                lflow_process_ctrl_lflows(lflows, ldp->datapath, &l_ctx_in,
+                                          &l_ctx_out);
+            }
+        }
+    }
+
+    engine_set_node_state(node, EN_UPDATED);
+    return true;
+}
+
 struct ed_type_pflow_output {
     /* Desired physical flows. */
     struct ovn_desired_flow_table flow_table;
@@ -2838,6 +3421,9 @@ main(int argc, char *argv[])
     ENGINE_NODE(flow_output, "flow_output");
     ENGINE_NODE(addr_sets, "addr_sets");
     ENGINE_NODE_WITH_CLEAR_TRACK_DATA(port_groups, "port_groups");
+    ENGINE_NODE_WITH_CLEAR_TRACK_DATA(lflow_needs_generation,
+                                      "lflow_needs_generation");
+    ENGINE_NODE_WITH_CLEAR_TRACK_DATA(lflow_generate, "lflow_generate");
 
 #define SB_NODE(NAME, NAME_STR) ENGINE_NODE_SB(NAME, NAME_STR);
     SB_NODES
@@ -2883,6 +3469,8 @@ main(int argc, char *argv[])
                      lflow_output_addr_sets_handler);
     engine_add_input(&en_lflow_output, &en_port_groups,
                      lflow_output_port_groups_handler);
+    engine_add_input(&en_lflow_output, &en_lflow_generate,
+                     lflow_output_lflow_generate_handler);
     engine_add_input(&en_lflow_output, &en_runtime_data,
                      lflow_output_runtime_data_handler);
 
@@ -2927,6 +3515,20 @@ main(int argc, char *argv[])
     engine_add_input(&en_ct_zones, &en_runtime_data,
                      ct_zones_runtime_data_handler);
 
+    engine_add_input(&en_lflow_needs_generation, &en_runtime_data,
+                     lflow_needs_generation_runtime_data_handler);
+    engine_add_input(&en_lflow_needs_generation, &en_sb_datapath_binding,
+                     lflow_needs_generation_datapath_binding_handler);
+    engine_add_input(&en_lflow_needs_generation, &en_sb_port_binding,
+                     lflow_needs_generation_port_binding_handler);
+    engine_add_input(&en_lflow_needs_generation, &en_sb_load_balancer,
+                     lflow_needs_generation_load_balancer_handler);
+
+    engine_add_input(&en_lflow_generate, &en_lflow_needs_generation,
+                     lflow_generate_lflow_needs_generation_handler);
+    engine_add_input(&en_lflow_generate, &en_runtime_data,
+                     engine_noop_handler);
+
     engine_add_input(&en_runtime_data, &en_ofctrl_is_connected, NULL);
 
     engine_add_input(&en_runtime_data, &en_ovs_open_vswitch, NULL);
diff --git a/controller/ovn-controller.h b/controller/ovn-controller.h
index 5d9466880b..172268f972 100644
--- a/controller/ovn-controller.h
+++ b/controller/ovn-controller.h
@@ -17,6 +17,8 @@
 #ifndef OVN_CONTROLLER_H
 #define OVN_CONTROLLER_H 1
 
+#include "ofctrl.h"
+
 #include "simap.h"
 #include "lib/ovn-sb-idl.h"
 
@@ -40,38 +42,6 @@ struct ct_zone_pending_entry {
     enum ct_zone_pending_state state;
 };
 
-/* A logical datapath that has some relevance to this hypervisor.  A logical
- * datapath D is relevant to hypervisor H if:
- *
- *     - Some VIF or l2gateway or l3gateway port in D is located on H.
- *
- *     - D is reachable over a series of hops across patch ports, starting from
- *       a datapath relevant to H.
- *
- * The 'hmap_node''s hash value is 'datapath->tunnel_key'. */
-struct local_datapath {
-    struct hmap_node hmap_node;
-    const struct sbrec_datapath_binding *datapath;
-
-    /* The localnet port in this datapath, if any (at most one is allowed). */
-    const struct sbrec_port_binding *localnet_port;
-
-    /* True if this datapath contains an l3gateway port located on this
-     * hypervisor. */
-    bool has_local_l3gateway;
-
-    struct {
-        const struct sbrec_port_binding *local;
-        const struct sbrec_port_binding *remote;
-    } *peer_ports;
-
-    size_t n_peer_ports;
-    size_t n_allocated_peer_ports;
-};
-
-struct local_datapath *get_local_datapath(const struct hmap *,
-                                          uint32_t tunnel_key);
-
 const struct ovsrec_bridge *get_bridge(const struct ovsrec_bridge_table *,
                                        const char *br_name);
 
diff --git a/controller/patch.c b/controller/patch.c
index e54b56354b..99a095c577 100644
--- a/controller/patch.c
+++ b/controller/patch.c
@@ -18,6 +18,7 @@
 #include "patch.h"
 
 #include "hash.h"
+#include "ldata.h"
 #include "lflow.h"
 #include "lib/vswitch-idl.h"
 #include "lport.h"
diff --git a/controller/physical.c b/controller/physical.c
index 17ca5afbbd..a029b32f8f 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -13,39 +13,47 @@
  * limitations under the License.
  */
 
+/* OVS includes. */
 #include <config.h>
 #include "binding.h"
 #include "coverage.h"
 #include "byte-order.h"
-#include "encaps.h"
 #include "flow.h"
-#include "ha-chassis.h"
-#include "lflow.h"
-#include "lport.h"
-#include "chassis.h"
+#include "include/openvswitch/poll-loop.h"
+#include "include/openvswitch/list.h"
+#include "include/openvswitch/hmap.h"
+#include "include/openvswitch/match.h"
+#include "include/openvswitch/ofp-actions.h"
+#include "include/openvswitch/ofpbuf.h"
+#include "include/openvswitch/ofp-parse.h"
+#include "include/openvswitch/shash.h"
+#include "include/openvswitch/vlog.h"
 #include "lib/bundle.h"
-#include "openvswitch/poll-loop.h"
+#include "lib/hmapx.h"
 #include "lib/uuid.h"
-#include "ofctrl.h"
-#include "openvswitch/list.h"
-#include "openvswitch/hmap.h"
-#include "openvswitch/match.h"
-#include "openvswitch/ofp-actions.h"
-#include "openvswitch/ofpbuf.h"
-#include "openvswitch/vlog.h"
-#include "openvswitch/ofp-parse.h"
-#include "ovn-controller.h"
+#include "lib/simap.h"
+#include "lib/smap.h"
+#include "lib/sset.h"
+#include "lib/util.h"
+#include "vswitch-idl.h"
+
+
+/* OVN includes. */
+#include "binding.h"
+#include "chassis.h"
+#include "encaps.h"
+#include "ha-chassis.h"
+#include "ldata.h"
+#include "lflow.h"
 #include "lib/chassis-index.h"
 #include "lib/ovn-sb-idl.h"
 #include "lib/ovn-util.h"
+#include "lport.h"
+#include "ofctrl.h"
+#include "ovn-controller.h"
 #include "physical.h"
-#include "openvswitch/shash.h"
-#include "simap.h"
-#include "smap.h"
-#include "sset.h"
-#include "util.h"
-#include "vswitch-idl.h"
-#include "hmapx.h"
+
+
 
 VLOG_DEFINE_THIS_MODULE(physical);
 
@@ -270,7 +278,7 @@ put_remote_port_redirect_bridged(const struct
         uint32_t ls_dp_key = 0;
         for (int i = 0; i < ld->n_peer_ports; i++) {
             const struct sbrec_port_binding *sport_binding =
-                ld->peer_ports[i].remote;
+                ld->peer_ports[i].remote->pb;
             const char *sport_peer_name =
                 smap_get(&sport_binding->options, "peer");
             const char *distributed_port =
@@ -545,7 +553,7 @@ put_replace_chassis_mac_flows(const struct simap *ct_zones,
 
     for (int i = 0; i < ld->n_peer_ports; i++) {
         const struct sbrec_port_binding *rport_binding =
-            ld->peer_ports[i].remote;
+            ld->peer_ports[i].remote->pb;
         struct eth_addr router_port_mac;
         char *err_str = NULL;
         struct match match;
@@ -683,7 +691,7 @@ put_replace_router_port_mac_flows(struct ovsdb_idl_index
 
     for (int i = 0; i < ld->n_peer_ports; i++) {
         const struct sbrec_port_binding *rport_binding =
-            ld->peer_ports[i].remote;
+            ld->peer_ports[i].remote->pb;
         struct eth_addr router_port_mac;
         struct match match;
         struct ofpact_mac *replace_mac;
diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index 78ecfed840..e31b1a9c39 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -50,6 +50,7 @@
 #include "lib/mcast-group-index.h"
 #include "lib/ovn-l7.h"
 #include "lib/ovn-util.h"
+#include "lib/ldata.h"
 #include "ovn/logical-fields.h"
 #include "openvswitch/poll-loop.h"
 #include "openvswitch/rconn.h"
@@ -1184,7 +1185,7 @@ fill_ipv6_prefix_state(struct ovsdb_idl_txn *ovnsb_idl_txn,
     bool changed = false;
 
     for (size_t i = 0; i < ld->n_peer_ports; i++) {
-        const struct sbrec_port_binding *pb = ld->peer_ports[i].local;
+        const struct sbrec_port_binding *pb = ld->peer_ports[i].local->pb;
         struct ipv6_prefixd_state *pfd;
 
         if (!smap_get_bool(&pb->options, "ipv6_prefix", false)) {
@@ -1264,7 +1265,7 @@ prepare_ipv6_prefixd(struct ovsdb_idl_txn *ovnsb_idl_txn,
         }
 
         for (size_t i = 0; i < ld->n_peer_ports; i++) {
-            const struct sbrec_port_binding *pb = ld->peer_ports[i].local;
+            const struct sbrec_port_binding *pb = ld->peer_ports[i].local->pb;
             int j;
 
             if (!smap_get_bool(&pb->options, "ipv6_prefix_delegation",
@@ -3895,10 +3896,11 @@ prepare_ipv6_ras(const struct hmap *local_datapaths)
     HMAP_FOR_EACH (ld, hmap_node, local_datapaths) {
 
         for (size_t i = 0; i < ld->n_peer_ports; i++) {
-            const struct sbrec_port_binding *peer = ld->peer_ports[i].remote;
-            const struct sbrec_port_binding *pb = ld->peer_ports[i].local;
+            const struct sbrec_port_binding *peer = ld->peer_ports[i].remote->pb;
+            const struct sbrec_port_binding *pb = ld->peer_ports[i].local->pb;
 
-            if (!smap_get_bool(&pb->options, "ipv6_ra_send_periodic", false)) {
+            if (!smap_get_bool(&pb->options, "ipv6_ra_send_periodic",
+                               false)) {
                 continue;
             }
 
@@ -4132,8 +4134,8 @@ send_garp_locally(struct ovsdb_idl_txn *ovnsb_idl_txn,
 
     ovs_assert(ldp);
     for (size_t i = 0; i < ldp->n_peer_ports; i++) {
-        const struct sbrec_port_binding *local = ldp->peer_ports[i].local;
-        const struct sbrec_port_binding *remote = ldp->peer_ports[i].remote;
+        const struct sbrec_port_binding *local = ldp->peer_ports[i].local->pb;
+        const struct sbrec_port_binding *remote = ldp->peer_ports[i].remote->pb;
 
         /* Skip "ingress" port. */
         if (local == in_pb) {
@@ -4228,7 +4230,7 @@ run_buffered_binding(struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip,
 
         for (size_t i = 0; i < ld->n_peer_ports; i++) {
 
-            const struct sbrec_port_binding *pb = ld->peer_ports[i].local;
+            const struct sbrec_port_binding *pb = ld->peer_ports[i].local->pb;
             struct buffered_packets *cur_qp, *next_qp;
             HMAP_FOR_EACH_SAFE (cur_qp, next_qp, hmap_node,
                                 &buffered_packets_map) {
diff --git a/lib/automake.mk b/lib/automake.mk
index 917b28e1ed..6d26f78383 100644
--- a/lib/automake.mk
+++ b/lib/automake.mk
@@ -29,7 +29,11 @@ lib_libovn_la_SOURCES = \
 	lib/inc-proc-eng.c \
 	lib/inc-proc-eng.h \
 	lib/lb.c \
-	lib/lb.h
+	lib/lb.h \
+	lib/lflow.c \
+	lib/lflow.h \
+	lib/ldata.c \
+	lib/ldata.h
 nodist_lib_libovn_la_SOURCES = \
 	lib/ovn-dirs.c \
 	lib/ovn-nb-idl.c \
diff --git a/lib/lb.c b/lib/lb.c
index 4cb46b346c..942e665049 100644
--- a/lib/lb.c
+++ b/lib/lb.c
@@ -48,6 +48,7 @@ bool ovn_lb_vip_init(struct ovn_lb_vip *lb_vip, const char *lb_key,
     /* Format for backend ips: "IP1:port1,IP2:port2,...". */
     size_t n_backends = 0;
     size_t n_allocated_backends = 0;
+    lb_vip->backend_ips = xstrdup(lb_value);
     char *tokstr = xstrdup(lb_value);
     char *save_ptr = NULL;
     for (char *token = strtok_r(tokstr, ",", &save_ptr);
@@ -95,6 +96,7 @@ void ovn_lb_vip_destroy(struct ovn_lb_vip *vip)
         free(vip->backends[i].ip_str);
     }
     free(vip->backends);
+    free(vip->backend_ips);
 }
 
 static
@@ -276,6 +278,8 @@ ovn_controller_lb_create(const struct sbrec_load_balancer *sbrec_lb)
     SMAP_FOR_EACH (node, &sbrec_lb->vips) {
         struct ovn_lb_vip *lb_vip = &lb->vips[n_vips];
 
+        lb_vip->empty_backend_rej = smap_get_bool(&sbrec_lb->options,
+                                                  "reject", false);
         if (!ovn_lb_vip_init(lb_vip, node->key, node->value)) {
             continue;
         }
@@ -292,6 +296,28 @@ ovn_controller_lb_create(const struct sbrec_load_balancer *sbrec_lb)
                                            false);
     ovn_lb_get_hairpin_snat_ip(&sbrec_lb->header_.uuid, &sbrec_lb->options,
                                &lb->hairpin_snat_ips);
+
+    if (lb->slb->n_selection_fields) {
+        char *proto = NULL;
+        if (sbrec_lb->protocol && sbrec_lb->protocol[0]) {
+            proto = sbrec_lb->protocol;
+        }
+
+        struct ds sel_fields = DS_EMPTY_INITIALIZER;
+        for (size_t i = 0; i < lb->slb->n_selection_fields; i++) {
+            char *field = lb->slb->selection_fields[i];
+            if (!strcmp(field, "tp_src") && proto) {
+                ds_put_format(&sel_fields, "%s_src,", proto);
+            } else if (!strcmp(field, "tp_dst") && proto) {
+                ds_put_format(&sel_fields, "%s_dst,", proto);
+            } else {
+                ds_put_format(&sel_fields, "%s,", field);
+            }
+        }
+        ds_chomp(&sel_fields, ',');
+        lb->selection_fields = ds_steal_cstr(&sel_fields);
+    }
+
     return lb;
 }
 
@@ -303,5 +329,6 @@ ovn_controller_lb_destroy(struct ovn_controller_lb *lb)
     }
     free(lb->vips);
     destroy_lport_addresses(&lb->hairpin_snat_ips);
+    free(lb->selection_fields);
     free(lb);
 }
diff --git a/lib/lb.h b/lib/lb.h
index 58e6bb031b..64ef4a5197 100644
--- a/lib/lb.h
+++ b/lib/lb.h
@@ -55,6 +55,7 @@ struct ovn_lb_vip {
     struct ovn_lb_backend *backends;
     size_t n_backends;
     bool empty_backend_rej;
+    char *backend_ips;
 };
 
 struct ovn_lb_backend {
@@ -99,6 +100,7 @@ struct ovn_controller_lb {
                                               * as source for hairpinned
                                               * traffic.
                                               */
+    char *selection_fields;
 };
 
 struct ovn_controller_lb *ovn_controller_lb_create(
diff --git a/lib/ldata.c b/lib/ldata.c
new file mode 100644
index 0000000000..992f5f2e45
--- /dev/null
+++ b/lib/ldata.c
@@ -0,0 +1,895 @@
+/* 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>
+
+/* OVS includes. */
+#include "include/openvswitch/json.h"
+#include "lib/hmapx.h"
+#include "lib/util.h"
+#include "openvswitch/vlog.h"
+
+/* OVN includes. */
+#include "ldata.h"
+#include "lib/ovn-util.h"
+#include "lib/ovn-sb-idl.h"
+#include "lib/lflow.h"
+#include "lib/lb.h"
+
+VLOG_DEFINE_THIS_MODULE(ldata);
+
+static struct local_datapath *local_datapath_add__(
+    struct hmap *local_datapaths,
+    const struct sbrec_datapath_binding *,
+    struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+    struct ovsdb_idl_index *sbrec_port_binding_by_datapath,
+    struct ovsdb_idl_index *sbrec_port_binding_by_name,
+    int depth,
+    void (*datapath_added)(struct local_datapath *,
+                           void *aux),
+    void *aux);
+
+static void local_lport_init_cache(struct local_lport *);
+static void local_lport_update_lsp_data(struct local_lport *);
+static void local_lport_update_lrp_data(struct local_lport *);
+static void local_lport_destroy_lsp_data(struct local_lport *);
+static void local_lport_destroy_lrp_data(struct local_lport *);
+static void local_lport_init_lflow_gen_data(struct local_lport *);
+static void local_lport_destroy_lflow_gen_data(struct local_lport *);
+
+static struct tracked_datapath *tracked_datapath_create(
+    const struct sbrec_datapath_binding *dp,
+    enum en_tracked_resource_type tracked_type,
+    struct hmap *tracked_datapaths);
+
+static struct local_load_balancer *local_load_balancer_add__(
+    struct hmap *local_lbs, const struct sbrec_load_balancer *);
+static void local_load_balancer_destroy(struct local_load_balancer *);
+
+struct local_datapath *
+get_local_datapath(const struct hmap *local_datapaths, uint32_t tunnel_key)
+{
+    struct hmap_node *node = hmap_first_with_hash(local_datapaths, tunnel_key);
+    return (node
+            ? CONTAINER_OF(node, struct local_datapath, hmap_node)
+            : NULL);
+}
+
+struct local_datapath *
+local_datapath_alloc(const struct sbrec_datapath_binding *dp)
+{
+    struct local_datapath *ld = xzalloc(sizeof *ld);
+    ld->datapath = dp;
+    ld->is_switch = datapath_is_switch(dp);
+    hmap_init(&ld->ctrl_lflows[0]);
+    hmap_init(&ld->ctrl_lflows[1]);
+    ld->active_lflows = &ld->ctrl_lflows[0];
+    ld->cleared_lflows = &ld->ctrl_lflows[1];
+    shash_init(&ld->lports);
+    smap_clone(&ld->dp_options, &dp->options);
+    return ld;
+}
+
+void
+local_datapaths_destroy(struct hmap *local_datapaths)
+{
+    struct local_datapath *ld;
+    HMAP_FOR_EACH_POP (ld, hmap_node, local_datapaths) {
+        local_datapath_destroy(ld);
+    }
+
+    hmap_destroy(local_datapaths);
+}
+
+void
+local_datapath_destroy(struct local_datapath *ld)
+{
+    ovn_ctrl_lflows_destroy(&ld->ctrl_lflows[0]);
+    ovn_ctrl_lflows_destroy(&ld->ctrl_lflows[1]);
+
+    struct shash_node *node, *next;
+    SHASH_FOR_EACH_SAFE (node, next, &ld->lports) {
+        hmap_remove(&ld->lports.map, &node->node);
+        local_lport_destroy(node->data);
+        free(node->name);
+        free(node);
+    }
+
+    hmap_destroy(&ld->lports.map);
+    free(ld->peer_ports);
+    smap_destroy(&ld->dp_options);
+    free(ld);
+}
+
+void
+local_datapath_add(struct hmap *local_datapaths,
+                   const struct sbrec_datapath_binding *dp,
+                   struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+                   struct ovsdb_idl_index *sbrec_port_binding_by_datapath,
+                   struct ovsdb_idl_index *sbrec_port_binding_by_name,
+                   void (*datapath_added_cb)(
+                         struct local_datapath *ld,
+                         void *aux),
+                   void *aux)
+{
+    local_datapath_add__(local_datapaths, dp, sbrec_datapath_binding_by_key,
+                         sbrec_port_binding_by_datapath,
+                         sbrec_port_binding_by_name, 0,
+                         datapath_added_cb, aux);
+}
+
+void
+local_datapath_switch_lflow_map(struct local_datapath *ldp)
+{
+    struct hmap *temp = ldp->active_lflows;
+    ldp->active_lflows = ldp->cleared_lflows;
+    ldp->cleared_lflows = temp;
+
+    /* Make sure that the active_lflows is empty. */
+    ovs_assert(hmap_is_empty(ldp->active_lflows));
+}
+
+void
+local_datapath_add_or_update_peer_port(
+    const struct sbrec_port_binding *pb,
+    struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+    struct ovsdb_idl_index *sbrec_port_binding_by_datapath,
+    struct ovsdb_idl_index *sbrec_port_binding_by_name,
+    struct local_datapath *ld,
+    struct hmap *local_datapaths,
+    void (*datapath_added_cb)(
+                         struct local_datapath *ld,
+                         void *aux),
+    void *aux)
+{
+    struct local_lport *lport = local_datapath_get_lport(ld, pb->logical_port);
+    ovs_assert(lport);
+
+    const struct sbrec_port_binding *peer;
+    peer = lport_get_peer(pb, sbrec_port_binding_by_name);
+
+    if (!peer) {
+        if (lport->peer) {
+            /* The peer is updated.  Remove the lport and the removed 'peer'
+             * from the local datapath's peer ports. */
+            local_datapath_remove_peer_port(pb, ld, local_datapaths);
+        }
+        return;
+    }
+
+    if (lport->peer && lport->peer->pb != peer) {
+        /* The peer port is updated. Remove the old one. */
+        local_datapath_remove_peer_port(pb, ld, local_datapaths);
+    }
+
+    struct local_datapath *peer_ld =
+        get_local_datapath(local_datapaths, peer->datapath->tunnel_key);
+    if (!peer_ld){
+        peer_ld = local_datapath_add__(local_datapaths, peer->datapath,
+                                       sbrec_datapath_binding_by_key,
+                                       sbrec_port_binding_by_datapath,
+                                       sbrec_port_binding_by_name, 1,
+                                       datapath_added_cb, aux);
+    }
+
+    struct local_lport *peer_lport =
+        local_datapath_get_lport(peer_ld, peer->logical_port);
+
+    if (!peer_lport) {
+        return;
+    }
+
+    bool present = false;
+    for (size_t i = 0; i < ld->n_peer_ports; i++) {
+        if (ld->peer_ports[i].local == lport) {
+            present = true;
+            break;
+        }
+    }
+
+    if (!present) {
+        ld->n_peer_ports++;
+        if (ld->n_peer_ports > ld->n_allocated_peer_ports) {
+            ld->peer_ports =
+                x2nrealloc(ld->peer_ports,
+                           &ld->n_allocated_peer_ports,
+                           sizeof *ld->peer_ports);
+        }
+        ld->peer_ports[ld->n_peer_ports - 1].local = lport;
+        ld->peer_ports[ld->n_peer_ports - 1].remote = peer_lport;
+    }
+
+    lport->peer = peer_lport;
+    peer_lport->peer = lport;
+
+    for (size_t i = 0; i < peer_ld->n_peer_ports; i++) {
+        if (peer_ld->peer_ports[i].local == peer_lport) {
+            return;
+        }
+    }
+
+    peer_ld->n_peer_ports++;
+    if (peer_ld->n_peer_ports > peer_ld->n_allocated_peer_ports) {
+        peer_ld->peer_ports =
+            x2nrealloc(peer_ld->peer_ports,
+                        &peer_ld->n_allocated_peer_ports,
+                        sizeof *peer_ld->peer_ports);
+    }
+    peer_ld->peer_ports[peer_ld->n_peer_ports - 1].local = peer_lport;
+    peer_ld->peer_ports[peer_ld->n_peer_ports - 1].remote = lport;
+}
+
+void
+local_datapath_remove_peer_port(const struct sbrec_port_binding *pb,
+                                struct local_datapath *ld,
+                                struct hmap *local_datapaths)
+{
+    struct local_lport *lport = local_datapath_get_lport(ld, pb->logical_port);
+    if (!lport) {
+        return;
+    }
+
+    size_t i = 0;
+    for (i = 0; i < ld->n_peer_ports; i++) {
+        if (ld->peer_ports[i].local == lport) {
+            break;
+        }
+    }
+
+    if (i == ld->n_peer_ports) {
+        return;
+    }
+
+    struct local_lport *peer = ld->peer_ports[i].remote;
+
+    /* Possible improvement: We can shrink the allocated peer ports
+     * if (ld->n_peer_ports < ld->n_allocated_peer_ports / 2).
+     */
+    ld->peer_ports[i].local = ld->peer_ports[ld->n_peer_ports - 1].local;
+    ld->peer_ports[i].remote = ld->peer_ports[ld->n_peer_ports - 1].remote;
+    ld->n_peer_ports--;
+
+    struct local_datapath *peer_ld = peer->ldp;
+    if (peer_ld) {
+        /* Remove the peer port from the peer datapath. The peer
+         * datapath also tries to remove its peer lport, but that would
+         * be no-op. */
+        local_datapath_remove_peer_port(peer->pb, peer_ld, local_datapaths);
+    }
+
+    if (lport->peer) {
+        lport->peer->peer = NULL;
+    }
+    lport->peer = NULL;
+}
+
+struct local_lport *
+local_datapath_add_lport(struct local_datapath *ld,
+                         const char *lport_name,
+                         const struct sbrec_port_binding *pb)
+{
+    struct local_lport *dp_lport = local_datapath_get_lport(ld, lport_name);
+    if (!dp_lport) {
+        dp_lport = xzalloc(sizeof *dp_lport);
+        dp_lport->pb = pb;
+
+        hmap_init(&dp_lport->ctrl_lflows[0]);
+        hmap_init(&dp_lport->ctrl_lflows[1]);
+        dp_lport->active_lflows = &dp_lport->ctrl_lflows[0];
+        dp_lport->cleared_lflows = &dp_lport->ctrl_lflows[1];
+        smap_init(&dp_lport->options);
+        shash_add(&ld->lports, lport_name, dp_lport);
+        dp_lport->ldp = ld;
+        dp_lport->type = get_lport_type(pb);
+        local_lport_init_cache(dp_lport);
+    }
+
+    return dp_lport;
+}
+
+struct local_lport *
+local_datapath_get_lport(struct local_datapath *ld, const char *lport_name)
+{
+    struct shash_node *node = shash_find(&ld->lports, lport_name);
+    return node ? node->data : NULL;
+}
+
+void
+local_datapath_remove_lport(struct local_datapath *ld, const char *lport_name)
+{
+    struct local_lport *dp_lport = shash_find_and_delete(&ld->lports,
+                                                         lport_name);
+    if (dp_lport) {
+        local_lport_destroy(dp_lport);
+    }
+}
+
+bool
+local_lport_update_cache(struct local_lport *lport)
+{
+    if (local_lport_is_cache_old(lport)) {
+        local_lport_clear_cache(lport);
+        local_lport_init_cache(lport);
+        return true;
+    }
+
+    return false;
+}
+
+
+void
+local_lport_clear_cache(struct local_lport *lport)
+{
+    for (size_t i = 0; i < lport->n_addresses; i++) {
+        free(lport->addresses[i]);
+    }
+    free(lport->addresses);
+    lport->n_addresses = 0;
+    for (size_t i = 0; i < lport->n_port_security; i++) {
+        free(lport->port_security[i]);
+    }
+    free(lport->port_security);
+    lport->n_port_security = 0;
+
+    smap_destroy(&lport->options);
+
+    local_lport_destroy_lflow_gen_data(lport);
+}
+
+bool
+local_lport_is_cache_old(struct local_lport *lport)
+{
+    const struct sbrec_port_binding *pb = lport->pb;
+
+    if (lport->n_addresses != pb->n_mac) {
+        return true;
+    }
+
+    if (lport->n_port_security != pb->n_port_security) {
+        return true;
+    }
+
+    if (!smap_equal(&lport->options, &pb->options)) {
+        return true;
+    }
+
+    for (size_t i = 0; i < lport->n_addresses; i++) {
+        if (strcmp(lport->addresses[i], pb->mac[i])) {
+            return true;
+        }
+    }
+
+    for (size_t i = 0; i < lport->n_port_security; i++) {
+        if (strcmp(lport->port_security[i], pb->port_security[i])) {
+            return true;
+        }
+    }
+
+    bool claimed_ = !!pb->chassis;
+
+    return (lport->claimed != claimed_);
+}
+
+static void
+local_lport_init_lflow_gen_data(struct local_lport *lport)
+{
+    struct ds json_key = DS_EMPTY_INITIALIZER;
+    json_string_escape(lport->pb->logical_port, &json_key);
+    lport->json_key = ds_steal_cstr(&json_key);
+
+    if (lport->ldp->is_switch) {
+        local_lport_update_lsp_data(lport);
+    } else {
+        local_lport_update_lrp_data(lport);
+    }
+}
+
+static void
+local_lport_destroy_lflow_gen_data(struct local_lport *lport)
+{
+    free(lport->json_key);
+    lport->json_key = NULL;
+    if (lport->ldp->is_switch) {
+        local_lport_destroy_lsp_data(lport);
+    } else {
+        local_lport_destroy_lrp_data(lport);
+    }
+}
+
+void
+local_lport_switch_lflow_map(struct local_lport *lport)
+{
+    struct hmap *temp = lport->active_lflows;
+    lport->active_lflows = lport->cleared_lflows;
+    lport->cleared_lflows = temp;
+
+    /* Make sure that the active_lflows is empty. */
+    ovs_assert(hmap_is_empty(lport->active_lflows));
+}
+
+struct local_lport *
+local_datapath_unlink_lport(struct local_datapath *ld,
+                                                const char *lport_name)
+{
+    return shash_find_and_delete(&ld->lports, lport_name);
+}
+
+void
+local_lport_destroy(struct local_lport *dp_lport)
+{
+    ovn_ctrl_lflows_destroy(&dp_lport->ctrl_lflows[0]);
+    ovn_ctrl_lflows_destroy(&dp_lport->ctrl_lflows[1]);
+    local_lport_clear_cache(dp_lport);
+    free(dp_lport);
+}
+
+struct tracked_datapath *
+tracked_datapath_add(const struct sbrec_datapath_binding *dp,
+                     enum en_tracked_resource_type tracked_type,
+                     struct hmap *tracked_datapaths)
+{
+    struct tracked_datapath *t_dp =
+        tracked_datapath_find(tracked_datapaths, dp);
+    if (!t_dp) {
+        t_dp = tracked_datapath_create(dp, tracked_type, tracked_datapaths);
+    } else {
+        t_dp->tracked_type = tracked_type;
+    }
+
+    return t_dp;
+}
+
+struct tracked_datapath *
+tracked_datapath_find(struct hmap *tracked_datapaths,
+                      const struct sbrec_datapath_binding *dp)
+{
+    struct tracked_datapath *t_dp;
+    size_t hash = uuid_hash(&dp->header_.uuid);
+    HMAP_FOR_EACH_WITH_HASH (t_dp, node, hash, tracked_datapaths) {
+        if (uuid_equals(&t_dp->dp->header_.uuid, &dp->header_.uuid)) {
+            return t_dp;
+        }
+    }
+
+    return NULL;
+}
+
+void
+tracked_datapath_lport_add(const struct sbrec_port_binding *pb,
+                           enum en_tracked_resource_type tracked_type,
+                           struct hmap *tracked_datapaths)
+{
+    struct tracked_datapath *tracked_dp =
+        tracked_datapath_find(tracked_datapaths, pb->datapath);
+    if (!tracked_dp) {
+        tracked_dp = tracked_datapath_create(pb->datapath,
+                                             TRACKED_RESOURCE_UPDATED,
+                                             tracked_datapaths);
+    }
+
+    /* Check if the lport is already present or not.
+     * If it is already present, then just update the 'pb' field. */
+    struct tracked_lport *lport =
+        shash_find_data(&tracked_dp->lports, pb->logical_port);
+
+    if (!lport) {
+        lport = xmalloc(sizeof *lport);
+        shash_add(&tracked_dp->lports, pb->logical_port, lport);
+    }
+
+    lport->pb = pb;
+    lport->tracked_type = tracked_type;
+}
+
+void
+tracked_datapaths_destroy(struct hmap *tracked_datapaths)
+{
+    struct tracked_datapath *t_dp;
+    HMAP_FOR_EACH_POP (t_dp, node, tracked_datapaths) {
+        shash_destroy_free_data(&t_dp->lports);
+        free(t_dp);
+    }
+
+    hmap_destroy(tracked_datapaths);
+}
+
+struct local_load_balancer *
+local_load_balancer_add(struct hmap *local_lbs,
+                        struct hmap *local_datapaths,
+                        const struct sbrec_load_balancer *sb_lb)
+{
+    bool is_local_lb = false;
+    for (size_t i = 0; i < sb_lb->n_datapaths; i++) {
+        if (get_local_datapath(local_datapaths,
+                               sb_lb->datapaths[i]->tunnel_key)) {
+            is_local_lb = true;
+            break;
+        }
+    }
+
+    if (!is_local_lb) {
+        return NULL;
+    }
+
+    return local_load_balancer_add__(local_lbs, sb_lb);
+}
+
+struct local_load_balancer *
+local_load_balancer_get(struct hmap *lbs, const struct uuid *uuid)
+{
+    struct local_load_balancer *local_lb;
+    size_t hash = uuid_hash(uuid);
+    HMAP_FOR_EACH_WITH_HASH (local_lb, hmap_node, hash, lbs) {
+        if (uuid_equals(&local_lb->ovn_lb->slb->header_.uuid, uuid)) {
+            return local_lb;
+        }
+    }
+    return NULL;
+}
+
+void local_load_balancer_remove(struct hmap *local_lbs,
+                                struct local_load_balancer *local_lb)
+{
+    hmap_remove(local_lbs, &local_lb->hmap_node);
+    local_load_balancer_destroy(local_lb);
+}
+
+void
+local_load_balancers_destroy(struct hmap *local_lbs)
+{
+    struct local_load_balancer *lb;
+    HMAP_FOR_EACH_POP (lb, hmap_node, local_lbs) {
+        local_load_balancer_destroy(lb);
+    }
+
+    hmap_destroy(local_lbs);
+}
+
+void
+local_load_balancer_update(struct local_load_balancer *local_lb)
+{
+    const struct sbrec_load_balancer *sb_lb = local_lb->ovn_lb->slb;
+    ovn_controller_lb_destroy(local_lb->ovn_lb);
+    local_lb->ovn_lb = ovn_controller_lb_create(sb_lb);
+}
+
+void
+local_load_balancer_switch_lflow_map(struct local_load_balancer *local_lb)
+{
+    struct hmap *temp = local_lb->active_lswitch_lflows;
+    local_lb->active_lswitch_lflows = local_lb->cleared_lswitch_lflows;
+    local_lb->cleared_lswitch_lflows = temp;
+
+    /* Make sure that the active_lflows is empty. */
+    ovs_assert(hmap_is_empty(local_lb->active_lswitch_lflows));
+
+    temp = local_lb->active_lrouter_lflows;
+    local_lb->active_lrouter_lflows = local_lb->cleared_lrouter_lflows;
+    local_lb->cleared_lrouter_lflows = temp;
+
+    /* Make sure that the active_lflows is empty. */
+    ovs_assert(hmap_is_empty(local_lb->active_lrouter_lflows));
+}
+
+void
+tracked_lb_add(struct local_load_balancer *local_lb,
+               enum en_tracked_resource_type tracked_type,
+               struct hmap *tracked_lbs)
+{
+    struct tracked_lb *t_lb = NULL;
+
+    struct tracked_lb *tmp = NULL;
+    uint32_t hash = uuid_hash(&local_lb->ovn_lb->slb->header_.uuid);
+    HMAP_FOR_EACH_WITH_HASH (tmp, node, hash, tracked_lbs) {
+        if (uuid_equals(&tmp->local_lb->ovn_lb->slb->header_.uuid,
+                        &local_lb->ovn_lb->slb->header_.uuid)) {
+            t_lb = tmp;
+            break;
+        }
+    }
+
+    if (!t_lb) {
+        t_lb = xzalloc(sizeof *t_lb);
+        hmap_insert(tracked_lbs, &t_lb->node,
+                    uuid_hash(&local_lb->ovn_lb->slb->header_.uuid));
+    }
+
+    t_lb->local_lb = local_lb;
+    t_lb->tracked_type = tracked_type;
+}
+
+void
+tracked_lbs_destroy(struct hmap *tracked_lbs)
+{
+    struct tracked_lb *t_lb;
+    HMAP_FOR_EACH_POP (t_lb, node, tracked_lbs) {
+        free(t_lb);
+    }
+
+    hmap_destroy(tracked_lbs);
+}
+
+/* static functions. */
+static void
+local_lport_init_cache(struct local_lport *lport)
+{
+    const struct sbrec_port_binding *pb = lport->pb;
+    smap_clone(&lport->options, &pb->options);
+
+    lport->addresses =
+        pb->n_mac ? xmalloc(pb->n_mac * sizeof *lport->addresses) :
+        NULL;
+
+    lport->n_addresses = pb->n_mac;
+    for (size_t i = 0; i < pb->n_mac; i++) {
+        lport->addresses[i] = xstrdup(pb->mac[i]);
+    }
+
+    lport->port_security =
+        pb->n_port_security ?
+        xmalloc(pb->n_port_security * sizeof *lport->port_security) :
+        NULL;
+
+    lport->n_port_security = pb->n_port_security;
+    for (size_t i = 0; i < pb->n_port_security; i++) {
+        lport->port_security[i] = xstrdup(pb->port_security[i]);
+    }
+
+    lport->claimed = !!pb->chassis;
+
+    local_lport_init_lflow_gen_data(lport);
+}
+
+static struct local_datapath *
+local_datapath_add__(struct hmap *local_datapaths,
+                     const struct sbrec_datapath_binding *dp,
+                     struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+                     struct ovsdb_idl_index *sbrec_port_binding_by_datapath,
+                     struct ovsdb_idl_index *sbrec_port_binding_by_name,
+                     int depth,
+                     void (*datapath_added_cb)(
+                           struct local_datapath *ld,
+                           void *aux),
+                     void *aux)
+{
+    uint32_t dp_key = dp->tunnel_key;
+    struct local_datapath *ld = get_local_datapath(local_datapaths, dp_key);
+    if (ld) {
+        return ld;
+    }
+
+    ld = local_datapath_alloc(dp);
+    hmap_insert(local_datapaths, &ld->hmap_node, dp_key);
+    ld->datapath = dp;
+
+    if (datapath_added_cb) {
+        datapath_added_cb(ld, aux);
+    }
+
+    if (depth >= 100) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+        VLOG_WARN_RL(&rl, "datapaths nested too deep");
+        return ld;
+    }
+
+    struct sbrec_port_binding *target =
+        sbrec_port_binding_index_init_row(sbrec_port_binding_by_datapath);
+    sbrec_port_binding_index_set_datapath(target, dp);
+
+    const struct sbrec_port_binding *pb;
+    SBREC_PORT_BINDING_FOR_EACH_EQUAL (pb, target,
+                                       sbrec_port_binding_by_datapath) {
+        struct local_lport *lport =
+            local_datapath_add_lport(ld, pb->logical_port, pb);
+
+        if (!strcmp(pb->type, "patch") || !strcmp(pb->type, "l3gateway")) {
+            const char *peer_name = smap_get(&pb->options, "peer");
+            if (peer_name) {
+                const struct sbrec_port_binding *peer;
+
+                peer = lport_lookup_by_name(sbrec_port_binding_by_name,
+                                            peer_name);
+
+                if (peer && peer->datapath) {
+                    if (!strcmp(pb->type, "patch")) {
+                        /* Add the datapath to local datapath only for patch
+                         * ports. For l3gateway ports, since gateway router
+                         * resides on one chassis, we don't need to add.
+                         * Otherwise, all other chassis might create patch
+                         * ports between br-int and the provider bridge. */
+                        local_datapath_add__(local_datapaths, peer->datapath,
+                                             sbrec_datapath_binding_by_key,
+                                             sbrec_port_binding_by_datapath,
+                                             sbrec_port_binding_by_name,
+                                             depth + 1, datapath_added_cb,
+                                             aux);
+                    }
+                    struct local_datapath *peer_ld =
+                        get_local_datapath(local_datapaths, peer->datapath->tunnel_key);
+                    if (peer_ld){
+                        struct local_lport *peer_lport =
+                            local_datapath_get_lport(peer_ld, peer->logical_port);
+
+                        if (peer_lport) {
+                            ld->n_peer_ports++;
+                            if (ld->n_peer_ports > ld->n_allocated_peer_ports) {
+                                ld->peer_ports =
+                                    x2nrealloc(ld->peer_ports,
+                                            &ld->n_allocated_peer_ports,
+                                            sizeof *ld->peer_ports);
+                            }
+
+                            ld->peer_ports[ld->n_peer_ports - 1].local = lport;
+                            ld->peer_ports[ld->n_peer_ports - 1].remote = peer_lport;
+
+                            lport->peer = peer_lport;
+                            peer_lport->peer = lport;
+                        }
+                    }
+                }
+            }
+        }
+    }
+    sbrec_port_binding_index_destroy_row(target);
+    return ld;
+}
+
+static struct tracked_datapath *
+tracked_datapath_create(const struct sbrec_datapath_binding *dp,
+                        enum en_tracked_resource_type tracked_type,
+                        struct hmap *tracked_datapaths)
+{
+    struct tracked_datapath *t_dp = xzalloc(sizeof *t_dp);
+    t_dp->dp = dp;
+    t_dp->tracked_type = tracked_type;
+    shash_init(&t_dp->lports);
+    hmap_insert(tracked_datapaths, &t_dp->node, uuid_hash(&dp->header_.uuid));
+    return t_dp;
+}
+
+static void
+local_lport_destroy_lsp_data(struct local_lport *lport)
+{
+    if (lport->lsp.n_addrs) {
+        destroy_lport_addresses(lport->lsp.addrs);
+    }
+
+    if (lport->lsp.n_ps_addrs){
+        destroy_lport_addresses(lport->lsp.ps_addrs);
+    }
+
+    free(lport->lsp.addrs);
+    free(lport->lsp.ps_addrs);
+    lport->lsp.addrs = NULL;
+    lport->lsp.ps_addrs = NULL;
+    lport->lsp.n_addrs = 0;
+    lport->lsp.n_ps_addrs = 0;
+}
+
+static void
+local_lport_destroy_lrp_data(struct local_lport *lport)
+{
+    destroy_lport_addresses(&lport->lrp.networks);
+    if (lport->lrp.is_l3dgw_port) {
+        free(lport->lrp.chassis_redirect_json_key);
+    }
+}
+
+static void
+local_lport_update_lsp_data(struct local_lport *lport)
+{
+    lport->lsp.addrs = xmalloc(sizeof *lport->lsp.addrs * lport->pb->n_mac);
+    lport->lsp.ps_addrs =
+        xmalloc(sizeof *lport->lsp.ps_addrs * lport->pb->n_mac);
+    for (size_t i = 0; i < lport->pb->n_mac; i++) {
+        if (!strcmp(lport->pb->mac[i], "unknown")) {
+            lport->lsp.has_unknown = true;
+            continue;
+        }
+        if (!strcmp(lport->pb->mac[i], "router")) {
+            continue;
+        }
+
+        if (!extract_lsp_addresses(lport->pb->mac[i],
+                                   &lport->lsp.addrs[lport->lsp.n_addrs])) {
+            continue;
+        }
+
+        lport->lsp.n_addrs++;
+    }
+
+    for (size_t i = 0; i < lport->pb->n_port_security; i++) {
+        if (!extract_lsp_addresses(
+            lport->pb->port_security[i],
+            &lport->lsp.ps_addrs[lport->lsp.n_ps_addrs])) {
+            continue;
+        }
+        lport->lsp.n_ps_addrs++;
+    }
+
+    lport->lsp.check_lport_is_up =
+        !smap_get_bool(&lport->pb->datapath->options,
+        "ignore_lport_down", false);
+}
+
+static void
+local_lport_update_lrp_data(struct local_lport *lport)
+{
+    if (!extract_lsp_addresses(lport->pb->mac[0], &lport->lrp.networks)) {
+        return;
+    }
+
+    /* Always add the IPv6 link local address. */
+    struct in6_addr lla;
+    in6_generate_lla(lport->lrp.networks.ea, &lla);
+    lport_addr_add_ip6ddr(&lport->lrp.networks, lla, 64);
+
+    lport->lrp.is_l3dgw_port = smap_get_bool(&lport->pb->options,
+                                             "is-l3dgw-port", false);
+    if (lport->lrp.is_l3dgw_port) {
+        struct ds json_key = DS_EMPTY_INITIALIZER;
+        char *chassis_redirect_name =
+            ovn_chassis_redirect_name(lport->pb->logical_port);
+        json_string_escape(chassis_redirect_name, &json_key);
+        lport->lrp.chassis_redirect_json_key = ds_steal_cstr(&json_key);
+        free(chassis_redirect_name);
+    }
+
+    lport->lrp.dp_has_l3dgw_port = smap_get_bool(&lport->pb->datapath->options,
+                                          "has-l3dgw-port", false);
+
+    lport->lrp.peer_dp_has_localnet_ports =
+        smap_get_bool(&lport->pb->options,
+                      "peer-dp-has-localnet-ports", false);
+}
+
+static struct local_load_balancer *
+local_load_balancer_add__(struct hmap *local_lbs,
+                          const struct sbrec_load_balancer *sb_lb)
+{
+    struct local_load_balancer *local_lb =
+        local_load_balancer_get(local_lbs, &sb_lb->header_.uuid);
+    if (!local_lb) {
+        struct ovn_controller_lb *ovn_lb = ovn_controller_lb_create(sb_lb);
+        local_lb = xzalloc(sizeof *local_lb);
+        local_lb->ovn_lb = ovn_lb;
+        hmap_init(&local_lb->lswitch_lflows[0]);
+        hmap_init(&local_lb->lswitch_lflows[1]);
+        hmap_init(&local_lb->lrouter_lflows[0]);
+        hmap_init(&local_lb->lrouter_lflows[1]);
+
+        local_lb->active_lswitch_lflows = &local_lb->lswitch_lflows[0];
+        local_lb->cleared_lswitch_lflows = &local_lb->lswitch_lflows[1];
+
+        local_lb->active_lrouter_lflows = &local_lb->lrouter_lflows[0];
+        local_lb->cleared_lrouter_lflows = &local_lb->lrouter_lflows[1];
+
+        hmap_insert(local_lbs, &local_lb->hmap_node,
+                    uuid_hash(&sb_lb->header_.uuid));
+    }
+
+    return local_lb;
+}
+
+static void
+local_load_balancer_destroy(struct local_load_balancer *local_lb)
+{
+    ovn_controller_lb_destroy(local_lb->ovn_lb);
+    ovn_ctrl_lflows_destroy(&local_lb->lswitch_lflows[0]);
+    ovn_ctrl_lflows_destroy(&local_lb->lswitch_lflows[1]);
+    ovn_ctrl_lflows_destroy(&local_lb->lrouter_lflows[0]);
+    ovn_ctrl_lflows_destroy(&local_lb->lrouter_lflows[1]);
+    free(local_lb);
+}
diff --git a/lib/ldata.h b/lib/ldata.h
new file mode 100644
index 0000000000..d51b2ddaef
--- /dev/null
+++ b/lib/ldata.h
@@ -0,0 +1,251 @@
+/* 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 LDATA_H
+#define LDATA_H 1
+
+/* OVS includes. */
+#include "include/openvswitch/shash.h"
+#include "lib/smap.h"
+
+/* OVN includes. */
+#include "lib/ovn-util.h"
+
+struct sbrec_datapath_binding;
+struct sbrec_port_binding;
+struct ovsdb_idl_index;
+struct sbrec_load_balancer;
+
+struct local_lport {
+    const struct sbrec_port_binding *pb;
+    enum en_lport_type type;
+
+    /* cached data. */
+    char **addresses;
+    size_t n_addresses;
+    char **port_security;
+    size_t n_port_security;
+    struct smap options;
+    bool claimed;
+
+    union {
+        struct {
+            /* Logical switch port data. */
+            struct lport_addresses *addrs;  /* Logical switch port
+                                             * addresses. */
+            unsigned int n_addrs;
+
+            struct lport_addresses *ps_addrs;  /* Port security addresses. */
+            unsigned int n_ps_addrs;
+
+            bool has_unknown;
+            bool check_lport_is_up;
+        } lsp;
+
+        struct {
+            struct lport_addresses networks;
+            bool has_bfd;
+            bool is_l3dgw_port;
+            char *chassis_redirect_json_key; /* Initialized only if
+                                              * 'is_l3dgw_port'. */
+            bool dp_has_l3dgw_port; /* True if the router datapath has a
+                                     * gw port. */
+            bool peer_dp_has_localnet_ports; /* True if the peer datapath has
+                                      * localnet ports. */
+        } lrp;
+    };
+    char *json_key;
+
+    /* The port's peer:
+     *
+     *     - A switch port S of type "router" has a router port R as a peer,
+     *       and R in turn has S has its peer.
+     *
+     *     - Two connected logical router ports have each other as peer.
+     *
+     *     - Other kinds of ports have no peer. */
+    struct local_lport *peer;
+
+    /* Logical port multicast data. */
+    /*struct mcast_port_info mcast_info; */
+
+    struct local_datapath *ldp;
+
+    struct hmap ctrl_lflows[2];
+    struct hmap *active_lflows;
+    struct hmap *cleared_lflows;
+
+};
+
+/* A logical datapath that has some relevance to this hypervisor.  A logical
+ * datapath D is relevant to hypervisor H if:
+ *
+ *     - Some VIF or l2gateway or l3gateway port in D is located on H.
+ *
+ *     - D is reachable over a series of hops across patch ports, starting from
+ *       a datapath relevant to H.
+ *
+ * The 'hmap_node''s hash value is 'datapath->tunnel_key'. */
+struct local_datapath {
+    struct hmap_node hmap_node;
+    const struct sbrec_datapath_binding *datapath;
+    bool is_switch;
+
+    /* The localnet port in this datapath, if any (at most one is allowed). */
+    const struct sbrec_port_binding *localnet_port;
+
+    /* True if this datapath contains an l3gateway port located on this
+     * hypervisor. */
+    bool has_local_l3gateway;
+
+    struct {
+        struct local_lport *local;
+        struct local_lport *remote;
+    } *peer_ports;
+
+    size_t n_peer_ports;
+    size_t n_allocated_peer_ports;
+
+    /* Multicast data. */
+    /*struct mcast_info mcast_info; */
+
+    /* Data related to lflow generation. */
+    struct smap dp_options;
+    struct hmap ctrl_lflows[2];
+    struct hmap *active_lflows;
+    struct hmap *cleared_lflows;
+
+    /* shash of 'struct local_lport'. */
+    struct shash lports;
+};
+
+struct local_datapath *local_datapath_alloc(
+    const struct sbrec_datapath_binding *);
+struct local_datapath *get_local_datapath(const struct hmap *,
+                                          uint32_t tunnel_key);
+void local_datapath_add(struct hmap *local_datapaths,
+                        const struct sbrec_datapath_binding *,
+                        struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+                        struct ovsdb_idl_index *sbrec_port_binding_by_datapath,
+                        struct ovsdb_idl_index *sbrec_port_binding_by_name,
+                        void (*datapath_added)(struct local_datapath *,
+                                               void *aux),
+                        void *aux);
+
+void local_datapaths_destroy(struct hmap *local_datapaths);
+void local_datapath_destroy(struct local_datapath *ld);
+void local_datapath_switch_lflow_map(struct local_datapath *);
+
+struct local_lport *local_datapath_get_lport(struct local_datapath *ld,
+                                             const char *lport_name);
+
+struct local_lport *local_datapath_add_lport(
+    struct local_datapath *ld, const char *lport_name,
+    const struct sbrec_port_binding *);
+
+void local_datapath_remove_lport(struct local_datapath *ld,
+                                 const char *lport_name);
+
+void local_datapath_add_or_update_peer_port(
+    const struct sbrec_port_binding *pb,
+    struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+    struct ovsdb_idl_index *sbrec_port_binding_by_datapath,
+    struct ovsdb_idl_index *sbrec_port_binding_by_name,
+    struct local_datapath *ld,
+    struct hmap *local_datapaths,
+    void (*datapath_added_cb)(
+                         struct local_datapath *ld,
+                         void *aux),
+    void *aux);
+
+void local_datapath_remove_peer_port(const struct sbrec_port_binding *pb,
+                                     struct local_datapath *ld,
+                                     struct hmap *local_datapaths);
+struct local_lport *local_datapath_unlink_lport(struct local_datapath *ld,
+                                                const char *lport_name);
+
+void local_lport_destroy(struct local_lport *);
+
+bool local_lport_update_cache(struct local_lport *);
+void local_lport_clear_cache(struct local_lport *);
+bool local_lport_is_cache_old(struct local_lport *);
+void local_lport_switch_lflow_map(struct local_lport *);
+
+/* Represents a tracked logical port. */
+enum en_tracked_resource_type {
+    TRACKED_RESOURCE_NEW,
+    TRACKED_RESOURCE_REMOVED,
+    TRACKED_RESOURCE_UPDATED
+};
+
+struct tracked_lport {
+    const struct sbrec_port_binding *pb;
+    enum en_tracked_resource_type tracked_type;
+};
+
+/* Represent a tracked datapath. */
+struct tracked_datapath {
+    struct hmap_node node;
+    const struct sbrec_datapath_binding *dp;
+    enum en_tracked_resource_type tracked_type;
+    struct shash lports; /* shash of struct tracked_binding_lport. */
+};
+
+struct tracked_datapath * tracked_datapath_add(
+    const struct sbrec_datapath_binding *, enum en_tracked_resource_type,
+    struct hmap *tracked_datapaths);
+struct tracked_datapath *tracked_datapath_find(
+    struct hmap *tracked_datapaths, const struct sbrec_datapath_binding *);
+void tracked_datapath_lport_add(const struct sbrec_port_binding *,
+                                enum en_tracked_resource_type,
+                                struct hmap *tracked_datapaths);
+void tracked_datapaths_destroy(struct hmap *tracked_datapaths);
+
+/* Load balancer. */
+struct local_load_balancer {
+    struct hmap_node hmap_node;
+
+    struct ovn_controller_lb *ovn_lb;
+    struct hmap lswitch_lflows[2];
+    struct hmap lrouter_lflows[2];
+    struct hmap *active_lswitch_lflows;
+    struct hmap *cleared_lswitch_lflows;
+    struct hmap *active_lrouter_lflows;
+    struct hmap *cleared_lrouter_lflows;
+};
+
+struct local_load_balancer *local_load_balancer_add(
+    struct hmap *local_lbs, struct hmap *local_datapaths,
+    const struct sbrec_load_balancer *);
+void local_load_balancer_remove(struct hmap *local_lbs,
+                                struct local_load_balancer *);
+void local_load_balancers_destroy(struct hmap *local_lbs);
+struct local_load_balancer *local_load_balancer_get(struct hmap *local_lbs,
+                                                    const struct uuid *);
+void local_load_balancer_update(struct local_load_balancer *);
+void local_load_balancer_switch_lflow_map(struct local_load_balancer *);
+
+struct tracked_lb {
+    struct hmap_node node;
+    struct local_load_balancer *local_lb;
+    enum en_tracked_resource_type tracked_type;
+};
+
+void tracked_lb_add(struct local_load_balancer *,
+                    enum en_tracked_resource_type,
+                    struct hmap *tracked_lbs);
+void tracked_lbs_destroy(struct hmap *tracked_lbs);
+
+#endif /* controller/ldata.h */
diff --git a/lib/lflow.c b/lib/lflow.c
new file mode 100644
index 0000000000..b2af57adc7
--- /dev/null
+++ b/lib/lflow.c
@@ -0,0 +1,3514 @@
+/*
+ * Copyright (c) 2021 Red Hat.
+ *
+ * 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 "ovn/expr.h"
+
+#include "lflow.h"
+#include "lib/lb.h"
+#include "lib/ldata.h"
+#include "lib/ovn-nb-idl.h"
+#include "lib/ovn-sb-idl.h"
+#include "lib/ovn-l7.h"
+#include "lib/ovn-util.h"
+
+/* OpenvSwitch lib includes. */
+#include "openvswitch/vlog.h"
+#include "openvswitch/hmap.h"
+#include "include/openvswitch/json.h"
+#include "lib/smap.h"
+
+VLOG_DEFINE_THIS_MODULE(lib_lflow);
+
+static char *ovn_ctrl_lflow_hint(const struct ovsdb_idl_row *row);
+static void ovn_ctrl_lflow_init(struct ovn_ctrl_lflow *lflow, uint32_t dp_key,
+                                enum ovn_stage stage, uint16_t priority,
+                                char *match, char *actions,
+                                const struct uuid *lflow_uuid,
+                                uint32_t lflow_idx,
+                                char *stage_hint, const char *where);
+static void ovn_ctrl_lflow_add_at(struct hmap *lflow_map, uint32_t dp_key,
+                                  enum ovn_stage stage,
+                                  uint16_t priority, const char *match,
+                                  const char *actions,
+                                  const struct uuid *lflow_uuid,
+                                  uint32_t lflow_idx,
+                                  const struct ovsdb_idl_row *stage_hint,
+                                  const char *where);
+static void ovn_ctrl_lflow_destroy(struct ovn_ctrl_lflow *lflow);
+
+
+#define ovn_ctrl_lflow_add(LFLOW_MAP, STAGE, PRIORITY, MATCH, ACTIONS) \
+    ovn_ctrl_lflow_add_at(LFLOW_MAP, 0, STAGE, PRIORITY, MATCH, ACTIONS, \
+                          NULL, 0, NULL, OVS_SOURCE_LOCATOR)
+
+#define ovn_ctrl_lflow_add_dp_key(LFLOW_MAP, DP_KEY, STAGE, PRIORITY, MATCH, ACTIONS, UUID, LFLOW_IDX) \
+    ovn_ctrl_lflow_add_at(LFLOW_MAP, DP_KEY, STAGE, PRIORITY, MATCH, ACTIONS, \
+                          UUID, *LFLOW_IDX, NULL, OVS_SOURCE_LOCATOR); \
+    (*LFLOW_IDX)++
+
+#define ovn_ctrl_lflow_add_uuid(LFLOW_MAP, STAGE, PRIORITY, MATCH, ACTIONS, UUID, LFLOW_IDX) \
+    ovn_ctrl_lflow_add_at(LFLOW_MAP, 0, STAGE, PRIORITY, MATCH, ACTIONS, \
+                          UUID, *LFLOW_IDX, NULL, OVS_SOURCE_LOCATOR); \
+    (*LFLOW_IDX)++
+
+static void build_generic_port_security(struct hmap *lflows);
+static void build_generic_pre_acl(struct hmap *lflows);
+static void build_generic_pre_lb(struct hmap *lflows);
+static void build_generic_pre_stateful(struct hmap *lflows);
+static void build_generic_acls(struct hmap *lflows);
+static void build_generic_qos(struct hmap *lflows);
+static void build_generic_stateful(struct hmap *lflows);
+static void build_generic_lb_hairpin(struct hmap *lflows);
+static void build_generic_l2_lkup(struct hmap *lflows);
+
+static void build_lswitch_dp_lflows(struct hmap *lflows,
+                                    struct local_datapath *,
+                                    bool use_ct_inv_match);
+static void build_lrouter_dp_lflows(struct hmap *lflows,
+                                    const struct sbrec_datapath_binding *dp);
+
+static void build_lswitch_port_lflows(struct hmap *lflows,
+                                      struct local_lport *);
+static void build_lrouter_port_lflows(struct hmap *lflows,
+                                      struct local_lport *);
+
+static void skip_lport_from_conntrack(struct hmap *lflows,
+                                      struct local_lport *,
+                                      uint32_t *lflow_uuid_idx,
+                                      enum ovn_stage in_stage,
+                                      enum ovn_stage out_stage,
+                                      uint16_t priority, struct ds *match);
+static void ovn_ctrl_build_lb_lswitch_lflows(struct hmap *lswitch_lflows,
+                                             struct ovn_controller_lb *);
+static void ovn_ctrl_build_lb_lrouter_lflows(struct hmap *lrouter_lflows,
+                                             struct ovn_controller_lb *);
+
+void
+ovn_ctrl_lflows_clear(struct hmap *lflows)
+{
+    struct ovn_ctrl_lflow *lflow;
+    HMAP_FOR_EACH_POP (lflow, hmap_node, lflows) {
+        ovn_ctrl_lflow_destroy(lflow);
+    }
+}
+
+void
+ovn_ctrl_lflows_destroy(struct hmap *lflows)
+{
+    ovn_ctrl_lflows_clear(lflows);
+    hmap_destroy(lflows);
+}
+
+size_t
+ovn_ctrl_lflow_hash(const struct ovn_ctrl_lflow *lflow)
+{
+    return ovn_logical_flow_hash(ovn_stage_get_table(lflow->stage),
+                                 ovn_stage_get_pipeline_name(lflow->stage),
+                                 lflow->priority, lflow->match,
+                                 lflow->actions);
+}
+
+void
+build_lswitch_generic_lflows(struct hmap *lflows)
+{
+    /* Port security stages. */
+    build_generic_port_security(lflows);
+
+    /* Lookup and learn FDB. */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_LOOKUP_FDB, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PUT_FDB, 0, "1", "next;");
+
+    build_generic_pre_acl(lflows);
+    build_generic_pre_lb(lflows);
+    build_generic_pre_stateful(lflows);
+    build_generic_acls(lflows);
+    build_generic_qos(lflows);
+    build_generic_stateful(lflows);
+    build_generic_lb_hairpin(lflows);
+
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_ARP_ND_RSP, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_DHCP_OPTIONS, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_DHCP_RESPONSE, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_DNS_LOOKUP, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_DNS_RESPONSE, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_EXTERNAL_PORT, 0, "1", "next;");
+
+    build_generic_l2_lkup(lflows);
+}
+
+void
+ovn_ctrl_lflows_build_dp_lflows(struct hmap *lflows,
+                                struct local_datapath *ldp)
+{
+    if (ldp->is_switch) {
+        build_lswitch_dp_lflows(lflows, ldp, true);
+    } else {
+        build_lrouter_dp_lflows(lflows, ldp->datapath);
+    }
+}
+
+void
+ovn_ctrl_build_lport_lflows(struct hmap *lflows, struct local_lport *op)
+{
+    if (op->ldp->is_switch) {
+        build_lswitch_port_lflows(lflows, op);
+    } else {
+        build_lrouter_port_lflows(lflows, op);
+    }
+}
+
+void ovn_ctrl_build_lb_lflows(struct hmap *lswitch_lflows,
+                              struct hmap *lrouter_lflows,
+                              struct ovn_controller_lb *ovn_lb)
+{
+    ovn_ctrl_build_lb_lswitch_lflows(lswitch_lflows, ovn_lb);
+    ovn_ctrl_build_lb_lrouter_lflows(lrouter_lflows, ovn_lb);
+}
+
+/* static functions. */
+static char *
+ovn_ctrl_lflow_hint(const struct ovsdb_idl_row *row)
+{
+    if (!row) {
+        return NULL;
+    }
+    return xasprintf("%08x", row->uuid.parts[0]);
+}
+
+static void
+ovn_ctrl_lflow_init(struct ovn_ctrl_lflow *lflow, uint32_t dp_key,
+                    enum ovn_stage stage, uint16_t priority,
+                    char *match, char *actions,
+                    const struct uuid *lflow_uuid, uint32_t lflow_idx,
+                    char *stage_hint,
+                    const char *where)
+{
+    lflow->stage = stage;
+    lflow->priority = priority;
+    lflow->match = match;
+    lflow->actions = actions;
+    lflow->stage_hint = stage_hint;
+    lflow->where = where;
+    lflow->dp_key = dp_key;
+    //lflow_uuid = NULL;
+    if (lflow_uuid) {
+        lflow->uuid_ = *lflow_uuid;
+        lflow->uuid_.parts[3] = lflow_idx;
+    } else {
+        uuid_generate(&lflow->uuid_);
+    }
+}
+
+/* Adds a row with the specified contents to the Logical_Flow table. */
+static void
+ovn_ctrl_lflow_add_at(struct hmap *lflow_map, uint32_t dp_key,
+                      enum ovn_stage stage,
+                      uint16_t priority, const char *match,
+                      const char *actions,
+                      const struct uuid *lflow_uuid,
+                      uint32_t lflow_idx,
+                      const struct ovsdb_idl_row *stage_hint,
+                      const char *where)
+{
+    struct ovn_ctrl_lflow *lflow;
+    size_t hash;
+
+    lflow = xzalloc(sizeof *lflow);
+    ovn_ctrl_lflow_init(lflow, dp_key, stage, priority,
+                        xstrdup(match), xstrdup(actions),
+                        lflow_uuid, lflow_idx,
+                        ovn_ctrl_lflow_hint(stage_hint), where);
+
+    hash = ovn_ctrl_lflow_hash(lflow);
+    hmap_insert(lflow_map, &lflow->hmap_node, hash);
+}
+
+static void
+ovn_ctrl_lflow_destroy(struct ovn_ctrl_lflow *lflow)
+{
+    if (lflow) {
+        free(lflow->match);
+        free(lflow->actions);
+        free(lflow->stage_hint);
+        free(lflow);
+    }
+}
+
+static void
+build_generic_port_security(struct hmap *lflows)
+{
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PORT_SEC_L2, 100, "eth.src[40]",
+                       "drop;");
+
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PORT_SEC_ND, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PORT_SEC_IP, 0, "1", "next;");
+
+    /* Egress tables 8: Egress port security - IP (priority 0)
+     * Egress table 9: Egress port security L2 - multicast/broadcast
+     *                 (priority 100). */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_PORT_SEC_IP, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_PORT_SEC_L2, 100, "eth.mcast",
+                          "output;");
+}
+
+static void
+build_generic_pre_acl(struct hmap *lflows)
+{
+    /* Ingress and Egress Pre-ACL Table (Priority 0): Packets are
+     * allowed by default. */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_ACL, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_PRE_ACL, 0, "1", "next;");
+
+#if 0
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_ACL, 110,
+                          "eth.dst == $svc_monitor_mac", "next;");
+
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_PRE_ACL, 110,
+                          "eth.src == $svc_monitor_mac", "next;");
+#endif
+}
+
+static void
+build_generic_pre_lb(struct hmap *lflows)
+{
+    /* Do not send ND packets to conntrack */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_LB, 110,
+                  "nd || nd_rs || nd_ra || mldv1 || mldv2",
+                  "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_PRE_LB, 110,
+                  "nd || nd_rs || nd_ra || mldv1 || mldv2",
+                  "next;");
+
+    /* Do not send service monitor packets to conntrack. */
+#if 0
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_LB, 110,
+                       "eth.dst == $svc_monitor_mac", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_PRE_LB, 110,
+                      "eth.src == $svc_monitor_mac", "next;");
+#endif
+
+    /* Allow all packets to go to next tables by default. */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_LB, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_PRE_LB, 0, "1", "next;");
+}
+
+static void
+build_generic_pre_stateful(struct hmap *lflows)
+{
+    /* Ingress and Egress pre-stateful Table (Priority 0): Packets are
+     * allowed by default. */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_STATEFUL, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, 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_ctrl_lflow_add(lflows, 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_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_STATEFUL, 120,
+                      ds_cstr(&match), ds_cstr(&actions));
+    }
+
+    ds_destroy(&actions);
+    ds_destroy(&match);
+
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_STATEFUL, 110,
+                       REGBIT_CONNTRACK_NAT" == 1", "ct_lb;");
+
+    ovn_ctrl_lflow_add(lflows, 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_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_STATEFUL, 100,
+                       REGBIT_CONNTRACK_DEFRAG" == 1", "ct_next;");
+
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_PRE_STATEFUL, 100,
+                       REGBIT_CONNTRACK_DEFRAG" == 1", "ct_next;");
+}
+
+static void
+build_generic_acls(struct hmap *lflows)
+{
+    /* Ingress and Egress ACL Table (Priority 0): Packets are allowed by
+     * default.  A related rule at priority 1 is added below if there
+     * are any stateful ACLs in this datapath. */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_ACL, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_ACL, 0, "1", "next;");
+
+#if 0
+    /* Add a 34000 priority flow to advance the service monitor reply
+     * packets to skip applying ingress ACLs. */
+    ovn_ctrl_lflow_add(lflows, 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_ctrl_lflow_add(lflows, S_SWITCH_OUT_ACL, 34000,
+                          "eth.src == $svc_monitor_mac", "next;");
+#endif
+}
+
+static void
+build_generic_qos(struct hmap *lflows)
+{
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_QOS_MARK, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_QOS_MARK, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_QOS_METER, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_QOS_METER, 0, "1", "next;");
+}
+
+static void
+build_generic_stateful(struct hmap *lflows)
+{
+    /* Ingress and Egress stateful Table (Priority 0): Packets are
+     * allowed by default. */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_STATEFUL, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_STATEFUL, 0, "1", "next;");
+
+    /* If REGBIT_CONNTRACK_COMMIT is set as 1, then the packets should be
+     * committed to conntrack. We always set ct_label.blocked to 0 here as
+     * any packet that makes it this far is part of a connection we
+     * want to allow to continue. */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_STATEFUL, 100,
+                       REGBIT_CONNTRACK_COMMIT" == 1",
+                       "ct_commit { ct_label.blocked = 0; }; next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_OUT_STATEFUL, 100,
+                       REGBIT_CONNTRACK_COMMIT" == 1",
+                       "ct_commit { ct_label.blocked = 0; }; next;");
+}
+
+static void
+build_generic_lb_hairpin(struct hmap *lflows)
+{
+    /* Ingress Pre-Hairpin/Nat-Hairpin/Hairpin tabled (Priority 0).
+     * Packets that don't need hairpinning should continue processing.
+     */
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_PRE_HAIRPIN, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_NAT_HAIRPIN, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_HAIRPIN, 0, "1", "next;");
+}
+
+static void
+build_generic_l2_lkup(struct hmap *lflows)
+{
+#if 0
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_L2_LKUP, 110,
+                          "eth.dst == $svc_monitor_mac",
+                          "handle_svc_check(inport);");
+#endif
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_L2_LKUP, 0, "1",
+                          "outport = get_fdb(eth.dst); next;");
+
+    ovn_ctrl_lflow_add(lflows, S_SWITCH_IN_L2_UNKNOWN, 0, "1", "output;");
+}
+
+static bool
+is_dp_vlan_transparent(const struct sbrec_datapath_binding *dp)
+{
+    return smap_get_bool(&dp->options, "vlan-passthru", false);
+}
+
+static bool
+has_dp_lb_vip(const struct sbrec_datapath_binding *dp)
+{
+    return smap_get_bool(&dp->options, "has-lb-vips", false);
+}
+
+static bool
+has_dp_stateful_acls(const struct sbrec_datapath_binding *dp)
+{
+    return smap_get_bool(&dp->options, "has-stateful-acls", false);
+}
+
+static bool
+has_dp_acls(const struct sbrec_datapath_binding *dp)
+{
+    return smap_get_bool(&dp->options, "has-acls", false);
+}
+
+static bool
+has_dp_unknown_lports(const struct sbrec_datapath_binding *dp)
+{
+    return smap_get_bool(&dp->options, "has-unknown", false);
+}
+
+static bool
+has_dp_dns_records(const struct sbrec_datapath_binding *dp)
+{
+    return smap_get_bool(&dp->options, "has-dns-records", false);
+}
+
+static void
+build_lswitch_pre_acls(struct hmap *lflows, bool has_stateful_acls,
+                       const struct uuid *lflow_uuid, uint32_t *lflow_uuid_idx)
+{
+    /* If there are any stateful ACL rules in this datapath, we may
+     * send IP packets for some (allow) filters through the conntrack action,
+     * which handles defragmentation, in order to match L4 headers. */
+    if (has_stateful_acls) {
+        /* Ingress and Egress Pre-ACL Table (Priority 110).
+         *
+         * Not to do conntrack on ND and ICMP destination
+         * unreachable packets. */
+        ovn_ctrl_lflow_add_uuid(
+            lflows, S_SWITCH_IN_PRE_ACL, 110,
+            "nd || nd_rs || nd_ra || mldv1 || mldv2 || "
+            "(udp && udp.src == 546 && udp.dst == 547)", "next;",
+            lflow_uuid, lflow_uuid_idx);
+
+        ovn_ctrl_lflow_add_uuid(
+            lflows, S_SWITCH_OUT_PRE_ACL, 110,
+            "nd || nd_rs || nd_ra || mldv1 || mldv2 || "
+            "(udp && udp.src == 546 && udp.dst == 547)", "next;",
+            lflow_uuid, lflow_uuid_idx);
+
+        /* Ingress and Egress Pre-ACL Table (Priority 100).
+         *
+         * Regardless of whether the ACL is "from-lport" or "to-lport",
+         * we need rules in both the ingress and egress table, because
+         * the return traffic needs to be followed.
+         *
+         * 'REGBIT_CONNTRACK_DEFRAG' is set to let the pre-stateful table send
+         * it to conntrack for tracking and defragmentation. */
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_PRE_ACL, 100, "ip",
+                                REGBIT_CONNTRACK_DEFRAG" = 1; next;",
+                                lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_OUT_PRE_ACL, 100, "ip",
+                                REGBIT_CONNTRACK_DEFRAG" = 1; next;",
+                                lflow_uuid, lflow_uuid_idx);
+    }
+}
+
+static void
+build_lswitch_pre_lb(struct hmap *lflows, bool vip_configured,
+                     const struct uuid *lflow_uuid, uint32_t *lflow_uuid_idx)
+{
+    /* '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
+     * we used to set the REGBIT_CONNTRACK_DEFRAG flag in the ingress pipeline
+     * if the IP destination matches the VIP. But this causes few issues when
+     * a logical switch has no ACLs configured with allow-related.
+     * To understand the issue, lets a take a TCP load balancer -
+     * 10.0.0.10:80=10.0.0.3:80.
+     * If a logical port - p1 with IP - 10.0.0.5 opens a TCP connection with
+     * the VIP - 10.0.0.10, then the packet in the ingress pipeline of 'p1'
+     * is sent to the p1's conntrack zone id and the packet is load balanced
+     * to the backend - 10.0.0.3. For the reply packet from the backend lport,
+     * it is not sent to the conntrack of backend lport's zone id. This is fine
+     * as long as the packet is valid. Suppose the backend lport sends an
+     *  invalid TCP packet (like incorrect sequence number), the packet gets
+     * delivered to the lport 'p1' without unDNATing the packet to the
+     * VIP - 10.0.0.10. And this causes the connection to be reset by the
+     * lport p1's VIF.
+     *
+     * We can't fix this issue by adding a logical flow to drop ct.inv packets
+     * in the egress pipeline since it will drop all other connections not
+     * destined to the load balancers.
+     *
+     * To fix this issue, we send all the packets to the conntrack in the
+     * ingress pipeline if a load balancer is configured. We can now
+     * add a lflow to drop ct.inv packets.
+     */
+    if (vip_configured) {
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_PRE_LB,
+                                100, "ip", REGBIT_CONNTRACK_NAT" = 1; next;",
+                                lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_OUT_PRE_LB,
+                                100, "ip", REGBIT_CONNTRACK_NAT" = 1; next;",
+                                lflow_uuid, lflow_uuid_idx);
+    }
+}
+
+static void
+build_lswitch_acl_hints(struct hmap *lflows, bool has_acls_or_lbs,
+                        const struct uuid *lflow_uuid, uint32_t *lflow_uuid_idx)
+{
+    /* This stage builds hints for the IN/OUT_ACL stage. Based on various
+     * combinations of ct flags packets may hit only a subset of the logical
+     * flows in the IN/OUT_ACL stage.
+     *
+     * Populating ACL hints first and storing them in registers simplifies
+     * the logical flow match expressions in the IN/OUT_ACL stage and
+     * generates less openflows.
+     *
+     * Certain combinations of ct flags might be valid matches for multiple
+     * types of ACL logical flows (e.g., allow/drop). In such cases hints
+     * corresponding to all potential matches are set.
+     */
+
+    enum ovn_stage stages[] = {
+        S_SWITCH_IN_ACL_HINT,
+        S_SWITCH_OUT_ACL_HINT,
+    };
+
+    for (size_t i = 0; i < ARRAY_SIZE(stages); i++) {
+        enum ovn_stage stage = stages[i];
+
+        /* In any case, advance to the next stage. */
+        if (!has_acls_or_lbs) {
+            ovn_ctrl_lflow_add_uuid(lflows, stage, UINT16_MAX, "1", "next;",
+                                    lflow_uuid, lflow_uuid_idx);
+        } else {
+            ovn_ctrl_lflow_add_uuid(lflows, stage, 0, "1", "next;",
+                                    lflow_uuid, lflow_uuid_idx);
+        }
+
+        if (!has_acls_or_lbs) {
+            continue;
+        }
+
+        /* New, not already established connections, may hit either allow
+         * or drop ACLs. For allow ACLs, the connection must also be committed
+         * to conntrack so we set REGBIT_ACL_HINT_ALLOW_NEW.
+         */
+        ovn_ctrl_lflow_add_uuid(lflows, stage, 7, "ct.new && !ct.est",
+                                REGBIT_ACL_HINT_ALLOW_NEW " = 1; "
+                                REGBIT_ACL_HINT_DROP " = 1; "
+                                "next;", lflow_uuid, lflow_uuid_idx);
+
+        /* Already established connections in the "request" direction that
+         * are already marked as "blocked" may hit either:
+         * - allow ACLs for connections that were previously allowed by a
+         *   policy that was deleted and is being readded now. In this case
+         *   the connection should be recommitted so we set
+         *   REGBIT_ACL_HINT_ALLOW_NEW.
+         * - drop ACLs.
+         */
+        ovn_ctrl_lflow_add_uuid(
+            lflows, stage, 6,
+            "!ct.new && ct.est && !ct.rpl && ct_label.blocked == 1",
+            REGBIT_ACL_HINT_ALLOW_NEW " = 1; "REGBIT_ACL_HINT_DROP " = 1; "
+            "next;", lflow_uuid, lflow_uuid_idx);
+
+        /* Not tracked traffic can either be allowed or dropped. */
+        ovn_ctrl_lflow_add_uuid(lflows, stage, 5, "!ct.trk",
+                                REGBIT_ACL_HINT_ALLOW " = 1; "
+                                REGBIT_ACL_HINT_DROP " = 1; "
+                                "next;", lflow_uuid, lflow_uuid_idx);
+
+        /* Already established connections in the "request" direction may hit
+         * either:
+         * - allow ACLs in which case the traffic should be allowed so we set
+         *   REGBIT_ACL_HINT_ALLOW.
+         * - drop ACLs in which case the traffic should be blocked and the
+         *   connection must be committed with ct_label.blocked set so we set
+         *   REGBIT_ACL_HINT_BLOCK.
+         */
+        ovn_ctrl_lflow_add_uuid(
+            lflows, stage, 4,
+            "!ct.new && ct.est && !ct.rpl && ct_label.blocked == 0",
+            REGBIT_ACL_HINT_ALLOW " = 1; "REGBIT_ACL_HINT_BLOCK " = 1; "
+            "next;", lflow_uuid, lflow_uuid_idx);
+
+        /* Not established or established and already blocked connections may
+         * hit drop ACLs.
+         */
+        ovn_ctrl_lflow_add_uuid(lflows, stage, 3, "!ct.est",
+                                REGBIT_ACL_HINT_DROP " = 1; "
+                                "next;", lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(lflows, stage, 2,
+                                "ct.est && ct_label.blocked == 1",
+                                REGBIT_ACL_HINT_DROP " = 1; next;",
+                                lflow_uuid, lflow_uuid_idx);
+
+        /* Established connections that were previously allowed might hit
+         * drop ACLs in which case the connection must be committed with
+         * ct_label.blocked set.
+         */
+        ovn_ctrl_lflow_add_uuid(lflows, stage, 1,
+                                "ct.est && ct_label.blocked == 0",
+                                REGBIT_ACL_HINT_BLOCK " = 1; next;",
+                                lflow_uuid, lflow_uuid_idx);
+    }
+}
+
+static void
+build_lswitch_acls(struct hmap *lflows, bool has_acls_or_lbs,
+                   bool has_stateful, bool use_ct_inv_match,
+                   const struct uuid *lflow_uuid, uint32_t *lflow_uuid_idx)
+{
+    /* Ingress and Egress ACL Table (Priority 0): Packets are allowed by
+     * 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. */
+    if (!has_acls_or_lbs) {
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ACL, UINT16_MAX, "1",
+                                "next;", lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_OUT_ACL, UINT16_MAX, "1",
+                                "next;", lflow_uuid, lflow_uuid_idx);
+    } else {
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ACL, 0, "1",
+                                "next;", lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_OUT_ACL, 0, "1",
+                                "next;", lflow_uuid, lflow_uuid_idx);
+    }
+
+    if (has_stateful) {
+        /* Ingress and Egress ACL Table (Priority 1).
+         *
+         * By default, traffic is allowed.  This is partially handled by
+         * the Priority 0 ACL flows added earlier, but we also need to
+         * commit IP flows.  This is because, while the initiater's
+         * direction may not have any stateful rules, the server's may
+         * and then its return traffic would not have an associated
+         * conntrack entry and would return "+invalid".
+         *
+         * We use "ct_commit" for a connection that is not already known
+         * by the connection tracker.  Once a connection is committed,
+         * subsequent packets will hit the flow at priority 0 that just
+         * uses "next;"
+         *
+         * We also check for established connections that have ct_label.blocked
+         * set on them.  That's a connection that was disallowed, but is
+         * now allowed by policy again since it hit this default-allow flow.
+         * We need to set ct_label.blocked=0 to let the connection continue,
+         * which will be done by ct_commit() in the "stateful" stage.
+         * Subsequent packets will hit the flow at priority 0 that just
+         * uses "next;". */
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ACL, 1,
+                            "ip && (!ct.est || "
+                            "(ct.est && ct_label.blocked == 1))",
+                            REGBIT_CONNTRACK_COMMIT" = 1; next;",
+                            lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(
+            lflows, S_SWITCH_OUT_ACL, 1,
+            "ip && (!ct.est || (ct.est && ct_label.blocked == 1))",
+            REGBIT_CONNTRACK_COMMIT" = 1; next;",
+            lflow_uuid, lflow_uuid_idx);
+
+        /* 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. */
+        char *match = xasprintf("%s(ct.est && ct.rpl && ct_label.blocked == 1)",
+                                use_ct_inv_match ? "ct.inv || " : "");
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ACL, UINT16_MAX - 3, match,
+                            "drop;", lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_OUT_ACL, UINT16_MAX - 3, match,
+                            "drop;", lflow_uuid, lflow_uuid_idx);
+        free(match);
+
+        /* 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
+         * (bit 0 of ct_label).  We only match traffic in the
+         * reply direction because we want traffic in the request
+         * direction to hit the currently defined policy from ACLs.
+         *
+         * This is enforced at a higher priority than ACLs can be defined. */
+        match = xasprintf("ct.est && !ct.rel && !ct.new%s && "
+                          "ct.rpl && ct_label.blocked == 0",
+                          use_ct_inv_match ? " && !ct.inv" : "");
+
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ACL, UINT16_MAX - 3,
+                                match, "next;", lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
+                                match, "next;", lflow_uuid, lflow_uuid_idx);
+        free(match);
+
+        /* Ingress and Egress ACL Table (Priority 65535).
+         *
+         * Allow traffic that is related to an existing conntrack entry that
+         * has not been marked for deletion (bit 0 of ct_label).
+         *
+         * This is enforced at a higher priority than ACLs can be defined.
+         *
+         * NOTE: This does not support related data sessions (eg,
+         * 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.  */
+        match = xasprintf("!ct.est && ct.rel && !ct.new%s && "
+                          "ct_label.blocked == 0",
+                          use_ct_inv_match ? " && !ct.inv" : "");
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ACL, UINT16_MAX - 3,
+                                match, "next;", lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
+                                match, "next;", lflow_uuid, lflow_uuid_idx);
+        free(match);
+
+        /* Ingress and Egress ACL Table (Priority 65532).
+         *
+         * Not to do conntrack on ND packets. */
+        ovn_ctrl_lflow_add_uuid(
+            lflows, S_SWITCH_IN_ACL, UINT16_MAX - 3,
+            "nd || nd_ra || nd_rs || mldv1 || mldv2", "next;",
+            lflow_uuid, lflow_uuid_idx);
+        ovn_ctrl_lflow_add_uuid(
+            lflows, S_SWITCH_OUT_ACL, UINT16_MAX - 3,
+            "nd || nd_ra || nd_rs || mldv1 || mldv2", "next;",
+            lflow_uuid, lflow_uuid_idx);
+    }
+}
+
+static void
+build_lswitch_lb_hairpin(struct hmap *lflows, bool has_lb_vips,
+                         const struct uuid *lflow_uuid, uint32_t *lflow_uuid_idx)
+{
+    if (has_lb_vips) {
+        /* Check if the packet needs to be hairpinned.
+         * Set REGBIT_HAIRPIN in the original direction and
+         * REGBIT_HAIRPIN_REPLY in the reply direction.
+         */
+        ovn_ctrl_lflow_add_uuid(
+            lflows, S_SWITCH_IN_PRE_HAIRPIN, 100, "ip && ct.trk",
+            REGBIT_HAIRPIN " = chk_lb_hairpin(); "
+            REGBIT_HAIRPIN_REPLY " = chk_lb_hairpin_reply(); "
+            "next;", lflow_uuid, lflow_uuid_idx);
+
+        /* If packet needs to be hairpinned, snat the src ip with the VIP
+         * for new sessions. */
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_NAT_HAIRPIN, 100,
+                                "ip && ct.new && ct.trk"
+                                " && "REGBIT_HAIRPIN " == 1",
+                                "ct_snat_to_vip; next;",
+                                lflow_uuid, lflow_uuid_idx);
+
+        /* If packet needs to be hairpinned, for established sessions there
+         * should already be an SNAT conntrack entry.
+         */
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_NAT_HAIRPIN, 100,
+                                "ip && ct.est && ct.trk"
+                                " && "REGBIT_HAIRPIN " == 1",
+                                "ct_snat;",
+                                lflow_uuid, lflow_uuid_idx);
+
+        /* For the reply of hairpinned traffic, snat the src ip to the VIP. */
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_NAT_HAIRPIN, 90,
+                                "ip && "REGBIT_HAIRPIN_REPLY " == 1",
+                                "ct_snat;",
+                                lflow_uuid, lflow_uuid_idx);
+
+        /* Ingress Hairpin table.
+        * - Priority 1: Packets that were SNAT-ed for hairpinning should be
+        *   looped back (i.e., swap ETH addresses and send back on inport).
+        */
+        ovn_ctrl_lflow_add_uuid(
+            lflows, S_SWITCH_IN_HAIRPIN, 1,
+            "("REGBIT_HAIRPIN " == 1 || " REGBIT_HAIRPIN_REPLY " == 1)",
+            "eth.dst <-> eth.src; outport = inport; flags.loopback = 1; "
+            "output;", lflow_uuid, lflow_uuid_idx);
+    }
+}
+
+static void
+build_lswitch_pre_acls_and_acls(struct hmap *lflows,
+                                const struct sbrec_datapath_binding *dp,
+                                bool use_ct_inv_match,
+                                const struct uuid *lflow_uuid,
+                                uint32_t *lflow_uuid_idx)
+{
+    bool has_stateful_acls = has_dp_stateful_acls(dp);
+    bool has_lb_vips = has_dp_lb_vip(dp);
+    bool has_stateful = (has_stateful_acls || has_lb_vips);
+    bool has_acls_or_lbs = has_dp_acls(dp) || has_lb_vips;
+
+    build_lswitch_pre_acls(lflows, has_stateful_acls, lflow_uuid,
+                           lflow_uuid_idx);
+    build_lswitch_pre_lb(lflows, has_lb_vips, lflow_uuid, lflow_uuid_idx);
+    build_lswitch_acl_hints(lflows, has_acls_or_lbs, lflow_uuid,
+                            lflow_uuid_idx);
+    build_lswitch_acls(lflows, has_acls_or_lbs, has_stateful,
+                       use_ct_inv_match, lflow_uuid, lflow_uuid_idx);
+    build_lswitch_lb_hairpin(lflows, has_lb_vips, lflow_uuid, lflow_uuid_idx);
+
+    /* Add a 34000 priority flow to advance the DNS reply from ovn-controller,
+     * if the CMS has configured DNS records for the datapath.
+     */
+    if (has_dp_dns_records(dp)) {
+        const char *actions = has_stateful ? "ct_commit; next;" : "next;";
+        ovn_ctrl_lflow_add_uuid(
+            lflows, S_SWITCH_OUT_ACL, 34000, "udp.src == 53",
+            actions, lflow_uuid, lflow_uuid_idx);
+    }
+
+#if 0
+    /* Add a 34000 priority flow to advance the service monitor reply
+     * packets to skip applying ingress ACLs. */
+    ovn_ctrl_lflow_add(lflows, 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_ctrl_lflow_add(lflows, S_SWITCH_OUT_ACL, 34000,
+                       "eth.src == $svc_monitor_mac", "next;");
+#endif
+}
+
+static void
+build_lswitch_dns_lkup(struct hmap *lflows,
+                       const struct uuid *lflow_uuid,
+                       uint32_t *lflow_uuid_idx)
+{
+    ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_DNS_LOOKUP, 100,
+                            "udp.dst == 53",
+                            REGBIT_DNS_LOOKUP_RESULT" = dns_lookup(); next;",
+                            lflow_uuid, lflow_uuid_idx);
+    const char *dns_action =
+        "eth.dst <-> eth.src; ip4.src <-> ip4.dst; "
+        "udp.dst = udp.src; udp.src = 53; outport = inport; "
+        "flags.loopback = 1; output;";
+    const char *dns_match = "udp.dst == 53 && "REGBIT_DNS_LOOKUP_RESULT;
+    ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_DNS_RESPONSE, 100,
+                            dns_match, dns_action,
+                            lflow_uuid, lflow_uuid_idx);
+    dns_action = "eth.dst <-> eth.src; ip6.src <-> ip6.dst; "
+                 "udp.dst = udp.src; udp.src = 53; outport = inport; "
+                 "flags.loopback = 1; output;";
+    ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_DNS_RESPONSE, 100,
+                            dns_match, dns_action,
+                            lflow_uuid, lflow_uuid_idx);
+}
+
+static void
+build_lswitch_dp_lflows(struct hmap *lflows,
+                        struct local_datapath *ldp,
+                        bool use_ct_inv_match)
+{
+    uint32_t lflow_uuid_idx = 1;
+
+    /* Logical VLANs not supported. */
+    if (!is_dp_vlan_transparent(ldp->datapath)) {
+        /* Block logical VLANs. */
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_PORT_SEC_L2, 100,
+                                "vlan.present", "drop;",
+                                &ldp->datapath->header_.uuid,
+                                &lflow_uuid_idx);
+    }
+
+    build_lswitch_pre_acls_and_acls(lflows, ldp->datapath, use_ct_inv_match,
+                                    &ldp->datapath->header_.uuid,
+                                    &lflow_uuid_idx);
+
+    if (has_dp_dns_records(ldp->datapath)) {
+        build_lswitch_dns_lkup(lflows, &ldp->datapath->header_.uuid,
+                               &lflow_uuid_idx);
+    }
+
+    if (has_dp_unknown_lports(ldp->datapath)) {
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_L2_LKUP, 0, "1",
+                                "outport = \""MC_UNKNOWN"\"; output;",
+                                 &ldp->datapath->header_.uuid,
+                                 &lflow_uuid_idx);
+    }
+}
+
+static void
+build_generic_lr_lookup(struct hmap *lflows)
+{
+    /* For other packet types, we can skip neighbor learning.
+         * So set REGBIT_LOOKUP_NEIGHBOR_RESULT to 1. */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LOOKUP_NEIGHBOR, 0, "1",
+                          REGBIT_LOOKUP_NEIGHBOR_RESULT" = 1; next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
+                          "arp", "put_arp(inport, arp.spa, arp.sha); next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
+                          "nd_na", "put_nd(inport, nd.target, nd.tll); next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
+                          "nd_ns", "put_nd(inport, ip6.src, nd.sll); next;");
+}
+
+static void
+build_generic_lr_ip_input(struct hmap *lflows)
+{
+    /* L3 admission control: drop multicast and broadcast source, localhost
+        * source or destination, and zero network source or destination
+        * (priority 100). */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_INPUT, 100,
+                          "ip4.src_mcast ||"
+                          "ip4.src == 255.255.255.255 || "
+                          "ip4.src == 127.0.0.0/8 || "
+                          "ip4.dst == 127.0.0.0/8 || "
+                          "ip4.src == 0.0.0.0/8 || "
+                          "ip4.dst == 0.0.0.0/8",
+                          "drop;");
+
+    /* Drop ARP packets (priority 85). ARP request packets for router's own
+        * IPs are handled with priority-90 flows.
+        * Drop IPv6 ND packets (priority 85). ND NA packets for router's own
+        * IPs are handled with priority-90 flows.
+        */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_INPUT, 85,
+                          "arp || nd", "drop;");
+
+    /* Allow IPv6 multicast traffic that's supposed to reach the
+        * router pipeline (e.g., router solicitations).
+        */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_INPUT, 84, "nd_rs || nd_ra",
+                          "next;");
+
+    /* Drop other reserved multicast. */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_INPUT, 83,
+                          "ip6.mcast_rsvd", "drop;");
+
+    /* Drop Ethernet local broadcast.  By definition this traffic should
+        * not be forwarded.*/
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_INPUT, 50,
+                       "eth.bcast", "drop;");
+
+    /* TTL discard */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_INPUT, 30,
+                       "ip4 && ip.ttl == {0, 1}", "drop;");
+
+    /* Pass other traffic not already handled to the next table for
+        * routing. */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_INPUT, 0, "1", "next;");
+}
+
+static void
+build_generic_lr_arp_resolve(struct hmap *lflows)
+{
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ARP_RESOLVE, 500,
+                          "ip4.mcast || ip6.mcast", "next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ARP_RESOLVE, 0, "ip4",
+                          "get_arp(outport, " REG_NEXT_HOP_IPV4 "); next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ARP_RESOLVE, 0, "ip6",
+                          "get_nd(outport, " REG_NEXT_HOP_IPV6 "); next;");
+}
+
+static void
+build_generic_lr_arp_request(struct hmap *lflows)
+{
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ARP_REQUEST, 100,
+                          "eth.dst == 00:00:00:00:00:00 && ip4",
+                          "arp { "
+                          "eth.dst = ff:ff:ff:ff:ff:ff; "
+                          "arp.spa = " REG_SRC_IPV4 "; "
+                          "arp.tpa = " REG_NEXT_HOP_IPV4 "; "
+                          "arp.op = 1; " /* ARP request */
+                          "output; "
+                          "};");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ARP_REQUEST, 100,
+                          "eth.dst == 00:00:00:00:00:00 && ip6",
+                          "nd_ns { "
+                          "nd.target = " REG_NEXT_HOP_IPV6 "; "
+                          "output; "
+                          "};");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ARP_REQUEST, 0, "1", "output;");
+}
+
+void
+build_lrouter_generic_lflows(struct hmap *lflows)
+{
+    /* Logical VLANs not supported.
+         * Broadcast/multicast source address is invalid. */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ADMISSION, 100,
+                          "vlan.present || eth.src[40]", "drop;");
+
+    build_generic_lr_lookup(lflows);
+    build_generic_lr_ip_input(lflows);
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_DEFRAG, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_UNSNAT, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_DNAT, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ECMP_STATEFUL, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ND_RA_OPTIONS, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_ND_RA_RESPONSE, 0, "1", "next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_ROUTING, 550,
+                       "nd_rs || nd_ra", "drop;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_ROUTING_ECMP, 150,
+                       REG_ECMP_GROUP_ID" == 0", "next;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_POLICY, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_POLICY_ECMP, 150,
+                       REG_ECMP_GROUP_ID" == 0", "next;");
+
+    build_generic_lr_arp_resolve(lflows);
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_CHK_PKT_LEN, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LARGER_PKTS, 0, "1", "next;");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_GW_REDIRECT, 0, "1", "next;");
+
+    build_generic_lr_arp_request(lflows);
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_OUT_UNDNAT, 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_ctrl_lflow_add(lflows, S_ROUTER_OUT_SNAT, 120, "nd_ns", "next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_OUT_SNAT, 0, "1", "next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_OUT_EGR_LOOP, 0, "1", "next;");
+}
+
+static bool
+is_learn_from_arp_request(const struct sbrec_datapath_binding *dp)
+{
+    return (!datapath_is_switch(dp) &&
+            smap_get_bool(&dp->options,
+                          "always-learn-from-arp-request", true));
+
+}
+
+static void build_lrouter_neigh_learning_flows(
+    struct hmap *lflows, const struct sbrec_datapath_binding *dp);
+static void build_misc_local_traffic_drop_flows_for_lrouter(
+    struct hmap *lflows, const struct sbrec_datapath_binding *dp);
+
+static void
+build_lrouter_dp_lflows(struct hmap *lflows,
+                        const struct sbrec_datapath_binding *dp)
+{
+    build_lrouter_neigh_learning_flows(lflows, dp);
+    build_misc_local_traffic_drop_flows_for_lrouter(lflows, dp);
+}
+
+static void
+build_lrouter_neigh_learning_flows(struct hmap *lflows,
+                                   const struct sbrec_datapath_binding *dp)
+{
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    bool learn_from_arp_request = is_learn_from_arp_request(dp);
+
+    ds_clear(&actions);
+    ds_put_format(&actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
+                  " = lookup_arp(inport, arp.spa, arp.sha); %snext;",
+                  learn_from_arp_request ? "" :
+                  REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1; ");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LOOKUP_NEIGHBOR, 100,
+                       "arp.op == 2", ds_cstr(&actions));
+
+    ds_clear(&actions);
+    ds_put_format(&actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
+                  " = lookup_nd(inport, nd.target, nd.tll); %snext;",
+                  learn_from_arp_request ? "" :
+                  REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1; ");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LOOKUP_NEIGHBOR, 100, "nd_na",
+                       ds_cstr(&actions));
+
+    ds_clear(&actions);
+    ds_put_format(&actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
+                  " = lookup_nd(inport, ip6.src, nd.sll); %snext;",
+                  learn_from_arp_request ? "" :
+                  REGBIT_LOOKUP_NEIGHBOR_IP_RESULT
+                  " = lookup_nd_ip(inport, ip6.src); ");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LOOKUP_NEIGHBOR, 100, "nd_ns",
+                    ds_cstr(&actions));
+
+    /* For other packet types, we can skip neighbor learning.
+        * So set REGBIT_LOOKUP_NEIGHBOR_RESULT to 1. */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LOOKUP_NEIGHBOR, 0, "1",
+                    REGBIT_LOOKUP_NEIGHBOR_RESULT" = 1; next;");
+
+    /* Flows for LEARN_NEIGHBOR. */
+    /* Skip Neighbor learning if not required. */
+    ds_clear(&match);
+    ds_put_format(&match, REGBIT_LOOKUP_NEIGHBOR_RESULT" == 1%s",
+                  learn_from_arp_request ? "" :
+                  " || "REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" == 0");
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LEARN_NEIGHBOR, 100,
+                       ds_cstr(&match), "next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
+                       "arp", "put_arp(inport, arp.spa, arp.sha); next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
+                       "nd_na", "put_nd(inport, nd.target, nd.tll); next;");
+
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
+                       "nd_ns", "put_nd(inport, ip6.src, nd.sll); next;");
+
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
+static void
+build_misc_local_traffic_drop_flows_for_lrouter(
+    struct hmap *lflows,
+    const struct sbrec_datapath_binding *dp)
+{
+    bool mcast_relay = smap_get_bool(&dp->options, "mcast-relay", false);
+    /* Allow other multicast if relay enabled (priority 82). */
+    ovn_ctrl_lflow_add(lflows, S_ROUTER_IN_IP_INPUT, 82,
+                      "ip4.mcast || ip6.mcast",
+                       mcast_relay ? "next;" : "drop;");
+}
+
+static bool
+lsp_is_enabled(const struct sbrec_port_binding *pb)
+{
+    return smap_get_bool(&pb->options, "enabled", true);
+}
+
+static bool
+lsp_is_up(const struct sbrec_port_binding *pb)
+{
+    return pb->n_up && *pb->up;
+}
+
+static void build_lswitch_input_port_sec_op(struct hmap *lflows,
+                                            struct local_lport *,
+                                            uint32_t *lflow_uuid_idx);
+static void build_lswitch_output_port_sec_op(struct hmap *lflows,
+                                             struct local_lport *,
+                                             uint32_t *lflow_uuid_idx);
+static void build_lswitch_learn_fdb_op(struct hmap *lflows,
+                                       struct local_lport *,
+                                       uint32_t *lflow_uuid_idx,
+                                       struct ds *match,
+                                       struct ds *actions);
+static void build_lswitch_skip_conntrack_flows_op(struct hmap *lflows,
+                                                  struct local_lport *,
+                                                  uint32_t *lflow_uuid_idx,
+                                                  struct ds *match);
+static void build_lswitch_arp_nd_responder_skip_local(struct hmap *lflows,
+                                                      struct local_lport *,
+                                                      uint32_t *lflow_uuid_idx,
+                                                      struct ds *match);
+static void build_lswitch_arp_nd_responder_known_ips(struct hmap *lflows,
+                                                     struct local_lport *,
+                                                     uint32_t *lflow_uuid_idx,
+                                                     struct ds *match,
+                                                     struct ds *actions);
+static void build_lswitch_ip_unicast_lookup(struct hmap *lflows,
+                                            struct local_lport *,
+                                            uint32_t *lflow_uuid_idx,
+                                            struct ds *match,
+                                            struct ds *actions);
+
+static void build_arp_resolve_flows_for_lsp_in_router(
+    struct hmap *lflow, struct local_lport *, uint32_t *lflow_uuid_idx,
+    struct ds *match, struct ds *actions);
+
+
+static bool is_ip4_in_router_network(struct local_lport *, ovs_be32 ip);
+static bool is_ip6_in_router_network(struct local_lport *, struct in6_addr);
+static void op_put_v4_networks(struct ds *ds, const struct local_lport *op,
+                               bool add_bcast);
+static void op_put_v6_networks(struct ds *ds, const struct local_lport *op);
+
+static void
+build_lswitch_port_lflows(struct hmap *lflows, struct local_lport *op)
+{
+    uint32_t lflow_uuid_idx = 1;
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    build_lswitch_input_port_sec_op(lflows, op, &lflow_uuid_idx);
+    build_lswitch_output_port_sec_op(lflows, op, &lflow_uuid_idx);
+    build_lswitch_learn_fdb_op(lflows, op, &lflow_uuid_idx,
+                               &match, &actions);
+    build_lswitch_skip_conntrack_flows_op(lflows, op, &lflow_uuid_idx, &match);
+    build_lswitch_arp_nd_responder_skip_local(lflows, op,
+                                              &lflow_uuid_idx, &match);
+    build_lswitch_arp_nd_responder_known_ips(lflows, op,
+                                             &lflow_uuid_idx, &match,
+                                             &actions);
+    build_lswitch_ip_unicast_lookup(lflows, op, &lflow_uuid_idx,
+                                    &match, &actions);
+
+    build_arp_resolve_flows_for_lsp_in_router(lflows, op, &lflow_uuid_idx,
+                                              &match, &actions);
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
+/* Appends port security constraints on L2 address field 'eth_addr_field'
+ * (e.g. "eth.src" or "eth.dst") to 'match'.  'ps_addrs', with 'n_ps_addrs'
+ * elements, is the collection of port_security constraints from an
+ * OVN_NB Logical_Switch_Port row generated by extract_lsp_addresses(). */
+static void
+build_port_security_l2(const char *eth_addr_field,
+                       struct lport_addresses *ps_addrs,
+                       unsigned int n_ps_addrs,
+                       struct ds *match)
+{
+    if (!n_ps_addrs) {
+        return;
+    }
+
+    ds_put_format(match, " && %s == {", eth_addr_field);
+
+    for (size_t i = 0; i < n_ps_addrs; i++) {
+        ds_put_format(match, "%s ", ps_addrs[i].ea_s);
+    }
+    ds_chomp(match, ' ');
+    ds_put_cstr(match, "}");
+}
+
+static void
+build_port_security_ipv6_flow(
+    enum ovn_pipeline pipeline, struct ds *match, struct eth_addr ea,
+    struct ipv6_netaddr *ipv6_addrs, int n_ipv6_addrs)
+{
+    char ip6_str[INET6_ADDRSTRLEN + 1];
+
+    ds_put_format(match, " && %s == {",
+                  pipeline == P_IN ? "ip6.src" : "ip6.dst");
+
+    /* Allow link-local address. */
+    struct in6_addr lla;
+    in6_generate_lla(ea, &lla);
+    ipv6_string_mapped(ip6_str, &lla);
+    ds_put_format(match, "%s, ", ip6_str);
+
+    /* Allow ip6.dst=ff00::/8 for multicast packets */
+    if (pipeline == P_OUT) {
+        ds_put_cstr(match, "ff00::/8, ");
+    }
+    for (size_t i = 0; i < n_ipv6_addrs; i++) {
+        /* When the netmask is applied, if the host portion is
+         * non-zero, the host can only use the specified
+         * address.  If zero, the host is allowed to use any
+         * address in the subnet.
+         */
+        if (ipv6_addrs[i].plen == 128
+            || !ipv6_addr_is_host_zero(&ipv6_addrs[i].addr,
+                                       &ipv6_addrs[i].mask)) {
+            ds_put_format(match, "%s, ", ipv6_addrs[i].addr_s);
+        } else {
+            ds_put_format(match, "%s/%d, ", ipv6_addrs[i].network_s,
+                          ipv6_addrs[i].plen);
+        }
+    }
+    /* Replace ", " by "}". */
+    ds_chomp(match, ' ');
+    ds_chomp(match, ',');
+    ds_put_cstr(match, "}");
+}
+
+static void
+build_port_security_ipv6_nd_flow(
+    struct ds *match, struct eth_addr ea, struct ipv6_netaddr *ipv6_addrs,
+    int n_ipv6_addrs)
+{
+    ds_put_format(match, " && ip6 && nd && ((nd.sll == "ETH_ADDR_FMT" || "
+                  "nd.sll == "ETH_ADDR_FMT") || ((nd.tll == "ETH_ADDR_FMT" || "
+                  "nd.tll == "ETH_ADDR_FMT")", ETH_ADDR_ARGS(eth_addr_zero),
+                  ETH_ADDR_ARGS(ea), ETH_ADDR_ARGS(eth_addr_zero),
+                  ETH_ADDR_ARGS(ea));
+    if (!n_ipv6_addrs) {
+        ds_put_cstr(match, "))");
+        return;
+    }
+
+    char ip6_str[INET6_ADDRSTRLEN + 1];
+    struct in6_addr lla;
+    in6_generate_lla(ea, &lla);
+    memset(ip6_str, 0, sizeof(ip6_str));
+    ipv6_string_mapped(ip6_str, &lla);
+    ds_put_format(match, " && (nd.target == %s", ip6_str);
+
+    for (size_t i = 0; i < n_ipv6_addrs; i++) {
+        /* When the netmask is applied, if the host portion is
+         * non-zero, the host can only use the specified
+         * address in the nd.target.  If zero, the host is allowed
+         * to use any address in the subnet.
+         */
+        if (ipv6_addrs[i].plen == 128
+            || !ipv6_addr_is_host_zero(&ipv6_addrs[i].addr,
+                                       &ipv6_addrs[i].mask)) {
+            ds_put_format(match, " || nd.target == %s", ipv6_addrs[i].addr_s);
+        } else {
+            ds_put_format(match, " || nd.target == %s/%d",
+                          ipv6_addrs[i].network_s, ipv6_addrs[i].plen);
+        }
+    }
+
+    ds_put_format(match, ")))");
+}
+
+/**
+ * Build port security constraints on IPv4 and IPv6 src and dst fields
+ * and add logical flows to S_SWITCH_(IN/OUT)_PORT_SEC_IP stage.
+ *
+ * For each port security of the logical port, following
+ * logical flows are added
+ *   - If the port security has IPv4 addresses,
+ *     - Priority 90 flow to allow IPv4 packets for known IPv4 addresses
+ *
+ *   - If the port security has IPv6 addresses,
+ *     - Priority 90 flow to allow IPv6 packets for known IPv6 addresses
+ *
+ *   - If the port security has IPv4 addresses or IPv6 addresses or both
+ *     - Priority 80 flow to drop all IPv4 and IPv6 traffic
+ */
+static void
+build_port_security_ip(enum ovn_pipeline pipeline, struct local_lport *op,
+                       struct hmap *lflows, uint32_t *lflow_uuid_idx)
+{
+    char *port_direction;
+    enum ovn_stage stage;
+    if (pipeline == P_IN) {
+        port_direction = "inport";
+        stage = S_SWITCH_IN_PORT_SEC_IP;
+    } else {
+        port_direction = "outport";
+        stage = S_SWITCH_OUT_PORT_SEC_IP;
+    }
+
+    for (size_t i = 0; i < op->lsp.n_ps_addrs; i++) {
+        struct lport_addresses *ps = &op->lsp.ps_addrs[i];
+
+        if (!(ps->n_ipv4_addrs || ps->n_ipv6_addrs)) {
+            continue;
+        }
+
+        if (ps->n_ipv4_addrs) {
+            struct ds match = DS_EMPTY_INITIALIZER;
+            if (pipeline == P_IN) {
+                /* Permit use of the unspecified address for DHCP discovery */
+                struct ds dhcp_match = DS_EMPTY_INITIALIZER;
+                ds_put_format(&dhcp_match, "inport == %s"
+                              " && eth.src == %s"
+                              " && ip4.src == 0.0.0.0"
+                              " && ip4.dst == 255.255.255.255"
+                              " && udp.src == 68 && udp.dst == 67",
+                              op->json_key, ps->ea_s);
+                ovn_ctrl_lflow_add_uuid(lflows, stage, 90,
+                                        ds_cstr(&dhcp_match), "next;",
+                                        &op->pb->header_.uuid, lflow_uuid_idx);
+                ds_destroy(&dhcp_match);
+                ds_put_format(&match, "inport == %s && eth.src == %s"
+                              " && ip4.src == {", op->json_key,
+                              ps->ea_s);
+            } else {
+                ds_put_format(&match, "outport == %s && eth.dst == %s"
+                              " && ip4.dst == {255.255.255.255, 224.0.0.0/4, ",
+                              op->json_key, ps->ea_s);
+            }
+
+            for (int j = 0; j < ps->n_ipv4_addrs; j++) {
+                ovs_be32 mask = ps->ipv4_addrs[j].mask;
+                /* When the netmask is applied, if the host portion is
+                 * non-zero, the host can only use the specified
+                 * address.  If zero, the host is allowed to use any
+                 * address in the subnet.
+                 */
+                if (ps->ipv4_addrs[j].plen == 32
+                    || ps->ipv4_addrs[j].addr & ~mask) {
+                    ds_put_format(&match, "%s", ps->ipv4_addrs[j].addr_s);
+                    if (pipeline == P_OUT && ps->ipv4_addrs[j].plen != 32) {
+                        /* Host is also allowed to receive packets to the
+                         * broadcast address in the specified subnet. */
+                        ds_put_format(&match, ", %s",
+                                      ps->ipv4_addrs[j].bcast_s);
+                    }
+                } else {
+                    /* host portion is zero */
+                    ds_put_format(&match, "%s/%d", ps->ipv4_addrs[j].network_s,
+                                  ps->ipv4_addrs[j].plen);
+                }
+                ds_put_cstr(&match, ", ");
+            }
+
+            /* Replace ", " by "}". */
+            ds_chomp(&match, ' ');
+            ds_chomp(&match, ',');
+            ds_put_cstr(&match, "}");
+            ovn_ctrl_lflow_add_uuid(lflows, stage, 90, ds_cstr(&match),
+                                    "next;", &op->pb->header_.uuid,
+                                    lflow_uuid_idx);
+            ds_destroy(&match);
+        }
+
+        if (ps->n_ipv6_addrs) {
+            struct ds match = DS_EMPTY_INITIALIZER;
+            if (pipeline == P_IN) {
+                /* Permit use of unspecified address for duplicate address
+                 * detection */
+                struct ds dad_match = DS_EMPTY_INITIALIZER;
+                ds_put_format(&dad_match, "inport == %s"
+                              " && eth.src == %s"
+                              " && ip6.src == ::"
+                              " && ip6.dst == ff02::/16"
+                              " && icmp6.type == {131, 135, 143}",
+                              op->json_key,
+                              ps->ea_s);
+                ovn_ctrl_lflow_add_uuid(lflows, stage, 90, ds_cstr(&dad_match),
+                                   "next;", &op->pb->header_.uuid,
+                                   lflow_uuid_idx);
+                ds_destroy(&dad_match);
+            }
+            ds_put_format(&match, "%s == %s && %s == %s",
+                          port_direction, op->json_key,
+                          pipeline == P_IN ? "eth.src" : "eth.dst", ps->ea_s);
+            build_port_security_ipv6_flow(pipeline, &match, ps->ea,
+                                          ps->ipv6_addrs, ps->n_ipv6_addrs);
+            ovn_ctrl_lflow_add_uuid(lflows, stage, 90, ds_cstr(&match),
+                                    "next;", &op->pb->header_.uuid,
+                                    lflow_uuid_idx);
+            ds_destroy(&match);
+        }
+
+        char *match = xasprintf("%s == %s && %s == %s && ip",
+                                port_direction, op->json_key,
+                                pipeline == P_IN ? "eth.src" : "eth.dst",
+                                ps->ea_s);
+        ovn_ctrl_lflow_add_uuid(lflows, stage, 80, match, "drop;",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+        free(match);
+    }
+
+}
+
+/**
+ * Build port security constraints on ARP and IPv6 ND fields
+ * and add logical flows to S_SWITCH_IN_PORT_SEC_ND stage.
+ *
+ * For each port security of the logical port, following
+ * logical flows are added
+ *   - If the port security has no IP (both IPv4 and IPv6) or
+ *     if it has IPv4 address(es)
+ *      - Priority 90 flow to allow ARP packets for known MAC addresses
+ *        in the eth.src and arp.spa fields. If the port security
+ *        has IPv4 addresses, allow known IPv4 addresses in the arp.tpa field.
+ *
+ *   - If the port security has no IP (both IPv4 and IPv6) or
+ *     if it has IPv6 address(es)
+ *     - Priority 90 flow to allow IPv6 ND packets for known MAC addresses
+ *       in the eth.src and nd.sll/nd.tll fields. If the port security
+ *       has IPv6 addresses, allow known IPv6 addresses in the nd.target field
+ *       for IPv6 Neighbor Advertisement packet.
+ *
+ *   - Priority 80 flow to drop ARP and IPv6 ND packets.
+ */
+static void
+build_port_security_nd(struct local_lport *op, struct hmap *lflows,
+                       uint32_t *lflow_uuid_idx)
+{
+    struct ds match = DS_EMPTY_INITIALIZER;
+
+    for (size_t i = 0; i < op->lsp.n_ps_addrs; i++) {
+        struct lport_addresses *ps = &op->lsp.ps_addrs[i];
+
+        bool no_ip = !(ps->n_ipv4_addrs || ps->n_ipv6_addrs);
+
+        ds_clear(&match);
+        if (ps->n_ipv4_addrs || no_ip) {
+            ds_put_format(&match,
+                          "inport == %s && eth.src == %s && arp.sha == %s",
+                          op->json_key, ps->ea_s, ps->ea_s);
+
+            if (ps->n_ipv4_addrs) {
+                ds_put_cstr(&match, " && arp.spa == {");
+                for (size_t j = 0; j < ps->n_ipv4_addrs; j++) {
+                    /* When the netmask is applied, if the host portion is
+                     * non-zero, the host can only use the specified
+                     * address in the arp.spa.  If zero, the host is allowed
+                     * to use any address in the subnet. */
+                    if (ps->ipv4_addrs[j].plen == 32
+                        || ps->ipv4_addrs[j].addr & ~ps->ipv4_addrs[j].mask) {
+                        ds_put_cstr(&match, ps->ipv4_addrs[j].addr_s);
+                    } else {
+                        ds_put_format(&match, "%s/%d",
+                                      ps->ipv4_addrs[j].network_s,
+                                      ps->ipv4_addrs[j].plen);
+                    }
+                    ds_put_cstr(&match, ", ");
+                }
+                ds_chomp(&match, ' ');
+                ds_chomp(&match, ',');
+                ds_put_cstr(&match, "}");
+            }
+            ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_PORT_SEC_ND,
+                                    90, ds_cstr(&match), "next;",
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+        }
+
+        if (ps->n_ipv6_addrs || no_ip) {
+            ds_clear(&match);
+            ds_put_format(&match, "inport == %s && eth.src == %s",
+                          op->json_key, ps->ea_s);
+            build_port_security_ipv6_nd_flow(&match, ps->ea, ps->ipv6_addrs,
+                                             ps->n_ipv6_addrs);
+            ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_PORT_SEC_ND, 90,
+                                    ds_cstr(&match), "next;",
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+        }
+    }
+
+    ds_clear(&match);
+    ds_put_format(&match, "inport == %s && (arp || nd)", op->json_key);
+    ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_PORT_SEC_ND, 80,
+                            ds_cstr(&match), "drop;", &op->pb->header_.uuid,
+                            lflow_uuid_idx);
+    ds_destroy(&match);
+}
+
+/* Logical switch ingress table 0: Ingress port security - L2
+ *  (priority 50).
+ *  Ingress table 1: Ingress port security - IP (priority 90 and 80)
+ *  Ingress table 2: Ingress port security - ND (priority 90 and 80)
+ */
+static void
+build_lswitch_input_port_sec_op(struct hmap *lflows, struct local_lport *op,
+                                uint32_t *lflow_uuid_idx)
+{
+    if (op->type == LP_EXTERNAL) {
+        return;
+    }
+
+    if (!lsp_is_enabled(op->pb)) {
+        /* Drop packets from disabled logical ports (since logical flow
+         * tables are default-drop). */
+        return;
+    }
+
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    ds_put_format(&match, "inport == %s", op->json_key);
+    build_port_security_l2("eth.src", op->lsp.ps_addrs, op->lsp.n_ps_addrs,
+                            &match);
+
+    const char *queue_id = smap_get(&op->pb->options, "qdisc_queue_id");
+    if (queue_id) {
+        ds_put_format(&actions, "set_queue(%s); ", queue_id);
+    }
+    ds_put_cstr(&actions, "next;");
+    ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_PORT_SEC_L2, 50,
+                            ds_cstr(&match), ds_cstr(&actions),
+                            &op->pb->header_.uuid, lflow_uuid_idx);
+
+    ds_destroy(&match);
+    ds_destroy(&actions);
+
+    if (op->lsp.n_ps_addrs) {
+        build_port_security_ip(P_IN, op, lflows, lflow_uuid_idx);
+        build_port_security_nd(op, lflows, lflow_uuid_idx);
+    }
+}
+
+/* Egress table 8: Egress port security - IP (priorities 90 and 80)
+ * if port security enabled.
+ *
+ * Egress table 9: Egress port security - L2 (priorities 50 and 150).
+ *
+ * Priority 50 rules implement port security for enabled logical port.
+ *
+ * Priority 150 rules drop packets to disabled logical ports, so that
+ * they don't even receive multicast or broadcast packets.
+ */
+static void
+build_lswitch_output_port_sec_op(struct hmap *lflows, struct local_lport *op,
+                                 uint32_t *lflow_uuid_idx)
+{
+    if (op->type == LP_EXTERNAL) {
+        return;
+    }
+
+    struct ds match = DS_EMPTY_INITIALIZER;
+
+    ds_put_format(&match, "outport == %s", op->json_key);
+    if (lsp_is_enabled(op->pb)) {
+        struct ds actions = DS_EMPTY_INITIALIZER;
+        build_port_security_l2("eth.dst", op->lsp.ps_addrs, op->lsp.n_ps_addrs,
+                                &match);
+
+        if (op->type == LP_LOCALNET) {
+            const char *queue_id = smap_get(&op->pb->options,
+                                            "qdisc_queue_id");
+            if (queue_id) {
+                ds_put_format(&actions, "set_queue(%s); ", queue_id);
+            }
+        }
+        ds_put_cstr(&actions, "output;");
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_OUT_PORT_SEC_L2,
+                                50, ds_cstr(&match), ds_cstr(&actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+        ds_destroy(&actions);
+    } else {
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_OUT_PORT_SEC_L2,
+                                150, ds_cstr(&match), "drop;",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+
+    ds_destroy(&match);
+
+    if (op->lsp.n_ps_addrs) {
+        build_port_security_ip(P_OUT, op, lflows, lflow_uuid_idx);
+    }
+}
+
+static void
+build_lswitch_learn_fdb_op(struct hmap *lflows, struct local_lport *op,
+                           uint32_t *lflow_uuid_idx, struct ds *match,
+                           struct ds *actions)
+{
+    if (!op->lsp.n_ps_addrs && op->type == LP_VIF &&
+            op->lsp.has_unknown) {
+        ds_clear(match);
+        ds_clear(actions);
+        ds_put_format(match, "inport == %s", op->json_key);
+        ds_put_format(actions, REGBIT_LKUP_FDB
+                      " = lookup_fdb(inport, eth.src); next;");
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_LOOKUP_FDB, 100,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+
+        ds_put_cstr(match, " && "REGBIT_LKUP_FDB" == 0");
+        ds_clear(actions);
+        ds_put_cstr(actions, "put_fdb(inport, eth.src); next;");
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_PUT_FDB, 100,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+}
+
+static void
+build_lswitch_skip_conntrack_flows_op(struct hmap *lflows,
+                                      struct local_lport *op,
+                                      uint32_t *lflow_uuid_idx,
+                                      struct ds *match)
+{
+    if (!op->peer) {
+        return;
+    }
+
+    bool has_stateful = (has_dp_stateful_acls(op->ldp->datapath) ||
+                         has_dp_lb_vip(op->ldp->datapath));
+    if (has_stateful) {
+        skip_lport_from_conntrack(lflows, op, lflow_uuid_idx,
+                                  S_SWITCH_IN_PRE_LB, S_SWITCH_OUT_PRE_LB,
+                                  110, match);
+        skip_lport_from_conntrack(lflows, op, lflow_uuid_idx,
+                                  S_SWITCH_IN_PRE_ACL, S_SWITCH_OUT_PRE_ACL,
+                                  110, match);
+    }
+}
+
+/* Ingress table 13: ARP/ND responder, skip requests coming from localnet
+ * and vtep ports. (priority 100); see ovn-northd.8.xml for the
+ * rationale. */
+
+static void
+build_lswitch_arp_nd_responder_skip_local(struct hmap *lflows,
+                                          struct local_lport *op,
+                                          uint32_t *lflow_uuid_idx,
+                                          struct ds *match)
+{
+    if (op->type == LP_LOCALNET || op->type == LP_VTEP) {
+        ds_clear(match);
+        ds_put_format(match, "inport == %s", op->json_key);
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ARP_ND_RSP, 100,
+                                ds_cstr(match), "next;",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+}
+
+/* Ingress table 13: ARP/ND responder, reply for known IPs.
+ * (priority 50). */
+static void
+build_lswitch_arp_nd_responder_known_ips(struct hmap *lflows,
+                                         struct local_lport *op,
+                                         uint32_t *lflow_uuid_idx,
+                                         struct ds *match,
+                                         struct ds *actions)
+{
+    if (op->type == LP_VIRTUAL) {
+        /* Handle
+            *  - GARPs for virtual ip which belongs to a logical port
+            *    of type 'virtual' and bind that port.
+            *
+            *  - ARP reply from the virtual ip which belongs to a logical
+            *    port of type 'virtual' and bind that port.
+            * */
+        ovs_be32 ip;
+        const char *virtual_ip = smap_get(&op->pb->options,
+                                          "virtual-ip");
+        const char *virtual_parents = smap_get(&op->pb->options,
+                                               "virtual-parents");
+        if (!virtual_ip || !virtual_parents ||
+            !ip_parse(virtual_ip, &ip)) {
+            return;
+        }
+
+        char *tokstr = xstrdup(virtual_parents);
+        char *save_ptr = NULL;
+        char *vparent;
+        for (vparent = strtok_r(tokstr, ",", &save_ptr); vparent != NULL;
+                vparent = strtok_r(NULL, ",", &save_ptr)) {
+            ds_clear(match);
+            ds_put_format(match, "inport == \"%s\" && "
+                          "((arp.op == 1 && arp.spa == %s && "
+                          "arp.tpa == %s) || (arp.op == 2 && "
+                          "arp.spa == %s))",
+                          vparent, virtual_ip, virtual_ip,
+                          virtual_ip);
+            ds_clear(actions);
+            ds_put_format(actions,
+                "bind_vport(%s, inport); "
+                "next;",
+                op->json_key);
+            ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ARP_ND_RSP, 100,
+                                    ds_cstr(match), ds_cstr(actions),
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+        }
+
+        free(tokstr);
+    } else {
+        /*
+         * Add ARP/ND reply flows if either the
+         *  - port is up and it doesn't have 'unknown' address defined or
+         *  - port type is router or
+         *  - port type is localport
+         */
+        if (op->lsp.check_lport_is_up &&
+            !lsp_is_up(op->pb) && op->type != LP_PATCH &&
+            op->type != LP_LOCALPORT) {
+            return;
+        }
+
+        if (op->type == LP_EXTERNAL || op->lsp.has_unknown) {
+            return;
+        }
+
+        for (size_t i = 0; i < op->lsp.n_addrs; i++) {
+            for (size_t j = 0; j < op->lsp.addrs[i].n_ipv4_addrs; j++) {
+                ds_clear(match);
+                ds_put_format(match, "arp.tpa == %s && arp.op == 1",
+                              op->lsp.addrs[i].ipv4_addrs[j].addr_s);
+                ds_clear(actions);
+                ds_put_format(actions,
+                    "eth.dst = eth.src; "
+                    "eth.src = %s; "
+                    "arp.op = 2; /* ARP reply */ "
+                    "arp.tha = arp.sha; "
+                    "arp.sha = %s; "
+                    "arp.tpa = arp.spa; "
+                    "arp.spa = %s; "
+                    "outport = inport; "
+                    "flags.loopback = 1; "
+                    "output;",
+                    op->lsp.addrs[i].ea_s, op->lsp.addrs[i].ea_s,
+                    op->lsp.addrs[i].ipv4_addrs[j].addr_s);
+                ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ARP_ND_RSP, 50,
+                                        ds_cstr(match), ds_cstr(actions),
+                                        &op->pb->header_.uuid, lflow_uuid_idx);
+
+                /* Do not reply to an ARP request from the port that owns
+                    * the address (otherwise a DHCP client that ARPs to check
+                    * for a duplicate address will fail).  Instead, forward
+                    * it the usual way.
+                    *
+                    * (Another alternative would be to simply drop the packet.
+                    * If everything is working as it is configured, then this
+                    * would produce equivalent results, since no one should
+                    * reply to the request.  But ARPing for one's own IP
+                    * address is intended to detect situations where the
+                    * network is not working as configured, so dropping the
+                    * request would frustrate that intent.) */
+                ds_put_format(match, " && inport == %s", op->json_key);
+                ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ARP_ND_RSP, 100,
+                                        ds_cstr(match), "next;",
+                                        &op->pb->header_.uuid, lflow_uuid_idx);
+            }
+
+            /* For ND solicitations, we need to listen for both the
+                * unicast IPv6 address and its all-nodes multicast address,
+                * but always respond with the unicast IPv6 address. */
+            for (size_t j = 0; j < op->lsp.addrs[i].n_ipv6_addrs; j++) {
+                ds_clear(match);
+                ds_put_format(
+                    match,
+                    "nd_ns && ip6.dst == {%s, %s} && nd.target == %s",
+                    op->lsp.addrs[i].ipv6_addrs[j].addr_s,
+                    op->lsp.addrs[i].ipv6_addrs[j].sn_addr_s,
+                    op->lsp.addrs[i].ipv6_addrs[j].addr_s);
+
+                ds_clear(actions);
+                ds_put_format(actions,
+                        "%s { "
+                        "eth.src = %s; "
+                        "ip6.src = %s; "
+                        "nd.target = %s; "
+                        "nd.tll = %s; "
+                        "outport = inport; "
+                        "flags.loopback = 1; "
+                        "output; "
+                        "};",
+                        op->type == LP_PATCH ? "nd_na_router" : "nd_na",
+                        op->lsp.addrs[i].ea_s,
+                        op->lsp.addrs[i].ipv6_addrs[j].addr_s,
+                        op->lsp.addrs[i].ipv6_addrs[j].addr_s,
+                        op->lsp.addrs[i].ea_s);
+                ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ARP_ND_RSP, 50,
+                                        ds_cstr(match), ds_cstr(actions),
+                                        &op->pb->header_.uuid, lflow_uuid_idx);
+
+                /* Do not reply to a solicitation from the port that owns
+                    * the address (otherwise DAD detection will fail). */
+                ds_put_format(match, " && inport == %s", op->json_key);
+                ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_ARP_ND_RSP, 100,
+                                        ds_cstr(match), "next;",
+                                        &op->pb->header_.uuid, lflow_uuid_idx);
+            }
+        }
+    }
+}
+
+/* Ingress table 19: Destination lookup, unicast handling (priority 50), */
+static void
+build_lswitch_ip_unicast_lookup(struct hmap *lflows, struct local_lport *op,
+                                uint32_t *lflow_uuid_idx,
+                                struct ds *match, struct ds *actions)
+{
+    if (op->type == LP_EXTERNAL) {
+        return;
+    }
+
+    /* For ports connected to logical routers add flows to bypass the
+     * broadcast flooding of ARP/ND requests in table 19. We direct the
+     * requests only to the router port that owns the IP address.
+     */
+#if 0
+    if (lsp_is_router(op->nbsp)) {
+        build_lswitch_rport_arp_req_flows(op->peer, op->od, op, lflows,
+                                            &op->nbsp->header_);
+    }
+#endif
+
+    for (size_t i = 0; i < op->lsp.n_addrs; i++) {
+        ds_clear(match);
+        ds_put_format(match, "eth.dst == %s", op->lsp.addrs[i].ea_s);
+        ds_clear(actions);
+        ds_put_format(actions, "outport = %s; output;", op->json_key);
+        ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_L2_LKUP, 50,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+}
+
+static void build_arp_resolve_flows_for_lsp_in_router(
+    struct hmap *lflows, struct local_lport *op, uint32_t *lflow_uuid_idx,
+    struct ds *match, struct ds *actions)
+{
+    if (op->type == LP_VIRTUAL) {
+        const char *vip_s = smap_get(&op->pb->options,
+                                    "virtual-ip");
+        const char *virtual_parents = smap_get(&op->pb->options,
+                                               "virtual-parents");
+        ovs_be32 vip;
+        if (!vip_s || !virtual_parents || !ip_parse(vip_s, &vip)) {
+            return;
+        }
+
+        if (!op->pb->virtual_parent || !op->pb->virtual_parent[0] ||
+            !op->pb->chassis) {
+            /* The virtual port is not claimed yet. */
+            for (size_t i = 0; i < op->ldp->n_peer_ports; i++) {
+                struct local_lport *peer = op->ldp->peer_ports[i].remote;
+
+                if (!is_ip4_in_router_network(peer, vip)) {
+                    continue;
+                }
+
+                ds_clear(match);
+                ds_put_format(match, "outport == %s && "
+                              REG_NEXT_HOP_IPV4 " == %s",
+                              peer->json_key, vip_s);
+
+                const char *arp_actions = "eth.dst = 00:00:00:00:00:00; next;";
+                ovn_ctrl_lflow_add_dp_key(
+                    lflows, peer->ldp->datapath->tunnel_key,
+                    S_ROUTER_IN_ARP_RESOLVE, 100,
+                    ds_cstr(match), arp_actions,
+                    &op->pb->header_.uuid, lflow_uuid_idx);
+                break;
+            }
+        } else {
+            struct local_lport *vp =
+                local_datapath_get_lport(op->ldp, op->pb->virtual_parent);
+            ovs_assert(vp);
+            for (size_t i = 0; i < vp->lsp.n_addrs; i++) {
+                bool found_vip_network = false;
+                const char *ea_s = vp->lsp.addrs[i].ea_s;
+                for (size_t j = 0; j < vp->ldp->n_peer_ports; j++) {
+                    struct local_lport *peer = op->ldp->peer_ports[i].remote;
+
+                    if (!is_ip4_in_router_network(peer, vip)) {
+                        continue;
+                    }
+
+                    ds_clear(match);
+                    ds_put_format(match, "outport == %s && "
+                                  REG_NEXT_HOP_IPV4 " == %s",
+                                  peer->json_key, vip_s);
+
+                    ds_clear(actions);
+                    ds_put_format(actions, "eth.dst = %s; next;", ea_s);
+                    ovn_ctrl_lflow_add_dp_key(
+                        lflows, peer->ldp->datapath->tunnel_key,
+                        S_ROUTER_IN_ARP_RESOLVE, 100,
+                        ds_cstr(match), ds_cstr(actions),
+                        &op->pb->header_.uuid, lflow_uuid_idx);
+                    found_vip_network = true;
+                    break;
+                }
+
+                if (found_vip_network) {
+                    break;
+                }
+            }
+        }
+    } else if (op->peer) {
+        /* This is a logical switch port that connects to a router. */
+
+        /* The peer of this switch port is the router port for which
+         * we need to add logical flows such that it can resolve
+         * ARP entries for all the other router ports connected to
+         * the switch in question. */
+        if (smap_get_bool(&op->peer->ldp->datapath->options,
+                          "dynamic_neigh_routers", false)) {
+            return;
+        }
+
+        for (size_t i = 0; i < op->ldp->n_peer_ports; i++) {
+            struct local_lport *router_port = op->ldp->peer_ports[i].remote;
+            /* Skip the router port under consideration. */
+            if (router_port == op->peer) {
+               continue;
+            }
+
+            if (router_port->lrp.networks.n_ipv4_addrs) {
+                ds_clear(match);
+                ds_put_format(match, "outport == %s && "
+                              REG_NEXT_HOP_IPV4 " == ",
+                              op->peer->json_key);
+                op_put_v4_networks(match, router_port, false);
+
+                ds_clear(actions);
+                ds_put_format(actions, "eth.dst = %s; next;",
+                              router_port->lrp.networks.ea_s);
+                ovn_ctrl_lflow_add_dp_key(
+                    lflows, op->peer->ldp->datapath->tunnel_key,
+                    S_ROUTER_IN_ARP_RESOLVE, 100,
+                    ds_cstr(match), ds_cstr(actions),
+                    &op->pb->header_.uuid, lflow_uuid_idx);
+
+            }
+
+            if (router_port->lrp.networks.n_ipv6_addrs) {
+                ds_clear(match);
+                ds_put_format(match, "outport == %s && "
+                              REG_NEXT_HOP_IPV6 " == ",
+                              op->peer->json_key);
+                op_put_v6_networks(match, router_port);
+
+                ds_clear(actions);
+                ds_put_format(actions, "eth.dst = %s; next;",
+                              router_port->lrp.networks.ea_s);
+                ovn_ctrl_lflow_add_dp_key(
+                    lflows, op->peer->ldp->datapath->tunnel_key,
+                    S_ROUTER_IN_ARP_RESOLVE, 100,
+                    ds_cstr(match), ds_cstr(actions),
+                    &op->pb->header_.uuid, lflow_uuid_idx);
+            }
+        }
+    } else {
+        for (size_t i = 0; i < op->lsp.n_addrs; i++) {
+            const char *ea_s = op->lsp.addrs[i].ea_s;
+            for (size_t j = 0; j < op->lsp.addrs[i].n_ipv4_addrs; j++) {
+                ovs_be32 ip = op->lsp.addrs[i].ipv4_addrs[j].addr;
+                for (size_t k = 0; k < op->ldp->n_peer_ports; k++) {
+                    /* Get the Logical_Router_Port that the Logical_Switch_Port
+                    * is connected to, as 'peer'. */
+                    struct local_lport *peer = op->ldp->peer_ports[k].remote;
+
+                    if (!is_ip4_in_router_network(peer, ip)) {
+                        continue;
+                    }
+
+                    ds_clear(match);
+                    ds_put_format(match, "outport == %s && "
+                                REG_NEXT_HOP_IPV4 " == %s", peer->json_key,
+                                op->lsp.addrs[i].ipv4_addrs[j].addr_s);
+
+                    ds_clear(actions);
+                    ds_put_format(actions, "eth.dst = %s; next;", ea_s);
+                    ovn_ctrl_lflow_add_dp_key(
+                        lflows, peer->ldp->datapath->tunnel_key,
+                        S_ROUTER_IN_ARP_RESOLVE, 100,
+                        ds_cstr(match), ds_cstr(actions),
+                        &op->pb->header_.uuid, lflow_uuid_idx);
+                }
+            }
+
+            for (size_t j = 0; j < op->lsp.addrs[i].n_ipv6_addrs; j++) {
+                for (size_t k = 0; k < op->ldp->n_peer_ports; k++) {
+                    /* Get the Logical_Router_Port that the Logical_Switch_Port
+                    * is connected to, as 'peer'. */
+                    struct local_lport *peer = op->ldp->peer_ports[k].remote;
+
+                    if (!is_ip6_in_router_network(
+                        peer, op->lsp.addrs[i].ipv6_addrs[j].addr)) {
+                        continue;
+                    }
+
+                    ds_clear(match);
+                    ds_put_format(match, "outport == %s && "
+                                REG_NEXT_HOP_IPV6 " == %s",
+                                peer->json_key,
+                                op->lsp.addrs[i].ipv6_addrs[j].addr_s);
+
+                    ds_clear(actions);
+                    ds_put_format(actions, "eth.dst = %s; next;", ea_s);
+                    ovn_ctrl_lflow_add_dp_key(
+                        lflows, peer->ldp->datapath->tunnel_key,
+                        S_ROUTER_IN_ARP_RESOLVE, 100,
+                        ds_cstr(match), ds_cstr(actions),
+                        &op->pb->header_.uuid, lflow_uuid_idx);
+                }
+            }
+        }
+    }
+}
+
+static void
+skip_lport_from_conntrack(struct hmap *lflows, struct local_lport *op,
+                          uint32_t *lflow_uuid_idx, enum ovn_stage in_stage,
+                          enum ovn_stage out_stage, uint16_t priority,
+                          struct ds *match)
+{
+    ds_clear(match);
+    ds_put_format(match, "ip && inport == %s", op->json_key);
+    ovn_ctrl_lflow_add_uuid(lflows, in_stage, priority,
+                            ds_cstr(match), "next;",
+                            &op->pb->header_.uuid, lflow_uuid_idx);
+
+    ds_clear(match);
+    ds_put_format(match, "ip && outport == %s", op->json_key);
+    ovn_ctrl_lflow_add_uuid(lflows, out_stage, priority,
+                            ds_cstr(match), "next;",
+                            &op->pb->header_.uuid, lflow_uuid_idx);
+}
+
+static void build_adm_ctrl_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions);
+static void build_neigh_learning_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions);
+static void build_ip_routing_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions);
+static void build_ND_RA_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions);
+static void build_dhcpv6_reply_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match);
+static void build_ipv6_input_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions);
+static void build_lrouter_nd_flow(struct hmap *lflows, struct local_lport *op,
+                                  const struct uuid *flow_uuid,
+                                  uint32_t *lflow_uuid_idx, const char *action,
+                                  const char *ip_address,
+                                  const char *sn_ip_address,
+                                  const char *eth_addr,
+                                  struct ds *extra_match, bool drop,
+                                  uint16_t priority);
+static void build_lrouter_bfd_flows(struct hmap *lflows,
+                                    struct local_lport *op,
+                                    uint32_t *lflow_uuid_idx);
+static void build_lrouter_arp_flow(
+    struct hmap *lflows, struct local_lport *,
+    const struct uuid *lflow_uuid, uint32_t *lflow_uuid_idx,
+    const char *ip_address, const char *eth_addr,
+    struct ds *extra_match, bool drop, uint16_t priority);
+static void build_lrouter_ipv4_ip_input(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions);
+static void build_lrouter_force_snat_flows_op(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx,
+    struct ds *match, struct ds *actions);
+static void build_arp_resolve_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions);
+static void build_egress_delivery_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions);
+
+static void add_route(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions,
+    const char *lrp_addr_s, const char *network_s, int plen,
+    const char *gateway, bool is_src_route, bool is_discard_route);
+
+/* Router port lflows. */
+static void
+build_lrouter_port_lflows(struct hmap *lflows, struct local_lport *op)
+{
+    uint32_t lflow_uuid_idx = 1;
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    build_adm_ctrl_flows_for_lrouter_port(lflows, op,
+                                          &lflow_uuid_idx, &match, &actions);
+    build_neigh_learning_flows_for_lrouter_port(lflows, op,
+                                                &lflow_uuid_idx, &match,
+                                                &actions);
+    build_ip_routing_flows_for_lrouter_port(lflows, op,
+                                            &lflow_uuid_idx, &match, &actions);
+    build_ND_RA_flows_for_lrouter_port(lflows, op,
+                                       &lflow_uuid_idx, &match, &actions);
+    build_dhcpv6_reply_flows_for_lrouter_port(lflows, op,
+                                              &lflow_uuid_idx, &match);
+    build_ipv6_input_flows_for_lrouter_port(lflows, op,
+                                            &lflow_uuid_idx, &match, &actions);
+    build_lrouter_ipv4_ip_input(lflows, op, &lflow_uuid_idx,
+                                &match, &actions);
+    build_lrouter_force_snat_flows_op(lflows, op, &lflow_uuid_idx,
+                                      &match, &actions);
+    build_arp_resolve_flows_for_lrouter_port(lflows, op, &lflow_uuid_idx,
+                                             &match, &actions);
+    build_egress_delivery_flows_for_lrouter_port(lflows, op,
+                                                 &lflow_uuid_idx,
+                                                 &match, &actions);
+
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
+/* Logical router ingress Table 0: L2 Admission Control
+ * This table drops packets that the router shouldn’t see at all based
+ * on their Ethernet headers.
+ */
+static void
+build_adm_ctrl_flows_for_lrouter_port(struct hmap *lflows,
+                                      struct local_lport *op,
+                                      uint32_t *lflow_uuid_idx,
+                                      struct ds *match, struct ds *actions)
+{
+#if 0
+TODO:
+    if (!lrport_is_enabled(op->nbrp)) {
+        /* Drop packets from disabled logical ports (since logical flow
+            * tables are default-drop). */
+        return;
+    }
+#endif
+
+    if (op->type == LP_CHASSISREDIRECT) {
+        /* No ingress packets should be received on a chassisredirect port. */
+        return;
+    }
+
+    /* 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;",
+                  op->lrp.networks.ea_s);
+
+    ds_clear(match);
+    ds_put_format(match, "eth.mcast && inport == %s", op->json_key);
+    ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_ADMISSION, 50, ds_cstr(match),
+                            ds_cstr(actions), &op->pb->header_.uuid,
+                            lflow_uuid_idx);
+
+    ds_clear(match);
+    ds_put_format(match, "eth.dst == %s && inport == %s",
+                  op->lrp.networks.ea_s, op->json_key);
+    if (op->lrp.is_l3dgw_port) {
+        /* Traffic with eth.dst = l3dgw_port->lrp_networks.ea_s
+         * should only be received on the gateway chassis. */
+        ds_put_format(match, " && is_chassis_resident(%s)",
+                      op->lrp.chassis_redirect_json_key);
+    }
+    ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_ADMISSION, 50, ds_cstr(match),
+                            ds_cstr(actions), &op->pb->header_.uuid,
+                            lflow_uuid_idx);
+}
+
+/* Logical router ingress Table 1: Neighbor lookup lflows
+ * for logical router ports. */
+static void
+build_neigh_learning_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx,
+    struct ds *match, struct ds *actions)
+{
+    if (op->type == LP_CHASSISREDIRECT) {
+        return;
+    }
+
+    bool learn_from_arp_request = is_learn_from_arp_request(op->pb->datapath);
+
+    /* Check if we need to learn mac-binding from ARP requests. */
+    for (int i = 0; i < op->lrp.networks.n_ipv4_addrs; i++) {
+        if (!learn_from_arp_request) {
+            /* ARP request to this address should always get learned,
+                * so add a priority-110 flow to set
+                * REGBIT_LOOKUP_NEIGHBOR_IP_RESULT to 1. */
+            ds_clear(match);
+            ds_put_format(match,
+                          "inport == %s && arp.spa == %s/%u && "
+                          "arp.tpa == %s && arp.op == 1",
+                          op->json_key,
+                          op->lrp.networks.ipv4_addrs[i].network_s,
+                          op->lrp.networks.ipv4_addrs[i].plen,
+                          op->lrp.networks.ipv4_addrs[i].addr_s);
+            if (op->lrp.is_l3dgw_port) {
+                ds_put_format(match, " && is_chassis_resident(%s)",
+                              op->lrp.chassis_redirect_json_key);
+            }
+            const char *actions_s = REGBIT_LOOKUP_NEIGHBOR_RESULT
+                                    " = lookup_arp(inport, arp.spa, arp.sha); "
+                                    REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1;"
+                                    " next;";
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_LOOKUP_NEIGHBOR, 110,
+                                    ds_cstr(match), actions_s,
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+        }
+        ds_clear(match);
+        ds_put_format(match,
+                      "inport == %s && arp.spa == %s/%u && arp.op == 1",
+                      op->json_key,
+                      op->lrp.networks.ipv4_addrs[i].network_s,
+                      op->lrp.networks.ipv4_addrs[i].plen);
+        if (op->lrp.is_l3dgw_port) {
+            ds_put_format(match, " && is_chassis_resident(%s)",
+                          op->lrp.chassis_redirect_json_key);
+        }
+        ds_clear(actions);
+        ds_put_format(actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
+                        " = lookup_arp(inport, arp.spa, arp.sha); %snext;",
+                        learn_from_arp_request ? "" :
+                        REGBIT_LOOKUP_NEIGHBOR_IP_RESULT
+                        " = lookup_arp_ip(inport, arp.spa); ");
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_LOOKUP_NEIGHBOR, 100,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+}
+
+/* Logical router ingress table IP_ROUTING : IP Routing.
+ *
+ * A packet that arrives at this table is an IP packet that should be
+ * routed to the address in 'ip[46].dst'.
+ *
+ * For regular routes without ECMP, table IP_ROUTING sets outport to the
+ * correct output port, eth.src to the output port's MAC address, and
+ * REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 to the next-hop IP address
+ * (leaving 'ip[46].dst', the packet’s final destination, unchanged), and
+ * advances to the next table.
+ *
+ * For ECMP routes, i.e. multiple routes with same policy and prefix, table
+ * IP_ROUTING remembers ECMP group id and selects a member id, and advances
+ * to table IP_ROUTING_ECMP, which sets outport, eth.src and
+ * REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 for the selected ECMP member.
+ */
+static void
+build_ip_routing_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions)
+{
+    if (op->type == LP_CHASSISREDIRECT) {
+        return;
+    }
+
+    for (int i = 0; i < op->lrp.networks.n_ipv4_addrs; i++) {
+        add_route(lflows, op, lflow_uuid_idx, match, actions,
+                  op->lrp.networks.ipv4_addrs[i].addr_s,
+                  op->lrp.networks.ipv4_addrs[i].network_s,
+                  op->lrp.networks.ipv4_addrs[i].plen, NULL, false, false);
+    }
+
+    for (int i = 0; i < op->lrp.networks.n_ipv6_addrs; i++) {
+        add_route(lflows, op, lflow_uuid_idx, match, actions,
+                  op->lrp.networks.ipv6_addrs[i].addr_s,
+                  op->lrp.networks.ipv6_addrs[i].network_s,
+                  op->lrp.networks.ipv6_addrs[i].plen, NULL, false, false);
+    }
+}
+
+/* Logical router ingress table ND_RA_OPTIONS & ND_RA_RESPONSE: IPv6 Router
+ * Adv (RA) options and response. */
+static void
+build_ND_RA_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions)
+{
+    if (op->type == LP_CHASSISREDIRECT) {
+        return;
+    }
+
+    if (!op->lrp.networks.n_ipv6_addrs) {
+        return;
+    }
+
+    const char *address_mode = smap_get(&op->pb->options,
+                                        "ipv6_ra_address_mode");
+    if (!address_mode) {
+        return;
+    }
+
+    if (strcmp(address_mode, "slaac") &&
+        strcmp(address_mode, "dhcpv6_stateful") &&
+        strcmp(address_mode, "dhcpv6_stateless")) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "Invalid address mode [%s] defined",
+                     address_mode);
+        return;
+    }
+
+    ds_clear(match);
+    ds_put_format(match, "inport == %s && ip6.dst == ff02::2 && nd_rs",
+                  op->json_key);
+    ds_clear(actions);
+
+    const char *mtu_s = smap_get(&op->pb->options, "ipv6_ra_mtu");
+
+    /* As per RFC 2460, 1280 is minimum IPv6 MTU. */
+    uint32_t mtu = (mtu_s && atoi(mtu_s) >= 1280) ? atoi(mtu_s) : 0;
+
+    ds_put_format(actions, REGBIT_ND_RA_OPTS_RESULT" = put_nd_ra_opts("
+                  "addr_mode = \"%s\", slla = %s",
+                  address_mode, op->lrp.networks.ea_s);
+    if (mtu > 0) {
+        ds_put_format(actions, ", mtu = %u", mtu);
+    }
+
+    const char *prf = smap_get_def(&op->pb->options, "ipv6_ra_prf", "MEDIUM");
+    if (strcmp(prf, "MEDIUM")) {
+        ds_put_format(actions, ", router_preference = \"%s\"", prf);
+    }
+
+    bool add_rs_response_flow = false;
+
+    for (size_t i = 0; i < op->lrp.networks.n_ipv6_addrs; i++) {
+        if (in6_is_lla(&op->lrp.networks.ipv6_addrs[i].network)) {
+            continue;
+        }
+
+        ds_put_format(actions, ", prefix = %s/%u",
+                      op->lrp.networks.ipv6_addrs[i].network_s,
+                      op->lrp.networks.ipv6_addrs[i].plen);
+
+        add_rs_response_flow = true;
+    }
+
+    if (add_rs_response_flow) {
+        ds_put_cstr(actions, "); next;");
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_ND_RA_OPTIONS,
+                                50, ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+        ds_clear(actions);
+        ds_clear(match);
+        ds_put_format(match, "inport == %s && ip6.dst == ff02::2 && "
+                      "nd_ra && "REGBIT_ND_RA_OPTS_RESULT, op->json_key);
+
+        char ip6_str[INET6_ADDRSTRLEN + 1];
+        struct in6_addr lla;
+        in6_generate_lla(op->lrp.networks.ea, &lla);
+        memset(ip6_str, 0, sizeof(ip6_str));
+        ipv6_string_mapped(ip6_str, &lla);
+        ds_put_format(actions, "eth.dst = eth.src; eth.src = %s; "
+                      "ip6.dst = ip6.src; ip6.src = %s; "
+                      "outport = inport; flags.loopback = 1; "
+                      "output;",
+                      op->lrp.networks.ea_s, ip6_str);
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_ND_RA_RESPONSE, 50,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+}
+
+static void
+build_dhcpv6_reply_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match)
+{
+    if (op->type == LP_CHASSISREDIRECT) {
+        return;
+    }
+
+    for (size_t i = 0; i < op->lrp.networks.n_ipv6_addrs; i++) {
+        ds_clear(match);
+        ds_put_format(match, "ip6.dst == %s && udp.src == 547 &&"
+                      " udp.dst == 546",
+                      op->lrp.networks.ipv6_addrs[i].addr_s);
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 100,
+                                ds_cstr(match),
+                                "reg0 = 0; handle_dhcpv6_reply;",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+}
+
+static void
+build_ipv6_input_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions)
+{
+    /* No ingress packets are accepted on a chassisredirect
+     * port, so no need to program flows for that port. */
+    if (op->type == LP_CHASSISREDIRECT) {
+        return;
+    }
+
+    if (op->lrp.networks.n_ipv6_addrs) {
+        /* ICMPv6 echo reply.  These flows reply to echo requests
+            * received for the router's IP address. */
+        ds_clear(match);
+        ds_put_cstr(match, "ip6.dst == ");
+        op_put_v6_networks(match, op);
+        ds_put_cstr(match, " && icmp6.type == 128 && icmp6.code == 0");
+
+        const char *lrp_actions =
+                    "ip6.dst <-> ip6.src; "
+                    "ip.ttl = 255; "
+                    "icmp6.type = 129; "
+                    "flags.loopback = 1; "
+                    "next; ";
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 90,
+                                ds_cstr(match), lrp_actions,
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+
+    /* ND reply.  These flows reply to ND solicitations for the
+     * router's own IP address. */
+    for (size_t i = 0; i < op->lrp.networks.n_ipv6_addrs; i++) {
+        ds_clear(match);
+        if (op->lrp.is_l3dgw_port && op->lrp.chassis_redirect_json_key) {
+            /* Traffic with eth.src = l3dgw_port->lrp_networks.ea_s
+                * should only be sent from the gateway chassi, so that
+                * upstream MAC learning points to the gateway chassis.
+                * Also need to avoid generation of multiple ND replies
+                * from different chassis. */
+            ds_put_format(match, "is_chassis_resident(%s)",
+                          op->lrp.chassis_redirect_json_key);
+        }
+
+        build_lrouter_nd_flow(lflows, op, &op->pb->header_.uuid,
+                              lflow_uuid_idx, "nd_na_router",
+                              op->lrp.networks.ipv6_addrs[i].addr_s,
+                              op->lrp.networks.ipv6_addrs[i].sn_addr_s,
+                              REG_INPORT_ETH_ADDR, match, false, 90);
+    }
+
+    /* UDP/TCP/SCTP port unreachable */
+    if (op->type != LP_L3GATEWAY && !op->lrp.dp_has_l3dgw_port) {
+        for (int i = 0; i < op->lrp.networks.n_ipv6_addrs; i++) {
+            ds_clear(match);
+            ds_put_format(match,
+                          "ip6 && ip6.dst == %s && !ip.later_frag && tcp",
+                          op->lrp.networks.ipv6_addrs[i].addr_s);
+            const char *action = "tcp_reset {"
+                                 "eth.dst <-> eth.src; "
+                                 "ip6.dst <-> ip6.src; "
+                                 "next; };";
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT,
+                                    80, ds_cstr(match), action,
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+
+            ds_clear(match);
+            ds_put_format(match,
+                            "ip6 && ip6.dst == %s && !ip.later_frag && sctp",
+                            op->lrp.networks.ipv6_addrs[i].addr_s);
+            action = "sctp_abort {"
+                        "eth.dst <-> eth.src; "
+                        "ip6.dst <-> ip6.src; "
+                        "next; };";
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT,
+                                    80, ds_cstr(match), action,
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+
+            ds_clear(match);
+            ds_put_format(match,
+                            "ip6 && ip6.dst == %s && !ip.later_frag && udp",
+                            op->lrp.networks.ipv6_addrs[i].addr_s);
+            action = "icmp6 {"
+                        "eth.dst <-> eth.src; "
+                        "ip6.dst <-> ip6.src; "
+                        "ip.ttl = 255; "
+                        "icmp6.type = 1; "
+                        "icmp6.code = 4; "
+                        "next; };";
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT,
+                                    80, ds_cstr(match), action,
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+
+            ds_clear(match);
+            ds_put_format(match,
+                            "ip6 && ip6.dst == %s && !ip.later_frag",
+                            op->lrp.networks.ipv6_addrs[i].addr_s);
+            action = "icmp6 {"
+                        "eth.dst <-> eth.src; "
+                        "ip6.dst <-> ip6.src; "
+                        "ip.ttl = 255; "
+                        "icmp6.type = 1; "
+                        "icmp6.code = 3; "
+                        "next; };";
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT,
+                                    70, ds_cstr(match), action,
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+        }
+    }
+
+    /* ICMPv6 time exceeded */
+    for (int i = 0; i < op->lrp.networks.n_ipv6_addrs; i++) {
+        /* skip link-local address */
+        if (in6_is_lla(&op->lrp.networks.ipv6_addrs[i].network)) {
+            continue;
+        }
+
+        ds_clear(match);
+        ds_clear(actions);
+
+        ds_put_format(match,
+                      "inport == %s && ip6 && "
+                      "ip6.src == %s/%d && "
+                      "ip.ttl == {0, 1} && !ip.later_frag",
+                      op->json_key,
+                      op->lrp.networks.ipv6_addrs[i].network_s,
+                      op->lrp.networks.ipv6_addrs[i].plen);
+        ds_put_format(actions,
+                      "icmp6 {"
+                      "eth.dst <-> eth.src; "
+                      "ip6.dst = ip6.src; "
+                      "ip6.src = %s; "
+                      "ip.ttl = 255; "
+                      "icmp6.type = 3; /* Time exceeded */ "
+                      "icmp6.code = 0; /* TTL exceeded in transit */ "
+                      "next; };",
+                      op->lrp.networks.ipv6_addrs[i].addr_s);
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 40,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+
+}
+
+/* Builds the logical flow that replies to NS requests for an 'ip_address'
+ * owned by the router. The flow is inserted in table S_ROUTER_IN_IP_INPUT
+ * with the given priority. If 'sn_ip_address' is non-NULL, requests are
+ * restricted only to packets with IP destination 'ip_address' or
+ * 'sn_ip_address'.
+ */
+static void
+build_lrouter_nd_flow(struct hmap *lflows, struct local_lport *op,
+                      const struct uuid *flow_uuid, uint32_t *lflow_uuid_idx,
+                      const char *action, const char *ip_address,
+                      const char *sn_ip_address, const char *eth_addr,
+                      struct ds *extra_match, bool drop, uint16_t priority)
+{
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    if (op) {
+        ds_put_format(&match, "inport == %s && ", op->json_key);
+    }
+
+    if (sn_ip_address) {
+        ds_put_format(&match, "ip6.dst == {%s, %s} && ",
+                      ip_address, sn_ip_address);
+    }
+
+    ds_put_format(&match, "nd_ns && nd.target == %s", ip_address);
+
+    if (extra_match && ds_last(extra_match) != EOF) {
+        ds_put_format(&match, " && %s", ds_cstr(extra_match));
+    }
+
+    if (drop) {
+        ds_put_format(&actions, "drop;");
+    } else {
+        ds_put_format(&actions,
+                      "%s { "
+                        "eth.src = %s; "
+                        "ip6.src = %s; "
+                        "nd.target = %s; "
+                        "nd.tll = %s; "
+                        "outport = inport; "
+                        "flags.loopback = 1; "
+                        "output; "
+                      "};",
+                      action,
+                      eth_addr,
+                      ip_address,
+                      ip_address,
+                      eth_addr);
+    }
+
+    ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, priority,
+                            ds_cstr(&match), ds_cstr(&actions),
+                            flow_uuid, lflow_uuid_idx);
+
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
+/* Logical router ingress table 3: IP Input for IPv4. */
+static void
+build_lrouter_ipv4_ip_input(struct hmap *lflows, struct local_lport *op,
+                            uint32_t *lflow_uuid_idx, struct ds *match,
+                            struct ds *actions)
+{
+    /* No ingress packets are accepted on a chassisredirect
+     * port, so no need to program flows for that port. */
+    if (op->type == LP_CHASSISREDIRECT) {
+        return;
+    }
+
+    if (op->lrp.networks.n_ipv4_addrs) {
+        /* L3 admission control: drop packets that originate from an
+         * IPv4 address owned by the router or a broadcast address
+         * known to the router (priority 100). */
+        ds_clear(match);
+        ds_put_cstr(match, "ip4.src == ");
+        op_put_v4_networks(match, op, true);
+        ds_put_cstr(match, " && "REGBIT_EGRESS_LOOPBACK" == 0");
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 100,
+                                ds_cstr(match), "drop;",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+
+        /* ICMP echo reply.  These flows reply to ICMP echo requests
+         * received for the router's IP address. Since packets only
+         * get here as part of the logical router datapath, the inport
+         * (i.e. the incoming locally attached net) does not matter.
+         * The ip.ttl also does not matter (RFC1812 section 4.2.2.9) */
+        ds_clear(match);
+        ds_put_cstr(match, "ip4.dst == ");
+        op_put_v4_networks(match, op, false);
+        ds_put_cstr(match, " && icmp4.type == 8 && icmp4.code == 0");
+
+        const char * icmp_actions = "ip4.dst <-> ip4.src; "
+                        "ip.ttl = 255; "
+                        "icmp4.type = 0; "
+                        "flags.loopback = 1; "
+                        "next; ";
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 90,
+                                ds_cstr(match), icmp_actions,
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+
+    /* BFD msg handling */
+    build_lrouter_bfd_flows(lflows, op, lflow_uuid_idx);
+
+    /* ICMP time exceeded */
+    for (int i = 0; i < op->lrp.networks.n_ipv4_addrs; i++) {
+        ds_clear(match);
+        ds_clear(actions);
+
+        ds_put_format(match,
+                      "inport == %s && ip4 && "
+                      "ip.ttl == {0, 1} && !ip.later_frag", op->json_key);
+        ds_put_format(actions,
+                      "icmp4 {"
+                      "eth.dst <-> eth.src; "
+                      "icmp4.type = 11; /* Time exceeded */ "
+                      "icmp4.code = 0; /* TTL exceeded in transit */ "
+                      "ip4.dst = ip4.src; "
+                      "ip4.src = %s; "
+                      "ip.ttl = 255; "
+                      "next; };",
+                      op->lrp.networks.ipv4_addrs[i].addr_s);
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 40,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+
+    /* ARP reply.  These flows reply to ARP requests for the router's own
+     * IP address. */
+    for (int i = 0; i < op->lrp.networks.n_ipv4_addrs; i++) {
+        ds_clear(match);
+        ds_put_format(match, "arp.spa == %s/%u",
+                      op->lrp.networks.ipv4_addrs[i].network_s,
+                      op->lrp.networks.ipv4_addrs[i].plen);
+
+        if (op->lrp.dp_has_l3dgw_port && op->peer
+                && op->lrp.peer_dp_has_localnet_ports) {
+            bool add_chassis_resident_check = false;
+            if (op->lrp.is_l3dgw_port) {
+                /* Traffic with eth.src = l3dgw_port->lrp_networks.ea_s
+                 * should only be sent from the gateway chassis, so that
+                 * upstream MAC learning points to the gateway chassis.
+                 * Also need to avoid generation of multiple ARP responses
+                 * from different chassis. */
+                add_chassis_resident_check = true;
+            } else {
+                /* Check if the option 'reside-on-redirect-chassis'
+                    * is set to true on the router port. If set to true
+                    * and if peer's logical switch has a localnet port, it
+                    * means the router pipeline for the packets from
+                    * peer's logical switch is be run on the chassis
+                    * hosting the gateway port and it should reply to the
+                    * ARP requests for the router port IPs.
+                    */
+                add_chassis_resident_check = smap_get_bool(
+                    &op->pb->options,
+                    "reside-on-redirect-chassis", false);
+            }
+
+            if (add_chassis_resident_check) {
+                ds_put_format(match, " && is_chassis_resident(%s)",
+                              op->lrp.chassis_redirect_json_key);
+            }
+        }
+
+        build_lrouter_arp_flow(lflows, op, &op->pb->header_.uuid,
+                               lflow_uuid_idx,
+                               op->lrp.networks.ipv4_addrs[i].addr_s,
+                               REG_INPORT_ETH_ADDR, match, false, 90);
+    }
+
+    if (op->type != LP_L3GATEWAY && !op->lrp.dp_has_l3dgw_port) {
+        /* UDP/TCP/SCTP port unreachable. */
+        for (int i = 0; i < op->lrp.networks.n_ipv4_addrs; i++) {
+            ds_clear(match);
+            ds_put_format(match,
+                          "ip4 && ip4.dst == %s && !ip.later_frag && udp",
+                          op->lrp.networks.ipv4_addrs[i].addr_s);
+            const char *action = "icmp4 {"
+                                    "eth.dst <-> eth.src; "
+                                    "ip4.dst <-> ip4.src; "
+                                    "ip.ttl = 255; "
+                                    "icmp4.type = 3; "
+                                    "icmp4.code = 3; "
+                                    "next; };";
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT,
+                                    80, ds_cstr(match), action,
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+
+            ds_clear(match);
+            ds_put_format(match,
+                            "ip4 && ip4.dst == %s && !ip.later_frag && tcp",
+                            op->lrp.networks.ipv4_addrs[i].addr_s);
+            action = "tcp_reset {"
+                        "eth.dst <-> eth.src; "
+                        "ip4.dst <-> ip4.src; "
+                        "next; };";
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT,
+                                    80, ds_cstr(match), action,
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+
+            ds_clear(match);
+            ds_put_format(match,
+                            "ip4 && ip4.dst == %s && !ip.later_frag && sctp",
+                            op->lrp.networks.ipv4_addrs[i].addr_s);
+            action = "sctp_abort {"
+                        "eth.dst <-> eth.src; "
+                        "ip4.dst <-> ip4.src; "
+                        "next; };";
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT,
+                                    80, ds_cstr(match), action,
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+
+            ds_clear(match);
+            ds_put_format(match,
+                            "ip4 && ip4.dst == %s && !ip.later_frag",
+                            op->lrp.networks.ipv4_addrs[i].addr_s);
+            action = "icmp4 {"
+                        "eth.dst <-> eth.src; "
+                        "ip4.dst <-> ip4.src; "
+                        "ip.ttl = 255; "
+                        "icmp4.type = 3; "
+                        "icmp4.code = 2; "
+                        "next; };";
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT,
+                                    70, ds_cstr(match), action,
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+        }
+    }
+}
+
+static void
+build_lrouter_bfd_flows(struct hmap *lflows, struct local_lport *op,
+                        uint32_t *lflow_uuid_idx)
+{
+    if (!op->lrp.has_bfd) {
+        return;
+    }
+
+    struct ds ip_list = DS_EMPTY_INITIALIZER;
+    struct ds match = DS_EMPTY_INITIALIZER;
+
+    if (op->lrp.networks.n_ipv4_addrs) {
+        op_put_v4_networks(&ip_list, op, false);
+        ds_put_format(&match, "ip4.src == %s && udp.dst == 3784",
+                      ds_cstr(&ip_list));
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 110,
+                                ds_cstr(&match), "next; ",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+        ds_clear(&match);
+        ds_put_format(&match, "ip4.dst == %s && udp.dst == 3784",
+                      ds_cstr(&ip_list));
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 110,
+                                ds_cstr(&match), "handle_bfd_msg(); ",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+
+    if (op->lrp.networks.n_ipv6_addrs) {
+        ds_clear(&ip_list);
+        ds_clear(&match);
+
+        op_put_v6_networks(&ip_list, op);
+        ds_put_format(&match, "ip6.src == %s && udp.dst == 3784",
+                      ds_cstr(&ip_list));
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 110,
+                                ds_cstr(&match), "next; ",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+        ds_clear(&match);
+        ds_put_format(&match, "ip6.dst == %s && udp.dst == 3784",
+                      ds_cstr(&ip_list));
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, 110,
+                                ds_cstr(&match), "handle_bfd_msg(); ",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+
+    ds_destroy(&ip_list);
+    ds_destroy(&match);
+}
+
+/* Builds the logical flow that replies to ARP requests for an 'ip_address'
+ * owned by the router. The flow is inserted in table S_ROUTER_IN_IP_INPUT
+ * with the given priority.
+ */
+static void
+build_lrouter_arp_flow(struct hmap *lflows, struct local_lport *op,
+                       const struct uuid *lflow_uuid, uint32_t *lflow_uuid_idx,
+                       const char *ip_address, const char *eth_addr,
+                       struct ds *extra_match, bool drop, uint16_t priority)
+{
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+
+    if (op) {
+        ds_put_format(&match, "inport == %s && ", op->json_key);
+    }
+
+    ds_put_format(&match, "arp.op == 1 && arp.tpa == %s", ip_address);
+
+    if (extra_match && ds_last(extra_match) != EOF) {
+        ds_put_format(&match, " && %s", ds_cstr(extra_match));
+    }
+    if (drop) {
+        ds_put_format(&actions, "drop;");
+    } else {
+        ds_put_format(&actions,
+                      "eth.dst = eth.src; "
+                      "eth.src = %s; "
+                      "arp.op = 2; /* ARP reply */ "
+                      "arp.tha = arp.sha; "
+                      "arp.sha = %s; "
+                      "arp.tpa <-> arp.spa; "
+                      "outport = inport; "
+                      "flags.loopback = 1; "
+                      "output;",
+                      eth_addr,
+                      eth_addr);
+    }
+
+    ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_INPUT, priority,
+                            ds_cstr(&match), ds_cstr(&actions),
+                            lflow_uuid, lflow_uuid_idx);
+
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
+static void
+build_lrouter_force_snat_flows_op(struct hmap *lflows, struct local_lport *op,
+                                  uint32_t *lflow_uuid_idx, struct ds *match,
+                                  struct ds *actions)
+{
+    if (op->type == LP_CHASSISREDIRECT) {
+        return;
+    }
+
+    if (!op->peer || !smap_get_bool(&op->pb->datapath->options,
+                                    "lb-force-snat-router-ip", false)) {
+        return;
+    }
+
+    if (op->lrp.networks.n_ipv4_addrs) {
+        ds_clear(match);
+        ds_clear(actions);
+
+        ds_put_format(match, "inport == %s && ip4.dst == %s",
+                      op->json_key, op->lrp.networks.ipv4_addrs[0].addr_s);
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_UNSNAT, 110,
+                                ds_cstr(match), "ct_snat;",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+
+        ds_clear(match);
+
+        /* Higher priority rules to force SNAT with the router port ip.
+         * This only takes effect when the packet has already been
+         * load balanced once. */
+        ds_put_format(match, "flags.force_snat_for_lb == 1 && ip4 && "
+                      "outport == %s", op->json_key);
+        ds_put_format(actions, "ct_snat(%s);",
+                      op->lrp.networks.ipv4_addrs[0].addr_s);
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_OUT_SNAT, 110,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+        if (op->lrp.networks.n_ipv4_addrs > 1) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_WARN_RL(&rl, "Logical router port %s is configured with "
+                              "multiple IPv4 addresses.  Only the first "
+                              "IP [%s] is considered as SNAT for load "
+                              "balancer", op->json_key,
+                              op->lrp.networks.ipv4_addrs[0].addr_s);
+        }
+    }
+
+    /* op->lrp.networks.ipv6_addrs will always have LLA and that will be
+     * last in the list. So add the flows only if n_ipv6_addrs > 1. */
+    if (op->lrp.networks.n_ipv6_addrs > 1) {
+        ds_clear(match);
+        ds_clear(actions);
+
+        ds_put_format(match, "inport == %s && ip6.dst == %s",
+                      op->json_key, op->lrp.networks.ipv6_addrs[0].addr_s);
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_UNSNAT, 110,
+                                ds_cstr(match), "ct_snat;",
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+
+        ds_clear(match);
+
+        /* Higher priority rules to force SNAT with the router port ip.
+         * This only takes effect when the packet has already been
+         * load balanced once. */
+        ds_put_format(match, "flags.force_snat_for_lb == 1 && ip6 && "
+                      "outport == %s", op->json_key);
+        ds_put_format(actions, "ct_snat(%s);",
+                      op->lrp.networks.ipv6_addrs[0].addr_s);
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_OUT_SNAT, 110,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+        if (op->lrp.networks.n_ipv6_addrs > 2) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_WARN_RL(&rl, "Logical router port %s is configured with "
+                              "multiple IPv6 addresses.  Only the first "
+                              "IP [%s] is considered as SNAT for load "
+                              "balancer", op->json_key,
+                              op->lrp.networks.ipv6_addrs[0].addr_s);
+        }
+    }
+}
+
+/* Local router ingress table ARP_RESOLVE: ARP Resolution.
+ *
+ * Any unicast packet that reaches this table is an IP packet whose
+ * next-hop IP address is in REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6
+ * (ip4.dst/ipv6.dst is the final destination).
+ * This table resolves the IP address in
+ * REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 into an output port in outport and
+ * an Ethernet address in eth.dst.
+ */
+static void
+build_arp_resolve_flows_for_lrouter_port(
+        struct hmap *lflows, struct local_lport *op, uint32_t *lflow_uuid_idx,
+        struct ds *match, struct ds *actions)
+{
+    /* This is a logical router port. If next-hop IP address in
+     * REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 matches IP address of this
+     * router port, then the packet is intended to eventually be sent
+     * to this logical port. Set the destination mac address using
+     * this port's mac address.
+     *
+     * The packet is still in peer's logical pipeline. So the match
+     * should be on peer's outport. */
+    if (op->peer && !op->peer->ldp->is_switch) {
+        /* Both the peer's are router ports. */
+        if (op->peer->lrp.networks.n_ipv4_addrs) {
+            ds_clear(match);
+            ds_put_format(match, "outport == %s && "
+                          REG_NEXT_HOP_IPV4 " == ",
+                          op->json_key);
+            op_put_v4_networks(match, op->peer, false);
+
+            ds_clear(actions);
+            ds_put_format(actions, "eth.dst = %s; next;",
+                          op->peer->lrp.networks.ea_s);
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_ARP_RESOLVE, 100,
+                                    ds_cstr(match), ds_cstr(actions),
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+        }
+
+        if (op->peer->lrp.networks.n_ipv6_addrs) {
+            ds_clear(match);
+            ds_put_format(match, "outport == %s && "
+                          REG_NEXT_HOP_IPV4 " == ",
+                          op->json_key);
+            op_put_v6_networks(match, op->peer);
+
+            ds_clear(actions);
+            ds_put_format(actions, "eth.dst = %s; next;",
+                          op->peer->lrp.networks.ea_s);
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_ARP_RESOLVE, 100,
+                               ds_cstr(match), ds_cstr(actions),
+                               &op->pb->header_.uuid, lflow_uuid_idx);
+        }
+    }
+
+    if (op->type == LP_CHASSISREDIRECT) {
+        const char *redir_type = smap_get(&op->pb->options, "redirect-type");
+        const char *gw_port = smap_get(&op->pb->options, "distributed-port");
+        if (redir_type && gw_port && !strcasecmp(redir_type, "bridged")) {
+            /* Packet is on a non gateway chassis and
+             * has an unresolved ARP on a network behind gateway
+             * chassis attached router port. Since, redirect type
+             * is "bridged", instead of calling "get_arp"
+             * on this node, we will redirect the packet to gateway
+             * chassis, by setting destination mac router port mac.*/
+            ds_clear(match);
+            ds_put_format(match, "outport == \"%s\" && "
+                          "!is_chassis_resident(%s)", gw_port, op->json_key);
+            ds_clear(actions);
+            ds_put_format(actions, "eth.dst = %s; next;",
+                          op->lrp.networks.ea_s);
+
+            ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_ARP_RESOLVE, 50,
+                                    ds_cstr(match), ds_cstr(actions),
+                                    &op->pb->header_.uuid, lflow_uuid_idx);
+        }
+    }
+}
+
+/* Logical router egress table DELIVERY: Delivery (priority 100-110).
+ *
+ * Priority 100 rules deliver packets to enabled logical ports.
+ * Priority 110 rules match multicast packets and update the source
+ * mac before delivering to enabled logical ports. IP multicast traffic
+ * bypasses S_ROUTER_IN_IP_ROUTING route lookups.
+ */
+static void
+build_egress_delivery_flows_for_lrouter_port(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions)
+{
+    if (op->type == LP_CHASSISREDIRECT) {
+        return;
+    }
+
+#if 0
+TODO
+    if (!lrport_is_enabled(op->nbrp)) {
+        /* Drop packets to disabled logical ports (since logical flow
+            * tables are default-drop). */
+        return;
+    }
+#endif
+
+    /* If multicast relay is enabled then also adjust source mac for IP
+        * multicast traffic.
+        */
+    if (smap_get_bool(&op->pb->datapath->options, "mcast-relay", false)) {
+        ds_clear(match);
+        ds_clear(actions);
+        ds_put_format(match, "(ip4.mcast || ip6.mcast) && outport == %s",
+                      op->json_key);
+        ds_put_format(actions, "eth.src = %s; output;", op->lrp.networks.ea_s);
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_OUT_DELIVERY, 110,
+                                ds_cstr(match), ds_cstr(actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+
+    ds_clear(match);
+    ds_put_format(match, "outport == %s", op->json_key);
+    ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_OUT_DELIVERY, 100,
+                            ds_cstr(match), "output;",
+                            &op->pb->header_.uuid, lflow_uuid_idx);
+}
+
+/* lrouter util functions. */
+static void
+op_put_v4_networks(struct ds *ds, const struct local_lport *op,
+                   bool add_bcast)
+{
+    if (!add_bcast && op->lrp.networks.n_ipv4_addrs == 1) {
+        ds_put_format(ds, "%s", op->lrp.networks.ipv4_addrs[0].addr_s);
+        return;
+    }
+
+    ds_put_cstr(ds, "{");
+    for (int i = 0; i < op->lrp.networks.n_ipv4_addrs; i++) {
+        ds_put_format(ds, "%s, ", op->lrp.networks.ipv4_addrs[i].addr_s);
+        if (add_bcast) {
+            ds_put_format(ds, "%s, ", op->lrp.networks.ipv4_addrs[i].bcast_s);
+        }
+    }
+    ds_chomp(ds, ' ');
+    ds_chomp(ds, ',');
+    ds_put_cstr(ds, "}");
+}
+
+static void
+op_put_v6_networks(struct ds *ds, const struct local_lport *op)
+{
+    if (op->lrp.networks.n_ipv6_addrs == 1) {
+        ds_put_format(ds, "%s", op->lrp.networks.ipv6_addrs[0].addr_s);
+        return;
+    }
+
+    ds_put_cstr(ds, "{");
+    for (int i = 0; i < op->lrp.networks.n_ipv6_addrs; i++) {
+        ds_put_format(ds, "%s, ", op->lrp.networks.ipv6_addrs[i].addr_s);
+    }
+    ds_chomp(ds, ' ');
+    ds_chomp(ds, ',');
+    ds_put_cstr(ds, "}");
+}
+
+static void
+build_route_match(const struct local_lport *op_inport, const char *network_s,
+                  int plen, bool is_src_route, bool is_ipv4, struct ds *match,
+                  uint16_t *priority)
+{
+    const char *dir;
+    /* The priority here is calculated to implement longest-prefix-match
+     * routing. */
+    if (is_src_route) {
+        dir = "src";
+        *priority = plen * 2;
+    } else {
+        dir = "dst";
+        *priority = (plen * 2) + 1;
+    }
+
+    if (op_inport) {
+        ds_put_format(match, "inport == %s && ", op_inport->json_key);
+    }
+    ds_put_format(match, "ip%s.%s == %s/%d", is_ipv4 ? "4" : "6", dir,
+                  network_s, plen);
+}
+
+static void
+add_route(
+    struct hmap *lflows, struct local_lport *op,
+    uint32_t *lflow_uuid_idx, struct ds *match, struct ds *actions,
+    const char *lrp_addr_s, const char *network_s, int plen,
+    const char *gateway, bool is_src_route, bool is_discard_route)
+{
+    bool is_ipv4 = strchr(network_s, '.') ? true : false;
+    uint16_t priority;
+    const struct local_lport *op_inport = NULL;
+
+    /* IPv6 link-local addresses must be scoped to the local router port. */
+    if (!is_ipv4) {
+        struct in6_addr network;
+        ovs_assert(ipv6_parse(network_s, &network));
+        if (in6_is_lla(&network)) {
+            op_inport = op;
+        }
+    }
+
+    ds_clear(match);
+    ds_clear(actions);
+
+    build_route_match(op_inport, network_s, plen, is_src_route, is_ipv4,
+                      match, &priority);
+
+    struct ds common_actions = DS_EMPTY_INITIALIZER;
+
+    if (is_discard_route) {
+        ds_put_format(actions, "drop;");
+    } else {
+        ds_put_format(&common_actions, REG_ECMP_GROUP_ID" = 0; %s = ",
+                      is_ipv4 ? REG_NEXT_HOP_IPV4 : REG_NEXT_HOP_IPV6);
+        if (gateway) {
+            ds_put_cstr(&common_actions, gateway);
+        } else {
+            ds_put_format(&common_actions, "ip%s.dst", is_ipv4 ? "4" : "6");
+        }
+        ds_put_format(&common_actions, "; "
+                      "%s = %s; "
+                      "eth.src = %s; "
+                      "outport = %s; "
+                      "flags.loopback = 1; "
+                      "next;",
+                      is_ipv4 ? REG_SRC_IPV4 : REG_SRC_IPV6,
+                      lrp_addr_s,
+                      op->lrp.networks.ea_s,
+                      op->json_key);
+        ds_put_format(actions, "ip.ttl--; %s", ds_cstr(&common_actions));
+    }
+
+    ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_ROUTING, priority,
+                            ds_cstr(match), ds_cstr(actions),
+                            &op->pb->header_.uuid, lflow_uuid_idx);
+    if (op && op->lrp.has_bfd) {
+        ds_put_format(match, " && udp.dst == 3784");
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_IP_ROUTING,
+                                priority + 1, ds_cstr(match),
+                                ds_cstr(&common_actions),
+                                &op->pb->header_.uuid, lflow_uuid_idx);
+    }
+
+    ds_destroy(&common_actions);
+}
+
+static bool
+is_ip4_in_router_network(struct local_lport *router_lport,
+                         ovs_be32 ip)
+{
+    ovs_assert(!router_lport->ldp->is_switch);
+
+    for (size_t i = 0; i < router_lport->lrp.networks.n_ipv4_addrs; i++) {
+        const struct ipv4_netaddr *na =
+            &router_lport->lrp.networks.ipv4_addrs[i];
+
+        if (!((na->network ^ ip) & na->mask)) {
+            /* There should be only 1 interface that matches the
+             * supplied IP.  Otherwise, it's a configuration error,
+             * because subnets of a router's interfaces should NOT
+             * overlap. */
+            return true;
+        }
+    }
+
+    return false;
+}
+
+static bool
+is_ip6_in_router_network(struct local_lport *router_lport,
+                         struct in6_addr ip6)
+{
+    ovs_assert(!router_lport->ldp->is_switch);
+
+    for (size_t i = 0; i < router_lport->lrp.networks.n_ipv6_addrs; i++) {
+        const struct ipv6_netaddr *na =
+            &router_lport->lrp.networks.ipv6_addrs[i];
+
+        struct in6_addr xor_addr = ipv6_addr_bitxor(&na->network, &ip6);
+        struct in6_addr and_addr = ipv6_addr_bitand(&xor_addr, &na->mask);
+
+        if (ipv6_is_zero(&and_addr)) {
+            /* There should be only 1 interface that matches the
+             * supplied IP.  Otherwise, it's a configuration error,
+             * because subnets of a router's interfaces should NOT
+             * overlap. */
+            return true;
+        }
+    }
+
+    return false;
+}
+
+static void build_lb_generic_lswitch_rules(
+    struct hmap *lflows, struct ovn_controller_lb *lb,
+    struct ds *match, struct ds *action, uint32_t *lflow_uuid_idx);
+static void build_lb_vip_actions(struct ovn_lb_vip *, struct ds *action,
+                                 char *selection_fields, bool ls_dp);
+static void build_lb_generic_lrouter_flows(struct hmap *lflows,
+                                           struct ovn_controller_lb *lb,
+                                           struct ds *match,
+                                           uint32_t *lflow_uuid_idx);
+
+static void ovn_ctrl_build_lb_lswitch_lflows(struct hmap *lswitch_lflows,
+                                             struct ovn_controller_lb *ovn_lb)
+{
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds action = DS_EMPTY_INITIALIZER;
+
+    uint32_t lflow_uuid_idx = 1;
+    build_lb_generic_lswitch_rules(lswitch_lflows, ovn_lb, &match, &action,
+                                   &lflow_uuid_idx);
+
+    ds_destroy(&match);
+    ds_destroy(&action);
+}
+
+static void ovn_ctrl_build_lb_lrouter_lflows(struct hmap *lrouter_lflows,
+                                             struct ovn_controller_lb *ovn_lb)
+{
+    struct ds match = DS_EMPTY_INITIALIZER;
+
+    uint32_t lflow_uuid_idx = 1;
+    build_lb_generic_lrouter_flows(lrouter_lflows, ovn_lb, &match,
+                                   &lflow_uuid_idx);
+
+    ds_destroy(&match);
+}
+
+static void
+build_lb_generic_lswitch_rules(struct hmap *lflows,
+                               struct ovn_controller_lb *lb,
+                               struct ds *match, struct ds *action,
+                               uint32_t *lflow_uuid_idx)
+{
+    for (size_t i = 0; i < lb->n_vips; i++) {
+        struct ovn_lb_vip *lb_vip = &lb->vips[i];
+        /* If health check is enabled on this vip skip adding flows.
+         * ovn-northd would take care of it. */
+        char *vip_key = xasprintf("%s_hc", lb_vip->vip_str);
+        if (smap_get_bool(&lb->slb->options, vip_key, false)) {
+            free(vip_key);
+            continue;
+        }
+        free(vip_key);
+
+        const char *ip_match = NULL;
+
+        ds_clear(action);
+        ds_clear(match);
+
+        /* Store the original destination IP to be used when generating
+         * hairpin flows.
+         */
+        if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
+            ip_match = "ip4";
+            ds_put_format(action, REG_ORIG_DIP_IPV4 " = %s; ",
+                          lb_vip->vip_str);
+        } else {
+            ip_match = "ip6";
+            ds_put_format(action, REG_ORIG_DIP_IPV6 " = %s; ",
+                          lb_vip->vip_str);
+        }
+
+        const char *proto = NULL;
+        if (lb_vip->vip_port) {
+            proto = "tcp";
+            if (lb->slb->protocol) {
+                if (!strcmp(lb->slb->protocol, "udp")) {
+                    proto = "udp";
+                } else if (!strcmp(lb->slb->protocol, "sctp")) {
+                    proto = "sctp";
+                }
+            }
+
+            /* Store the original destination port to be used when generating
+             * hairpin flows.
+             */
+            ds_put_format(action, REG_ORIG_TP_DPORT " = %"PRIu16"; ",
+                          lb_vip->vip_port);
+        }
+
+        /* New connections in Ingress table. */
+
+        build_lb_vip_actions(lb_vip, action,
+                             lb->selection_fields, true);
+
+        ds_put_format(match, "ct.new && %s.dst == %s", ip_match,
+                      lb_vip->vip_str);
+        if (lb_vip->vip_port) {
+            ds_put_format(match, " && %s.dst == %d", proto, lb_vip->vip_port);
+            ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_STATEFUL, 120,
+                                    ds_cstr(match), ds_cstr(action),
+                                    &lb->slb->header_.uuid, lflow_uuid_idx);
+        } else {
+            ovn_ctrl_lflow_add_uuid(lflows, S_SWITCH_IN_STATEFUL, 110,
+                                    ds_cstr(match), ds_cstr(action),
+                                    &lb->slb->header_.uuid, lflow_uuid_idx);
+        }
+    }
+}
+
+static void
+build_lb_vip_actions(struct ovn_lb_vip *lb_vip, struct ds *action,
+                     char *selection_fields, bool ls_dp)
+{
+    bool skip_hash_fields = false, reject = false;
+
+    if (lb_vip->empty_backend_rej && !lb_vip->n_backends) {
+        reject = true;
+    } else {
+        ds_put_format(action, "ct_lb(backends=%s);", lb_vip->backend_ips);
+    }
+
+    if (reject) {
+        int stage = ls_dp ? ovn_stage_get_table(S_SWITCH_OUT_QOS_MARK)
+                          : ovn_stage_get_table(S_ROUTER_OUT_SNAT);
+        ds_clear(action);
+        ds_put_format(action, "reg0 = 0; reject { outport <-> inport; "
+                      "next(pipeline=egress,table=%d);};", stage);
+    } else if (!skip_hash_fields && selection_fields && selection_fields[0]) {
+        ds_chomp(action, ';');
+        ds_chomp(action, ')');
+        ds_put_format(action, "; hash_fields=\"%s\");", selection_fields);
+    }
+}
+
+static void
+build_lb_generic_lrouter_flows(struct hmap *lflows,
+                               struct ovn_controller_lb *lb,
+                               struct ds *match,
+                               uint32_t *lflow_uuid_idx)
+{
+    /* A set to hold all ips that need defragmentation and tracking. */
+    struct sset all_ips = SSET_INITIALIZER(&all_ips);
+
+    bool lb_skip_snat = smap_get_bool(&lb->slb->options, "skip_snat", false);
+    if (lb_skip_snat) {
+        ovn_ctrl_lflow_add_uuid(lflows, S_ROUTER_OUT_SNAT, 120,
+                                "flags.skip_snat_for_lb == 1 && ip", "next;",
+                                &lb->slb->header_.uuid, lflow_uuid_idx);
+    }
+
+    for (size_t j = 0; j < lb->n_vips; j++) {
+        struct ovn_lb_vip *lb_vip = &lb->vips[j];
+        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_ctrl_lflow_add_uuid(lflows, S_ROUTER_IN_DEFRAG,
+                                    100, ds_cstr(match), "ct_next;",
+                                    &lb->slb->header_.uuid, lflow_uuid_idx);
+        }
+    }
+
+    sset_destroy(&all_ips);
+}
diff --git a/lib/lflow.h b/lib/lflow.h
new file mode 100644
index 0000000000..0cec2794cf
--- /dev/null
+++ b/lib/lflow.h
@@ -0,0 +1,333 @@
+/*
+ * 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 OVN_LIB_LFLOW_H
+#define OVN_LIB_LFLOW_H 1
+
+#include "lib/util.h"
+#include "openvswitch/hmap.h"
+#include "openvswitch/uuid.h"
+
+struct sbrec_datapath_binding;
+struct sbrec_port_binding;
+struct hmap;
+struct ofpbuf;
+struct local_datapath;
+struct local_lport;
+struct ovn_controller_lb;
+
+/* Pipeline stages. */
+
+/* The two pipelines in an OVN logical flow table. */
+enum ovn_pipeline {
+    P_IN,                       /* Ingress pipeline. */
+    P_OUT                       /* Egress pipeline. */
+};
+
+/* The two purposes for which ovn-northd uses OVN logical datapaths. */
+enum ovn_datapath_type {
+    DP_SWITCH,                  /* OVN logical switch. */
+    DP_ROUTER                   /* OVN logical router. */
+};
+
+/* Returns an "enum ovn_stage" built from the arguments.
+ *
+ * (It's better to use ovn_stage_build() for type-safety reasons, but inline
+ * functions can't be used in enums or switch cases.) */
+#define OVN_STAGE_BUILD(DP_TYPE, PIPELINE, TABLE) \
+    (((DP_TYPE) << 9) | ((PIPELINE) << 8) | (TABLE))
+
+/* A stage within an OVN logical switch or router.
+ *
+ * An "enum ovn_stage" indicates whether the stage is part of a logical switch
+ * or router, whether the stage is part of the ingress or egress pipeline, and
+ * the table within that pipeline.  The first three components are combined to
+ * form the stage's full name, e.g. S_SWITCH_IN_PORT_SEC_L2,
+ * S_ROUTER_OUT_DELIVERY. */
+enum ovn_stage {
+#define PIPELINE_STAGES                                                   \
+    /* Logical switch ingress stages. */                                  \
+    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_L2,    0, "ls_in_port_sec_l2")   \
+    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_IP,    1, "ls_in_port_sec_ip")   \
+    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_ND,    2, "ls_in_port_sec_nd")   \
+    PIPELINE_STAGE(SWITCH, IN,  LOOKUP_FDB ,    3, "ls_in_lookup_fdb")    \
+    PIPELINE_STAGE(SWITCH, IN,  PUT_FDB,        4, "ls_in_put_fdb")       \
+    PIPELINE_STAGE(SWITCH, IN,  PRE_ACL,        5, "ls_in_pre_acl")       \
+    PIPELINE_STAGE(SWITCH, IN,  PRE_LB,         6, "ls_in_pre_lb")        \
+    PIPELINE_STAGE(SWITCH, IN,  PRE_STATEFUL,   7, "ls_in_pre_stateful")  \
+    PIPELINE_STAGE(SWITCH, IN,  ACL_HINT,       8, "ls_in_acl_hint")      \
+    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,  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, 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")    \
+    PIPELINE_STAGE(ROUTER, IN,  LOOKUP_NEIGHBOR, 1, "lr_in_lookup_neighbor") \
+    PIPELINE_STAGE(ROUTER, IN,  LEARN_NEIGHBOR,  2, "lr_in_learn_neighbor") \
+    PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,        3, "lr_in_ip_input")     \
+    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          4, "lr_in_unsnat")       \
+    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          5, "lr_in_defrag")       \
+    PIPELINE_STAGE(ROUTER, IN,  DNAT,            6, "lr_in_dnat")         \
+    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   7, "lr_in_ecmp_stateful") \
+    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   8, "lr_in_nd_ra_options") \
+    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  9, "lr_in_nd_ra_response") \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      10, "lr_in_ip_routing")   \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 11, "lr_in_ip_routing_ecmp") \
+    PIPELINE_STAGE(ROUTER, IN,  POLICY,          12, "lr_in_policy")       \
+    PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     13, "lr_in_policy_ecmp")  \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     14, "lr_in_arp_resolve")  \
+    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN   ,  15, "lr_in_chk_pkt_len")  \
+    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     16, "lr_in_larger_pkts")  \
+    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     17, "lr_in_gw_redirect")  \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     18, "lr_in_arp_request")  \
+                                                                      \
+    /* Logical router egress stages. */                               \
+    PIPELINE_STAGE(ROUTER, OUT, UNDNAT,    0, "lr_out_undnat")        \
+    PIPELINE_STAGE(ROUTER, OUT, SNAT,      1, "lr_out_snat")          \
+    PIPELINE_STAGE(ROUTER, OUT, EGR_LOOP,  2, "lr_out_egr_loop")      \
+    PIPELINE_STAGE(ROUTER, OUT, DELIVERY,  3, "lr_out_delivery")
+
+#define PIPELINE_STAGE(DP_TYPE, PIPELINE, STAGE, TABLE, NAME)   \
+    S_##DP_TYPE##_##PIPELINE##_##STAGE                          \
+        = OVN_STAGE_BUILD(DP_##DP_TYPE, P_##PIPELINE, TABLE),
+    PIPELINE_STAGES
+#undef PIPELINE_STAGE
+};
+
+
+/* Due to various hard-coded priorities need to implement ACLs, the
+ * northbound database supports a smaller range of ACL priorities than
+ * are available to logical flows.  This value is added to an ACL
+ * priority to determine the ACL's logical flow priority. */
+#define OVN_ACL_PRI_OFFSET 1000
+
+/* Register definitions specific to switches. */
+#define REGBIT_CONNTRACK_DEFRAG   "reg0[0]"
+#define REGBIT_CONNTRACK_COMMIT   "reg0[1]"
+#define REGBIT_CONNTRACK_NAT      "reg0[2]"
+#define REGBIT_DHCP_OPTS_RESULT   "reg0[3]"
+#define REGBIT_DNS_LOOKUP_RESULT  "reg0[4]"
+#define REGBIT_ND_RA_OPTS_RESULT  "reg0[5]"
+#define REGBIT_HAIRPIN            "reg0[6]"
+#define REGBIT_ACL_HINT_ALLOW_NEW "reg0[7]"
+#define REGBIT_ACL_HINT_ALLOW     "reg0[8]"
+#define REGBIT_ACL_HINT_DROP      "reg0[9]"
+#define REGBIT_ACL_HINT_BLOCK     "reg0[10]"
+#define REGBIT_LKUP_FDB           "reg0[11]"
+#define REGBIT_HAIRPIN_REPLY      "reg0[12]"
+
+#define REG_ORIG_DIP_IPV4         "reg1"
+#define REG_ORIG_DIP_IPV6         "xxreg1"
+#define REG_ORIG_TP_DPORT         "reg2[0..15]"
+
+/* Register definitions for switches and routers. */
+
+/* Indicate that this packet has been recirculated using egress
+ * loopback.  This allows certain checks to be bypassed, such as a
+ * logical router dropping packets with source IP address equals
+ * one of the logical router's own IP addresses. */
+#define REGBIT_EGRESS_LOOPBACK  "reg9[0]"
+/* Register to store the result of check_pkt_larger action. */
+#define REGBIT_PKT_LARGER        "reg9[1]"
+#define REGBIT_LOOKUP_NEIGHBOR_RESULT "reg9[2]"
+#define REGBIT_LOOKUP_NEIGHBOR_IP_RESULT "reg9[3]"
+
+/* Register to store the eth address associated to a router port for packets
+ * received in S_ROUTER_IN_ADMISSION.
+ */
+#define REG_INPORT_ETH_ADDR "xreg0[0..47]"
+
+/* Register for ECMP bucket selection. */
+#define REG_ECMP_GROUP_ID       "reg8[0..15]"
+#define REG_ECMP_MEMBER_ID      "reg8[16..31]"
+
+/* Registers used for routing. */
+#define REG_NEXT_HOP_IPV4 "reg0"
+#define REG_NEXT_HOP_IPV6 "xxreg0"
+#define REG_SRC_IPV4 "reg1"
+#define REG_SRC_IPV6 "xxreg1"
+
+#define FLAGBIT_NOT_VXLAN "flags[1] == 0"
+
+/*
+ * OVS register usage:
+ *
+ * Logical Switch pipeline:
+ * +---------+----------------------------------------------+
+ * | R0      |     REGBIT_{CONNTRACK/DHCP/DNS/HAIRPIN}      |
+ * |         | REGBIT_ACL_HINT_{ALLOW_NEW/ALLOW/DROP/BLOCK} |
+ * +---------+----------------------------------------------+
+ * | R1 - R9 |                   UNUSED                     |
+ * +---------+----------------------------------------------+
+ *
+ * Logical Router pipeline:
+ * +-----+--------------------------+---+-----------------+---+---------------+
+ * | R0  | REGBIT_ND_RA_OPTS_RESULT |   |                 |   |               |
+ * |     |   (= IN_ND_RA_OPTIONS)   | X |                 |   |               |
+ * |     |      NEXT_HOP_IPV4       | R |                 |   |               |
+ * |     |      (>= IP_INPUT)       | E | INPORT_ETH_ADDR | X |               |
+ * +-----+--------------------------+ G |   (< IP_INPUT)  | X |               |
+ * | R1  |   SRC_IPV4 for ARP-REQ   | 0 |                 | R |               |
+ * |     |      (>= IP_INPUT)       |   |                 | E | NEXT_HOP_IPV6 |
+ * +-----+--------------------------+---+-----------------+ G | (>= IP_INPUT) |
+ * | R2  |        UNUSED            | X |                 | 0 |               |
+ * |     |                          | R |                 |   |               |
+ * +-----+--------------------------+ E |     UNUSED      |   |               |
+ * | R3  |        UNUSED            | G |                 |   |               |
+ * |     |                          | 1 |                 |   |               |
+ * +-----+--------------------------+---+-----------------+---+---------------+
+ * | R4  |        UNUSED            | X |                 |   |               |
+ * |     |                          | R |                 |   |               |
+ * +-----+--------------------------+ E |     UNUSED      | X |               |
+ * | R5  |        UNUSED            | G |                 | X |               |
+ * |     |                          | 2 |                 | R |SRC_IPV6 for NS|
+ * +-----+--------------------------+---+-----------------+ E | (>= IP_INPUT) |
+ * | R6  |        UNUSED            | X |                 | G |               |
+ * |     |                          | R |                 | 1 |               |
+ * +-----+--------------------------+ E |     UNUSED      |   |               |
+ * | R7  |        UNUSED            | G |                 |   |               |
+ * |     |                          | 3 |                 |   |               |
+ * +-----+--------------------------+---+-----------------+---+---------------+
+ * | R8  |     ECMP_GROUP_ID        |   |                 |
+ * |     |     ECMP_MEMBER_ID       | X |                 |
+ * +-----+--------------------------+ R |                 |
+ * |     | REGBIT_{                 | E |                 |
+ * |     |   EGRESS_LOOPBACK/       | G |     UNUSED      |
+ * | R9  |   PKT_LARGER/            | 4 |                 |
+ * |     |   LOOKUP_NEIGHBOR_RESULT/|   |                 |
+ * |     |   SKIP_LOOKUP_NEIGHBOR}  |   |                 |
+ * +-----+--------------------------+---+-----------------+
+ *
+ */
+
+/* Returns an "enum ovn_stage" built from the arguments. */
+static inline enum ovn_stage
+ovn_stage_build(enum ovn_datapath_type dp_type, enum ovn_pipeline pipeline,
+                uint8_t table)
+{
+    return OVN_STAGE_BUILD(dp_type, pipeline, table);
+}
+
+/* Returns the pipeline to which 'stage' belongs. */
+static inline enum ovn_pipeline
+ovn_stage_get_pipeline(enum ovn_stage stage)
+{
+    return (stage >> 8) & 1;
+}
+
+/* Returns the pipeline name to which 'stage' belongs. */
+static inline const char *
+ovn_stage_get_pipeline_name(enum ovn_stage stage)
+{
+    return ovn_stage_get_pipeline(stage) == P_IN ? "ingress" : "egress";
+}
+
+/* Returns the table to which 'stage' belongs. */
+static inline uint8_t
+ovn_stage_get_table(enum ovn_stage stage)
+{
+    return stage & 0xff;
+}
+
+/* Returns a string name for 'stage'. */
+static inline const char *
+ovn_stage_to_str(enum ovn_stage stage)
+{
+    switch (stage) {
+#define PIPELINE_STAGE(DP_TYPE, PIPELINE, STAGE, TABLE, NAME)       \
+        case S_##DP_TYPE##_##PIPELINE##_##STAGE: return NAME;
+    PIPELINE_STAGES
+#undef PIPELINE_STAGE
+        default: return "<unknown>";
+    }
+}
+
+/* Returns the type of the datapath to which a flow with the given 'stage' may
+ * be added. */
+static inline enum ovn_datapath_type
+ovn_stage_to_datapath_type(enum ovn_stage stage)
+{
+    switch (stage) {
+#define PIPELINE_STAGE(DP_TYPE, PIPELINE, STAGE, TABLE, NAME)       \
+        case S_##DP_TYPE##_##PIPELINE##_##STAGE: return DP_##DP_TYPE;
+    PIPELINE_STAGES
+#undef PIPELINE_STAGE
+    default: OVS_NOT_REACHED();
+    }
+}
+
+#define MC_FLOOD "_MC_flood"
+#define MC_MROUTER_FLOOD "_MC_mrouter_flood"
+#define MC_MROUTER_STATIC "_MC_mrouter_static"
+#define MC_STATIC "_MC_static"
+#define MC_UNKNOWN "_MC_unknown"
+#define MC_FLOOD_L2 "_MC_flood_l2"
+
+struct ovn_ctrl_lflow {
+    struct hmap_node hmap_node;
+    struct uuid uuid_;
+
+    uint32_t dp_key; /* Datapath tunnel key. */
+    enum ovn_stage stage;
+    uint16_t priority;
+    char *match;
+    char *actions;
+    char *stage_hint;
+    const char *where;
+};
+
+size_t ovn_ctrl_lflow_hash(const struct ovn_ctrl_lflow *);
+void build_lswitch_generic_lflows(struct hmap *lflows);
+void build_lrouter_generic_lflows(struct hmap *lflows);
+
+void ovn_ctrl_lflows_build_dp_lflows(
+    struct hmap *lflows, struct local_datapath *);
+
+void ovn_ctrl_lflows_clear(struct hmap *lflows);
+void ovn_ctrl_lflows_destroy(struct hmap *lflows);
+
+void ovn_ctrl_build_lport_lflows(
+    struct hmap *lflows, struct local_lport *);
+
+void ovn_ctrl_build_lb_lflows(struct hmap *lswitch_lflows,
+                              struct hmap *lrouter_lflows,
+                              struct ovn_controller_lb *);
+
+#endif /* OVN_LIB_LFLOW_H */
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index c5af8d1ab3..8e34a5c362 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -324,6 +324,14 @@ extract_lrp_networks__(char *mac, char **networks, size_t n_networks,
     return true;
 }
 
+/* Appends the IPv6 address - 'addr' to already allocated 'laddrs'. */
+void
+lport_addr_add_ip6ddr(struct lport_addresses *laddrs, struct in6_addr addr,
+                      unsigned int plen)
+{
+    add_ipv6_netaddr(laddrs, addr, plen);
+}
+
 bool
 extract_sbrec_binding_first_mac(const struct sbrec_port_binding *binding,
                                 struct eth_addr *ea)
@@ -367,6 +375,10 @@ destroy_lport_addresses(struct lport_addresses *laddrs)
 {
     free(laddrs->ipv4_addrs);
     free(laddrs->ipv6_addrs);
+    laddrs->ipv4_addrs = NULL;
+    laddrs->ipv6_addrs = NULL;
+    laddrs->n_ipv4_addrs = 0;
+    laddrs->n_ipv6_addrs = 0;
 }
 
 /* Go through 'addresses' and add found IPv4 addresses to 'ipv4_addrs' and
@@ -785,3 +797,74 @@ ddlog_err(const char *msg)
     VLOG_ERR("%s", msg);
 }
 #endif
+
+const struct sbrec_port_binding *
+lport_lookup_by_name(struct ovsdb_idl_index *sbrec_port_binding_by_name,
+                     const char *name)
+{
+    struct sbrec_port_binding *pb = sbrec_port_binding_index_init_row(
+        sbrec_port_binding_by_name);
+    sbrec_port_binding_index_set_logical_port(pb, name);
+
+    const struct sbrec_port_binding *retval = sbrec_port_binding_index_find(
+        sbrec_port_binding_by_name, pb);
+
+    sbrec_port_binding_index_destroy_row(pb);
+
+    return retval;
+}
+
+const struct sbrec_port_binding *
+lport_get_peer(const struct sbrec_port_binding *pb,
+               struct ovsdb_idl_index *sbrec_port_binding_by_name)
+{
+    const char *peer_name = smap_get(&pb->options, "peer");
+
+    if (!peer_name) {
+        return NULL;
+    }
+
+    const struct sbrec_port_binding *peer;
+    peer = lport_lookup_by_name(sbrec_port_binding_by_name,
+                                peer_name);
+    return (peer && peer->datapath) ? peer : NULL;
+}
+
+static bool
+is_lport_vif(const struct sbrec_port_binding *pb)
+{
+    return !pb->type[0];
+}
+
+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;
+    } else if (!strcmp(pb->type, "chassisredirect")) {
+        return LP_CHASSISREDIRECT;
+    } else if (!strcmp(pb->type, "l3gateway")) {
+        return LP_L3GATEWAY;
+    } else if (!strcmp(pb->type, "localnet")) {
+        return LP_LOCALNET;
+    } else if (!strcmp(pb->type, "localport")) {
+        return LP_LOCALPORT;
+    } else if (!strcmp(pb->type, "l2gateway")) {
+        return LP_L2GATEWAY;
+    } else if (!strcmp(pb->type, "virtual")) {
+        return LP_VIRTUAL;
+    } else if (!strcmp(pb->type, "external")) {
+        return LP_EXTERNAL;
+    } else if (!strcmp(pb->type, "remote")) {
+        return LP_REMOTE;
+    } else if (!strcmp(pb->type, "vtep")) {
+        return LP_VTEP;
+    }
+
+    return LP_UNKNOWN;
+}
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index 9935cad34c..ea07097573 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -80,6 +80,8 @@ bool extract_sbrec_binding_first_mac(const struct sbrec_port_binding *binding,
 
 bool extract_lrp_networks__(char *mac, char **networks, size_t n_networks,
                             struct lport_addresses *laddrs);
+void lport_addr_add_ip6ddr(struct lport_addresses *laddrs,
+                           struct in6_addr addr, unsigned int plen);
 
 bool lport_addresses_is_empty(struct lport_addresses *);
 void destroy_lport_addresses(struct lport_addresses *);
@@ -279,4 +281,34 @@ void ddlog_warn(const char *msg);
 void ddlog_err(const char *msg);
 #endif
 
+struct sbrec_port_binding;
+struct ovsdb_idl_index;
+
+const struct sbrec_port_binding *lport_lookup_by_name(
+    struct ovsdb_idl_index *sbrec_port_binding_by_name,
+    const char *name);
+
+const struct sbrec_port_binding *lport_get_peer(
+    const struct sbrec_port_binding *pb,
+    struct ovsdb_idl_index *sbrec_port_binding_by_name);
+
+/* 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
+};
+
+enum en_lport_type get_lport_type(const struct sbrec_port_binding *);
+
 #endif
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index df42c1824b..b1c1fcbce5 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -38,6 +38,7 @@
 #include "lib/ovn-sb-idl.h"
 #include "lib/ovn-util.h"
 #include "lib/lb.h"
+#include "lib/lflow.h"
 #include "memory.h"
 #include "lib/ovn-parallel-hmap.h"
 #include "ovn/actions.h"
@@ -113,286 +114,7 @@ static const char *ssl_certificate_file;
 static const char *ssl_ca_cert_file;
 
 #define MAX_OVN_TAGS 4096
-
-/* Pipeline stages. */
-
-/* The two pipelines in an OVN logical flow table. */
-enum ovn_pipeline {
-    P_IN,                       /* Ingress pipeline. */
-    P_OUT                       /* Egress pipeline. */
-};
-
-/* The two purposes for which ovn-northd uses OVN logical datapaths. */
-enum ovn_datapath_type {
-    DP_SWITCH,                  /* OVN logical switch. */
-    DP_ROUTER                   /* OVN logical router. */
-};
-
-/* Returns an "enum ovn_stage" built from the arguments.
- *
- * (It's better to use ovn_stage_build() for type-safety reasons, but inline
- * functions can't be used in enums or switch cases.) */
-#define OVN_STAGE_BUILD(DP_TYPE, PIPELINE, TABLE) \
-    (((DP_TYPE) << 9) | ((PIPELINE) << 8) | (TABLE))
-
-/* A stage within an OVN logical switch or router.
- *
- * An "enum ovn_stage" indicates whether the stage is part of a logical switch
- * or router, whether the stage is part of the ingress or egress pipeline, and
- * the table within that pipeline.  The first three components are combined to
- * form the stage's full name, e.g. S_SWITCH_IN_PORT_SEC_L2,
- * S_ROUTER_OUT_DELIVERY. */
-enum ovn_stage {
-#define PIPELINE_STAGES                                                   \
-    /* Logical switch ingress stages. */                                  \
-    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_L2,    0, "ls_in_port_sec_l2")   \
-    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_IP,    1, "ls_in_port_sec_ip")   \
-    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_ND,    2, "ls_in_port_sec_nd")   \
-    PIPELINE_STAGE(SWITCH, IN,  LOOKUP_FDB ,    3, "ls_in_lookup_fdb")    \
-    PIPELINE_STAGE(SWITCH, IN,  PUT_FDB,        4, "ls_in_put_fdb")       \
-    PIPELINE_STAGE(SWITCH, IN,  PRE_ACL,        5, "ls_in_pre_acl")       \
-    PIPELINE_STAGE(SWITCH, IN,  PRE_LB,         6, "ls_in_pre_lb")        \
-    PIPELINE_STAGE(SWITCH, IN,  PRE_STATEFUL,   7, "ls_in_pre_stateful")  \
-    PIPELINE_STAGE(SWITCH, IN,  ACL_HINT,       8, "ls_in_acl_hint")      \
-    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,  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, 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")    \
-    PIPELINE_STAGE(ROUTER, IN,  LOOKUP_NEIGHBOR, 1, "lr_in_lookup_neighbor") \
-    PIPELINE_STAGE(ROUTER, IN,  LEARN_NEIGHBOR,  2, "lr_in_learn_neighbor") \
-    PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,        3, "lr_in_ip_input")     \
-    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          4, "lr_in_defrag")       \
-    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          5, "lr_in_unsnat")       \
-    PIPELINE_STAGE(ROUTER, IN,  DNAT,            6, "lr_in_dnat")         \
-    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   7, "lr_in_ecmp_stateful") \
-    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   8, "lr_in_nd_ra_options") \
-    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  9, "lr_in_nd_ra_response") \
-    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      10, "lr_in_ip_routing")   \
-    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 11, "lr_in_ip_routing_ecmp") \
-    PIPELINE_STAGE(ROUTER, IN,  POLICY,          12, "lr_in_policy")       \
-    PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     13, "lr_in_policy_ecmp")  \
-    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     14, "lr_in_arp_resolve")  \
-    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN   ,  15, "lr_in_chk_pkt_len")  \
-    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     16, "lr_in_larger_pkts")  \
-    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     17, "lr_in_gw_redirect")  \
-    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     18, "lr_in_arp_request")  \
-                                                                      \
-    /* Logical router egress stages. */                               \
-    PIPELINE_STAGE(ROUTER, OUT, UNDNAT,    0, "lr_out_undnat")        \
-    PIPELINE_STAGE(ROUTER, OUT, SNAT,      1, "lr_out_snat")          \
-    PIPELINE_STAGE(ROUTER, OUT, EGR_LOOP,  2, "lr_out_egr_loop")      \
-    PIPELINE_STAGE(ROUTER, OUT, DELIVERY,  3, "lr_out_delivery")
-
-#define PIPELINE_STAGE(DP_TYPE, PIPELINE, STAGE, TABLE, NAME)   \
-    S_##DP_TYPE##_##PIPELINE##_##STAGE                          \
-        = OVN_STAGE_BUILD(DP_##DP_TYPE, P_##PIPELINE, TABLE),
-    PIPELINE_STAGES
-#undef PIPELINE_STAGE
-};
-
-/* Due to various hard-coded priorities need to implement ACLs, the
- * northbound database supports a smaller range of ACL priorities than
- * are available to logical flows.  This value is added to an ACL
- * priority to determine the ACL's logical flow priority. */
-#define OVN_ACL_PRI_OFFSET 1000
-
-/* Register definitions specific to switches. */
-#define REGBIT_CONNTRACK_DEFRAG   "reg0[0]"
-#define REGBIT_CONNTRACK_COMMIT   "reg0[1]"
-#define REGBIT_CONNTRACK_NAT      "reg0[2]"
-#define REGBIT_DHCP_OPTS_RESULT   "reg0[3]"
-#define REGBIT_DNS_LOOKUP_RESULT  "reg0[4]"
-#define REGBIT_ND_RA_OPTS_RESULT  "reg0[5]"
-#define REGBIT_HAIRPIN            "reg0[6]"
-#define REGBIT_ACL_HINT_ALLOW_NEW "reg0[7]"
-#define REGBIT_ACL_HINT_ALLOW     "reg0[8]"
-#define REGBIT_ACL_HINT_DROP      "reg0[9]"
-#define REGBIT_ACL_HINT_BLOCK     "reg0[10]"
-#define REGBIT_LKUP_FDB           "reg0[11]"
-#define REGBIT_HAIRPIN_REPLY      "reg0[12]"
-
-#define REG_ORIG_DIP_IPV4         "reg1"
-#define REG_ORIG_DIP_IPV6         "xxreg1"
-#define REG_ORIG_TP_DPORT         "reg2[0..15]"
-
-/* Register definitions for switches and routers. */
-
-/* Indicate that this packet has been recirculated using egress
- * loopback.  This allows certain checks to be bypassed, such as a
- * logical router dropping packets with source IP address equals
- * one of the logical router's own IP addresses. */
-#define REGBIT_EGRESS_LOOPBACK  "reg9[0]"
-/* Register to store the result of check_pkt_larger action. */
-#define REGBIT_PKT_LARGER        "reg9[1]"
-#define REGBIT_LOOKUP_NEIGHBOR_RESULT "reg9[2]"
-#define REGBIT_LOOKUP_NEIGHBOR_IP_RESULT "reg9[3]"
-
-/* Register to store the eth address associated to a router port for packets
- * received in S_ROUTER_IN_ADMISSION.
- */
-#define REG_INPORT_ETH_ADDR "xreg0[0..47]"
-
-/* Register for ECMP bucket selection. */
-#define REG_ECMP_GROUP_ID       "reg8[0..15]"
-#define REG_ECMP_MEMBER_ID      "reg8[16..31]"
-
-/* Registers used for routing. */
-#define REG_NEXT_HOP_IPV4 "reg0"
-#define REG_NEXT_HOP_IPV6 "xxreg0"
-#define REG_SRC_IPV4 "reg1"
-#define REG_SRC_IPV6 "xxreg1"
-
-#define FLAGBIT_NOT_VXLAN "flags[1] == 0"
-
-/*
- * OVS register usage:
- *
- * Logical Switch pipeline:
- * +----+----------------------------------------------+---+------------------+
- * | R0 |     REGBIT_{CONNTRACK/DHCP/DNS}              |   |                  |
- * |    |     REGBIT_{HAIRPIN/HAIRPIN_REPLY}           | X |                  |
- * |    | REGBIT_ACL_HINT_{ALLOW_NEW/ALLOW/DROP/BLOCK} | X |                  |
- * +----+----------------------------------------------+ X |                  |
- * | R1 |         ORIG_DIP_IPV4 (>= IN_STATEFUL)       | R |                  |
- * +----+----------------------------------------------+ E |                  |
- * | R2 |         ORIG_TP_DPORT (>= IN_STATEFUL)       | G |                  |
- * +----+----------------------------------------------+ 0 |                  |
- * | R3 |                   UNUSED                     |   |                  |
- * +----+----------------------------------------------+---+------------------+
- * | R4 |                   UNUSED                     |   |                  |
- * +----+----------------------------------------------+ X |   ORIG_DIP_IPV6  |
- * | R5 |                   UNUSED                     | X | (>= IN_STATEFUL) |
- * +----+----------------------------------------------+ R |                  |
- * | R6 |                   UNUSED                     | E |                  |
- * +----+----------------------------------------------+ G |                  |
- * | R7 |                   UNUSED                     | 1 |                  |
- * +----+----------------------------------------------+---+------------------+
- * | R8 |                   UNUSED                     |
- * +----+----------------------------------------------+
- * | R9 |                   UNUSED                     |
- * +----+----------------------------------------------+
- *
- * Logical Router pipeline:
- * +-----+--------------------------+---+-----------------+---+---------------+
- * | R0  | REGBIT_ND_RA_OPTS_RESULT |   |                 |   |               |
- * |     |   (= IN_ND_RA_OPTIONS)   | X |                 |   |               |
- * |     |      NEXT_HOP_IPV4       | R |                 |   |               |
- * |     |      (>= IP_INPUT)       | E | INPORT_ETH_ADDR | X |               |
- * +-----+--------------------------+ G |   (< IP_INPUT)  | X |               |
- * | R1  |   SRC_IPV4 for ARP-REQ   | 0 |                 | R |               |
- * |     |      (>= IP_INPUT)       |   |                 | E | NEXT_HOP_IPV6 |
- * +-----+--------------------------+---+-----------------+ G | (>= IP_INPUT) |
- * | R2  |        UNUSED            | X |                 | 0 |               |
- * |     |                          | R |                 |   |               |
- * +-----+--------------------------+ E |     UNUSED      |   |               |
- * | R3  |        UNUSED            | G |                 |   |               |
- * |     |                          | 1 |                 |   |               |
- * +-----+--------------------------+---+-----------------+---+---------------+
- * | R4  |        UNUSED            | X |                 |   |               |
- * |     |                          | R |                 |   |               |
- * +-----+--------------------------+ E |     UNUSED      | X |               |
- * | R5  |        UNUSED            | G |                 | X |               |
- * |     |                          | 2 |                 | R |SRC_IPV6 for NS|
- * +-----+--------------------------+---+-----------------+ E | (>= IP_INPUT) |
- * | R6  |        UNUSED            | X |                 | G |               |
- * |     |                          | R |                 | 1 |               |
- * +-----+--------------------------+ E |     UNUSED      |   |               |
- * | R7  |        UNUSED            | G |                 |   |               |
- * |     |                          | 3 |                 |   |               |
- * +-----+--------------------------+---+-----------------+---+---------------+
- * | R8  |     ECMP_GROUP_ID        |   |                 |
- * |     |     ECMP_MEMBER_ID       | X |                 |
- * +-----+--------------------------+ R |                 |
- * |     | REGBIT_{                 | E |                 |
- * |     |   EGRESS_LOOPBACK/       | G |     UNUSED      |
- * | R9  |   PKT_LARGER/            | 4 |                 |
- * |     |   LOOKUP_NEIGHBOR_RESULT/|   |                 |
- * |     |   SKIP_LOOKUP_NEIGHBOR}  |   |                 |
- * +-----+--------------------------+---+-----------------+
- *
- */
-
-/* Returns an "enum ovn_stage" built from the arguments. */
-static enum ovn_stage
-ovn_stage_build(enum ovn_datapath_type dp_type, enum ovn_pipeline pipeline,
-                uint8_t table)
-{
-    return OVN_STAGE_BUILD(dp_type, pipeline, table);
-}
-
-/* Returns the pipeline to which 'stage' belongs. */
-static enum ovn_pipeline
-ovn_stage_get_pipeline(enum ovn_stage stage)
-{
-    return (stage >> 8) & 1;
-}
-
-/* Returns the pipeline name to which 'stage' belongs. */
-static const char *
-ovn_stage_get_pipeline_name(enum ovn_stage stage)
-{
-    return ovn_stage_get_pipeline(stage) == P_IN ? "ingress" : "egress";
-}
-
-/* Returns the table to which 'stage' belongs. */
-static uint8_t
-ovn_stage_get_table(enum ovn_stage stage)
-{
-    return stage & 0xff;
-}
-
-/* Returns a string name for 'stage'. */
-static const char *
-ovn_stage_to_str(enum ovn_stage stage)
-{
-    switch (stage) {
-#define PIPELINE_STAGE(DP_TYPE, PIPELINE, STAGE, TABLE, NAME)       \
-        case S_##DP_TYPE##_##PIPELINE##_##STAGE: return NAME;
-    PIPELINE_STAGES
-#undef PIPELINE_STAGE
-        default: return "<unknown>";
-    }
-}
 
-/* Returns the type of the datapath to which a flow with the given 'stage' may
- * be added. */
-static enum ovn_datapath_type
-ovn_stage_to_datapath_type(enum ovn_stage stage)
-{
-    switch (stage) {
-#define PIPELINE_STAGE(DP_TYPE, PIPELINE, STAGE, TABLE, NAME)       \
-        case S_##DP_TYPE##_##PIPELINE##_##STAGE: return DP_##DP_TYPE;
-    PIPELINE_STAGES
-#undef PIPELINE_STAGE
-    default: OVS_NOT_REACHED();
-    }
-}
 
 static void
 usage(void)
@@ -674,6 +396,13 @@ struct ovn_datapath {
 
     /* Port groups related to the datapath, used only when nbs is NOT NULL. */
     struct hmap nb_pgs;
+
+    /* Applicable for logical switch datapaths. */
+    bool vlan_passthru;
+    bool has_dns_records;
+
+    /* Applicable for logical router datapaths. */
+    bool always_learn_from_arp_request;
 };
 
 /* Contains a NAT entry with the external addresses pre-parsed. */
@@ -697,6 +426,7 @@ struct ovn_snat_ip {
 static bool
 get_force_snat_ip(struct ovn_datapath *od, const char *key_type,
                   struct lport_addresses *laddrs);
+static bool ls_has_dns_records(const struct nbrec_logical_switch *nbs);
 
 /* Returns true if a 'nat_entry' is valid, i.e.:
  * - parsing was successful.
@@ -1212,6 +942,9 @@ join_datapaths(struct northd_context *ctx, struct hmap *datapaths,
             ovs_list_push_back(nb_only, &od->list);
         }
 
+        od->vlan_passthru = smap_get_bool(&nbs->other_config,
+                                          "vlan-passthru", false);
+        od->has_dns_records = ls_has_dns_records(od->nbs);
         init_ipam_info_for_datapath(od);
         init_mcast_info_for_datapath(od);
         init_lb_ips(od);
@@ -1244,6 +977,11 @@ join_datapaths(struct northd_context *ctx, struct hmap *datapaths,
                                      NULL, nbr, NULL);
             ovs_list_push_back(nb_only, &od->list);
         }
+
+        od->always_learn_from_arp_request =
+            smap_get_bool(&nbr->options,
+                          "always_learn_from_arp_request", true);
+
         init_mcast_info_for_datapath(od);
         init_nat_entries(od);
         init_lb_ips(od);
@@ -1544,14 +1282,6 @@ lsp_is_enabled(const struct nbrec_logical_switch_port *lsp)
     return !lsp->n_enabled || *lsp->enabled;
 }
 
-/* Returns true only if the logical switch port 'up' column is set to true.
- * Otherwise, if the column is not set or set to false, returns false. */
-static bool
-lsp_is_up(const struct nbrec_logical_switch_port *lsp)
-{
-    return lsp->n_up && *lsp->up;
-}
-
 static bool
 lsp_is_external(const struct nbrec_logical_switch_port *nbsp)
 {
@@ -2833,6 +2563,8 @@ op_get_name(const struct ovn_port *op)
     return name;
 }
 
+static void sync_ND_RA_options_for_lrouter_port(const struct ovn_port *op);
+
 static void
 ovn_update_ipv6_prefix(struct hmap *ports)
 {
@@ -2842,6 +2574,8 @@ ovn_update_ipv6_prefix(struct hmap *ports)
             continue;
         }
 
+        sync_ND_RA_options_for_lrouter_port(op);
+
         if (!smap_get_bool(&op->nbrp->options, "prefix", false)) {
             continue;
         }
@@ -2930,10 +2664,20 @@ ovn_port_update_sbrec(struct northd_context *ctx,
         } else {
             if (op->peer) {
                 smap_add(&new, "peer", op->peer->key);
+                if (op->peer->od->n_localnet_ports) {
+                    smap_add(&new, "peer-dp-has-localnet-ports", "true");
+                }
+            }
+            if (op->od->l3dgw_port == op) {
+                smap_add(&new, "is-l3dgw-port", "true");
             }
             if (chassis_name) {
                 smap_add(&new, "l3gateway-chassis", chassis_name);
             }
+
+            if (op->has_bfd) {
+                smap_add(&new, "has-bfd", "true");
+            }
         }
 
         const char *ipv6_pd_list = smap_get(&op->sb->options,
@@ -3139,9 +2883,17 @@ ovn_port_update_sbrec(struct northd_context *ctx,
 
         sbrec_port_binding_set_parent_port(op->sb, op->nbsp->parent_name);
         sbrec_port_binding_set_tag(op->sb, op->nbsp->tag, op->nbsp->n_tag);
-        sbrec_port_binding_set_mac(op->sb, (const char **) op->nbsp->addresses,
-                                   op->nbsp->n_addresses);
-
+        if (op->nbsp->dynamic_addresses) {
+            sbrec_port_binding_set_mac(
+                op->sb, (const char **)&op->nbsp->dynamic_addresses, 1);
+        } else {
+            sbrec_port_binding_set_mac(op->sb,
+                                       (const char **) op->nbsp->addresses,
+                                       op->nbsp->n_addresses);
+        }
+        sbrec_port_binding_set_port_security(
+            op->sb, (const char **) op->nbsp->port_security,
+            op->nbsp->n_port_security);
         struct smap ids = SMAP_INITIALIZER(&ids);
         smap_clone(&ids, &op->nbsp->external_ids);
         const char *name = smap_get(&ids, "neutron:port_name");
@@ -3400,16 +3152,22 @@ build_ovn_lbs(struct northd_context *ctx, struct hmap *datapaths,
 
     struct ovn_datapath *od;
     HMAP_FOR_EACH (od, key_node, datapaths) {
-        if (!od->nbs) {
-            continue;
-        }
+        if (od->nbs) {
+            for (size_t i = 0; i < od->nbs->n_load_balancer; i++) {
+                const struct uuid *lb_uuid =
+                    &od->nbs->load_balancer[i]->header_.uuid;
+                lb = ovn_northd_lb_find(lbs, lb_uuid);
 
-        for (size_t i = 0; i < od->nbs->n_load_balancer; i++) {
-            const struct uuid *lb_uuid =
-                &od->nbs->load_balancer[i]->header_.uuid;
-            lb = ovn_northd_lb_find(lbs, lb_uuid);
+                ovn_northd_lb_add_datapath(lb, od->sb);
+            }
+        } else {
+            for (size_t i = 0; i < od->nbr->n_load_balancer; i++) {
+                const struct uuid *lb_uuid =
+                    &od->nbr->load_balancer[i]->header_.uuid;
+                lb = ovn_northd_lb_find(lbs, lb_uuid);
 
-            ovn_northd_lb_add_datapath(lb, od->sb);
+                ovn_northd_lb_add_datapath(lb, od->sb);
+            }
         }
     }
 
@@ -3442,9 +3200,22 @@ build_ovn_lbs(struct northd_context *ctx, struct hmap *datapaths,
         /* Store the fact that northd provides the original (destination IP +
          * transport port) tuple.
          */
-        struct smap options;
-        smap_clone(&options, &lb->nlb->options);
-        smap_replace(&options, "hairpin_orig_tuple", "true");
+        struct smap options = SMAP_INITIALIZER(&options);
+        smap_add(&options, "hairpin_orig_tuple", "true");
+
+        for (size_t i = 0; i < lb->n_vips; i++) {
+            struct ovn_northd_lb_vip *vip_nb = &lb->vips_nb[i];
+            if (vip_nb->lb_health_check) {
+                struct ovn_lb_vip *vip = &lb->vips[i];
+                char *vip_key = xasprintf("%s_hc", vip->vip_str);
+                smap_replace(&options, vip_key, "true");
+                free(vip_key);
+            }
+        }
+
+        if (smap_get_bool(&lb->nlb->options, "reject", false)) {
+            smap_add(&options, "reject", "true");
+        }
 
         if (!lb->slb) {
             sbrec_lb = sbrec_load_balancer_insert(ctx->ovnsb_txn);
@@ -3463,29 +3234,44 @@ build_ovn_lbs(struct northd_context *ctx, struct hmap *datapaths,
         sbrec_load_balancer_set_datapaths(
             lb->slb, (struct sbrec_datapath_binding **)lb->dps,
             lb->n_dps);
+        sbrec_load_balancer_set_selection_fields(
+            lb->slb, (char const **) lb->nlb->selection_fields,
+            lb->nlb->n_selection_fields);
         smap_destroy(&options);
     }
 
     /* Set the list of associated load balanacers to a logical switch
      * datapath binding in the SB DB. */
     HMAP_FOR_EACH (od, key_node, datapaths) {
-        if (!od->nbs) {
-            continue;
-        }
+        if (od->nbs) {
+            const struct sbrec_load_balancer **sbrec_lbs =
+                xmalloc(od->nbs->n_load_balancer * sizeof *sbrec_lbs);
+            for (size_t i = 0; i < od->nbs->n_load_balancer; i++) {
+                const struct uuid *lb_uuid =
+                    &od->nbs->load_balancer[i]->header_.uuid;
+                lb = ovn_northd_lb_find(lbs, lb_uuid);
+                sbrec_lbs[i] = lb->slb;
+            }
+
+            sbrec_datapath_binding_set_load_balancers(
+                od->sb, (struct sbrec_load_balancer **)sbrec_lbs,
+                od->nbs->n_load_balancer);
+            free(sbrec_lbs);
+        } else {
+            const struct sbrec_load_balancer **sbrec_lbs =
+                xmalloc(od->nbr->n_load_balancer * sizeof *sbrec_lbs);
+            for (size_t i = 0; i < od->nbr->n_load_balancer; i++) {
+                const struct uuid *lb_uuid =
+                    &od->nbr->load_balancer[i]->header_.uuid;
+                lb = ovn_northd_lb_find(lbs, lb_uuid);
+                sbrec_lbs[i] = lb->slb;
+            }
 
-        const struct sbrec_load_balancer **sbrec_lbs =
-            xmalloc(od->nbs->n_load_balancer * sizeof *sbrec_lbs);
-        for (size_t i = 0; i < od->nbs->n_load_balancer; i++) {
-            const struct uuid *lb_uuid =
-                &od->nbs->load_balancer[i]->header_.uuid;
-            lb = ovn_northd_lb_find(lbs, lb_uuid);
-            sbrec_lbs[i] = lb->slb;
+            sbrec_datapath_binding_set_load_balancers(
+                od->sb, (struct sbrec_load_balancer **)sbrec_lbs,
+                od->nbr->n_load_balancer);
+            free(sbrec_lbs);
         }
-
-        sbrec_datapath_binding_set_load_balancers(
-            od->sb, (struct sbrec_load_balancer **)sbrec_lbs,
-            od->nbs->n_load_balancer);
-        free(sbrec_lbs);
     }
 }
 
@@ -3696,27 +3482,21 @@ struct multicast_group {
     uint16_t key;               /* OVN_MIN_MULTICAST...OVN_MAX_MULTICAST. */
 };
 
-#define MC_FLOOD "_MC_flood"
 static const struct multicast_group mc_flood =
     { MC_FLOOD, OVN_MCAST_FLOOD_TUNNEL_KEY };
 
-#define MC_MROUTER_FLOOD "_MC_mrouter_flood"
 static const struct multicast_group mc_mrouter_flood =
     { MC_MROUTER_FLOOD, OVN_MCAST_MROUTER_FLOOD_TUNNEL_KEY };
 
-#define MC_MROUTER_STATIC "_MC_mrouter_static"
 static const struct multicast_group mc_mrouter_static =
     { MC_MROUTER_STATIC, OVN_MCAST_MROUTER_STATIC_TUNNEL_KEY };
 
-#define MC_STATIC "_MC_static"
 static const struct multicast_group mc_static =
     { MC_STATIC, OVN_MCAST_STATIC_TUNNEL_KEY };
 
-#define MC_UNKNOWN "_MC_unknown"
 static const struct multicast_group mc_unknown =
     { MC_UNKNOWN, OVN_MCAST_UNKNOWN_TUNNEL_KEY };
 
-#define MC_FLOOD_L2 "_MC_flood_l2"
 static const struct multicast_group mc_flood_l2 =
     { MC_FLOOD_L2, OVN_MCAST_FLOOD_L2_TUNNEL_KEY };
 
@@ -4222,326 +4002,6 @@ ovn_lflow_destroy(struct hmap *lflows, struct ovn_lflow *lflow)
     }
 }
 
-/* Appends port security constraints on L2 address field 'eth_addr_field'
- * (e.g. "eth.src" or "eth.dst") to 'match'.  'ps_addrs', with 'n_ps_addrs'
- * elements, is the collection of port_security constraints from an
- * OVN_NB Logical_Switch_Port row generated by extract_lsp_addresses(). */
-static void
-build_port_security_l2(const char *eth_addr_field,
-                       struct lport_addresses *ps_addrs,
-                       unsigned int n_ps_addrs,
-                       struct ds *match)
-{
-    if (!n_ps_addrs) {
-        return;
-    }
-
-    ds_put_format(match, " && %s == {", eth_addr_field);
-
-    for (size_t i = 0; i < n_ps_addrs; i++) {
-        ds_put_format(match, "%s ", ps_addrs[i].ea_s);
-    }
-    ds_chomp(match, ' ');
-    ds_put_cstr(match, "}");
-}
-
-static void
-build_port_security_ipv6_nd_flow(
-    struct ds *match, struct eth_addr ea, struct ipv6_netaddr *ipv6_addrs,
-    int n_ipv6_addrs)
-{
-    ds_put_format(match, " && ip6 && nd && ((nd.sll == "ETH_ADDR_FMT" || "
-                  "nd.sll == "ETH_ADDR_FMT") || ((nd.tll == "ETH_ADDR_FMT" || "
-                  "nd.tll == "ETH_ADDR_FMT")", ETH_ADDR_ARGS(eth_addr_zero),
-                  ETH_ADDR_ARGS(ea), ETH_ADDR_ARGS(eth_addr_zero),
-                  ETH_ADDR_ARGS(ea));
-    if (!n_ipv6_addrs) {
-        ds_put_cstr(match, "))");
-        return;
-    }
-
-    char ip6_str[INET6_ADDRSTRLEN + 1];
-    struct in6_addr lla;
-    in6_generate_lla(ea, &lla);
-    memset(ip6_str, 0, sizeof(ip6_str));
-    ipv6_string_mapped(ip6_str, &lla);
-    ds_put_format(match, " && (nd.target == %s", ip6_str);
-
-    for (size_t i = 0; i < n_ipv6_addrs; i++) {
-        /* When the netmask is applied, if the host portion is
-         * non-zero, the host can only use the specified
-         * address in the nd.target.  If zero, the host is allowed
-         * to use any address in the subnet.
-         */
-        if (ipv6_addrs[i].plen == 128
-            || !ipv6_addr_is_host_zero(&ipv6_addrs[i].addr,
-                                       &ipv6_addrs[i].mask)) {
-            ds_put_format(match, " || nd.target == %s", ipv6_addrs[i].addr_s);
-        } else {
-            ds_put_format(match, " || nd.target == %s/%d",
-                          ipv6_addrs[i].network_s, ipv6_addrs[i].plen);
-        }
-    }
-
-    ds_put_format(match, ")))");
-}
-
-static void
-build_port_security_ipv6_flow(
-    enum ovn_pipeline pipeline, struct ds *match, struct eth_addr ea,
-    struct ipv6_netaddr *ipv6_addrs, int n_ipv6_addrs)
-{
-    char ip6_str[INET6_ADDRSTRLEN + 1];
-
-    ds_put_format(match, " && %s == {",
-                  pipeline == P_IN ? "ip6.src" : "ip6.dst");
-
-    /* Allow link-local address. */
-    struct in6_addr lla;
-    in6_generate_lla(ea, &lla);
-    ipv6_string_mapped(ip6_str, &lla);
-    ds_put_format(match, "%s, ", ip6_str);
-
-    /* Allow ip6.dst=ff00::/8 for multicast packets */
-    if (pipeline == P_OUT) {
-        ds_put_cstr(match, "ff00::/8, ");
-    }
-    for (size_t i = 0; i < n_ipv6_addrs; i++) {
-        /* When the netmask is applied, if the host portion is
-         * non-zero, the host can only use the specified
-         * address.  If zero, the host is allowed to use any
-         * address in the subnet.
-         */
-        if (ipv6_addrs[i].plen == 128
-            || !ipv6_addr_is_host_zero(&ipv6_addrs[i].addr,
-                                       &ipv6_addrs[i].mask)) {
-            ds_put_format(match, "%s, ", ipv6_addrs[i].addr_s);
-        } else {
-            ds_put_format(match, "%s/%d, ", ipv6_addrs[i].network_s,
-                          ipv6_addrs[i].plen);
-        }
-    }
-    /* Replace ", " by "}". */
-    ds_chomp(match, ' ');
-    ds_chomp(match, ',');
-    ds_put_cstr(match, "}");
-}
-
-/**
- * Build port security constraints on ARP and IPv6 ND fields
- * and add logical flows to S_SWITCH_IN_PORT_SEC_ND stage.
- *
- * For each port security of the logical port, following
- * logical flows are added
- *   - If the port security has no IP (both IPv4 and IPv6) or
- *     if it has IPv4 address(es)
- *      - Priority 90 flow to allow ARP packets for known MAC addresses
- *        in the eth.src and arp.spa fields. If the port security
- *        has IPv4 addresses, allow known IPv4 addresses in the arp.tpa field.
- *
- *   - If the port security has no IP (both IPv4 and IPv6) or
- *     if it has IPv6 address(es)
- *     - Priority 90 flow to allow IPv6 ND packets for known MAC addresses
- *       in the eth.src and nd.sll/nd.tll fields. If the port security
- *       has IPv6 addresses, allow known IPv6 addresses in the nd.target field
- *       for IPv6 Neighbor Advertisement packet.
- *
- *   - Priority 80 flow to drop ARP and IPv6 ND packets.
- */
-static void
-build_port_security_nd(struct ovn_port *op, struct hmap *lflows,
-                       const struct ovsdb_idl_row *stage_hint)
-{
-    struct ds match = DS_EMPTY_INITIALIZER;
-
-    for (size_t i = 0; i < op->n_ps_addrs; i++) {
-        struct lport_addresses *ps = &op->ps_addrs[i];
-
-        bool no_ip = !(ps->n_ipv4_addrs || ps->n_ipv6_addrs);
-
-        ds_clear(&match);
-        if (ps->n_ipv4_addrs || no_ip) {
-            ds_put_format(&match,
-                          "inport == %s && eth.src == %s && arp.sha == %s",
-                          op->json_key, ps->ea_s, ps->ea_s);
-
-            if (ps->n_ipv4_addrs) {
-                ds_put_cstr(&match, " && arp.spa == {");
-                for (size_t j = 0; j < ps->n_ipv4_addrs; j++) {
-                    /* When the netmask is applied, if the host portion is
-                     * non-zero, the host can only use the specified
-                     * address in the arp.spa.  If zero, the host is allowed
-                     * to use any address in the subnet. */
-                    if (ps->ipv4_addrs[j].plen == 32
-                        || ps->ipv4_addrs[j].addr & ~ps->ipv4_addrs[j].mask) {
-                        ds_put_cstr(&match, ps->ipv4_addrs[j].addr_s);
-                    } else {
-                        ds_put_format(&match, "%s/%d",
-                                      ps->ipv4_addrs[j].network_s,
-                                      ps->ipv4_addrs[j].plen);
-                    }
-                    ds_put_cstr(&match, ", ");
-                }
-                ds_chomp(&match, ' ');
-                ds_chomp(&match, ',');
-                ds_put_cstr(&match, "}");
-            }
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND,
-                                    90, ds_cstr(&match), "next;", stage_hint);
-        }
-
-        if (ps->n_ipv6_addrs || no_ip) {
-            ds_clear(&match);
-            ds_put_format(&match, "inport == %s && eth.src == %s",
-                          op->json_key, ps->ea_s);
-            build_port_security_ipv6_nd_flow(&match, ps->ea, ps->ipv6_addrs,
-                                             ps->n_ipv6_addrs);
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND,
-                                    90, ds_cstr(&match), "next;", stage_hint);
-        }
-    }
-
-    ds_clear(&match);
-    ds_put_format(&match, "inport == %s && (arp || nd)", op->json_key);
-    ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND, 80,
-                            ds_cstr(&match), "drop;", stage_hint);
-    ds_destroy(&match);
-}
-
-/**
- * Build port security constraints on IPv4 and IPv6 src and dst fields
- * and add logical flows to S_SWITCH_(IN/OUT)_PORT_SEC_IP stage.
- *
- * For each port security of the logical port, following
- * logical flows are added
- *   - If the port security has IPv4 addresses,
- *     - Priority 90 flow to allow IPv4 packets for known IPv4 addresses
- *
- *   - If the port security has IPv6 addresses,
- *     - Priority 90 flow to allow IPv6 packets for known IPv6 addresses
- *
- *   - If the port security has IPv4 addresses or IPv6 addresses or both
- *     - Priority 80 flow to drop all IPv4 and IPv6 traffic
- */
-static void
-build_port_security_ip(enum ovn_pipeline pipeline, struct ovn_port *op,
-                       struct hmap *lflows,
-                       const struct ovsdb_idl_row *stage_hint)
-{
-    char *port_direction;
-    enum ovn_stage stage;
-    if (pipeline == P_IN) {
-        port_direction = "inport";
-        stage = S_SWITCH_IN_PORT_SEC_IP;
-    } else {
-        port_direction = "outport";
-        stage = S_SWITCH_OUT_PORT_SEC_IP;
-    }
-
-    for (size_t i = 0; i < op->n_ps_addrs; i++) {
-        struct lport_addresses *ps = &op->ps_addrs[i];
-
-        if (!(ps->n_ipv4_addrs || ps->n_ipv6_addrs)) {
-            continue;
-        }
-
-        if (ps->n_ipv4_addrs) {
-            struct ds match = DS_EMPTY_INITIALIZER;
-            if (pipeline == P_IN) {
-                /* Permit use of the unspecified address for DHCP discovery */
-                struct ds dhcp_match = DS_EMPTY_INITIALIZER;
-                ds_put_format(&dhcp_match, "inport == %s"
-                              " && eth.src == %s"
-                              " && ip4.src == 0.0.0.0"
-                              " && ip4.dst == 255.255.255.255"
-                              " && udp.src == 68 && udp.dst == 67",
-                              op->json_key, ps->ea_s);
-                ovn_lflow_add_with_hint(lflows, op->od, stage, 90,
-                                        ds_cstr(&dhcp_match), "next;",
-                                        stage_hint);
-                ds_destroy(&dhcp_match);
-                ds_put_format(&match, "inport == %s && eth.src == %s"
-                              " && ip4.src == {", op->json_key,
-                              ps->ea_s);
-            } else {
-                ds_put_format(&match, "outport == %s && eth.dst == %s"
-                              " && ip4.dst == {255.255.255.255, 224.0.0.0/4, ",
-                              op->json_key, ps->ea_s);
-            }
-
-            for (int j = 0; j < ps->n_ipv4_addrs; j++) {
-                ovs_be32 mask = ps->ipv4_addrs[j].mask;
-                /* When the netmask is applied, if the host portion is
-                 * non-zero, the host can only use the specified
-                 * address.  If zero, the host is allowed to use any
-                 * address in the subnet.
-                 */
-                if (ps->ipv4_addrs[j].plen == 32
-                    || ps->ipv4_addrs[j].addr & ~mask) {
-                    ds_put_format(&match, "%s", ps->ipv4_addrs[j].addr_s);
-                    if (pipeline == P_OUT && ps->ipv4_addrs[j].plen != 32) {
-                        /* Host is also allowed to receive packets to the
-                         * broadcast address in the specified subnet. */
-                        ds_put_format(&match, ", %s",
-                                      ps->ipv4_addrs[j].bcast_s);
-                    }
-                } else {
-                    /* host portion is zero */
-                    ds_put_format(&match, "%s/%d", ps->ipv4_addrs[j].network_s,
-                                  ps->ipv4_addrs[j].plen);
-                }
-                ds_put_cstr(&match, ", ");
-            }
-
-            /* Replace ", " by "}". */
-            ds_chomp(&match, ' ');
-            ds_chomp(&match, ',');
-            ds_put_cstr(&match, "}");
-            ovn_lflow_add_with_hint(lflows, op->od, stage, 90,
-                                    ds_cstr(&match), "next;",
-                                    stage_hint);
-            ds_destroy(&match);
-        }
-
-        if (ps->n_ipv6_addrs) {
-            struct ds match = DS_EMPTY_INITIALIZER;
-            if (pipeline == P_IN) {
-                /* Permit use of unspecified address for duplicate address
-                 * detection */
-                struct ds dad_match = DS_EMPTY_INITIALIZER;
-                ds_put_format(&dad_match, "inport == %s"
-                              " && eth.src == %s"
-                              " && ip6.src == ::"
-                              " && ip6.dst == ff02::/16"
-                              " && icmp6.type == {131, 135, 143}", op->json_key,
-                              ps->ea_s);
-                ovn_lflow_add_with_hint(lflows, op->od, stage, 90,
-                                        ds_cstr(&dad_match), "next;",
-                                        stage_hint);
-                ds_destroy(&dad_match);
-            }
-            ds_put_format(&match, "%s == %s && %s == %s",
-                          port_direction, op->json_key,
-                          pipeline == P_IN ? "eth.src" : "eth.dst", ps->ea_s);
-            build_port_security_ipv6_flow(pipeline, &match, ps->ea,
-                                          ps->ipv6_addrs, ps->n_ipv6_addrs);
-            ovn_lflow_add_with_hint(lflows, op->od, stage, 90,
-                                    ds_cstr(&match), "next;",
-                                    stage_hint);
-            ds_destroy(&match);
-        }
-
-        char *match = xasprintf("%s == %s && %s == %s && ip",
-                                port_direction, op->json_key,
-                                pipeline == P_IN ? "eth.src" : "eth.dst",
-                                ps->ea_s);
-        ovn_lflow_add_with_hint(lflows, op->od, stage, 80, match, "drop;",
-                                stage_hint);
-        free(match);
-    }
-
-}
-
 static bool
 build_dhcpv4_action(struct ovn_port *op, ovs_be32 offer_ip,
                     struct ds *options_action, struct ds *response_action,
@@ -4821,166 +4281,6 @@ ls_get_acl_flags(struct ovn_datapath *od)
     }
 }
 
-/* Logical switch ingress table 0: Ingress port security - L2
- *  (priority 50).
- *  Ingress table 1: Ingress port security - IP (priority 90 and 80)
- *  Ingress table 2: Ingress port security - ND (priority 90 and 80)
- */
-static void
-build_lswitch_input_port_sec_op(
-        struct ovn_port *op, struct hmap *lflows,
-        struct ds *actions, struct ds *match)
-{
-
-    if (!op->nbsp) {
-        return;
-    }
-
-    if (!lsp_is_enabled(op->nbsp)) {
-        /* Drop packets from disabled logical ports (since logical flow
-         * tables are default-drop). */
-        return;
-    }
-
-    if (lsp_is_external(op->nbsp)) {
-        return;
-    }
-
-    ds_clear(match);
-    ds_clear(actions);
-    ds_put_format(match, "inport == %s", op->json_key);
-    build_port_security_l2("eth.src", op->ps_addrs, op->n_ps_addrs,
-                           match);
-
-    const char *queue_id = smap_get(&op->sb->options, "qdisc_queue_id");
-    if (queue_id) {
-        ds_put_format(actions, "set_queue(%s); ", queue_id);
-    }
-    ds_put_cstr(actions, "next;");
-    ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PORT_SEC_L2, 50,
-                            ds_cstr(match), ds_cstr(actions),
-                            &op->nbsp->header_);
-
-    if (op->nbsp->n_port_security) {
-        build_port_security_ip(P_IN, op, lflows, &op->nbsp->header_);
-        build_port_security_nd(op, lflows, &op->nbsp->header_);
-    }
-}
-
-/* Ingress table 1 and 2: Port security - IP and ND, by default
- * goto next. (priority 0)
- */
-static void
-build_lswitch_input_port_sec_od(
-        struct ovn_datapath *od, struct hmap *lflows)
-{
-
-    if (od->nbs) {
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_ND, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_IP, 0, "1", "next;");
-    }
-}
-
-static void
-build_lswitch_learn_fdb_op(
-        struct ovn_port *op, struct hmap *lflows,
-        struct ds *actions, struct ds *match)
-{
-    if (op->nbsp && !op->n_ps_addrs && !strcmp(op->nbsp->type, "") &&
-        op->has_unknown) {
-        ds_clear(match);
-        ds_clear(actions);
-        ds_put_format(match, "inport == %s", op->json_key);
-        ds_put_format(actions, REGBIT_LKUP_FDB
-                      " = lookup_fdb(inport, eth.src); next;");
-        ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_LOOKUP_FDB, 100,
-                                ds_cstr(match), ds_cstr(actions),
-                                &op->nbsp->header_);
-
-        ds_put_cstr(match, " && "REGBIT_LKUP_FDB" == 0");
-        ds_clear(actions);
-        ds_put_cstr(actions, "put_fdb(inport, eth.src); next;");
-        ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_PUT_FDB, 100,
-                                ds_cstr(match), ds_cstr(actions),
-                                &op->nbsp->header_);
-    }
-}
-
-static void
-build_lswitch_learn_fdb_od(
-        struct ovn_datapath *od, struct hmap *lflows)
-{
-
-    if (od->nbs) {
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_LOOKUP_FDB, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_PUT_FDB, 0, "1", "next;");
-    }
-}
-
-/* Egress table 8: Egress port security - IP (priorities 90 and 80)
- * if port security enabled.
- *
- * Egress table 9: Egress port security - L2 (priorities 50 and 150).
- *
- * Priority 50 rules implement port security for enabled logical port.
- *
- * Priority 150 rules drop packets to disabled logical ports, so that
- * they don't even receive multicast or broadcast packets.
- */
-static void
-build_lswitch_output_port_sec_op(struct ovn_port *op,
-                                 struct hmap *lflows,
-                                 struct ds *match,
-                                 struct ds *actions)
-{
-
-    if (op->nbsp && (!lsp_is_external(op->nbsp))) {
-
-        ds_clear(actions);
-        ds_clear(match);
-
-        ds_put_format(match, "outport == %s", op->json_key);
-        if (lsp_is_enabled(op->nbsp)) {
-            build_port_security_l2("eth.dst", op->ps_addrs, op->n_ps_addrs,
-                                   match);
-
-            if (!strcmp(op->nbsp->type, "localnet")) {
-                const char *queue_id = smap_get(&op->sb->options,
-                                                "qdisc_queue_id");
-                if (queue_id) {
-                    ds_put_format(actions, "set_queue(%s); ", queue_id);
-                }
-            }
-            ds_put_cstr(actions, "output;");
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_OUT_PORT_SEC_L2,
-                                    50, ds_cstr(match), ds_cstr(actions),
-                                    &op->nbsp->header_);
-        } else {
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_OUT_PORT_SEC_L2,
-                                    150, ds_cstr(match), "drop;",
-                                    &op->nbsp->header_);
-        }
-
-        if (op->nbsp->n_port_security) {
-            build_port_security_ip(P_OUT, op, lflows, &op->nbsp->header_);
-        }
-    }
-}
-
-/* Egress tables 8: Egress port security - IP (priority 0)
- * Egress table 9: Egress port security L2 - multicast/broadcast
- *                 (priority 100). */
-static void
-build_lswitch_output_port_sec_od(struct ovn_datapath *od,
-                              struct hmap *lflows)
-{
-    if (od->nbs) {
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PORT_SEC_IP, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PORT_SEC_L2, 100, "eth.mcast",
-                      "output;");
-    }
-}
-
 static void
 skip_port_from_conntrack(struct ovn_datapath *od, struct ovn_port *op,
                          enum ovn_stage in_stage, enum ovn_stage out_stage,
@@ -5061,9 +4361,6 @@ build_pre_acls(struct ovn_datapath *od, struct hmap *port_groups,
 {
     /* Ingress and Egress Pre-ACL Table (Priority 0): Packets are
      * allowed by default. */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 0, "1", "next;");
-
     ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 110,
                   "eth.dst == $svc_monitor_mac", "next;");
 
@@ -5074,11 +4371,6 @@ build_pre_acls(struct ovn_datapath *od, struct hmap *port_groups,
      * send IP packets for some (allow) filters through the conntrack action,
      * which handles defragmentation, in order to match L4 headers. */
     if (od->has_stateful_acl) {
-        for (size_t i = 0; i < od->n_router_ports; i++) {
-            skip_port_from_conntrack(od, od->router_ports[i],
-                                     S_SWITCH_IN_PRE_ACL, S_SWITCH_OUT_PRE_ACL,
-                                     110, lflows);
-        }
         for (size_t i = 0; i < od->n_localnet_ports; i++) {
             skip_port_from_conntrack(od, od->localnet_ports[i],
                                      S_SWITCH_IN_PRE_ACL, S_SWITCH_OUT_PRE_ACL,
@@ -5087,30 +4379,6 @@ build_pre_acls(struct ovn_datapath *od, struct hmap *port_groups,
 
         /* stateless filters always take precedence over stateful ACLs. */
         build_stateless_filters(od, port_groups, lflows);
-
-        /* Ingress and Egress Pre-ACL Table (Priority 110).
-         *
-         * Not to do conntrack on ND and ICMP destination
-         * unreachable packets. */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 110,
-                      "nd || nd_rs || nd_ra || mldv1 || mldv2 || "
-                      "(udp && udp.src == 546 && udp.dst == 547)", "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
-                      "nd || nd_rs || nd_ra || mldv1 || mldv2 || "
-                      "(udp && udp.src == 546 && udp.dst == 547)", "next;");
-
-        /* Ingress and Egress Pre-ACL Table (Priority 100).
-         *
-         * Regardless of whether the ACL is "from-lport" or "to-lport",
-         * we need rules in both the ingress and egress table, because
-         * the return traffic needs to be followed.
-         *
-         * 'REGBIT_CONNTRACK_DEFRAG' is set to let the pre-stateful table send
-         * it to conntrack for tracking and defragmentation. */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_ACL, 100, "ip",
-                      REGBIT_CONNTRACK_DEFRAG" = 1; next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 100, "ip",
-                      REGBIT_CONNTRACK_DEFRAG" = 1; next;");
     }
 }
 
@@ -5195,36 +4463,12 @@ static void
 build_pre_lb(struct ovn_datapath *od, struct hmap *lflows,
              struct shash *meter_groups, struct hmap *lbs)
 {
-    /* Do not send ND packets to conntrack */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_LB, 110,
-                  "nd || nd_rs || nd_ra || mldv1 || mldv2",
-                  "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_LB, 110,
-                  "nd || nd_rs || nd_ra || mldv1 || mldv2",
-                  "next;");
-
-    /* Do not send service monitor packets to conntrack. */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_LB, 110,
-                  "eth.dst == $svc_monitor_mac", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_LB, 110,
-                  "eth.src == $svc_monitor_mac", "next;");
-
-    /* Allow all packets to go to next tables by default. */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_LB, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_LB, 0, "1", "next;");
-
-    for (size_t i = 0; i < od->n_router_ports; i++) {
-        skip_port_from_conntrack(od, od->router_ports[i],
-                                 S_SWITCH_IN_PRE_LB, S_SWITCH_OUT_PRE_LB,
-                                 110, lflows);
-    }
     for (size_t i = 0; i < od->n_localnet_ports; i++) {
         skip_port_from_conntrack(od, od->localnet_ports[i],
                                  S_SWITCH_IN_PRE_LB, S_SWITCH_OUT_PRE_LB,
                                  110, lflows);
     }
 
-    bool vip_configured = false;
     for (int i = 0; i < od->nbs->n_load_balancer; i++) {
         struct nbrec_load_balancer *nb_lb = od->nbs->load_balancer[i];
         struct ovn_northd_lb *lb =
@@ -5241,194 +4485,6 @@ build_pre_lb(struct ovn_datapath *od, struct hmap *lflows,
              * the packet through ct() action to de-fragment. In stateful
              * table, we will eventually look at L4 information. */
         }
-
-        vip_configured = (vip_configured || lb->n_vips);
-    }
-
-    /* '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
-     * we used to set the REGBIT_CONNTRACK_DEFRAG flag in the ingress pipeline
-     * if the IP destination matches the VIP. But this causes few issues when
-     * a logical switch has no ACLs configured with allow-related.
-     * To understand the issue, lets a take a TCP load balancer -
-     * 10.0.0.10:80=10.0.0.3:80.
-     * If a logical port - p1 with IP - 10.0.0.5 opens a TCP connection with
-     * the VIP - 10.0.0.10, then the packet in the ingress pipeline of 'p1'
-     * is sent to the p1's conntrack zone id and the packet is load balanced
-     * to the backend - 10.0.0.3. For the reply packet from the backend lport,
-     * it is not sent to the conntrack of backend lport's zone id. This is fine
-     * as long as the packet is valid. Suppose the backend lport sends an
-     *  invalid TCP packet (like incorrect sequence number), the packet gets
-     * delivered to the lport 'p1' without unDNATing the packet to the
-     * VIP - 10.0.0.10. And this causes the connection to be reset by the
-     * lport p1's VIF.
-     *
-     * We can't fix this issue by adding a logical flow to drop ct.inv packets
-     * in the egress pipeline since it will drop all other connections not
-     * destined to the load balancers.
-     *
-     * To fix this issue, we send all the packets to the conntrack in the
-     * ingress pipeline if a load balancer is configured. We can now
-     * add a lflow to drop ct.inv packets.
-     */
-    if (vip_configured) {
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_LB,
-                      100, "ip", REGBIT_CONNTRACK_NAT" = 1; next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_LB,
-                      100, "ip", REGBIT_CONNTRACK_NAT" = 1; next;");
-    }
-}
-
-static void
-build_pre_stateful(struct ovn_datapath *od, struct hmap *lflows)
-{
-    /* Ingress and Egress pre-stateful Table (Priority 0): Packets are
-     * allowed by default. */
-    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;");
-}
-
-static void
-build_acl_hints(struct ovn_datapath *od, struct hmap *lflows)
-{
-    /* This stage builds hints for the IN/OUT_ACL stage. Based on various
-     * combinations of ct flags packets may hit only a subset of the logical
-     * flows in the IN/OUT_ACL stage.
-     *
-     * Populating ACL hints first and storing them in registers simplifies
-     * the logical flow match expressions in the IN/OUT_ACL stage and
-     * generates less openflows.
-     *
-     * Certain combinations of ct flags might be valid matches for multiple
-     * types of ACL logical flows (e.g., allow/drop). In such cases hints
-     * corresponding to all potential matches are set.
-     */
-
-    enum ovn_stage stages[] = {
-        S_SWITCH_IN_ACL_HINT,
-        S_SWITCH_OUT_ACL_HINT,
-    };
-
-    for (size_t i = 0; i < ARRAY_SIZE(stages); i++) {
-        enum ovn_stage stage = stages[i];
-
-        /* In any case, advance to the next stage. */
-        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;
-        }
-
-        /* New, not already established connections, may hit either allow
-         * or drop ACLs. For allow ACLs, the connection must also be committed
-         * to conntrack so we set REGBIT_ACL_HINT_ALLOW_NEW.
-         */
-        ovn_lflow_add(lflows, od, stage, 7, "ct.new && !ct.est",
-                      REGBIT_ACL_HINT_ALLOW_NEW " = 1; "
-                      REGBIT_ACL_HINT_DROP " = 1; "
-                      "next;");
-
-        /* Already established connections in the "request" direction that
-         * are already marked as "blocked" may hit either:
-         * - allow ACLs for connections that were previously allowed by a
-         *   policy that was deleted and is being readded now. In this case
-         *   the connection should be recommitted so we set
-         *   REGBIT_ACL_HINT_ALLOW_NEW.
-         * - drop ACLs.
-         */
-        ovn_lflow_add(lflows, od, stage, 6,
-                      "!ct.new && ct.est && !ct.rpl && ct_label.blocked == 1",
-                      REGBIT_ACL_HINT_ALLOW_NEW " = 1; "
-                      REGBIT_ACL_HINT_DROP " = 1; "
-                      "next;");
-
-        /* Not tracked traffic can either be allowed or dropped. */
-        ovn_lflow_add(lflows, od, stage, 5, "!ct.trk",
-                      REGBIT_ACL_HINT_ALLOW " = 1; "
-                      REGBIT_ACL_HINT_DROP " = 1; "
-                      "next;");
-
-        /* Already established connections in the "request" direction may hit
-         * either:
-         * - allow ACLs in which case the traffic should be allowed so we set
-         *   REGBIT_ACL_HINT_ALLOW.
-         * - drop ACLs in which case the traffic should be blocked and the
-         *   connection must be committed with ct_label.blocked set so we set
-         *   REGBIT_ACL_HINT_BLOCK.
-         */
-        ovn_lflow_add(lflows, od, stage, 4,
-                      "!ct.new && ct.est && !ct.rpl && ct_label.blocked == 0",
-                      REGBIT_ACL_HINT_ALLOW " = 1; "
-                      REGBIT_ACL_HINT_BLOCK " = 1; "
-                      "next;");
-
-        /* Not established or established and already blocked connections may
-         * hit drop ACLs.
-         */
-        ovn_lflow_add(lflows, od, stage, 3, "!ct.est",
-                      REGBIT_ACL_HINT_DROP " = 1; "
-                      "next;");
-        ovn_lflow_add(lflows, od, stage, 2, "ct.est && ct_label.blocked == 1",
-                      REGBIT_ACL_HINT_DROP " = 1; "
-                      "next;");
-
-        /* Established connections that were previously allowed might hit
-         * drop ACLs in which case the connection must be committed with
-         * ct_label.blocked set.
-         */
-        ovn_lflow_add(lflows, od, stage, 1, "ct.est && ct_label.blocked == 0",
-                      REGBIT_ACL_HINT_BLOCK " = 1; "
-                      "next;");
     }
 }
 
@@ -5770,115 +4826,7 @@ static void
 build_acls(struct ovn_datapath *od, struct hmap *lflows,
            struct hmap *port_groups, const struct shash *meter_groups)
 {
-    bool has_stateful = od->has_stateful_acl || od->has_lb_vip;
-
-    /* Ingress and Egress ACL Table (Priority 0): Packets are allowed by
-     * 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. */
-    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).
-         *
-         * By default, traffic is allowed.  This is partially handled by
-         * the Priority 0 ACL flows added earlier, but we also need to
-         * commit IP flows.  This is because, while the initiater's
-         * direction may not have any stateful rules, the server's may
-         * and then its return traffic would not have an associated
-         * conntrack entry and would return "+invalid".
-         *
-         * We use "ct_commit" for a connection that is not already known
-         * by the connection tracker.  Once a connection is committed,
-         * subsequent packets will hit the flow at priority 0 that just
-         * uses "next;"
-         *
-         * We also check for established connections that have ct_label.blocked
-         * set on them.  That's a connection that was disallowed, but is
-         * now allowed by policy again since it hit this default-allow flow.
-         * We need to set ct_label.blocked=0 to let the connection continue,
-         * which will be done by ct_commit() in the "stateful" stage.
-         * Subsequent packets will hit the flow at priority 0 that just
-         * uses "next;". */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL, 1,
-                      "ip && (!ct.est || (ct.est && ct_label.blocked == 1))",
-                       REGBIT_CONNTRACK_COMMIT" = 1; next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL, 1,
-                      "ip && (!ct.est || (ct.est && ct_label.blocked == 1))",
-                       REGBIT_CONNTRACK_COMMIT" = 1; next;");
-
-        /* 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. */
-        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 - 3).
-         *
-         * Allow reply traffic that is part of an established
-         * conntrack entry that has not been marked for deletion
-         * (bit 0 of ct_label).  We only match traffic in the
-         * reply direction because we want traffic in the request
-         * direction to hit the currently defined policy from ACLs.
-         *
-         * This is enforced at a higher priority than ACLs can be defined. */
-        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).
-         *
-         * Allow traffic that is related to an existing conntrack entry that
-         * has not been marked for deletion (bit 0 of ct_label).
-         *
-         * This is enforced at a higher priority than ACLs can be defined.
-         *
-         * NOTE: This does not support related data sessions (eg,
-         * 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.  */
-        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 65532).
-         *
-         * Not to do conntrack on ND packets. */
-        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 - 3,
-                      "nd || nd_ra || nd_rs || mldv1 || mldv2", "next;");
-    }
+    bool has_stateful = (od->has_stateful_acl || od->has_lb_vip);
 
     /* Ingress or Egress ACL Table (Various priorities). */
     for (size_t i = 0; i < od->nbs->n_acls; i++) {
@@ -5953,37 +4901,22 @@ build_acls(struct ovn_datapath *od, struct hmap *lflows,
         }
     }
 
-    /* Add a 34000 priority flow to advance the DNS reply from ovn-controller,
-     * if the CMS has configured DNS records for the datapath.
-     */
-    if (ls_has_dns_records(od->nbs)) {
-        const char *actions = has_stateful ? "ct_commit; next;" : "next;";
-        ovn_lflow_add(
-            lflows, od, S_SWITCH_OUT_ACL, 34000, "udp.src == 53",
-            actions);
-    }
-
-
     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;");
+                      "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;");
+                      "eth.src == $svc_monitor_mac", "next;");
     }
 }
 
 static void
-build_qos(struct ovn_datapath *od, struct hmap *lflows) {
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_QOS_MARK, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_QOS_MARK, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_QOS_METER, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_QOS_METER, 0, "1", "next;");
-
+build_qos(struct ovn_datapath *od, struct hmap *lflows)
+{
     for (size_t i = 0; i < od->nbs->n_qos_rules; i++) {
         struct nbrec_qos *qos = od->nbs->qos_rules[i];
         bool ingress = !strcmp(qos->direction, "from-lport") ? true :false;
@@ -6046,8 +4979,15 @@ build_lb_rules(struct ovn_datapath *od, struct hmap *lflows,
     struct ds match = DS_EMPTY_INITIALIZER;
 
     for (size_t i = 0; i < lb->n_vips; i++) {
-        struct ovn_lb_vip *lb_vip = &lb->vips[i];
         struct ovn_northd_lb_vip *lb_vip_nb = &lb->vips_nb[i];
+        if (!lb_vip_nb->lb_health_check) {
+            /* Only add lflows if health check is configured on the vip.
+             * ovn-controller will add the lflows if no health check
+             * configured. */
+            continue;
+        }
+
+        struct ovn_lb_vip *lb_vip = &lb->vips[i];
         const char *ip_match = NULL;
 
         ds_clear(&action);
@@ -6108,22 +5048,6 @@ build_lb_rules(struct ovn_datapath *od, struct hmap *lflows,
 static void
 build_stateful(struct ovn_datapath *od, struct hmap *lflows, struct hmap *lbs)
 {
-    /* Ingress and Egress stateful Table (Priority 0): Packets are
-     * allowed by default. */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_STATEFUL, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_STATEFUL, 0, "1", "next;");
-
-    /* If REGBIT_CONNTRACK_COMMIT is set as 1, then the packets should be
-     * committed to conntrack. We always set ct_label.blocked to 0 here as
-     * any packet that makes it this far is part of a connection we
-     * want to allow to continue. */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_STATEFUL, 100,
-                  REGBIT_CONNTRACK_COMMIT" == 1",
-                  "ct_commit { ct_label.blocked = 0; }; next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_STATEFUL, 100,
-                  REGBIT_CONNTRACK_COMMIT" == 1",
-                  "ct_commit { ct_label.blocked = 0; }; next;");
-
     /* 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
@@ -6138,63 +5062,6 @@ build_stateful(struct ovn_datapath *od, struct hmap *lflows, struct hmap *lbs)
     }
 }
 
-static void
-build_lb_hairpin(struct ovn_datapath *od, struct hmap *lflows)
-{
-    /* Ingress Pre-Hairpin/Nat-Hairpin/Hairpin tabled (Priority 0).
-     * Packets that don't need hairpinning should continue processing.
-     */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_HAIRPIN, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_NAT_HAIRPIN, 0, "1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_HAIRPIN, 0, "1", "next;");
-
-    if (od->has_lb_vip) {
-        /* Check if the packet needs to be hairpinned.
-         * Set REGBIT_HAIRPIN in the original direction and
-         * REGBIT_HAIRPIN_REPLY in the reply direction.
-         */
-        ovn_lflow_add_with_hint(
-            lflows, od, S_SWITCH_IN_PRE_HAIRPIN, 100, "ip && ct.trk",
-            REGBIT_HAIRPIN " = chk_lb_hairpin(); "
-            REGBIT_HAIRPIN_REPLY " = chk_lb_hairpin_reply(); "
-            "next;",
-            &od->nbs->header_);
-
-        /* If packet needs to be hairpinned, snat the src ip with the VIP
-         * for new sessions. */
-        ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_NAT_HAIRPIN, 100,
-                                "ip && ct.new && ct.trk"
-                                " && "REGBIT_HAIRPIN " == 1",
-                                "ct_snat_to_vip; next;",
-                                &od->nbs->header_);
-
-        /* If packet needs to be hairpinned, for established sessions there
-         * should already be an SNAT conntrack entry.
-         */
-        ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_NAT_HAIRPIN, 100,
-                                "ip && ct.est && ct.trk"
-                                " && "REGBIT_HAIRPIN " == 1",
-                                "ct_snat;",
-                                &od->nbs->header_);
-
-        /* For the reply of hairpinned traffic, snat the src ip to the VIP. */
-        ovn_lflow_add_with_hint(lflows, od, S_SWITCH_IN_NAT_HAIRPIN, 90,
-                                "ip && "REGBIT_HAIRPIN_REPLY " == 1",
-                                "ct_snat;",
-                                &od->nbs->header_);
-
-        /* Ingress Hairpin table.
-        * - Priority 1: Packets that were SNAT-ed for hairpinning should be
-        *   looped back (i.e., swap ETH addresses and send back on inport).
-        */
-        ovn_lflow_add(
-            lflows, od, S_SWITCH_IN_HAIRPIN, 1,
-            "("REGBIT_HAIRPIN " == 1 || " REGBIT_HAIRPIN_REPLY " == 1)",
-            "eth.dst <-> eth.src; outport = inport; flags.loopback = 1; "
-            "output;");
-    }
-}
-
 /* Build logical flows for the forwarding groups */
 static void
 build_fwd_group_lflows(struct ovn_datapath *od, struct hmap *lflows)
@@ -6848,47 +5715,6 @@ build_drop_arp_nd_flows_for_unbound_router_ports(struct ovn_port *op,
     ds_destroy(&match);
 }
 
-static bool
-is_vlan_transparent(const struct ovn_datapath *od)
-{
-    return smap_get_bool(&od->nbs->other_config, "vlan-passthru", false);
-}
-
-static void
-build_lswitch_flows(struct hmap *datapaths, struct hmap *lflows)
-{
-    /* This flow table structure is documented in ovn-northd(8), so please
-     * update ovn-northd.8.xml if you change anything. */
-
-    struct ds match = DS_EMPTY_INITIALIZER;
-    struct ds actions = DS_EMPTY_INITIALIZER;
-    struct ovn_datapath *od;
-
-    /* Ingress table 23: Destination lookup for unknown MACs (priority 0). */
-    HMAP_FOR_EACH (od, key_node, datapaths) {
-        if (!od->nbs) {
-            continue;
-        }
-
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_LKUP, 0, "1",
-                      "outport = get_fdb(eth.dst); next;");
-
-        if (od->has_unknown) {
-            ovn_lflow_add_unique(lflows, od, S_SWITCH_IN_L2_UNKNOWN, 50,
-                                 "outport == \"none\"",
-                                 "outport = \""MC_UNKNOWN "\"; output;");
-        } else {
-            ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_UNKNOWN, 50,
-                          "outport == \"none\"", "drop;");
-        }
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_UNKNOWN, 0, "1",
-                      "output;");
-    }
-
-    ds_destroy(&match);
-    ds_destroy(&actions);
-}
-
 /* Build pre-ACL and ACL tables for both ingress and egress.
  * Ingress tables 3 through 10.  Egress tables 0 through 7. */
 static void
@@ -6904,238 +5730,9 @@ build_lswitch_lflows_pre_acl_and_acl(struct ovn_datapath *od,
 
         build_pre_acls(od, port_groups, lflows);
         build_pre_lb(od, lflows, meter_groups, lbs);
-        build_pre_stateful(od, lflows);
-        build_acl_hints(od, lflows);
         build_acls(od, lflows, port_groups, meter_groups);
         build_qos(od, lflows);
         build_stateful(od, lflows, lbs);
-        build_lb_hairpin(od, lflows);
-    }
-}
-
-/* Logical switch ingress table 0: Admission control framework (priority
- * 100). */
-static void
-build_lswitch_lflows_admission_control(struct ovn_datapath *od,
-                                       struct hmap *lflows)
-{
-    if (od->nbs) {
-        /* Logical VLANs not supported. */
-        if (!is_vlan_transparent(od)) {
-            /* Block logical VLANs. */
-            ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_L2, 100,
-                          "vlan.present", "drop;");
-        }
-
-        /* Broadcast/multicast source address is invalid. */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_L2, 100, "eth.src[40]",
-                      "drop;");
-
-        /* Port security flows have priority 50
-         * (see build_lswitch_input_port_sec()) and will continue
-         * to the next table if packet source is acceptable. */
-    }
-}
-
-/* Ingress table 13: ARP/ND responder, skip requests coming from localnet
- * and vtep ports. (priority 100); see ovn-northd.8.xml for the
- * rationale. */
-
-static void
-build_lswitch_arp_nd_responder_skip_local(struct ovn_port *op,
-                                          struct hmap *lflows,
-                                          struct ds *match)
-{
-    if (op->nbsp) {
-        if ((!strcmp(op->nbsp->type, "localnet")) ||
-            (!strcmp(op->nbsp->type, "vtep"))) {
-            ds_clear(match);
-            ds_put_format(match, "inport == %s", op->json_key);
-            ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP,
-                                    100, ds_cstr(match), "next;",
-                                    &op->nbsp->header_);
-        }
-    }
-}
-
-/* Ingress table 13: ARP/ND responder, reply for known IPs.
- * (priority 50). */
-static void
-build_lswitch_arp_nd_responder_known_ips(struct ovn_port *op,
-                                         struct hmap *lflows,
-                                         struct hmap *ports,
-                                         struct ds *actions,
-                                         struct ds *match)
-{
-    if (op->nbsp) {
-        if (!strcmp(op->nbsp->type, "virtual")) {
-            /* Handle
-             *  - GARPs for virtual ip which belongs to a logical port
-             *    of type 'virtual' and bind that port.
-             *
-             *  - ARP reply from the virtual ip which belongs to a logical
-             *    port of type 'virtual' and bind that port.
-             * */
-            ovs_be32 ip;
-            const char *virtual_ip = smap_get(&op->nbsp->options,
-                                              "virtual-ip");
-            const char *virtual_parents = smap_get(&op->nbsp->options,
-                                                   "virtual-parents");
-            if (!virtual_ip || !virtual_parents ||
-                !ip_parse(virtual_ip, &ip)) {
-                return;
-            }
-
-            char *tokstr = xstrdup(virtual_parents);
-            char *save_ptr = NULL;
-            char *vparent;
-            for (vparent = strtok_r(tokstr, ",", &save_ptr); vparent != NULL;
-                 vparent = strtok_r(NULL, ",", &save_ptr)) {
-                struct ovn_port *vp = ovn_port_find(ports, vparent);
-                if (!vp || vp->od != op->od) {
-                    /* vparent name should be valid and it should belong
-                     * to the same logical switch. */
-                    continue;
-                }
-
-                ds_clear(match);
-                ds_put_format(match, "inport == \"%s\" && "
-                              "((arp.op == 1 && arp.spa == %s && "
-                              "arp.tpa == %s) || (arp.op == 2 && "
-                              "arp.spa == %s))",
-                              vparent, virtual_ip, virtual_ip,
-                              virtual_ip);
-                ds_clear(actions);
-                ds_put_format(actions,
-                    "bind_vport(%s, inport); "
-                    "next;",
-                    op->json_key);
-                ovn_lflow_add_with_hint(lflows, op->od,
-                                        S_SWITCH_IN_ARP_ND_RSP, 100,
-                                        ds_cstr(match), ds_cstr(actions),
-                                        &vp->nbsp->header_);
-            }
-
-            free(tokstr);
-        } else {
-            /*
-             * Add ARP/ND reply flows if either the
-             *  - port is up and it doesn't have 'unknown' address defined or
-             *  - port type is router or
-             *  - port type is localport
-             */
-            if (check_lsp_is_up &&
-                !lsp_is_up(op->nbsp) && !lsp_is_router(op->nbsp) &&
-                strcmp(op->nbsp->type, "localport")) {
-                return;
-            }
-
-            if (lsp_is_external(op->nbsp) || op->has_unknown) {
-                return;
-            }
-
-            if (is_vlan_transparent(op->od)) {
-                return;
-            }
-
-            for (size_t i = 0; i < op->n_lsp_addrs; i++) {
-                for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) {
-                    ds_clear(match);
-                    ds_put_format(match, "arp.tpa == %s && arp.op == 1",
-                                op->lsp_addrs[i].ipv4_addrs[j].addr_s);
-                    ds_clear(actions);
-                    ds_put_format(actions,
-                        "eth.dst = eth.src; "
-                        "eth.src = %s; "
-                        "arp.op = 2; /* ARP reply */ "
-                        "arp.tha = arp.sha; "
-                        "arp.sha = %s; "
-                        "arp.tpa = arp.spa; "
-                        "arp.spa = %s; "
-                        "outport = inport; "
-                        "flags.loopback = 1; "
-                        "output;",
-                        op->lsp_addrs[i].ea_s, op->lsp_addrs[i].ea_s,
-                        op->lsp_addrs[i].ipv4_addrs[j].addr_s);
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_ARP_ND_RSP, 50,
-                                            ds_cstr(match),
-                                            ds_cstr(actions),
-                                            &op->nbsp->header_);
-
-                    /* Do not reply to an ARP request from the port that owns
-                     * the address (otherwise a DHCP client that ARPs to check
-                     * for a duplicate address will fail).  Instead, forward
-                     * it the usual way.
-                     *
-                     * (Another alternative would be to simply drop the packet.
-                     * If everything is working as it is configured, then this
-                     * would produce equivalent results, since no one should
-                     * reply to the request.  But ARPing for one's own IP
-                     * address is intended to detect situations where the
-                     * network is not working as configured, so dropping the
-                     * request would frustrate that intent.) */
-                    ds_put_format(match, " && inport == %s", op->json_key);
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_ARP_ND_RSP, 100,
-                                            ds_cstr(match), "next;",
-                                            &op->nbsp->header_);
-                }
-
-                /* For ND solicitations, we need to listen for both the
-                 * unicast IPv6 address and its all-nodes multicast address,
-                 * but always respond with the unicast IPv6 address. */
-                for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
-                    ds_clear(match);
-                    ds_put_format(match,
-                            "nd_ns && ip6.dst == {%s, %s} && nd.target == %s",
-                            op->lsp_addrs[i].ipv6_addrs[j].addr_s,
-                            op->lsp_addrs[i].ipv6_addrs[j].sn_addr_s,
-                            op->lsp_addrs[i].ipv6_addrs[j].addr_s);
-
-                    ds_clear(actions);
-                    ds_put_format(actions,
-                            "%s { "
-                            "eth.src = %s; "
-                            "ip6.src = %s; "
-                            "nd.target = %s; "
-                            "nd.tll = %s; "
-                            "outport = inport; "
-                            "flags.loopback = 1; "
-                            "output; "
-                            "};",
-                            lsp_is_router(op->nbsp) ? "nd_na_router" : "nd_na",
-                            op->lsp_addrs[i].ea_s,
-                            op->lsp_addrs[i].ipv6_addrs[j].addr_s,
-                            op->lsp_addrs[i].ipv6_addrs[j].addr_s,
-                            op->lsp_addrs[i].ea_s);
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_ARP_ND_RSP, 50,
-                                            ds_cstr(match),
-                                            ds_cstr(actions),
-                                            &op->nbsp->header_);
-
-                    /* Do not reply to a solicitation from the port that owns
-                     * the address (otherwise DAD detection will fail). */
-                    ds_put_format(match, " && inport == %s", op->json_key);
-                    ovn_lflow_add_with_hint(lflows, op->od,
-                                            S_SWITCH_IN_ARP_ND_RSP, 100,
-                                            ds_cstr(match), "next;",
-                                            &op->nbsp->header_);
-                }
-            }
-        }
-    }
-}
-
-/* Ingress table 13: ARP/ND responder, by default goto next.
- * (priority 0)*/
-static void
-build_lswitch_arp_nd_responder_default(struct ovn_datapath *od,
-                                       struct hmap *lflows)
-{
-    if (od->nbs) {
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_ND_RSP, 0, "1", "next;");
     }
 }
 
@@ -7236,51 +5833,6 @@ build_lswitch_dhcp_options_and_response(struct ovn_port *op,
     }
 }
 
-/* Ingress table 14 and 15: DHCP options and response, by default goto
- * next. (priority 0).
- * Ingress table 16 and 17: DNS lookup and response, by default goto next.
- * (priority 0).
- * Ingress table 18 - External port handling, by default goto next.
- * (priority 0). */
-static void
-build_lswitch_dhcp_and_dns_defaults(struct ovn_datapath *od,
-                                        struct hmap *lflows)
-{
-    if (od->nbs) {
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DHCP_OPTIONS, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DHCP_RESPONSE, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_LOOKUP, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_EXTERNAL_PORT, 0, "1", "next;");
-    }
-}
-
-/* Logical switch ingress table 17 and 18: DNS lookup and response
-* priority 100 flows.
-*/
-static void
-build_lswitch_dns_lookup_and_response(struct ovn_datapath *od,
-                                      struct hmap *lflows)
-{
-    if (od->nbs && ls_has_dns_records(od->nbs)) {
-
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_LOOKUP, 100,
-                      "udp.dst == 53",
-                      REGBIT_DNS_LOOKUP_RESULT" = dns_lookup(); next;");
-        const char *dns_action = "eth.dst <-> eth.src; ip4.src <-> ip4.dst; "
-                      "udp.dst = udp.src; udp.src = 53; outport = inport; "
-                      "flags.loopback = 1; output;";
-        const char *dns_match = "udp.dst == 53 && "REGBIT_DNS_LOOKUP_RESULT;
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 100,
-                      dns_match, dns_action);
-        dns_action = "eth.dst <-> eth.src; ip6.src <-> ip6.dst; "
-                      "udp.dst = udp.src; udp.src = 53; outport = inport; "
-                      "flags.loopback = 1; output;";
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 100,
-                      dns_match, dns_action);
-    }
-}
-
 /* Table 18: External port. Drop ARP request for router ips from
  * external ports  on chassis not binding those ports.
  * This makes the router pipeline to be run only on the chassis
@@ -7491,16 +6043,7 @@ build_lswitch_ip_unicast_lookup(struct ovn_port *op,
             struct eth_addr mac;
             if (ovs_scan(op->nbsp->addresses[i],
                         ETH_ADDR_SCAN_FMT, ETH_ADDR_SCAN_ARGS(mac))) {
-                ds_clear(match);
-                ds_put_format(match, "eth.dst == "ETH_ADDR_FMT,
-                              ETH_ADDR_ARGS(mac));
-
-                ds_clear(actions);
-                ds_put_format(actions, "outport = %s; output;", op->json_key);
-                ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_L2_LKUP,
-                                        50, ds_cstr(match),
-                                        ds_cstr(actions),
-                                        &op->nbsp->header_);
+                /* Do nothing. */
             } else if (!strcmp(op->nbsp->addresses[i], "unknown")) {
                 if (lsp_is_enabled(op->nbsp)) {
                     ovs_mutex_lock(&mcgroup_mutex);
@@ -7509,21 +6052,7 @@ build_lswitch_ip_unicast_lookup(struct ovn_port *op,
                     op->od->has_unknown = true;
                 }
             } else if (is_dynamic_lsp_address(op->nbsp->addresses[i])) {
-                if (!op->nbsp->dynamic_addresses
-                    || !ovs_scan(op->nbsp->dynamic_addresses,
-                            ETH_ADDR_SCAN_FMT, ETH_ADDR_SCAN_ARGS(mac))) {
-                    continue;
-                }
-                ds_clear(match);
-                ds_put_format(match, "eth.dst == "ETH_ADDR_FMT,
-                              ETH_ADDR_ARGS(mac));
-
-                ds_clear(actions);
-                ds_put_format(actions, "outport = %s; output;", op->json_key);
-                ovn_lflow_add_with_hint(lflows, op->od, S_SWITCH_IN_L2_LKUP,
-                                        50, ds_cstr(match),
-                                        ds_cstr(actions),
-                                        &op->nbsp->header_);
+                /* Do nothing. */
             } else if (!strcmp(op->nbsp->addresses[i], "router")) {
                 if (!op->peer || !op->peer->nbrp
                     || !ovs_scan(op->peer->nbrp->mac,
@@ -8653,43 +7182,6 @@ build_static_route_flow(struct hmap *lflows, struct ovn_datapath *od,
     free(prefix_s);
 }
 
-static void
-op_put_v4_networks(struct ds *ds, const struct ovn_port *op, bool add_bcast)
-{
-    if (!add_bcast && op->lrp_networks.n_ipv4_addrs == 1) {
-        ds_put_format(ds, "%s", op->lrp_networks.ipv4_addrs[0].addr_s);
-        return;
-    }
-
-    ds_put_cstr(ds, "{");
-    for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-        ds_put_format(ds, "%s, ", op->lrp_networks.ipv4_addrs[i].addr_s);
-        if (add_bcast) {
-            ds_put_format(ds, "%s, ", op->lrp_networks.ipv4_addrs[i].bcast_s);
-        }
-    }
-    ds_chomp(ds, ' ');
-    ds_chomp(ds, ',');
-    ds_put_cstr(ds, "}");
-}
-
-static void
-op_put_v6_networks(struct ds *ds, const struct ovn_port *op)
-{
-    if (op->lrp_networks.n_ipv6_addrs == 1) {
-        ds_put_format(ds, "%s", op->lrp_networks.ipv6_addrs[0].addr_s);
-        return;
-    }
-
-    ds_put_cstr(ds, "{");
-    for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
-        ds_put_format(ds, "%s, ", op->lrp_networks.ipv6_addrs[i].addr_s);
-    }
-    ds_chomp(ds, ' ');
-    ds_chomp(ds, ',');
-    ds_put_cstr(ds, "}");
-}
-
 static bool
 get_force_snat_ip(struct ovn_datapath *od, const char *key_type,
                   struct lport_addresses *laddrs)
@@ -8845,7 +7337,6 @@ build_lrouter_lb_flows(struct hmap *lflows, struct ovn_datapath *od,
                        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);
 
@@ -8856,10 +7347,6 @@ build_lrouter_lb_flows(struct hmap *lflows, struct ovn_datapath *od,
         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];
@@ -8868,29 +7355,6 @@ build_lrouter_lb_flows(struct hmap *lflows, struct ovn_datapath *od,
             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
@@ -8934,7 +7398,6 @@ build_lrouter_lb_flows(struct hmap *lflows, struct ovn_datapath *od,
                                meter_groups, nat_entries);
         }
     }
-    sset_destroy(&all_ips);
 }
 
 #define ND_RA_MAX_INTERVAL_MAX 1800
@@ -8944,13 +7407,12 @@ build_lrouter_lb_flows(struct hmap *lflows, struct ovn_datapath *od,
 #define ND_RA_MIN_INTERVAL_MIN 3
 
 static void
-copy_ra_to_sb(struct ovn_port *op, const char *address_mode)
+copy_ra_to_sb(const struct ovn_port *op)
 {
     struct smap options;
     smap_clone(&options, &op->sb->options);
 
     smap_add(&options, "ipv6_ra_send_periodic", "true");
-    smap_add(&options, "ipv6_ra_address_mode", address_mode);
 
     int max_interval = smap_get_int(&op->nbrp->ipv6_ra_configs,
             "max_interval", ND_RA_MAX_INTERVAL_DEFAULT);
@@ -9389,359 +7851,7 @@ build_lrouter_force_snat_flows(struct hmap *lflows, struct ovn_datapath *od,
 }
 
 static void
-build_lrouter_force_snat_flows_op(struct ovn_port *op,
-                                  struct hmap *lflows,
-                                  struct ds *match, struct ds *actions)
-{
-    if (!op->nbrp || !op->peer || !op->od->lb_force_snat_router_ip) {
-        return;
-    }
-
-    if (op->lrp_networks.n_ipv4_addrs) {
-        ds_clear(match);
-        ds_clear(actions);
-
-        ds_put_format(match, "inport == %s && ip4.dst == %s",
-                      op->json_key, op->lrp_networks.ipv4_addrs[0].addr_s);
-        ovn_lflow_add(lflows, op->od, S_ROUTER_IN_UNSNAT, 110,
-                      ds_cstr(match), "ct_snat;");
-
-        ds_clear(match);
-
-        /* Higher priority rules to force SNAT with the router port ip.
-         * This only takes effect when the packet has already been
-         * load balanced once. */
-        ds_put_format(match, "flags.force_snat_for_lb == 1 && ip4 && "
-                      "outport == %s", op->json_key);
-        ds_put_format(actions, "ct_snat(%s);",
-                      op->lrp_networks.ipv4_addrs[0].addr_s);
-        ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_SNAT, 110,
-                      ds_cstr(match), ds_cstr(actions));
-        if (op->lrp_networks.n_ipv4_addrs > 1) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
-            VLOG_WARN_RL(&rl, "Logical router port %s is configured with "
-                              "multiple IPv4 addresses.  Only the first "
-                              "IP [%s] is considered as SNAT for load "
-                              "balancer", op->json_key,
-                              op->lrp_networks.ipv4_addrs[0].addr_s);
-        }
-    }
-
-    /* op->lrp_networks.ipv6_addrs will always have LLA and that will be
-     * last in the list. So add the flows only if n_ipv6_addrs > 1. */
-    if (op->lrp_networks.n_ipv6_addrs > 1) {
-        ds_clear(match);
-        ds_clear(actions);
-
-        ds_put_format(match, "inport == %s && ip6.dst == %s",
-                      op->json_key, op->lrp_networks.ipv6_addrs[0].addr_s);
-        ovn_lflow_add(lflows, op->od, S_ROUTER_IN_UNSNAT, 110,
-                      ds_cstr(match), "ct_snat;");
-
-        ds_clear(match);
-
-        /* Higher priority rules to force SNAT with the router port ip.
-         * This only takes effect when the packet has already been
-         * load balanced once. */
-        ds_put_format(match, "flags.force_snat_for_lb == 1 && ip6 && "
-                      "outport == %s", op->json_key);
-        ds_put_format(actions, "ct_snat(%s);",
-                      op->lrp_networks.ipv6_addrs[0].addr_s);
-        ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_SNAT, 110,
-                      ds_cstr(match), ds_cstr(actions));
-        if (op->lrp_networks.n_ipv6_addrs > 2) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
-            VLOG_WARN_RL(&rl, "Logical router port %s is configured with "
-                              "multiple IPv6 addresses.  Only the first "
-                              "IP [%s] is considered as SNAT for load "
-                              "balancer", op->json_key,
-                              op->lrp_networks.ipv6_addrs[0].addr_s);
-        }
-    }
-}
-
-static void
-build_lrouter_bfd_flows(struct hmap *lflows, struct ovn_port *op)
-{
-    if (!op->has_bfd) {
-        return;
-    }
-
-    struct ds ip_list = DS_EMPTY_INITIALIZER;
-    struct ds match = DS_EMPTY_INITIALIZER;
-
-    if (op->lrp_networks.n_ipv4_addrs) {
-        op_put_v4_networks(&ip_list, op, false);
-        ds_put_format(&match, "ip4.src == %s && udp.dst == 3784",
-                      ds_cstr(&ip_list));
-        ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 110,
-                                ds_cstr(&match), "next; ",
-                                &op->nbrp->header_);
-        ds_clear(&match);
-        ds_put_format(&match, "ip4.dst == %s && udp.dst == 3784",
-                      ds_cstr(&ip_list));
-        ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 110,
-                                ds_cstr(&match), "handle_bfd_msg(); ",
-                                &op->nbrp->header_);
-    }
-    if (op->lrp_networks.n_ipv6_addrs) {
-        ds_clear(&ip_list);
-        ds_clear(&match);
-
-        op_put_v6_networks(&ip_list, op);
-        ds_put_format(&match, "ip6.src == %s && udp.dst == 3784",
-                      ds_cstr(&ip_list));
-        ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 110,
-                                ds_cstr(&match), "next; ",
-                                &op->nbrp->header_);
-        ds_clear(&match);
-        ds_put_format(&match, "ip6.dst == %s && udp.dst == 3784",
-                      ds_cstr(&ip_list));
-        ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 110,
-                                ds_cstr(&match), "handle_bfd_msg(); ",
-                                &op->nbrp->header_);
-    }
-
-    ds_destroy(&ip_list);
-    ds_destroy(&match);
-}
-
-/* Logical router ingress Table 0: L2 Admission Control
- * Generic admission control flows (without inport check).
- */
-static void
-build_adm_ctrl_flows_for_lrouter(
-        struct ovn_datapath *od, struct hmap *lflows)
-{
-    if (od->nbr) {
-        /* Logical VLANs not supported.
-         * Broadcast/multicast source address is invalid. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ADMISSION, 100,
-                      "vlan.present || eth.src[40]", "drop;");
-    }
-}
-
-/* Logical router ingress Table 0: L2 Admission Control
- * This table drops packets that the router shouldn’t see at all based
- * on their Ethernet headers.
- */
-static void
-build_adm_ctrl_flows_for_lrouter_port(
-        struct ovn_port *op, struct hmap *lflows,
-        struct ds *match, struct ds *actions)
-{
-    if (op->nbrp) {
-        if (!lrport_is_enabled(op->nbrp)) {
-            /* Drop packets from disabled logical ports (since logical flow
-             * tables are default-drop). */
-            return;
-        }
-
-        if (op->derived) {
-            /* No ingress packets should be received on a chassisredirect
-             * port. */
-            return;
-        }
-
-        /* 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;",
-                      op->lrp_networks.ea_s);
-
-        ds_clear(match);
-        ds_put_format(match, "eth.mcast && inport == %s", op->json_key);
-        ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_ADMISSION, 50,
-                                ds_cstr(match), ds_cstr(actions),
-                                &op->nbrp->header_);
-
-        ds_clear(match);
-        ds_put_format(match, "eth.dst == %s && inport == %s",
-                      op->lrp_networks.ea_s, op->json_key);
-        if (op->od->l3dgw_port && op == op->od->l3dgw_port
-            && op->od->l3redirect_port) {
-            /* Traffic with eth.dst = l3dgw_port->lrp_networks.ea_s
-             * should only be received on the gateway chassis. */
-            ds_put_format(match, " && is_chassis_resident(%s)",
-                          op->od->l3redirect_port->json_key);
-        }
-        ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_ADMISSION, 50,
-                                ds_cstr(match),  ds_cstr(actions),
-                                &op->nbrp->header_);
-    }
-}
-
-
-/* Logical router ingress Table 1 and 2: Neighbor lookup and learning
- * lflows for logical routers. */
-static void
-build_neigh_learning_flows_for_lrouter(
-        struct ovn_datapath *od, struct hmap *lflows,
-        struct ds *match, struct ds *actions)
-{
-    if (od->nbr) {
-
-        /* Learn MAC bindings from ARP/IPv6 ND.
-         *
-         * For ARP packets, table LOOKUP_NEIGHBOR does a lookup for the
-         * (arp.spa, arp.sha) in the mac binding table using the 'lookup_arp'
-         * action and stores the result in REGBIT_LOOKUP_NEIGHBOR_RESULT bit.
-         * If "always_learn_from_arp_request" is set to false, it will also
-         * lookup for the (arp.spa) in the mac binding table using the
-         * "lookup_arp_ip" action for ARP request packets, and stores the
-         * result in REGBIT_LOOKUP_NEIGHBOR_IP_RESULT bit; or set that bit
-         * to "1" directly for ARP response packets.
-         *
-         * For IPv6 ND NA packets, table LOOKUP_NEIGHBOR does a lookup
-         * for the (nd.target, nd.tll) in the mac binding table using the
-         * 'lookup_nd' action and stores the result in
-         * REGBIT_LOOKUP_NEIGHBOR_RESULT bit. If
-         * "always_learn_from_arp_request" is set to false,
-         * REGBIT_LOOKUP_NEIGHBOR_IP_RESULT bit is set.
-         *
-         * For IPv6 ND NS packets, table LOOKUP_NEIGHBOR does a lookup
-         * for the (ip6.src, nd.sll) in the mac binding table using the
-         * 'lookup_nd' action and stores the result in
-         * REGBIT_LOOKUP_NEIGHBOR_RESULT bit. If
-         * "always_learn_from_arp_request" is set to false, it will also lookup
-         * for the (ip6.src) in the mac binding table using the "lookup_nd_ip"
-         * action and stores the result in REGBIT_LOOKUP_NEIGHBOR_IP_RESULT
-         * bit.
-         *
-         * Table LEARN_NEIGHBOR learns the mac-binding using the action
-         * - 'put_arp/put_nd'. Learning mac-binding is skipped if
-         *   REGBIT_LOOKUP_NEIGHBOR_RESULT bit is set or
-         *   REGBIT_LOOKUP_NEIGHBOR_IP_RESULT is not set.
-         *
-         * */
-
-        /* Flows for LOOKUP_NEIGHBOR. */
-        bool learn_from_arp_request = smap_get_bool(&od->nbr->options,
-            "always_learn_from_arp_request", true);
-        ds_clear(actions);
-        ds_put_format(actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
-                      " = lookup_arp(inport, arp.spa, arp.sha); %snext;",
-                      learn_from_arp_request ? "" :
-                      REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1; ");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_NEIGHBOR, 100,
-                      "arp.op == 2", ds_cstr(actions));
-
-        ds_clear(actions);
-        ds_put_format(actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
-                      " = lookup_nd(inport, nd.target, nd.tll); %snext;",
-                      learn_from_arp_request ? "" :
-                      REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1; ");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_NEIGHBOR, 100, "nd_na",
-                      ds_cstr(actions));
-
-        ds_clear(actions);
-        ds_put_format(actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
-                      " = lookup_nd(inport, ip6.src, nd.sll); %snext;",
-                      learn_from_arp_request ? "" :
-                      REGBIT_LOOKUP_NEIGHBOR_IP_RESULT
-                      " = lookup_nd_ip(inport, ip6.src); ");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_NEIGHBOR, 100, "nd_ns",
-                      ds_cstr(actions));
-
-        /* For other packet types, we can skip neighbor learning.
-         * So set REGBIT_LOOKUP_NEIGHBOR_RESULT to 1. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LOOKUP_NEIGHBOR, 0, "1",
-                      REGBIT_LOOKUP_NEIGHBOR_RESULT" = 1; next;");
-
-        /* Flows for LEARN_NEIGHBOR. */
-        /* Skip Neighbor learning if not required. */
-        ds_clear(match);
-        ds_put_format(match, REGBIT_LOOKUP_NEIGHBOR_RESULT" == 1%s",
-                      learn_from_arp_request ? "" :
-                      " || "REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" == 0");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 100,
-                      ds_cstr(match), "next;");
-
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
-                      "arp", "put_arp(inport, arp.spa, arp.sha); next;");
-
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
-                      "nd_na", "put_nd(inport, nd.target, nd.tll); next;");
-
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LEARN_NEIGHBOR, 90,
-                      "nd_ns", "put_nd(inport, ip6.src, nd.sll); next;");
-    }
-
-}
-
-/* Logical router ingress Table 1: Neighbor lookup lflows
- * for logical router ports. */
-static void
-build_neigh_learning_flows_for_lrouter_port(
-        struct ovn_port *op, struct hmap *lflows,
-        struct ds *match, struct ds *actions)
-{
-    if (op->nbrp) {
-
-        bool learn_from_arp_request = smap_get_bool(&op->od->nbr->options,
-            "always_learn_from_arp_request", true);
-
-        /* Check if we need to learn mac-binding from ARP requests. */
-        for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-            if (!learn_from_arp_request) {
-                /* ARP request to this address should always get learned,
-                 * so add a priority-110 flow to set
-                 * REGBIT_LOOKUP_NEIGHBOR_IP_RESULT to 1. */
-                ds_clear(match);
-                ds_put_format(match,
-                              "inport == %s && arp.spa == %s/%u && "
-                              "arp.tpa == %s && arp.op == 1",
-                              op->json_key,
-                              op->lrp_networks.ipv4_addrs[i].network_s,
-                              op->lrp_networks.ipv4_addrs[i].plen,
-                              op->lrp_networks.ipv4_addrs[i].addr_s);
-                if (op->od->l3dgw_port && op == op->od->l3dgw_port
-                    && op->od->l3redirect_port) {
-                    ds_put_format(match, " && is_chassis_resident(%s)",
-                                  op->od->l3redirect_port->json_key);
-                }
-                const char *actions_s = REGBIT_LOOKUP_NEIGHBOR_RESULT
-                                  " = lookup_arp(inport, arp.spa, arp.sha); "
-                                  REGBIT_LOOKUP_NEIGHBOR_IP_RESULT" = 1;"
-                                  " next;";
-                ovn_lflow_add_with_hint(lflows, op->od,
-                                        S_ROUTER_IN_LOOKUP_NEIGHBOR, 110,
-                                        ds_cstr(match), actions_s,
-                                        &op->nbrp->header_);
-            }
-            ds_clear(match);
-            ds_put_format(match,
-                          "inport == %s && arp.spa == %s/%u && arp.op == 1",
-                          op->json_key,
-                          op->lrp_networks.ipv4_addrs[i].network_s,
-                          op->lrp_networks.ipv4_addrs[i].plen);
-            if (op->od->l3dgw_port && op == op->od->l3dgw_port
-                && op->od->l3redirect_port) {
-                ds_put_format(match, " && is_chassis_resident(%s)",
-                              op->od->l3redirect_port->json_key);
-            }
-            ds_clear(actions);
-            ds_put_format(actions, REGBIT_LOOKUP_NEIGHBOR_RESULT
-                          " = lookup_arp(inport, arp.spa, arp.sha); %snext;",
-                          learn_from_arp_request ? "" :
-                          REGBIT_LOOKUP_NEIGHBOR_IP_RESULT
-                          " = lookup_arp_ip(inport, arp.spa); ");
-            ovn_lflow_add_with_hint(lflows, op->od,
-                                    S_ROUTER_IN_LOOKUP_NEIGHBOR, 100,
-                                    ds_cstr(match), ds_cstr(actions),
-                                    &op->nbrp->header_);
-        }
-    }
-}
-
-/* Logical router ingress table ND_RA_OPTIONS & ND_RA_RESPONSE: IPv6 Router
- * Adv (RA) options and response. */
-static void
-build_ND_RA_flows_for_lrouter_port(
-        struct ovn_port *op, struct hmap *lflows,
-        struct ds *match, struct ds *actions)
+sync_ND_RA_options_for_lrouter_port(const struct ovn_port *op)
 {
     if (!op->nbrp || op->nbrp->peer || !op->peer) {
         return;
@@ -9770,141 +7880,33 @@ build_ND_RA_flows_for_lrouter_port(
     }
     smap_add(&options, "ipv6_prefix",
              ipv6_prefix ? "true" : "false");
-    sbrec_port_binding_set_options(op->sb, &options);
-
-    smap_destroy(&options);
 
     const char *address_mode = smap_get(
         &op->nbrp->ipv6_ra_configs, "address_mode");
 
-    if (!address_mode) {
-        return;
-    }
-    if (strcmp(address_mode, "slaac") &&
-        strcmp(address_mode, "dhcpv6_stateful") &&
-        strcmp(address_mode, "dhcpv6_stateless")) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
-        VLOG_WARN_RL(&rl, "Invalid address mode [%s] defined",
-                     address_mode);
-        return;
-    }
-
-    if (smap_get_bool(&op->nbrp->ipv6_ra_configs, "send_periodic",
-                      false)) {
-        copy_ra_to_sb(op, address_mode);
-    }
-
-    ds_clear(match);
-    ds_put_format(match, "inport == %s && ip6.dst == ff02::2 && nd_rs",
-                          op->json_key);
-    ds_clear(actions);
-
-    const char *mtu_s = smap_get(
-        &op->nbrp->ipv6_ra_configs, "mtu");
-
-    /* As per RFC 2460, 1280 is minimum IPv6 MTU. */
-    uint32_t mtu = (mtu_s && atoi(mtu_s) >= 1280) ? atoi(mtu_s) : 0;
-
-    ds_put_format(actions, REGBIT_ND_RA_OPTS_RESULT" = put_nd_ra_opts("
-                  "addr_mode = \"%s\", slla = %s",
-                  address_mode, op->lrp_networks.ea_s);
-    if (mtu > 0) {
-        ds_put_format(actions, ", mtu = %u", mtu);
-    }
-
-    const char *prf = smap_get_def(
-        &op->nbrp->ipv6_ra_configs, "router_preference", "MEDIUM");
-    if (strcmp(prf, "MEDIUM")) {
-        ds_put_format(actions, ", router_preference = \"%s\"", prf);
-    }
-
-    bool add_rs_response_flow = false;
-
-    for (size_t i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
-        if (in6_is_lla(&op->lrp_networks.ipv6_addrs[i].network)) {
-            continue;
+    if (address_mode) {
+        if (strcmp(address_mode, "slaac") &&
+            strcmp(address_mode, "dhcpv6_stateful") &&
+            strcmp(address_mode, "dhcpv6_stateless")) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_WARN_RL(&rl, "Invalid address mode [%s] defined",
+                        address_mode);
+            address_mode = NULL;
+        } else {
+            smap_add(&options, "ipv6_ra_address_mode", address_mode);
         }
-
-        ds_put_format(actions, ", prefix = %s/%u",
-                      op->lrp_networks.ipv6_addrs[i].network_s,
-                      op->lrp_networks.ipv6_addrs[i].plen);
-
-        add_rs_response_flow = true;
     }
 
-    if (add_rs_response_flow) {
-        ds_put_cstr(actions, "); next;");
-        ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_ND_RA_OPTIONS,
-                                50, ds_cstr(match), ds_cstr(actions),
-                                &op->nbrp->header_);
-        ds_clear(actions);
-        ds_clear(match);
-        ds_put_format(match, "inport == %s && ip6.dst == ff02::2 && "
-                      "nd_ra && "REGBIT_ND_RA_OPTS_RESULT, op->json_key);
-
-        char ip6_str[INET6_ADDRSTRLEN + 1];
-        struct in6_addr lla;
-        in6_generate_lla(op->lrp_networks.ea, &lla);
-        memset(ip6_str, 0, sizeof(ip6_str));
-        ipv6_string_mapped(ip6_str, &lla);
-        ds_put_format(actions, "eth.dst = eth.src; eth.src = %s; "
-                      "ip6.dst = ip6.src; ip6.src = %s; "
-                      "outport = inport; flags.loopback = 1; "
-                      "output;",
-                      op->lrp_networks.ea_s, ip6_str);
-        ovn_lflow_add_with_hint(lflows, op->od,
-                                S_ROUTER_IN_ND_RA_RESPONSE, 50,
-                                ds_cstr(match), ds_cstr(actions),
-                                &op->nbrp->header_);
-    }
-}
+    sbrec_port_binding_set_options(op->sb, &options);
+    smap_destroy(&options);
 
-/* Logical router ingress table ND_RA_OPTIONS & ND_RA_RESPONSE: RS
- * responder, by default goto next. (priority 0). */
-static void
-build_ND_RA_flows_for_lrouter(struct ovn_datapath *od, struct hmap *lflows)
-{
-    if (od->nbr) {
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ND_RA_OPTIONS, 0, "1", "next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ND_RA_RESPONSE, 0, "1", "next;");
+    if (!address_mode) {
+        return;
     }
-}
 
-/* Logical router ingress table IP_ROUTING : IP Routing.
- *
- * A packet that arrives at this table is an IP packet that should be
- * routed to the address in 'ip[46].dst'.
- *
- * For regular routes without ECMP, table IP_ROUTING sets outport to the
- * correct output port, eth.src to the output port's MAC address, and
- * REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 to the next-hop IP address
- * (leaving 'ip[46].dst', the packet’s final destination, unchanged), and
- * advances to the next table.
- *
- * For ECMP routes, i.e. multiple routes with same policy and prefix, table
- * IP_ROUTING remembers ECMP group id and selects a member id, and advances
- * to table IP_ROUTING_ECMP, which sets outport, eth.src and
- * REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 for the selected ECMP member.
- */
-static void
-build_ip_routing_flows_for_lrouter_port(
-        struct ovn_port *op, struct hmap *lflows)
-{
-    if (op->nbrp) {
-
-        for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-            add_route(lflows, op->od, op, op->lrp_networks.ipv4_addrs[i].addr_s,
-                      op->lrp_networks.ipv4_addrs[i].network_s,
-                      op->lrp_networks.ipv4_addrs[i].plen, NULL, false,
-                      &op->nbrp->header_, false);
-        }
-
-        for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
-            add_route(lflows, op->od, op, op->lrp_networks.ipv6_addrs[i].addr_s,
-                      op->lrp_networks.ipv6_addrs[i].network_s,
-                      op->lrp_networks.ipv6_addrs[i].plen, NULL, false,
-                      &op->nbrp->header_, false);
-        }
+    if (smap_get_bool(&op->nbrp->ipv6_ra_configs, "send_periodic",
+                      false)) {
+        copy_ra_to_sb(op);
     }
 }
 
@@ -9914,9 +7916,6 @@ build_static_route_flows_for_lrouter(
         struct hmap *ports, struct hmap *bfd_connections)
 {
     if (od->nbr) {
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_ROUTING_ECMP, 150,
-                      REG_ECMP_GROUP_ID" == 0", "next;");
-
         struct hmap ecmp_groups = HMAP_INITIALIZER(&ecmp_groups);
         struct hmap unique_routes = HMAP_INITIALIZER(&unique_routes);
         struct ovs_list parsed_routes = OVS_LIST_INITIALIZER(&parsed_routes);
@@ -9968,12 +7967,6 @@ build_mcast_lookup_flows_for_lrouter(
         struct ds *match, struct ds *actions)
 {
     if (od->nbr) {
-
-        /* Drop IPv6 multicast traffic that shouldn't be forwarded,
-         * i.e., router solicitation and router advertisement.
-         */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_ROUTING, 550,
-                      "nd_rs || nd_ra", "drop;");
         if (!od->mcast_info.rtr.relay) {
             return;
         }
@@ -10017,429 +8010,73 @@ build_mcast_lookup_flows_for_lrouter(
                           "};");
         } else {
             ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_ROUTING, 450,
-                          "ip4.mcast || ip6.mcast", "drop;");
-        }
-    }
-}
-
-/* Logical router ingress table POLICY: Policy.
- *
- * A packet that arrives at this table is an IP packet that should be
- * permitted/denied/rerouted to the address in the rule's nexthop.
- * This table sets outport to the correct out_port,
- * eth.src to the output port's MAC address,
- * and REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 to the next-hop IP address
- * (leaving 'ip[46].dst', the packet’s final destination, unchanged), and
- * advances to the next table for ARP/ND resolution. */
-static void
-build_ingress_policy_flows_for_lrouter(
-        struct ovn_datapath *od, struct hmap *lflows,
-        struct hmap *ports)
-{
-    if (od->nbr) {
-        /* This is a catch-all rule. It has the lowest priority (0)
-         * does a match-all("1") and pass-through (next) */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_POLICY, 0, "1",
-                      REG_ECMP_GROUP_ID" = 0; next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_POLICY_ECMP, 150,
-                      REG_ECMP_GROUP_ID" == 0", "next;");
-
-        /* Convert routing policies to flows. */
-        uint16_t ecmp_group_id = 1;
-        for (int i = 0; i < od->nbr->n_policies; i++) {
-            const struct nbrec_logical_router_policy *rule
-                = od->nbr->policies[i];
-            bool is_ecmp_reroute =
-                (!strcmp(rule->action, "reroute") && rule->n_nexthops > 1);
-
-            if (is_ecmp_reroute) {
-                build_ecmp_routing_policy_flows(lflows, od, ports, rule,
-                                                ecmp_group_id);
-                ecmp_group_id++;
-            } else {
-                build_routing_policy_flow(lflows, od, ports, rule,
-                                          &rule->header_);
-            }
-        }
-    }
-}
-
-/* Local router ingress table ARP_RESOLVE: ARP Resolution. */
-static void
-build_arp_resolve_flows_for_lrouter(
-        struct ovn_datapath *od, struct hmap *lflows)
-{
-    if (od->nbr) {
-        /* Multicast packets already have the outport set so just advance to
-         * next table (priority 500). */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_RESOLVE, 500,
-                      "ip4.mcast || ip6.mcast", "next;");
-
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_RESOLVE, 0, "ip4",
-                      "get_arp(outport, " REG_NEXT_HOP_IPV4 "); next;");
-
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_RESOLVE, 0, "ip6",
-                      "get_nd(outport, " REG_NEXT_HOP_IPV6 "); next;");
-    }
-}
-
-/* Local router ingress table ARP_RESOLVE: ARP Resolution.
- *
- * Any unicast packet that reaches this table is an IP packet whose
- * next-hop IP address is in REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6
- * (ip4.dst/ipv6.dst is the final destination).
- * This table resolves the IP address in
- * REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 into an output port in outport and
- * an Ethernet address in eth.dst.
- */
-static void
-build_arp_resolve_flows_for_lrouter_port(
-        struct ovn_port *op, struct hmap *lflows,
-        struct hmap *ports,
-        struct ds *match, struct ds *actions)
-{
-    if (op->nbsp && !lsp_is_enabled(op->nbsp)) {
-        return;
-    }
-
-    if (op->nbrp) {
-        /* This is a logical router port. If next-hop IP address in
-         * REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 matches IP address of this
-         * router port, then the packet is intended to eventually be sent
-         * to this logical port. Set the destination mac address using
-         * this port's mac address.
-         *
-         * The packet is still in peer's logical pipeline. So the match
-         * should be on peer's outport. */
-        if (op->peer && op->nbrp->peer) {
-            if (op->lrp_networks.n_ipv4_addrs) {
-                ds_clear(match);
-                ds_put_format(match, "outport == %s && "
-                              REG_NEXT_HOP_IPV4 "== ",
-                              op->peer->json_key);
-                op_put_v4_networks(match, op, false);
-
-                ds_clear(actions);
-                ds_put_format(actions, "eth.dst = %s; next;",
-                              op->lrp_networks.ea_s);
-                ovn_lflow_add_with_hint(lflows, op->peer->od,
-                                        S_ROUTER_IN_ARP_RESOLVE, 100,
-                                        ds_cstr(match), ds_cstr(actions),
-                                        &op->nbrp->header_);
-            }
-
-            if (op->lrp_networks.n_ipv6_addrs) {
-                ds_clear(match);
-                ds_put_format(match, "outport == %s && "
-                              REG_NEXT_HOP_IPV6 " == ",
-                              op->peer->json_key);
-                op_put_v6_networks(match, op);
-
-                ds_clear(actions);
-                ds_put_format(actions, "eth.dst = %s; next;",
-                              op->lrp_networks.ea_s);
-                ovn_lflow_add_with_hint(lflows, op->peer->od,
-                                        S_ROUTER_IN_ARP_RESOLVE, 100,
-                                        ds_cstr(match), ds_cstr(actions),
-                                        &op->nbrp->header_);
-            }
-        }
-
-        if (!op->derived && op->od->l3redirect_port) {
-            const char *redirect_type = smap_get(&op->nbrp->options,
-                                                 "redirect-type");
-            if (redirect_type && !strcasecmp(redirect_type, "bridged")) {
-                /* Packet is on a non gateway chassis and
-                 * has an unresolved ARP on a network behind gateway
-                 * chassis attached router port. Since, redirect type
-                 * is "bridged", instead of calling "get_arp"
-                 * on this node, we will redirect the packet to gateway
-                 * chassis, by setting destination mac router port mac.*/
-                ds_clear(match);
-                ds_put_format(match, "outport == %s && "
-                              "!is_chassis_resident(%s)", op->json_key,
-                              op->od->l3redirect_port->json_key);
-                ds_clear(actions);
-                ds_put_format(actions, "eth.dst = %s; next;",
-                              op->lrp_networks.ea_s);
-
-                ovn_lflow_add_with_hint(lflows, op->od,
-                                        S_ROUTER_IN_ARP_RESOLVE, 50,
-                                        ds_cstr(match), ds_cstr(actions),
-                                        &op->nbrp->header_);
-            }
-        }
-
-        /* Drop IP traffic destined to router owned IPs. Part of it is dropped
-         * in stage "lr_in_ip_input" but traffic that could have been unSNATed
-         * but didn't match any existing session might still end up here.
-         *
-         * Priority 1.
-         */
-        build_lrouter_drop_own_dest(op, S_ROUTER_IN_ARP_RESOLVE, 1, true,
-                                    lflows);
-    } else if (op->od->n_router_ports && !lsp_is_router(op->nbsp)
-               && strcmp(op->nbsp->type, "virtual")) {
-        /* This is a logical switch port that backs a VM or a container.
-         * Extract its addresses. For each of the address, go through all
-         * the router ports attached to the switch (to which this port
-         * connects) and if the address in question is reachable from the
-         * router port, add an ARP/ND entry in that router's pipeline. */
-
-        for (size_t i = 0; i < op->n_lsp_addrs; i++) {
-            const char *ea_s = op->lsp_addrs[i].ea_s;
-            for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) {
-                const char *ip_s = op->lsp_addrs[i].ipv4_addrs[j].addr_s;
-                for (size_t k = 0; k < op->od->n_router_ports; k++) {
-                    /* Get the Logical_Router_Port that the
-                     * Logical_Switch_Port is connected to, as
-                     * 'peer'. */
-                    const char *peer_name = smap_get(
-                        &op->od->router_ports[k]->nbsp->options,
-                        "router-port");
-                    if (!peer_name) {
-                        continue;
-                    }
-
-                    struct ovn_port *peer = ovn_port_find(ports, peer_name);
-                    if (!peer || !peer->nbrp) {
-                        continue;
-                    }
-
-                    if (!find_lrp_member_ip(peer, ip_s)) {
-                        continue;
-                    }
-
-                    ds_clear(match);
-                    ds_put_format(match, "outport == %s && "
-                                  REG_NEXT_HOP_IPV4 " == %s",
-                                  peer->json_key, ip_s);
-
-                    ds_clear(actions);
-                    ds_put_format(actions, "eth.dst = %s; next;", ea_s);
-                    ovn_lflow_add_with_hint(lflows, peer->od,
-                                            S_ROUTER_IN_ARP_RESOLVE, 100,
-                                            ds_cstr(match),
-                                            ds_cstr(actions),
-                                            &op->nbsp->header_);
-                }
-            }
-
-            for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
-                const char *ip_s = op->lsp_addrs[i].ipv6_addrs[j].addr_s;
-                for (size_t k = 0; k < op->od->n_router_ports; k++) {
-                    /* Get the Logical_Router_Port that the
-                     * Logical_Switch_Port is connected to, as
-                     * 'peer'. */
-                    const char *peer_name = smap_get(
-                        &op->od->router_ports[k]->nbsp->options,
-                        "router-port");
-                    if (!peer_name) {
-                        continue;
-                    }
-
-                    struct ovn_port *peer = ovn_port_find(ports, peer_name);
-                    if (!peer || !peer->nbrp) {
-                        continue;
-                    }
-
-                    if (!find_lrp_member_ip(peer, ip_s)) {
-                        continue;
-                    }
-
-                    ds_clear(match);
-                    ds_put_format(match, "outport == %s && "
-                                  REG_NEXT_HOP_IPV6 " == %s",
-                                  peer->json_key, ip_s);
-
-                    ds_clear(actions);
-                    ds_put_format(actions, "eth.dst = %s; next;", ea_s);
-                    ovn_lflow_add_with_hint(lflows, peer->od,
-                                            S_ROUTER_IN_ARP_RESOLVE, 100,
-                                            ds_cstr(match),
-                                            ds_cstr(actions),
-                                            &op->nbsp->header_);
-                }
-            }
-        }
-    } else if (op->od->n_router_ports && !lsp_is_router(op->nbsp)
-               && !strcmp(op->nbsp->type, "virtual")) {
-        /* This is a virtual port. Add ARP replies for the virtual ip with
-         * the mac of the present active virtual parent.
-         * If the logical port doesn't have virtual parent set in
-         * Port_Binding table, then add the flow to set eth.dst to
-         * 00:00:00:00:00:00 and advance to next table so that ARP is
-         * resolved by router pipeline using the arp{} action.
-         * The MAC_Binding entry for the virtual ip might be invalid. */
-        ovs_be32 ip;
-
-        const char *vip = smap_get(&op->nbsp->options,
-                                   "virtual-ip");
-        const char *virtual_parents = smap_get(&op->nbsp->options,
-                                               "virtual-parents");
-        if (!vip || !virtual_parents ||
-            !ip_parse(vip, &ip) || !op->sb) {
-            return;
-        }
-
-        if (!op->sb->virtual_parent || !op->sb->virtual_parent[0] ||
-            !op->sb->chassis) {
-            /* The virtual port is not claimed yet. */
-            for (size_t i = 0; i < op->od->n_router_ports; i++) {
-                const char *peer_name = smap_get(
-                    &op->od->router_ports[i]->nbsp->options,
-                    "router-port");
-                if (!peer_name) {
-                    continue;
-                }
-
-                struct ovn_port *peer = ovn_port_find(ports, peer_name);
-                if (!peer || !peer->nbrp) {
-                    continue;
-                }
-
-                if (find_lrp_member_ip(peer, vip)) {
-                    ds_clear(match);
-                    ds_put_format(match, "outport == %s && "
-                                  REG_NEXT_HOP_IPV4 " == %s",
-                                  peer->json_key, vip);
-
-                    const char *arp_actions =
-                                  "eth.dst = 00:00:00:00:00:00; next;";
-                    ovn_lflow_add_with_hint(lflows, peer->od,
-                                            S_ROUTER_IN_ARP_RESOLVE, 100,
-                                            ds_cstr(match),
-                                            arp_actions,
-                                            &op->nbsp->header_);
-                    break;
-                }
-            }
-        } else {
-            struct ovn_port *vp =
-                ovn_port_find(ports, op->sb->virtual_parent);
-            if (!vp || !vp->nbsp) {
-                return;
-            }
-
-            for (size_t i = 0; i < vp->n_lsp_addrs; i++) {
-                bool found_vip_network = false;
-                const char *ea_s = vp->lsp_addrs[i].ea_s;
-                for (size_t j = 0; j < vp->od->n_router_ports; j++) {
-                    /* Get the Logical_Router_Port that the
-                    * Logical_Switch_Port is connected to, as
-                    * 'peer'. */
-                    const char *peer_name = smap_get(
-                        &vp->od->router_ports[j]->nbsp->options,
-                        "router-port");
-                    if (!peer_name) {
-                        continue;
-                    }
-
-                    struct ovn_port *peer =
-                        ovn_port_find(ports, peer_name);
-                    if (!peer || !peer->nbrp) {
-                        continue;
-                    }
-
-                    if (!find_lrp_member_ip(peer, vip)) {
-                        continue;
-                    }
-
-                    ds_clear(match);
-                    ds_put_format(match, "outport == %s && "
-                                  REG_NEXT_HOP_IPV4 " == %s",
-                                  peer->json_key, vip);
-
-                    ds_clear(actions);
-                    ds_put_format(actions, "eth.dst = %s; next;", ea_s);
-                    ovn_lflow_add_with_hint(lflows, peer->od,
-                                            S_ROUTER_IN_ARP_RESOLVE, 100,
-                                            ds_cstr(match),
-                                            ds_cstr(actions),
-                                            &op->nbsp->header_);
-                    found_vip_network = true;
-                    break;
-                }
-
-                if (found_vip_network) {
-                    break;
-                }
-            }
-        }
-    } else if (lsp_is_router(op->nbsp)) {
-        /* This is a logical switch port that connects to a router. */
-
-        /* The peer of this switch port is the router port for which
-         * we need to add logical flows such that it can resolve
-         * ARP entries for all the other router ports connected to
-         * the switch in question. */
-
-        const char *peer_name = smap_get(&op->nbsp->options,
-                                         "router-port");
-        if (!peer_name) {
-            return;
-        }
-
-        struct ovn_port *peer = ovn_port_find(ports, peer_name);
-        if (!peer || !peer->nbrp) {
-            return;
-        }
-
-        if (peer->od->nbr &&
-            smap_get_bool(&peer->od->nbr->options,
-                          "dynamic_neigh_routers", false)) {
-            return;
-        }
-
-        for (size_t i = 0; i < op->od->n_router_ports; i++) {
-            const char *router_port_name = smap_get(
-                                &op->od->router_ports[i]->nbsp->options,
-                                "router-port");
-            struct ovn_port *router_port = ovn_port_find(ports,
-                                                         router_port_name);
-            if (!router_port || !router_port->nbrp) {
-                continue;
-            }
-
-            /* Skip the router port under consideration. */
-            if (router_port == peer) {
-               continue;
-            }
-
-            if (router_port->lrp_networks.n_ipv4_addrs) {
-                ds_clear(match);
-                ds_put_format(match, "outport == %s && "
-                              REG_NEXT_HOP_IPV4 " == ",
-                              peer->json_key);
-                op_put_v4_networks(match, router_port, false);
-
-                ds_clear(actions);
-                ds_put_format(actions, "eth.dst = %s; next;",
-                                          router_port->lrp_networks.ea_s);
-                ovn_lflow_add_with_hint(lflows, peer->od,
-                                        S_ROUTER_IN_ARP_RESOLVE, 100,
-                                        ds_cstr(match), ds_cstr(actions),
-                                        &op->nbsp->header_);
-            }
+                          "ip4.mcast || ip6.mcast", "drop;");
+        }
+    }
+}
 
-            if (router_port->lrp_networks.n_ipv6_addrs) {
-                ds_clear(match);
-                ds_put_format(match, "outport == %s && "
-                              REG_NEXT_HOP_IPV6 " == ",
-                              peer->json_key);
-                op_put_v6_networks(match, router_port);
+/* Logical router ingress table POLICY: Policy.
+ *
+ * A packet that arrives at this table is an IP packet that should be
+ * permitted/denied/rerouted to the address in the rule's nexthop.
+ * This table sets outport to the correct out_port,
+ * eth.src to the output port's MAC address,
+ * and REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 to the next-hop IP address
+ * (leaving 'ip[46].dst', the packet’s final destination, unchanged), and
+ * advances to the next table for ARP/ND resolution. */
+static void
+build_ingress_policy_flows_for_lrouter(
+        struct ovn_datapath *od, struct hmap *lflows,
+        struct hmap *ports)
+{
+    if (od->nbr) {
+        /* Convert routing policies to flows. */
+        uint16_t ecmp_group_id = 1;
+        for (int i = 0; i < od->nbr->n_policies; i++) {
+            const struct nbrec_logical_router_policy *rule
+                = od->nbr->policies[i];
+            bool is_ecmp_reroute =
+                (!strcmp(rule->action, "reroute") && rule->n_nexthops > 1);
 
-                ds_clear(actions);
-                ds_put_format(actions, "eth.dst = %s; next;",
-                              router_port->lrp_networks.ea_s);
-                ovn_lflow_add_with_hint(lflows, peer->od,
-                                        S_ROUTER_IN_ARP_RESOLVE, 100,
-                                        ds_cstr(match), ds_cstr(actions),
-                                        &op->nbsp->header_);
+            if (is_ecmp_reroute) {
+                build_ecmp_routing_policy_flows(lflows, od, ports, rule,
+                                                ecmp_group_id);
+                ecmp_group_id++;
+            } else {
+                build_routing_policy_flow(lflows, od, ports, rule,
+                                          &rule->header_);
             }
         }
     }
+}
+
+/* Local router ingress table ARP_RESOLVE: ARP Resolution.
+ *
+ * Any unicast packet that reaches this table is an IP packet whose
+ * next-hop IP address is in REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6
+ * (ip4.dst/ipv6.dst is the final destination).
+ * This table resolves the IP address in
+ * REG_NEXT_HOP_IPV4/REG_NEXT_HOP_IPV6 into an output port in outport and
+ * an Ethernet address in eth.dst.
+ */
+static void
+build_arp_resolve_flows_for_lrouter_port(
+        struct ovn_port *op, struct hmap *lflows)
+{
+    if (op->nbsp && !lsp_is_enabled(op->nbsp)) {
+        return;
+    }
 
+    if (op->nbrp) {
+        /* Drop IP traffic destined to router owned IPs. Part of it is dropped
+         * in stage "lr_in_ip_input" but traffic that could have been unSNATed
+         * but didn't match any existing session might still end up here.
+         *
+         * Priority 1.
+         */
+        build_lrouter_drop_own_dest(op, S_ROUTER_IN_ARP_RESOLVE, 1, true,
+                                    lflows);
+    }
 }
 
 /* Local router ingress table CHK_PKT_LEN: Check packet length.
@@ -10462,13 +8099,6 @@ build_check_pkt_len_flows_for_lrouter(
         struct ds *match, struct ds *actions)
 {
     if (od->nbr) {
-
-        /* Packets are allowed by default. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_CHK_PKT_LEN, 0, "1",
-                      "next;");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_LARGER_PKTS, 0, "1",
-                      "next;");
-
         if (od->l3dgw_port && od->l3redirect_port) {
             int gw_mtu = 0;
             if (od->l3dgw_port->nbrp) {
@@ -10594,9 +8224,6 @@ build_gateway_redirect_flows_for_lrouter(
                                     ds_cstr(match), ds_cstr(actions),
                                     stage_hint);
         }
-
-        /* Packets are allowed by default. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_GW_REDIRECT, 0, "1", "next;");
     }
 }
 
@@ -10649,291 +8276,7 @@ build_arp_request_flows_for_lrouter(
                                     ds_cstr(match), ds_cstr(actions),
                                     &route->header_);
         }
-
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 100,
-                      "eth.dst == 00:00:00:00:00:00 && ip4",
-                      "arp { "
-                      "eth.dst = ff:ff:ff:ff:ff:ff; "
-                      "arp.spa = " REG_SRC_IPV4 "; "
-                      "arp.tpa = " REG_NEXT_HOP_IPV4 "; "
-                      "arp.op = 1; " /* ARP request */
-                      "output; "
-                      "};");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 100,
-                      "eth.dst == 00:00:00:00:00:00 && ip6",
-                      "nd_ns { "
-                      "nd.target = " REG_NEXT_HOP_IPV6 "; "
-                      "output; "
-                      "};");
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_ARP_REQUEST, 0, "1", "output;");
-    }
-}
-
-/* Logical router egress table DELIVERY: Delivery (priority 100-110).
- *
- * Priority 100 rules deliver packets to enabled logical ports.
- * Priority 110 rules match multicast packets and update the source
- * mac before delivering to enabled logical ports. IP multicast traffic
- * bypasses S_ROUTER_IN_IP_ROUTING route lookups.
- */
-static void
-build_egress_delivery_flows_for_lrouter_port(
-        struct ovn_port *op, struct hmap *lflows,
-        struct ds *match, struct ds *actions)
-{
-    if (op->nbrp) {
-        if (!lrport_is_enabled(op->nbrp)) {
-            /* Drop packets to disabled logical ports (since logical flow
-             * tables are default-drop). */
-            return;
-        }
-
-        if (op->derived) {
-            /* No egress packets should be processed in the context of
-             * a chassisredirect port.  The chassisredirect port should
-             * be replaced by the l3dgw port in the local output
-             * pipeline stage before egress processing. */
-            return;
-        }
-
-        /* If multicast relay is enabled then also adjust source mac for IP
-         * multicast traffic.
-         */
-        if (op->od->mcast_info.rtr.relay) {
-            ds_clear(match);
-            ds_clear(actions);
-            ds_put_format(match, "(ip4.mcast || ip6.mcast) && outport == %s",
-                          op->json_key);
-            ds_put_format(actions, "eth.src = %s; output;",
-                          op->lrp_networks.ea_s);
-            ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_DELIVERY, 110,
-                          ds_cstr(match), ds_cstr(actions));
-        }
-
-        ds_clear(match);
-        ds_put_format(match, "outport == %s", op->json_key);
-        ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_DELIVERY, 100,
-                      ds_cstr(match), "output;");
-    }
-
-}
-
-static void
-build_misc_local_traffic_drop_flows_for_lrouter(
-        struct ovn_datapath *od, struct hmap *lflows)
-{
-    if (od->nbr) {
-        /* L3 admission control: drop multicast and broadcast source, localhost
-         * source or destination, and zero network source or destination
-         * (priority 100). */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 100,
-                      "ip4.src_mcast ||"
-                      "ip4.src == 255.255.255.255 || "
-                      "ip4.src == 127.0.0.0/8 || "
-                      "ip4.dst == 127.0.0.0/8 || "
-                      "ip4.src == 0.0.0.0/8 || "
-                      "ip4.dst == 0.0.0.0/8",
-                      "drop;");
-
-        /* Drop ARP packets (priority 85). ARP request packets for router's own
-         * IPs are handled with priority-90 flows.
-         * Drop IPv6 ND packets (priority 85). ND NA packets for router's own
-         * IPs are handled with priority-90 flows.
-         */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 85,
-                      "arp || nd", "drop;");
-
-        /* Allow IPv6 multicast traffic that's supposed to reach the
-         * router pipeline (e.g., router solicitations).
-         */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 84, "nd_rs || nd_ra",
-                      "next;");
-
-        /* Drop other reserved multicast. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 83,
-                      "ip6.mcast_rsvd", "drop;");
-
-        /* Allow other multicast if relay enabled (priority 82). */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 82,
-                      "ip4.mcast || ip6.mcast",
-                      od->mcast_info.rtr.relay ? "next;" : "drop;");
-
-        /* Drop Ethernet local broadcast.  By definition this traffic should
-         * not be forwarded.*/
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 50,
-                      "eth.bcast", "drop;");
-
-        /* TTL discard */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 30,
-                      "ip4 && ip.ttl == {0, 1}", "drop;");
-
-        /* Pass other traffic not already handled to the next table for
-         * routing. */
-        ovn_lflow_add(lflows, od, S_ROUTER_IN_IP_INPUT, 0, "1", "next;");
-    }
-}
-
-static void
-build_dhcpv6_reply_flows_for_lrouter_port(
-        struct ovn_port *op, struct hmap *lflows,
-        struct ds *match)
-{
-    if (op->nbrp && (!op->derived)) {
-        for (size_t i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
-            ds_clear(match);
-            ds_put_format(match, "ip6.dst == %s && udp.src == 547 &&"
-                          " udp.dst == 546",
-                          op->lrp_networks.ipv6_addrs[i].addr_s);
-            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 100,
-                          ds_cstr(match),
-                          "reg0 = 0; handle_dhcpv6_reply;");
-        }
-    }
-
-}
-
-static void
-build_ipv6_input_flows_for_lrouter_port(
-        struct ovn_port *op, struct hmap *lflows,
-        struct ds *match, struct ds *actions)
-{
-    if (op->nbrp && (!op->derived)) {
-        /* No ingress packets are accepted on a chassisredirect
-         * port, so no need to program flows for that port. */
-        if (op->lrp_networks.n_ipv6_addrs) {
-            /* ICMPv6 echo reply.  These flows reply to echo requests
-             * received for the router's IP address. */
-            ds_clear(match);
-            ds_put_cstr(match, "ip6.dst == ");
-            op_put_v6_networks(match, op);
-            ds_put_cstr(match, " && icmp6.type == 128 && icmp6.code == 0");
-
-            const char *lrp_actions =
-                        "ip6.dst <-> ip6.src; "
-                        "ip.ttl = 255; "
-                        "icmp6.type = 129; "
-                        "flags.loopback = 1; "
-                        "next; ";
-            ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
-                                    ds_cstr(match), lrp_actions,
-                                    &op->nbrp->header_);
-        }
-
-        /* ND reply.  These flows reply to ND solicitations for the
-         * router's own IP address. */
-        for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
-            ds_clear(match);
-            if (op->od->l3dgw_port && op == op->od->l3dgw_port
-                && op->od->l3redirect_port) {
-                /* Traffic with eth.src = l3dgw_port->lrp_networks.ea_s
-                 * should only be sent from the gateway chassi, so that
-                 * upstream MAC learning points to the gateway chassis.
-                 * Also need to avoid generation of multiple ND replies
-                 * from different chassis. */
-                ds_put_format(match, "is_chassis_resident(%s)",
-                              op->od->l3redirect_port->json_key);
-            }
-
-            build_lrouter_nd_flow(op->od, op, "nd_na_router",
-                                  op->lrp_networks.ipv6_addrs[i].addr_s,
-                                  op->lrp_networks.ipv6_addrs[i].sn_addr_s,
-                                  REG_INPORT_ETH_ADDR, match, false, 90,
-                                  &op->nbrp->header_, lflows);
-        }
-
-        /* UDP/TCP/SCTP port unreachable */
-        if (!smap_get(&op->od->nbr->options, "chassis")
-            && !op->od->l3dgw_port) {
-            for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
-                ds_clear(match);
-                ds_put_format(match,
-                              "ip6 && ip6.dst == %s && !ip.later_frag && tcp",
-                              op->lrp_networks.ipv6_addrs[i].addr_s);
-                const char *action = "tcp_reset {"
-                                     "eth.dst <-> eth.src; "
-                                     "ip6.dst <-> ip6.src; "
-                                     "next; };";
-                ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT,
-                                        80, ds_cstr(match), action,
-                                        &op->nbrp->header_);
-
-                ds_clear(match);
-                ds_put_format(match,
-                              "ip6 && ip6.dst == %s && !ip.later_frag && sctp",
-                              op->lrp_networks.ipv6_addrs[i].addr_s);
-                action = "sctp_abort {"
-                         "eth.dst <-> eth.src; "
-                         "ip6.dst <-> ip6.src; "
-                         "next; };";
-                ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT,
-                                        80, ds_cstr(match), action,
-                                        &op->nbrp->header_);
-
-                ds_clear(match);
-                ds_put_format(match,
-                              "ip6 && ip6.dst == %s && !ip.later_frag && udp",
-                              op->lrp_networks.ipv6_addrs[i].addr_s);
-                action = "icmp6 {"
-                         "eth.dst <-> eth.src; "
-                         "ip6.dst <-> ip6.src; "
-                         "ip.ttl = 255; "
-                         "icmp6.type = 1; "
-                         "icmp6.code = 4; "
-                         "next; };";
-                ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT,
-                                        80, ds_cstr(match), action,
-                                        &op->nbrp->header_);
-
-                ds_clear(match);
-                ds_put_format(match,
-                              "ip6 && ip6.dst == %s && !ip.later_frag",
-                              op->lrp_networks.ipv6_addrs[i].addr_s);
-                action = "icmp6 {"
-                         "eth.dst <-> eth.src; "
-                         "ip6.dst <-> ip6.src; "
-                         "ip.ttl = 255; "
-                         "icmp6.type = 1; "
-                         "icmp6.code = 3; "
-                         "next; };";
-                ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT,
-                                        70, ds_cstr(match), action,
-                                        &op->nbrp->header_);
-            }
-        }
-
-        /* ICMPv6 time exceeded */
-        for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
-            /* skip link-local address */
-            if (in6_is_lla(&op->lrp_networks.ipv6_addrs[i].network)) {
-                continue;
-            }
-
-            ds_clear(match);
-            ds_clear(actions);
-
-            ds_put_format(match,
-                          "inport == %s && ip6 && "
-                          "ip6.src == %s/%d && "
-                          "ip.ttl == {0, 1} && !ip.later_frag",
-                          op->json_key,
-                          op->lrp_networks.ipv6_addrs[i].network_s,
-                          op->lrp_networks.ipv6_addrs[i].plen);
-            ds_put_format(actions,
-                          "icmp6 {"
-                          "eth.dst <-> eth.src; "
-                          "ip6.dst = ip6.src; "
-                          "ip6.src = %s; "
-                          "ip.ttl = 255; "
-                          "icmp6.type = 3; /* Time exceeded */ "
-                          "icmp6.code = 0; /* TTL exceeded in transit */ "
-                          "next; };",
-                          op->lrp_networks.ipv6_addrs[i].addr_s);
-            ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 40,
-                                    ds_cstr(match), ds_cstr(actions),
-                                    &op->nbrp->header_);
-        }
     }
-
 }
 
 static void
@@ -10988,113 +8331,11 @@ build_lrouter_arp_nd_for_datapath(struct ovn_datapath *od,
 static void
 build_lrouter_ipv4_ip_input(struct ovn_port *op,
                             struct hmap *lflows,
-                            struct ds *match, struct ds *actions)
+                            struct ds *match)
 {
     /* No ingress packets are accepted on a chassisredirect
      * port, so no need to program flows for that port. */
     if (op->nbrp && (!op->derived)) {
-        if (op->lrp_networks.n_ipv4_addrs) {
-            /* L3 admission control: drop packets that originate from an
-             * IPv4 address owned by the router or a broadcast address
-             * known to the router (priority 100). */
-            ds_clear(match);
-            ds_put_cstr(match, "ip4.src == ");
-            op_put_v4_networks(match, op, true);
-            ds_put_cstr(match, " && "REGBIT_EGRESS_LOOPBACK" == 0");
-            ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 100,
-                                    ds_cstr(match), "drop;",
-                                    &op->nbrp->header_);
-
-            /* ICMP echo reply.  These flows reply to ICMP echo requests
-             * received for the router's IP address. Since packets only
-             * get here as part of the logical router datapath, the inport
-             * (i.e. the incoming locally attached net) does not matter.
-             * The ip.ttl also does not matter (RFC1812 section 4.2.2.9) */
-            ds_clear(match);
-            ds_put_cstr(match, "ip4.dst == ");
-            op_put_v4_networks(match, op, false);
-            ds_put_cstr(match, " && icmp4.type == 8 && icmp4.code == 0");
-
-            const char * icmp_actions = "ip4.dst <-> ip4.src; "
-                          "ip.ttl = 255; "
-                          "icmp4.type = 0; "
-                          "flags.loopback = 1; "
-                          "next; ";
-            ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90,
-                                    ds_cstr(match), icmp_actions,
-                                    &op->nbrp->header_);
-        }
-
-        /* BFD msg handling */
-        build_lrouter_bfd_flows(lflows, op);
-
-        /* ICMP time exceeded */
-        for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-            ds_clear(match);
-            ds_clear(actions);
-
-            ds_put_format(match,
-                          "inport == %s && ip4 && "
-                          "ip.ttl == {0, 1} && !ip.later_frag", op->json_key);
-            ds_put_format(actions,
-                          "icmp4 {"
-                          "eth.dst <-> eth.src; "
-                          "icmp4.type = 11; /* Time exceeded */ "
-                          "icmp4.code = 0; /* TTL exceeded in transit */ "
-                          "ip4.dst = ip4.src; "
-                          "ip4.src = %s; "
-                          "ip.ttl = 255; "
-                          "next; };",
-                          op->lrp_networks.ipv4_addrs[i].addr_s);
-            ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 40,
-                                    ds_cstr(match), ds_cstr(actions),
-                                    &op->nbrp->header_);
-        }
-
-        /* ARP reply.  These flows reply to ARP requests for the router's own
-         * IP address. */
-        for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-            ds_clear(match);
-            ds_put_format(match, "arp.spa == %s/%u",
-                          op->lrp_networks.ipv4_addrs[i].network_s,
-                          op->lrp_networks.ipv4_addrs[i].plen);
-
-            if (op->od->l3dgw_port && op->od->l3redirect_port && op->peer
-                && op->peer->od->n_localnet_ports) {
-                bool add_chassis_resident_check = false;
-                if (op == op->od->l3dgw_port) {
-                    /* Traffic with eth.src = l3dgw_port->lrp_networks.ea_s
-                     * should only be sent from the gateway chassis, so that
-                     * upstream MAC learning points to the gateway chassis.
-                     * Also need to avoid generation of multiple ARP responses
-                     * from different chassis. */
-                    add_chassis_resident_check = true;
-                } else {
-                    /* Check if the option 'reside-on-redirect-chassis'
-                     * is set to true on the router port. If set to true
-                     * and if peer's logical switch has a localnet port, it
-                     * means the router pipeline for the packets from
-                     * peer's logical switch is be run on the chassis
-                     * hosting the gateway port and it should reply to the
-                     * ARP requests for the router port IPs.
-                     */
-                    add_chassis_resident_check = smap_get_bool(
-                        &op->nbrp->options,
-                        "reside-on-redirect-chassis", false);
-                }
-
-                if (add_chassis_resident_check) {
-                    ds_put_format(match, " && is_chassis_resident(%s)",
-                                  op->od->l3redirect_port->json_key);
-                }
-            }
-
-            build_lrouter_arp_flow(op->od, op,
-                                   op->lrp_networks.ipv4_addrs[i].addr_s,
-                                   REG_INPORT_ETH_ADDR, match, false, 90,
-                                   &op->nbrp->header_, lflows);
-        }
-
         const char *ip_address;
         if (sset_count(&op->od->lb_ips_v4)) {
             ds_clear(match);
@@ -11128,66 +8369,6 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op,
                                   match, false, 90, NULL, lflows);
         }
 
-        if (!smap_get(&op->od->nbr->options, "chassis")
-            && !op->od->l3dgw_port) {
-            /* UDP/TCP/SCTP port unreachable. */
-            for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
-                ds_clear(match);
-                ds_put_format(match,
-                              "ip4 && ip4.dst == %s && !ip.later_frag && udp",
-                              op->lrp_networks.ipv4_addrs[i].addr_s);
-                const char *action = "icmp4 {"
-                                     "eth.dst <-> eth.src; "
-                                     "ip4.dst <-> ip4.src; "
-                                     "ip.ttl = 255; "
-                                     "icmp4.type = 3; "
-                                     "icmp4.code = 3; "
-                                     "next; };";
-                ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT,
-                                        80, ds_cstr(match), action,
-                                        &op->nbrp->header_);
-
-                ds_clear(match);
-                ds_put_format(match,
-                              "ip4 && ip4.dst == %s && !ip.later_frag && tcp",
-                              op->lrp_networks.ipv4_addrs[i].addr_s);
-                action = "tcp_reset {"
-                         "eth.dst <-> eth.src; "
-                         "ip4.dst <-> ip4.src; "
-                         "next; };";
-                ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT,
-                                        80, ds_cstr(match), action,
-                                        &op->nbrp->header_);
-
-                ds_clear(match);
-                ds_put_format(match,
-                              "ip4 && ip4.dst == %s && !ip.later_frag && sctp",
-                              op->lrp_networks.ipv4_addrs[i].addr_s);
-                action = "sctp_abort {"
-                         "eth.dst <-> eth.src; "
-                         "ip4.dst <-> ip4.src; "
-                         "next; };";
-                ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT,
-                                        80, ds_cstr(match), action,
-                                        &op->nbrp->header_);
-
-                ds_clear(match);
-                ds_put_format(match,
-                              "ip4 && ip4.dst == %s && !ip.later_frag",
-                              op->lrp_networks.ipv4_addrs[i].addr_s);
-                action = "icmp4 {"
-                         "eth.dst <-> eth.src; "
-                         "ip4.dst <-> ip4.src; "
-                         "ip.ttl = 255; "
-                         "icmp4.type = 3; "
-                         "icmp4.code = 2; "
-                         "next; };";
-                ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT,
-                                        70, ds_cstr(match), action,
-                                        &op->nbrp->header_);
-            }
-        }
-
         /* Drop IP traffic destined to router owned IPs except if the IP is
          * also a SNAT IP. Those are dropped later, in stage
          * "lr_in_arp_resolve", if unSNAT was unsuccessful.
@@ -11687,20 +8868,6 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath *od,
         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). */
@@ -11904,6 +9071,33 @@ struct lswitch_flow_build_info {
     struct ds actions;
 };
 
+static void
+build_lswitch_flows(struct hmap *datapaths, struct hmap *lflows)
+{
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds actions = DS_EMPTY_INITIALIZER;
+    struct ovn_datapath *od;
+
+    /* Ingress table 23: Destination lookup for unknown MACs (priority 0). */
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        if (!od->nbs) {
+            continue;
+        }
+
+        if (od->has_unknown) {
+            ovn_lflow_add_unique(lflows, od, S_SWITCH_IN_L2_UNKNOWN, 50,
+                                 "outport == \"none\"",
+                                 "outport = \""MC_UNKNOWN "\"; output;");
+        } else {
+            ovn_lflow_add(lflows, od, S_SWITCH_IN_L2_UNKNOWN, 50,
+                          "outport == \"none\"", "drop;");
+        }
+    }
+
+    ds_destroy(&match);
+    ds_destroy(&actions);
+}
+
 /* Helper function to combine all lflow generation which is iterated by
  * datapath.
  *
@@ -11920,33 +9114,20 @@ build_lswitch_and_lrouter_iterate_by_od(struct ovn_datapath *od,
                                          lsi->meter_groups, lsi->lbs);
 
     build_fwd_group_lflows(od, lsi->lflows);
-    build_lswitch_lflows_admission_control(od, lsi->lflows);
-    build_lswitch_input_port_sec_od(od, lsi->lflows);
-    build_lswitch_learn_fdb_od(od, lsi->lflows);
-    build_lswitch_arp_nd_responder_default(od, lsi->lflows);
-    build_lswitch_dns_lookup_and_response(od, lsi->lflows);
-    build_lswitch_dhcp_and_dns_defaults(od, lsi->lflows);
     build_lswitch_destination_lookup_bmcast(od, lsi->lflows, &lsi->actions);
-    build_lswitch_output_port_sec_od(od, lsi->lflows);
 
     /* Build Logical Router Flows. */
-    build_adm_ctrl_flows_for_lrouter(od, lsi->lflows);
-    build_neigh_learning_flows_for_lrouter(od, lsi->lflows, &lsi->match,
-                                           &lsi->actions);
-    build_ND_RA_flows_for_lrouter(od, lsi->lflows);
     build_static_route_flows_for_lrouter(od, lsi->lflows, lsi->ports,
                                          lsi->bfd_connections);
     build_mcast_lookup_flows_for_lrouter(od, lsi->lflows, &lsi->match,
                                          &lsi->actions);
     build_ingress_policy_flows_for_lrouter(od, lsi->lflows, lsi->ports);
-    build_arp_resolve_flows_for_lrouter(od, lsi->lflows);
     build_check_pkt_len_flows_for_lrouter(od, lsi->lflows, lsi->ports,
                                           &lsi->match, &lsi->actions);
     build_gateway_redirect_flows_for_lrouter(od, lsi->lflows, &lsi->match,
                                              &lsi->actions);
     build_arp_request_flows_for_lrouter(od, lsi->lflows, &lsi->match,
                                         &lsi->actions);
-    build_misc_local_traffic_drop_flows_for_lrouter(od, lsi->lflows);
     build_lrouter_arp_nd_for_datapath(od, lsi->lflows);
     build_lrouter_nat_defrag_and_lb(od, lsi->lflows, lsi->meter_groups,
                                     lsi->lbs, &lsi->match, &lsi->actions);
@@ -11958,43 +9139,14 @@ static void
 build_lswitch_and_lrouter_iterate_by_op(struct ovn_port *op,
                                         struct lswitch_flow_build_info *lsi)
 {
-    /* Build Logical Switch Flows. */
-    build_lswitch_input_port_sec_op(op, lsi->lflows, &lsi->actions,
-                                    &lsi->match);
-    build_lswitch_learn_fdb_op(op, lsi->lflows, &lsi->actions,
-                               &lsi->match);
-    build_lswitch_arp_nd_responder_skip_local(op, lsi->lflows,
-                                              &lsi->match);
-    build_lswitch_arp_nd_responder_known_ips(op, lsi->lflows,
-                                             lsi->ports,
-                                             &lsi->actions,
-                                             &lsi->match);
     build_lswitch_dhcp_options_and_response(op, lsi->lflows);
     build_lswitch_external_port(op, lsi->lflows);
     build_lswitch_ip_unicast_lookup(op, lsi->lflows, lsi->mcgroups,
                                     &lsi->actions, &lsi->match);
-    build_lswitch_output_port_sec_op(op, lsi->lflows,
-                                     &lsi->actions, &lsi->match);
 
     /* Build Logical Router Flows. */
-    build_adm_ctrl_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
-                                          &lsi->actions);
-    build_neigh_learning_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
-                                                &lsi->actions);
-    build_ip_routing_flows_for_lrouter_port(op, lsi->lflows);
-    build_ND_RA_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
-                                       &lsi->actions);
-    build_arp_resolve_flows_for_lrouter_port(op, lsi->lflows, lsi->ports,
-                                             &lsi->match, &lsi->actions);
-    build_egress_delivery_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
-                                                 &lsi->actions);
-    build_dhcpv6_reply_flows_for_lrouter_port(op, lsi->lflows, &lsi->match);
-    build_ipv6_input_flows_for_lrouter_port(op, lsi->lflows,
-                                            &lsi->match, &lsi->actions);
-    build_lrouter_ipv4_ip_input(op, lsi->lflows,
-                                &lsi->match, &lsi->actions);
-    build_lrouter_force_snat_flows_op(op, lsi->lflows, &lsi->match,
-                                      &lsi->actions);
+    build_arp_resolve_flows_for_lrouter_port(op, lsi->lflows);
+    build_lrouter_ipv4_ip_input(op, lsi->lflows, &lsi->match);
 }
 
 struct lflows_thread_pool {
@@ -12536,6 +9688,55 @@ build_lflows(struct northd_context *ctx, struct hmap *datapaths,
     }
 }
 
+static void
+sync_datapath_options(struct hmap *datapaths)
+{
+    struct ovn_datapath *od;
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        if (!od->sb) {
+            continue;
+        }
+
+        struct smap options = SMAP_INITIALIZER(&options);
+        if (od->nbs) {
+            if (od->has_stateful_acl) {
+                smap_add(&options, "has-stateful-acls", "true");
+            }
+            if (od->has_acls) {
+                smap_add(&options, "has-acls", "true");
+            }
+            if (od->has_lb_vip) {
+                smap_add(&options, "has-lb-vips", "true");
+            }
+            if (od->has_unknown) {
+                smap_add(&options, "has-unknown", "true");
+            }
+            if (od->vlan_passthru) {
+                smap_add(&options, "vlan-passthru", "true");
+            }
+            if (od->has_dns_records) {
+                smap_add(&options, "has-dns-records", "true");
+            }
+        } else {
+            if (od->always_learn_from_arp_request) {
+                smap_add(&options, "always-learn-from-arp-request", "true");
+            }
+            if (od->mcast_info.rtr.relay) {
+                smap_add(&options, "mcast-relay", "true");
+            }
+            if (od->l3dgw_port) {
+                smap_add(&options, "has-l3dgw-port", "true");
+            }
+            if (od->lb_force_snat_router_ip) {
+                smap_add(&options, "lb-force-snat-router-ip", "true");
+            }
+        }
+
+        sbrec_datapath_binding_set_options(od->sb, &options);
+        smap_destroy(&options);
+    }
+}
+
 static void
 sync_address_set(struct northd_context *ctx, const char *name,
                  const char **addrs, size_t n_addrs,
@@ -13347,7 +10548,7 @@ ovnnb_db_run(struct northd_context *ctx,
     build_lflows(ctx, datapaths, ports, &port_groups, &mcast_groups,
                  &igmp_groups, &meter_groups, &lbs, &bfd_connections);
     ovn_update_ipv6_prefix(ports);
-
+    sync_datapath_options(datapaths);
     sync_address_sets(ctx);
     sync_port_groups(ctx, &port_groups);
     sync_meters(ctx, &meter_groups);
@@ -14228,6 +11429,8 @@ main(int argc, char *argv[])
                        &sbrec_datapath_binding_col_tunnel_key);
     add_column_noalert(ovnsb_idl_loop.idl,
                        &sbrec_datapath_binding_col_load_balancers);
+    add_column_noalert(ovnsb_idl_loop.idl,
+                       &sbrec_datapath_binding_col_options);
     add_column_noalert(ovnsb_idl_loop.idl,
                        &sbrec_datapath_binding_col_external_ids);
 
@@ -14243,6 +11446,8 @@ main(int argc, char *argv[])
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_port_binding_col_type);
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_port_binding_col_options);
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_port_binding_col_mac);
+    add_column_noalert(ovnsb_idl_loop.idl,
+                       &sbrec_port_binding_col_port_security);
     add_column_noalert(ovnsb_idl_loop.idl,
                        &sbrec_port_binding_col_nat_addresses);
     ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_port_binding_col_chassis);
@@ -14403,6 +11608,8 @@ main(int argc, char *argv[])
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_load_balancer_col_vips);
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_load_balancer_col_protocol);
     add_column_noalert(ovnsb_idl_loop.idl, &sbrec_load_balancer_col_options);
+    add_column_noalert(ovnsb_idl_loop.idl,
+                       &sbrec_load_balancer_col_selection_fields);
     add_column_noalert(ovnsb_idl_loop.idl,
                        &sbrec_load_balancer_col_external_ids);
 
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index bbf60781dd..15c5224b7f 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@
 {
     "name": "OVN_Southbound",
-    "version": "20.18.0",
-    "cksum": "1816525029 26536",
+    "version": "21.1.0",
+    "cksum": "3048733342 27198",
     "tables": {
         "SB_Global": {
             "columns": {
@@ -171,6 +171,9 @@
                                                    "refType": "weak"},
                                             "min": 0,
                                             "max": "unlimited"}},
+                "options": {
+                    "type": {"key": "string", "value": "string",
+                             "min": 0, "max": "unlimited"}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
@@ -222,6 +225,9 @@
                 "mac": {"type": {"key": "string",
                                  "min": 0,
                                  "max": "unlimited"}},
+                "port_security": {"type": {"key": "string",
+                                           "min": 0,
+                                           "max": "unlimited"}},
                 "nat_addresses": {"type": {"key": "string",
                                            "min": 0,
                                            "max": "unlimited"}},
@@ -487,6 +493,12 @@
                               "value": "string",
                               "min": 0,
                               "max": "unlimited"}},
+                "selection_fields": {
+                    "type": {"key": {"type": "string",
+                             "enum": ["set",
+                                ["eth_src", "eth_dst", "ip_src", "ip_dst",
+                                 "tp_src", "tp_dst"]]},
+                             "min": 0, "max": "unlimited"}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
diff --git a/ovn-sb.xml b/ovn-sb.xml
index b29866e88d..70ad3f8f5e 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -2630,6 +2630,12 @@ tcp.flags = RST;
       </p>
     </column>
 
+    <column name="options">
+      <p>
+        Options.
+      </p>
+    </column>
+
     <group title="OVN_Northbound Relationship">
       <p>
         Each row in <ref table="Datapath_Binding"/> is associated with some
@@ -2873,6 +2879,12 @@ tcp.flags = RST;
         </p>
       </column>
 
+      <column name="port_security">
+        <p>
+          Port security addresses.
+        </p>
+      </column>
+
       <column name="type">
         <p>
           A type for this logical port.  Logical ports can be used to model other
@@ -4300,6 +4312,10 @@ tcp.flags = RST;
       Datapaths to which this load balancer applies to.
     </column>
 
+    <column name="selection_fields">
+      Selection fields.
+    </column>
+
     <group title="Load_Balancer options">
     <column name="options" key="hairpin_snat_ip">
       IP to be used as source IP for packets that have been hair-pinned after
diff --git a/utilities/ovn-dbctl.c b/utilities/ovn-dbctl.c
index 9c3e219159..9e9d3b772e 100644
--- a/utilities/ovn-dbctl.c
+++ b/utilities/ovn-dbctl.c
@@ -183,9 +183,14 @@ ovn_dbctl_main(int argc, char *argv[],
         }
         daemon_mode = true;
     }
+
     /* Initialize IDL. */
     idl = the_idl = ovsdb_idl_create_unconnected(dbctl_options->idl_class,
-                                                 daemon_mode);
+                                                 true);
+    if (dbctl_options->pre_idl_run) {
+        dbctl_options->pre_idl_run(idl);
+    }
+                                                 
     ovsdb_idl_set_shuffle_remotes(idl, shuffle_remotes);
     /* "retry" is true iff in daemon mode. */
     ovsdb_idl_set_remote(idl, db, daemon_mode);
diff --git a/utilities/ovn-dbctl.h b/utilities/ovn-dbctl.h
index a1fbede6b5..6c97590409 100644
--- a/utilities/ovn-dbctl.h
+++ b/utilities/ovn-dbctl.h
@@ -43,7 +43,8 @@ struct ovn_dbctl_options {
     const struct ctl_command_syntax *commands;
 
     void (*usage)(void);
-
+    void (*pre_idl_run)(struct ovsdb_idl *);
+    void (*pre_idl_destroy)(struct ovsdb_idl *);
     void (*add_base_prerequisites)(struct ovsdb_idl *, enum nbctl_wait_type);
     void (*pre_execute)(struct ovsdb_idl *, struct ovsdb_idl_txn *,
                         enum nbctl_wait_type);
diff --git a/utilities/ovn-sbctl.c b/utilities/ovn-sbctl.c
index 4146384e74..de28a1129b 100644
--- a/utilities/ovn-sbctl.c
+++ b/utilities/ovn-sbctl.c
@@ -43,8 +43,11 @@
 #include "openvswitch/shash.h"
 #include "openvswitch/vconn.h"
 #include "openvswitch/vlog.h"
+#include "lib/lb.h"
+#include "lib/ldata.h"
 #include "lib/ovn-sb-idl.h"
 #include "lib/ovn-util.h"
+#include "lib/lflow.h"
 #include "memory.h"
 #include "ovn-dbctl.h"
 #include "ovsdb-data.h"
@@ -143,6 +146,25 @@ Other options:\n\
     stream_usage("database", true, true, true);
     exit(EXIT_SUCCESS);
 }
+
+static struct ovsdb_idl_index *sbrec_datapath_binding_by_key;
+static struct ovsdb_idl_index *sbrec_port_binding_by_datapath;
+static struct ovsdb_idl_index *sbrec_port_binding_by_name;
+
+static void
+sbctl_pre_idl_run(struct ovsdb_idl *sb_idl)
+{
+    sbrec_datapath_binding_by_key
+        = ovsdb_idl_index_create1(sb_idl,
+                                  &sbrec_datapath_binding_col_tunnel_key);
+    sbrec_port_binding_by_datapath
+        = ovsdb_idl_index_create1(sb_idl,
+                                  &sbrec_port_binding_col_datapath);
+    sbrec_port_binding_by_name
+        = ovsdb_idl_index_create1(sb_idl,
+                                  &sbrec_port_binding_col_logical_port);
+}
+
 
 /* One should not use ctl_fatal() within commands because it will kill the
  * daemon if we're in daemon mode.  Use ctl_error() instead and return
@@ -303,9 +325,13 @@ pre_get_info(struct ctl_context *ctx)
 
     ovsdb_idl_add_column(ctx->idl, &sbrec_port_binding_col_logical_port);
     ovsdb_idl_add_column(ctx->idl, &sbrec_port_binding_col_tunnel_key);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_port_binding_col_mac);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_port_binding_col_port_security);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_port_binding_col_options);
     ovsdb_idl_add_column(ctx->idl, &sbrec_port_binding_col_chassis);
     ovsdb_idl_add_column(ctx->idl, &sbrec_port_binding_col_datapath);
     ovsdb_idl_add_column(ctx->idl, &sbrec_port_binding_col_up);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_port_binding_col_type);
 
     ovsdb_idl_add_column(ctx->idl, &sbrec_logical_flow_col_logical_datapath);
     ovsdb_idl_add_column(ctx->idl, &sbrec_logical_flow_col_logical_dp_group);
@@ -319,6 +345,9 @@ pre_get_info(struct ctl_context *ctx)
     ovsdb_idl_add_column(ctx->idl, &sbrec_logical_dp_group_col_datapaths);
 
     ovsdb_idl_add_column(ctx->idl, &sbrec_datapath_binding_col_external_ids);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_datapath_binding_col_options);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_datapath_binding_col_tunnel_key);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_datapath_binding_col_load_balancers);
 
     ovsdb_idl_add_column(ctx->idl, &sbrec_ip_multicast_col_datapath);
     ovsdb_idl_add_column(ctx->idl, &sbrec_ip_multicast_col_seq_no);
@@ -337,6 +366,8 @@ pre_get_info(struct ctl_context *ctx)
     ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_vips);
     ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_name);
     ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_protocol);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_options);
+    ovsdb_idl_add_column(ctx->idl, &sbrec_load_balancer_col_selection_fields);
 }
 
 static struct cmd_show_table cmd_show_tables[] = {
@@ -1039,6 +1070,227 @@ cmd_lflow_list(struct ctl_context *ctx)
     free(lflows);
 }
 
+static int
+sbctl_gen_lflow_cmp(const void *a_, const void *b_)
+{
+    const struct ovn_ctrl_lflow *const *ap = a_;
+    const struct ovn_ctrl_lflow *const *bp = b_;
+
+    const struct ovn_ctrl_lflow *a = *ap;
+    const struct ovn_ctrl_lflow *b = *bp;
+
+    int a_pipeline = ovn_stage_get_pipeline(a->stage);
+    int b_pipeline = ovn_stage_get_pipeline(b->stage);
+    int a_table_id = ovn_stage_get_table(a->stage);
+    int b_table_id = ovn_stage_get_table(b->stage);
+    int cmp = (a_pipeline > b_pipeline ? 1
+               : a_pipeline < b_pipeline ? -1
+               : a_table_id > b_table_id ? 1
+               : a_table_id < b_table_id ? -1
+               : a->priority > b->priority ? -1
+               : a->priority < b->priority ? 1
+               : strcmp(a->match, b->match));
+    return cmp ? cmp : strcmp(a->actions, b->actions);
+}
+
+static void
+ctrl_lflow_list(struct hmap *lflows, struct hmap *gen_flows, struct hmap *lbs,
+                const struct sbrec_datapath_binding *dp, bool is_switch,
+                bool print_uuid, struct ctl_context *ctx)
+{
+    size_t n_total_flows = hmap_count(lflows) + hmap_count(gen_flows);
+
+    for (size_t j = 0; j < dp->n_load_balancers; j++) {
+        struct local_load_balancer *local_lb =
+            local_load_balancer_get(lbs, &dp->load_balancers[j]->header_.uuid);
+        ovs_assert(local_lb);
+
+        n_total_flows += is_switch ? hmap_count(local_lb->active_lswitch_lflows) :
+                         hmap_count(local_lb->active_lrouter_lflows);
+    }
+
+    struct ovn_ctrl_lflow **ctrl_lflows =
+        xmalloc(n_total_flows * sizeof *ctrl_lflows);
+
+    struct ovn_ctrl_lflow *f;
+    size_t i = 0;
+    HMAP_FOR_EACH (f, hmap_node, lflows) {
+        ctrl_lflows[i++] = f;
+    }
+
+    HMAP_FOR_EACH (f, hmap_node, gen_flows) {
+        ctrl_lflows[i++] = f;
+    }
+
+    for (size_t j = 0; j < dp->n_load_balancers; j++) {
+        struct local_load_balancer *local_lb =
+            local_load_balancer_get(lbs, &dp->load_balancers[j]->header_.uuid);
+        ovs_assert(local_lb);
+
+        struct hmap *lb_flows = is_switch ? local_lb->active_lswitch_lflows :
+                                local_lb->active_lrouter_lflows;
+
+        HMAP_FOR_EACH (f, hmap_node, lb_flows) {
+            ctrl_lflows[i++] = f;
+        }
+    }
+
+    ovs_assert(i == n_total_flows);
+
+    qsort(ctrl_lflows, n_total_flows, sizeof *ctrl_lflows,
+          sbctl_gen_lflow_cmp);
+
+    const struct ovn_ctrl_lflow *curr, *prev = NULL;
+    for (i = 0; i < n_total_flows; i++) {
+        curr = ctrl_lflows[i];
+
+        /* Print a header line for this datapath or pipeline, if we haven't
+         * already done so. */
+        if (!prev
+            || ovn_stage_get_pipeline(curr->stage) !=
+                ovn_stage_get_pipeline(prev->stage)) {
+            ds_put_cstr(&ctx->output, "Datapath: ");
+            print_datapath_name(dp, &ctx->output);
+            ds_put_format(&ctx->output, " ("UUID_FMT")  Pipeline: %s\n",
+                   UUID_ARGS(&dp->header_.uuid),
+                   ovn_stage_get_pipeline(curr->stage) == P_IN ?
+                   "ingress" : "egress");
+        }
+
+        /* Print the flow. */
+        ds_put_cstr(&ctx->output, "  ");
+        print_uuid_part(&curr->uuid_, print_uuid, &ctx->output);
+        ds_put_format(
+            &ctx->output, "table=%-2"PRId8"(%-19s), priority=%-5"PRId16
+            ", match=(%s), action=(%s)\n",
+            ovn_stage_get_table(curr->stage),
+            ovn_stage_to_str(curr->stage),
+            curr->priority, curr->match,
+            curr->actions);
+        prev = curr;
+    }
+
+    free(ctrl_lflows);
+}
+
+static void
+build_sbctl_datapaths(struct hmap *datapaths,
+                      struct hmap *lbs,
+                      const struct sbrec_datapath_binding *dp,
+                      struct ctl_context *ctx)
+{
+    if (!dp) {
+        SBREC_DATAPATH_BINDING_FOR_EACH (dp, ctx->idl) {
+           local_datapath_add(datapaths, dp, sbrec_datapath_binding_by_key,
+                              sbrec_port_binding_by_datapath,
+                              sbrec_port_binding_by_name, NULL, NULL);
+        }
+    } else {
+        local_datapath_add(datapaths, dp, sbrec_datapath_binding_by_key,
+                           sbrec_port_binding_by_datapath,
+                           sbrec_port_binding_by_name, NULL, NULL);
+    }
+
+    const struct sbrec_port_binding *pb;
+    SBREC_PORT_BINDING_FOR_EACH (pb, ctx->idl) {
+        struct local_datapath *ldp =
+            get_local_datapath(datapaths, pb->datapath->tunnel_key);
+        if (!ldp) {
+            continue;
+        }
+
+        local_datapath_add_lport(ldp, pb->logical_port, pb);
+    }
+
+    const struct sbrec_load_balancer *sb_lb;
+    SBREC_LOAD_BALANCER_FOR_EACH (sb_lb, ctx->idl) {
+        local_load_balancer_add(lbs, datapaths, sb_lb);
+    }
+}
+
+static void
+cmd_ctrl_lflow_list(struct ctl_context *ctx)
+{
+    struct hmap gen_lswitch_flows = HMAP_INITIALIZER(&gen_lswitch_flows);
+    struct hmap gen_lrouter_flows = HMAP_INITIALIZER(&gen_lrouter_flows);
+
+    build_lswitch_generic_lflows(&gen_lswitch_flows);
+    build_lrouter_generic_lflows(&gen_lrouter_flows);
+
+    bool print_uuid = shash_find(&ctx->options, "--uuid") != NULL;
+
+    const struct sbrec_datapath_binding *dp = NULL;
+    if (ctx->argc > 1) {
+        const struct ovsdb_idl_row *row;
+        char *error = ctl_get_row(ctx, &sbrec_table_datapath_binding,
+                                  ctx->argv[1], false, &row);
+        if (error) {
+            ctl_error(ctx, "%s", error);
+            free(error);
+            return;
+        }
+
+        dp = (const struct sbrec_datapath_binding *)row;
+        if (dp) {
+            ctx->argc--;
+            ctx->argv++;
+        }
+    }
+
+    struct hmap datapaths = HMAP_INITIALIZER(&datapaths);
+    struct hmap lbs = HMAP_INITIALIZER(&lbs);
+    build_sbctl_datapaths(&datapaths, &lbs, dp, ctx);
+
+    struct local_datapath *ldp;
+    HMAP_FOR_EACH (ldp, hmap_node, &datapaths) {
+        ovn_ctrl_lflows_build_dp_lflows(&ldp->ctrl_lflows[0], ldp);
+
+        struct shash_node *shash_node;
+        SHASH_FOR_EACH (shash_node, &ldp->lports) {
+            local_lport_update_cache(shash_node->data);
+            ovn_ctrl_build_lport_lflows(&ldp->ctrl_lflows[0],
+                                        shash_node->data);
+        }
+    }
+
+    struct local_load_balancer *lb;
+    HMAP_FOR_EACH (lb, hmap_node, &lbs) {
+        ovn_ctrl_build_lb_lflows(lb->active_lswitch_lflows,
+                                 lb->active_lrouter_lflows,
+                                 lb->ovn_lb);
+    }
+
+    HMAP_FOR_EACH (ldp, hmap_node, &datapaths) {
+        struct ovn_ctrl_lflow *lflow, *next;
+        HMAP_FOR_EACH_SAFE (lflow, next, hmap_node, &ldp->ctrl_lflows[0]) {
+            if (lflow->dp_key && lflow->dp_key != ldp->datapath->tunnel_key ) {
+                hmap_remove(&ldp->ctrl_lflows[0], &lflow->hmap_node);
+                struct local_datapath *other_ldp =
+                    get_local_datapath(&datapaths, lflow->dp_key);
+                if (other_ldp) {
+                    hmap_insert(&other_ldp->ctrl_lflows[0], &lflow->hmap_node,
+                                ovn_ctrl_lflow_hash(lflow));
+                }
+            }
+        }
+    }
+
+    HMAP_FOR_EACH (ldp, hmap_node, &datapaths) {
+        if (dp && ldp->datapath != dp) {
+            continue;
+        }
+
+        ctrl_lflow_list(&ldp->ctrl_lflows[0], ldp->is_switch ?
+                        &gen_lswitch_flows : &gen_lrouter_flows, &lbs,
+                        ldp->datapath, ldp->is_switch, print_uuid, ctx);
+    }
+
+    local_datapaths_destroy(&datapaths);
+    local_load_balancers_destroy(&lbs);
+    ovn_ctrl_lflows_destroy(&gen_lswitch_flows);
+    ovn_ctrl_lflows_destroy(&gen_lrouter_flows);
+}
+
 static void
 sbctl_ip_mcast_flush_switch(struct ctl_context *ctx,
                             const struct sbrec_datapath_binding *dp)
@@ -1387,6 +1639,9 @@ static const struct ctl_command_syntax sbctl_commands[] = {
      pre_get_info, cmd_lflow_list, NULL,
      "--uuid,--ovs?,--stats,--vflows?",
      RO}, /* Friendly alias for lflow-list */
+    {"ctrl-lflow-list", 0, INT_MAX, "[DATAPATH] [LFLOW...]",
+     pre_get_info, cmd_ctrl_lflow_list, NULL,
+     "--uuid,--ovs?,--stats,--vflows?", RO},
 
     /* IP multicast commands. */
     {"ip-multicast-flush", 0, 1, "SWITCH",
@@ -1425,6 +1680,7 @@ main(int argc, char *argv[])
         .commands = sbctl_commands,
 
         .usage = sbctl_usage,
+        .pre_idl_run = sbctl_pre_idl_run,
         .add_base_prerequisites = sbctl_add_base_prerequisites,
         .pre_execute = sbctl_pre_execute,
         .post_execute = NULL,
