This is similar to a recent change that refactored datapath syncing.
This works in a very similar way.

* Input nodes create type-agnostic ovn_unpaired_port_bindings. These are
  fed into the en-port-binding-pair node.
* The en-port-binding-pair node ensures that a southbound Port_Binding
  is created for each unpaired port binding. Any remaining soutbound
  Port_Bindings are deleted.
* Type-specific nodes then convert the paired port bindings into
  type-specific paired port bindings that can be consumed by other
  nodes.

However, there are some important differences to note between this and
the datapath sync patch:

* This patch opts for the word "pair" instead of "sync". This is because
  en-port-binding-pair ensures that all southbound Port_Bindings are
  paired with some northbound structure. However, the columns in the
  southobund Port_Binding are not all synced with their northd
  counterpart. Other engine nodes are responsible for fully syncing the
  data.
* Not all southbound Port_Bindings have a corresponding northbound row.
  Therefore, the engine nodes that create unpaired port bindings pass an
  opaque cookie pointer to the pairing node instead of a database row.
* Port bindings tend to be identified by name. This means the code has
  to have several safeguards in place to catch scenarios such as a port
  "moving" from one datapath to another, or a port being deleted and
  re-added quickly.
* Unlike with the datapath syncing code, this required changes to
  northd's incremental processing since northd was already incrementally
  processing some northbound logical switch port changes.

Signed-off-by: Mark Michelson <mmich...@redhat.com>
---
v3 -> v4:
 * Rebased.
 * Addressed newly-added mirror port functionality.
 * Fixed many memory leaks.

v3: This is the first version with this patch present.
---
 TODO.rst                                     |  12 +
 northd/automake.mk                           |  14 +-
 northd/en-global-config.c                    |   3 +
 northd/en-global-config.h                    |   1 +
 northd/en-northd.c                           |  49 +-
 northd/en-northd.h                           |   1 -
 northd/en-port-binding-chassisredirect.c     | 319 ++++++++
 northd/en-port-binding-chassisredirect.h     |  53 ++
 northd/en-port-binding-logical-router-port.c | 177 +++++
 northd/en-port-binding-logical-router-port.h |  47 ++
 northd/en-port-binding-logical-switch-port.c | 231 ++++++
 northd/en-port-binding-logical-switch-port.h |  48 ++
 northd/en-port-binding-mirror.c              | 189 +++++
 northd/en-port-binding-mirror.h              |  48 ++
 northd/en-port-binding-pair.c                | 467 ++++++++++++
 northd/en-port-binding-pair.h                |  32 +
 northd/inc-proc-northd.c                     |  62 +-
 northd/northd.c                              | 745 +++++--------------
 northd/northd.h                              |  17 +-
 northd/port_binding_pair.c                   |  81 ++
 northd/port_binding_pair.h                   | 117 +++
 21 files changed, 2126 insertions(+), 587 deletions(-)
 create mode 100644 northd/en-port-binding-chassisredirect.c
 create mode 100644 northd/en-port-binding-chassisredirect.h
 create mode 100644 northd/en-port-binding-logical-router-port.c
 create mode 100644 northd/en-port-binding-logical-router-port.h
 create mode 100644 northd/en-port-binding-logical-switch-port.c
 create mode 100644 northd/en-port-binding-logical-switch-port.h
 create mode 100644 northd/en-port-binding-mirror.c
 create mode 100644 northd/en-port-binding-mirror.h
 create mode 100644 northd/en-port-binding-pair.c
 create mode 100644 northd/en-port-binding-pair.h
 create mode 100644 northd/port_binding_pair.c
 create mode 100644 northd/port_binding_pair.h

diff --git a/TODO.rst b/TODO.rst
index 78962bb92..60ae155c5 100644
--- a/TODO.rst
+++ b/TODO.rst
@@ -168,3 +168,15 @@ OVN To-do List
     ovn\_synced\_logical_router and ovn\_synced\_logical\_switch. This will
     allow for the eventual removal of the ovn\_datapath structure from the
     codebase.
+
+* Port Binding sync nodes
+
+  * Southbound Port bindings are synced across three engine nodes:
+    - en_port_binding_pair
+    - en_northd
+    - en_sync_to_sb
+    It would be easier to work with if these were combined into a
+    single node instead.
+
+  * Add incremental processing to the en-port-binding-pair node, as
+    well as derivative nodes.
diff --git a/northd/automake.mk b/northd/automake.mk
index bf9978dd2..f475e0cd9 100644
--- a/northd/automake.mk
+++ b/northd/automake.mk
@@ -54,6 +54,16 @@ northd_ovn_northd_SOURCES = \
        northd/en-learned-route-sync.h \
        northd/en-group-ecmp-route.c \
        northd/en-group-ecmp-route.h \
+       northd/en-port-binding-logical-router-port.c \
+       northd/en-port-binding-logical-router-port.h \
+       northd/en-port-binding-logical-switch-port.c \
+       northd/en-port-binding-logical-switch-port.h \
+       northd/en-port-binding-chassisredirect.c \
+       northd/en-port-binding-chassisredirect.h \
+       northd/en-port-binding-mirror.c \
+       northd/en-port-binding-mirror.h \
+       northd/en-port-binding-pair.c \
+       northd/en-port-binding-pair.h \
        northd/inc-proc-northd.c \
        northd/inc-proc-northd.h \
        northd/ipam.c \
@@ -61,7 +71,9 @@ northd_ovn_northd_SOURCES = \
        northd/lflow-mgr.c \
        northd/lflow-mgr.h \
        northd/lb.c \
-       northd/lb.h
+       northd/lb.h \
+       northd/port_binding_pair.c \
+       northd/port_binding_pair.h
 northd_ovn_northd_LDADD = \
        lib/libovn.la \
        $(OVSDB_LIBDIR)/libovsdb.la \
diff --git a/northd/en-global-config.c b/northd/en-global-config.c
index b5410d556..addb09e3b 100644
--- a/northd/en-global-config.c
+++ b/northd/en-global-config.c
@@ -148,6 +148,9 @@ en_global_config_run(struct engine_node *node , void *data)
     config_data->max_dp_tunnel_id =
         get_ovn_max_dp_key_local(config_data->vxlan_mode, ic_vxlan_mode);
 
+    uint8_t pb_tunnel_bits = config_data->vxlan_mode ? 12 : 16;
+    config_data->max_pb_tunnel_id = (1u << (pb_tunnel_bits - 1)) - 1;
+
     char *max_tunid = xasprintf("%d", config_data->max_dp_tunnel_id);
     smap_replace(options, "max_tunid", max_tunid);
     free(max_tunid);
diff --git a/northd/en-global-config.h b/northd/en-global-config.h
index c37ddc2d9..5aac98e4a 100644
--- a/northd/en-global-config.h
+++ b/northd/en-global-config.h
@@ -51,6 +51,7 @@ struct ed_type_global_config {
 
     bool vxlan_mode;
     uint32_t max_dp_tunnel_id;
+    uint32_t max_pb_tunnel_id;
 
     bool tracked;
     struct global_config_tracked_data tracked_data;
diff --git a/northd/en-northd.c b/northd/en-northd.c
index 6f13660b4..314ec5776 100644
--- a/northd/en-northd.c
+++ b/northd/en-northd.c
@@ -123,6 +123,19 @@ northd_get_input_data(struct engine_node *node,
 
     input_data->synced_lrs =
         engine_get_input_data("datapath_synced_logical_router", node);
+
+    input_data->paired_lsps =
+        engine_get_input_data("port_binding_paired_logical_switch_port", node);
+
+    input_data->paired_lrps =
+        engine_get_input_data("port_binding_paired_logical_router_port", node);
+
+    input_data->paired_crps =
+        engine_get_input_data("port_binding_paired_chassisredirect_port",
+                              node);
+
+    input_data->paired_mirrors =
+        engine_get_input_data("port_binding_paired_mirror", node);
 }
 
 void
@@ -477,42 +490,6 @@ en_northd_clear_tracked_data(void *data_)
     destroy_northd_data_tracked_changes(data);
 }
 
-bool
-northd_sb_fdb_change_handler(struct engine_node *node, void *data)
-{
-    struct northd_data *nd = data;
-    const struct sbrec_fdb_table *sbrec_fdb_table =
-        EN_OVSDB_GET(engine_get_input("SB_fdb", node));
-
-    /* check if changed rows are stale and delete them */
-    const struct sbrec_fdb *fdb_e, *fdb_prev_del = NULL;
-    SBREC_FDB_TABLE_FOR_EACH_TRACKED (fdb_e, sbrec_fdb_table) {
-        if (sbrec_fdb_is_deleted(fdb_e)) {
-            continue;
-        }
-
-        if (fdb_prev_del) {
-            sbrec_fdb_delete(fdb_prev_del);
-        }
-
-        fdb_prev_del = fdb_e;
-        struct ovn_datapath *od
-            = ovn_datapath_find_by_key(&nd->ls_datapaths.datapaths,
-                                       fdb_e->dp_key);
-        if (od) {
-            if (ovn_tnlid_present(&od->port_tnlids, fdb_e->port_key)) {
-                fdb_prev_del = NULL;
-            }
-        }
-    }
-
-    if (fdb_prev_del) {
-        sbrec_fdb_delete(fdb_prev_del);
-    }
-
-    return true;
-}
-
 void
 en_route_policies_cleanup(void *data)
 {
diff --git a/northd/en-northd.h b/northd/en-northd.h
index b676a0ad3..20d37865e 100644
--- a/northd/en-northd.h
+++ b/northd/en-northd.h
@@ -19,7 +19,6 @@ bool northd_nb_logical_switch_handler(struct engine_node *, 
void *data);
 bool northd_nb_logical_router_handler(struct engine_node *, void *data);
 bool northd_sb_port_binding_handler(struct engine_node *, void *data);
 bool northd_lb_data_handler(struct engine_node *, void *data);
-bool northd_sb_fdb_change_handler(struct engine_node *node, void *data);
 void *en_routes_init(struct engine_node *node OVS_UNUSED,
                             struct engine_arg *arg OVS_UNUSED);
 void en_route_policies_cleanup(void *data);
diff --git a/northd/en-port-binding-chassisredirect.c 
b/northd/en-port-binding-chassisredirect.c
new file mode 100644
index 000000000..fc0b1d0dc
--- /dev/null
+++ b/northd/en-port-binding-chassisredirect.c
@@ -0,0 +1,319 @@
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <config.h>
+
+#include "inc-proc-eng.h"
+#include "ovn-nb-idl.h"
+#include "en-datapath-logical-router.h"
+#include "en-datapath-logical-switch.h"
+#include "en-port-binding-chassisredirect.h"
+#include "port_binding_pair.h"
+#include "ovn-util.h"
+
+#include "openvswitch/vlog.h"
+
+#include "hmapx.h"
+
+#define CR_SWITCH_PORT_TYPE "chassisredirect-logical-switch-port"
+#define CR_ROUTER_PORT_TYPE "chassisredirect-logical-router-port"
+
+VLOG_DEFINE_THIS_MODULE(en_port_binding_chassisredirect_port);
+
+struct router_dgps {
+    const struct ovn_synced_logical_router *lr;
+    const struct nbrec_logical_router_port **dgps;
+    size_t n_dgps;
+};
+
+struct switch_localnets {
+    const struct ovn_synced_logical_switch *ls;
+    size_t n_localnet_ports;
+};
+
+struct port_router_dgps {
+    const struct nbrec_logical_router_port *nbrp;
+    struct router_dgps *r;
+};
+
+static struct port_router_dgps *
+port_router_dgps_alloc(const struct nbrec_logical_router_port *nbrp,
+                       struct router_dgps *r)
+{
+    struct port_router_dgps *p_dgps = xmalloc(sizeof *p_dgps);
+    p_dgps->nbrp = nbrp;
+    p_dgps->r = r;
+
+    return p_dgps;
+}
+
+void *
+en_port_binding_chassisredirect_port_init(struct engine_node *node OVS_UNUSED,
+                                         struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+    return map;
+}
+
+struct chassisredirect_router_port {
+    char *name;
+    const struct nbrec_logical_router_port *nbrp;
+};
+
+static struct chassisredirect_router_port *
+chassisredirect_router_port_alloc(const struct nbrec_logical_router_port *nbrp)
+{
+    struct chassisredirect_router_port *crp = xmalloc(sizeof *crp);
+    crp->name = ovn_chassis_redirect_name(nbrp->name);
+    crp->nbrp = nbrp;
+
+    return crp;
+}
+
+static void
+chassisredirect_router_port_free(struct chassisredirect_router_port *crp)
+{
+    free(crp->name);
+    free(crp);
+}
+
+struct chassisredirect_switch_port {
+    char *name;
+    const struct nbrec_logical_switch_port *nbsp;
+};
+
+static struct chassisredirect_switch_port *
+chassisredirect_switch_port_alloc(const struct nbrec_logical_switch_port *nbsp)
+{
+    struct chassisredirect_switch_port *crp = xmalloc(sizeof *crp);
+    crp->name = ovn_chassis_redirect_name(nbsp->name);
+    crp->nbsp = nbsp;
+
+    return crp;
+}
+
+static void
+chassisredirect_switch_port_free(struct chassisredirect_switch_port *csp)
+{
+    free(csp->name);
+    free(csp);
+}
+
+static void
+chassisredirect_port_binding_map_destroy(
+    struct ovn_unpaired_port_binding_map *map)
+{
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &map->ports) {
+        struct ovn_unpaired_port_binding *upb = node->data;
+        if (!strcmp(upb->type, CR_ROUTER_PORT_TYPE)) {
+            chassisredirect_router_port_free(upb->cookie);
+        } else {
+            chassisredirect_switch_port_free(upb->cookie);
+        }
+    }
+    ovn_unpaired_port_binding_map_destroy(map);
+}
+
+void
+en_port_binding_chassisredirect_port_run(struct engine_node *node, void *data)
+{
+    const struct ovn_synced_logical_router_map *lr_map =
+        engine_get_input_data("datapath_synced_logical_router", node);
+    const struct ovn_synced_logical_switch_map *ls_map =
+        engine_get_input_data("datapath_synced_logical_switch", node);
+
+    struct ovn_unpaired_port_binding_map *map = data;
+
+    chassisredirect_port_binding_map_destroy(map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+
+    struct shash ports = SHASH_INITIALIZER(&ports);
+    const struct ovn_synced_logical_router *lr;
+    struct hmapx all_rdgps = HMAPX_INITIALIZER(&all_rdgps);
+    HMAP_FOR_EACH (lr, hmap_node, &lr_map->synced_routers) {
+        if (smap_get(&lr->nb->options, "chassis")) {
+            /* If the logical router has the chassis option set,
+             * then we ignore any ports that have gateway_chassis
+             * or ha_chassis_group options set.
+             */
+            continue;
+        }
+        struct router_dgps *rdgps = xzalloc(sizeof *rdgps);
+        rdgps->lr = lr;
+        rdgps->dgps = xzalloc(sizeof(*rdgps->dgps) * lr->nb->n_ports);
+        hmapx_add(&all_rdgps, rdgps);
+        const struct nbrec_logical_router_port *nbrp;
+        for (size_t i = 0; i < lr->nb->n_ports; i++) {
+            nbrp = lr->nb->ports[i];
+            if (nbrp->ha_chassis_group || nbrp->n_gateway_chassis) {
+                rdgps->dgps[rdgps->n_dgps++] = nbrp;
+                shash_add(&ports, nbrp->name,
+                          port_router_dgps_alloc(nbrp, rdgps));
+            }
+        }
+    }
+
+    struct hmapx all_localnets = HMAPX_INITIALIZER(&all_localnets);
+    const struct ovn_synced_logical_switch *ls;
+    HMAP_FOR_EACH (ls, hmap_node, &ls_map->synced_switches) {
+        struct switch_localnets *localnets = xzalloc(sizeof *localnets);
+        localnets->ls = ls;
+        hmapx_add(&all_localnets, localnets);
+        for (size_t i = 0; i < ls->nb->n_ports; i++) {
+            const struct nbrec_logical_switch_port *nbsp = ls->nb->ports[i];
+            if (!strcmp(nbsp->type, "localnet")) {
+                localnets->n_localnet_ports++;
+            }
+        }
+    }
+
+    /* All logical router DGPs need corresponding chassisredirect ports
+     * made
+     */
+    struct hmapx_node *hmapx_node;
+    HMAPX_FOR_EACH (hmapx_node, &all_rdgps) {
+        struct router_dgps *rdgps = hmapx_node->data;
+        struct ovn_unpaired_port_binding *upb;
+        for (size_t i = 0; i < rdgps->n_dgps; i++) {
+            const struct nbrec_logical_router_port *nbrp = rdgps->dgps[i];
+            struct chassisredirect_router_port *crp =
+                chassisredirect_router_port_alloc(nbrp);
+            upb = ovn_unpaired_port_binding_alloc(0, crp->name,
+                                                  CR_ROUTER_PORT_TYPE,
+                                                  crp, rdgps->lr->sb);
+            shash_add(&map->ports, crp->name, upb);
+        }
+    }
+
+    /* Logical switch ports that are peered with DGPs need chassisredirect
+     * ports created if
+     * 1. The DGP it is paired with is the only one on its router, and
+     * 2. There are no localnet ports on the switch
+     */
+    HMAPX_FOR_EACH (hmapx_node, &all_localnets) {
+        struct switch_localnets *localnets = hmapx_node->data;
+        if (localnets->n_localnet_ports > 0) {
+            continue;
+        }
+        for (size_t i = 0; i < localnets->ls->nb->n_ports; i++) {
+            const struct nbrec_logical_switch_port *nbsp =
+                localnets->ls->nb->ports[i];
+            if (strcmp(nbsp->type, "router")) {
+                continue;
+            }
+            const char *peer_name = smap_get(&nbsp->options, "router-port");
+            if (!peer_name) {
+                continue;
+            }
+            struct port_router_dgps *prdgps = shash_find_data(&ports,
+                                                              peer_name);
+            if (!prdgps) {
+                continue;
+            }
+            if (prdgps->r->n_dgps > 1) {
+                continue;
+            }
+            struct ovn_unpaired_port_binding *upb;
+            struct chassisredirect_switch_port *crp =
+                chassisredirect_switch_port_alloc(nbsp);
+            upb = ovn_unpaired_port_binding_alloc(0, crp->name,
+                                                  CR_SWITCH_PORT_TYPE,
+                                                  crp, localnets->ls->sb);
+            shash_add(&map->ports, crp->name, upb);
+        }
+    }
+
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+void
+en_port_binding_chassisredirect_port_cleanup(void *data)
+{
+    struct ovn_unpaired_port_binding_map *map = data;
+    chassisredirect_port_binding_map_destroy(map);
+}
+
+
+static void
+ovn_paired_chassisredirect_port_map_init(
+    struct ovn_paired_chassisredirect_port_map *map)
+{
+    shash_init(&map->paired_chassisredirect_router_ports);
+    shash_init(&map->paired_chassisredirect_switch_ports);
+}
+
+static void
+ovn_paired_chassisredirect_port_map_destroy(
+    struct ovn_paired_chassisredirect_port_map *map)
+{
+    shash_destroy_free_data(&map->paired_chassisredirect_switch_ports);
+    shash_destroy_free_data(&map->paired_chassisredirect_router_ports);
+}
+
+void *
+en_port_binding_paired_chassisredirect_port_init(
+    struct engine_node *node OVS_UNUSED, struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_paired_chassisredirect_port_map *map = xzalloc(sizeof *map);
+    ovn_paired_chassisredirect_port_map_init(map);
+    return map;
+}
+
+void
+en_port_binding_paired_chassisredirect_port_run(struct engine_node *node,
+                                                void *data)
+{
+    const struct ovn_paired_port_bindings *pbs =
+        engine_get_input_data("port_binding_pair", node);
+    struct ovn_paired_chassisredirect_port_map *map = data;
+
+    ovn_paired_chassisredirect_port_map_destroy(map);
+    ovn_paired_chassisredirect_port_map_init(map);
+
+    struct ovn_paired_port_binding *port;
+    LIST_FOR_EACH (port, list_node, &pbs->paired_pbs) {
+        if (!strcmp(port->type, CR_SWITCH_PORT_TYPE)) {
+            const struct chassisredirect_switch_port *cr_port = port->cookie;
+            struct ovn_paired_chassisredirect_switch_port *paired_cr_port;
+            paired_cr_port = xmalloc(sizeof *cr_port);
+            paired_cr_port->name = cr_port->name;
+            paired_cr_port->sb = port->sb_pb;
+            paired_cr_port->primary_port = cr_port->nbsp;
+            shash_add(&map->paired_chassisredirect_switch_ports, cr_port->name,
+                      paired_cr_port);
+        } else if (!strcmp(port->type, CR_ROUTER_PORT_TYPE)) {
+            const struct chassisredirect_router_port *cr_port = port->cookie;
+            struct ovn_paired_chassisredirect_router_port *paired_cr_port;
+            paired_cr_port = xmalloc(sizeof *cr_port);
+            paired_cr_port->name = cr_port->name;
+            paired_cr_port->sb = port->sb_pb;
+            paired_cr_port->primary_port = cr_port->nbrp;
+            shash_add(&map->paired_chassisredirect_router_ports, cr_port->name,
+                      paired_cr_port);
+        }
+    }
+
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+void
+en_port_binding_paired_chassisredirect_port_cleanup(void *data)
+{
+    struct ovn_paired_chassisredirect_port_map *map = data;
+
+    ovn_paired_chassisredirect_port_map_destroy(map);
+}
diff --git a/northd/en-port-binding-chassisredirect.h 
b/northd/en-port-binding-chassisredirect.h
new file mode 100644
index 000000000..b316681db
--- /dev/null
+++ b/northd/en-port-binding-chassisredirect.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2025, 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 EN_PORT_BINDING_CHASSISREDIRECT_PORT_H
+#define EN_PORT_BINDING_CHASSISREDIRECT_PORT_H
+
+#include "lib/inc-proc-eng.h"
+#include "openvswitch/shash.h"
+
+
+void *en_port_binding_chassisredirect_port_init(struct engine_node *,
+                                           struct engine_arg *);
+
+void en_port_binding_chassisredirect_port_run(struct engine_node *,
+                                         void *data);
+void en_port_binding_chassisredirect_port_cleanup(void *data);
+
+struct ovn_paired_chassisredirect_switch_port {
+    const char *name;
+    const struct nbrec_logical_switch_port *primary_port;
+    const struct sbrec_port_binding *sb;
+};
+
+struct ovn_paired_chassisredirect_router_port {
+    const char *name;
+    const struct nbrec_logical_router_port *primary_port;
+    const struct sbrec_port_binding *sb;
+};
+
+struct ovn_paired_chassisredirect_port_map {
+    struct shash paired_chassisredirect_router_ports;
+    struct shash paired_chassisredirect_switch_ports;
+};
+
+void *en_port_binding_paired_chassisredirect_port_init(struct engine_node *,
+                                                       struct engine_arg *);
+void en_port_binding_paired_chassisredirect_port_run(struct engine_node *,
+                                                     void *data);
+void en_port_binding_paired_chassisredirect_port_cleanup(void *data);
+#endif /* EN_PORT_BINDING_CHASSISREDIRECT_PORT_H */
diff --git a/northd/en-port-binding-logical-router-port.c 
b/northd/en-port-binding-logical-router-port.c
new file mode 100644
index 000000000..df9d12eb8
--- /dev/null
+++ b/northd/en-port-binding-logical-router-port.c
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "openvswitch/hmap.h"
+#include "openvswitch/vlog.h"
+#include "util.h"
+
+#include "inc-proc-eng.h"
+#include "ovn-nb-idl.h"
+#include "port_binding_pair.h"
+#include "en-datapath-logical-router.h"
+#include "en-port-binding-logical-router-port.h"
+
+#define LRP_TYPE_NAME "logical-router-port"
+
+VLOG_DEFINE_THIS_MODULE(en_port_binding_logical_router_port);
+
+struct logical_router_port_cookie {
+    const struct nbrec_logical_router_port *nbrp;
+    const struct ovn_synced_logical_router *router;
+};
+
+static struct logical_router_port_cookie *
+logical_router_port_cookie_alloc(const struct nbrec_logical_router_port *nbrp,
+                                 const struct ovn_synced_logical_router *lr)
+{
+    struct logical_router_port_cookie *cookie = xmalloc(sizeof *cookie);
+    cookie->nbrp = nbrp;
+    cookie->router = lr;
+
+    return cookie;
+}
+
+static void
+logical_router_port_cookie_free(struct logical_router_port_cookie *cookie)
+{
+    free(cookie);
+}
+
+static void
+unpaired_logical_router_port_map_destroy(struct ovn_unpaired_port_binding_map 
*map)
+{
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &map->ports) {
+        struct ovn_unpaired_port_binding *upb = node->data;
+        logical_router_port_cookie_free(upb->cookie);
+    }
+    ovn_unpaired_port_binding_map_destroy(map);
+}
+
+void *
+en_port_binding_logical_router_port_init(struct engine_node *node OVS_UNUSED,
+                                         struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+    return map;
+}
+
+void
+en_port_binding_logical_router_port_run(struct engine_node *node, void *data)
+{
+    const struct ovn_synced_logical_router_map *lr_map =
+        engine_get_input_data("datapath_synced_logical_router", node);
+
+    struct ovn_unpaired_port_binding_map *map = data;
+
+    unpaired_logical_router_port_map_destroy(map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+
+    const struct ovn_synced_logical_router *paired_lr;
+    HMAP_FOR_EACH (paired_lr, hmap_node, &lr_map->synced_routers) {
+        const struct nbrec_logical_router_port *nbrp;
+        for (size_t i = 0; i < paired_lr->nb->n_ports; i++) {
+            nbrp = paired_lr->nb->ports[i];
+            uint32_t requested_tunnel_key = smap_get_int(&nbrp->options,
+                                                         "requested-tnl-key",
+                                                         0);
+            struct logical_router_port_cookie *cookie =
+                logical_router_port_cookie_alloc(nbrp, paired_lr);
+            struct ovn_unpaired_port_binding *upb;
+            upb = ovn_unpaired_port_binding_alloc(requested_tunnel_key,
+                                                  nbrp->name,
+                                                  LRP_TYPE_NAME,
+                                                  cookie,
+                                                  paired_lr->sb);
+            smap_clone(&upb->external_ids, &nbrp->external_ids);
+            if (!shash_add_once(&map->ports, nbrp->name, upb)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "duplicate logical router port %s",
+                             nbrp->name);
+            }
+        }
+    }
+
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+void
+en_port_binding_logical_router_port_cleanup(void *data)
+{
+    struct ovn_unpaired_port_binding_map *map = data;
+    unpaired_logical_router_port_map_destroy(map);
+}
+
+static void
+paired_logical_router_port_map_init(
+    struct ovn_paired_logical_router_port_map *router_port_map)
+{
+    shash_init(&router_port_map->paired_router_ports);
+}
+
+static void
+paired_logical_router_port_map_destroy(
+    struct ovn_paired_logical_router_port_map *router_port_map)
+{
+    shash_destroy_free_data(&router_port_map->paired_router_ports);
+}
+
+void *
+en_port_binding_paired_logical_router_port_init(
+    struct engine_node *node OVS_UNUSED, struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_paired_logical_router_port_map *router_port_map;
+    router_port_map = xzalloc(sizeof *router_port_map);
+    paired_logical_router_port_map_init(router_port_map);
+
+    return router_port_map;
+}
+
+void
+en_port_binding_paired_logical_router_port_run(struct engine_node *node,
+                                               void *data)
+{
+    const struct ovn_paired_port_bindings *pbs =
+        engine_get_input_data("port_binding_pair", node);
+    struct ovn_paired_logical_router_port_map *router_port_map = data;
+
+    paired_logical_router_port_map_destroy(router_port_map);
+    paired_logical_router_port_map_init(router_port_map);
+
+    struct ovn_paired_port_binding *spb;
+    LIST_FOR_EACH (spb, list_node, &pbs->paired_pbs) {
+        if (strcmp(spb->type, LRP_TYPE_NAME)) {
+            continue;
+        }
+        const struct logical_router_port_cookie *cookie = spb->cookie;
+        struct ovn_paired_logical_router_port *lrp = xzalloc(sizeof *lrp);
+        lrp->nb = cookie->nbrp;
+        lrp->router = cookie->router;
+        lrp->sb = spb->sb_pb;
+        shash_add(&router_port_map->paired_router_ports, lrp->nb->name, lrp);
+    }
+
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+void en_port_binding_paired_logical_router_port_cleanup(void *data)
+{
+    struct ovn_paired_logical_router_port_map *map = data;
+    paired_logical_router_port_map_destroy(map);
+}
diff --git a/northd/en-port-binding-logical-router-port.h 
b/northd/en-port-binding-logical-router-port.h
new file mode 100644
index 000000000..21ac02ff0
--- /dev/null
+++ b/northd/en-port-binding-logical-router-port.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2025, 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 EN_PORT_BINDING_LOGICAL_ROUTER_PORT_H
+#define EN_PORT_BINDING_LOGICAL_ROUTER_PORT_H
+
+#include "lib/inc-proc-eng.h"
+#include "openvswitch/shash.h"
+
+void *en_port_binding_logical_router_port_init(struct engine_node *,
+                                           struct engine_arg *);
+
+void en_port_binding_logical_router_port_run(struct engine_node *,
+                                         void *data);
+void en_port_binding_logical_router_port_cleanup(void *data);
+
+struct ovn_paired_logical_router_port {
+    const struct nbrec_logical_router_port *nb;
+    const struct sbrec_port_binding *sb;
+    const struct ovn_synced_logical_router *router;
+};
+
+struct ovn_paired_logical_router_port_map {
+    struct shash paired_router_ports;
+};
+
+void *en_port_binding_paired_logical_router_port_init(struct engine_node *,
+                                                      struct engine_arg *);
+
+void en_port_binding_paired_logical_router_port_run(struct engine_node *,
+                                                    void *data);
+void en_port_binding_paired_logical_router_port_cleanup(void *data);
+
+#endif /* EN_PORT_BINDING_LOGICAL_ROUTER_PORT_H */
diff --git a/northd/en-port-binding-logical-switch-port.c 
b/northd/en-port-binding-logical-switch-port.c
new file mode 100644
index 000000000..5c2e1de40
--- /dev/null
+++ b/northd/en-port-binding-logical-switch-port.c
@@ -0,0 +1,231 @@
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "openvswitch/hmap.h"
+#include "openvswitch/vlog.h"
+#include "util.h"
+
+#include "inc-proc-eng.h"
+#include "ovn-nb-idl.h"
+#include "ovn-sb-idl.h"
+#include "port_binding_pair.h"
+#include "en-datapath-logical-switch.h"
+#include "en-port-binding-logical-switch-port.h"
+#include "northd.h"
+
+#define LSP_TYPE_NAME "logical-switch-port"
+
+VLOG_DEFINE_THIS_MODULE(en_port_binding_logical_switch_port);
+
+struct logical_switch_port_cookie {
+    const struct nbrec_logical_switch_port *nbsp;
+    const struct ovn_synced_logical_switch *sw;
+};
+
+static struct logical_switch_port_cookie *
+logical_switch_port_cookie_alloc(const struct nbrec_logical_switch_port *nbsp,
+                                 const struct ovn_synced_logical_switch *sw)
+{
+    struct logical_switch_port_cookie *cookie = xmalloc(sizeof *cookie);
+    cookie->nbsp = nbsp;
+    cookie->sw = sw;
+    return cookie;
+}
+
+static void
+logical_switch_port_cookie_free(struct logical_switch_port_cookie *cookie)
+{
+    free(cookie);
+}
+
+static bool
+switch_port_sb_is_valid(const struct sbrec_port_binding *sb_pb,
+                        const struct ovn_unpaired_port_binding *upb)
+{
+    const struct logical_switch_port_cookie *cookie = upb->cookie;
+
+    bool update_sbrec = false;
+    if (lsp_is_type_changed(sb_pb, cookie->nbsp, &update_sbrec)
+        && update_sbrec) {
+        return false;
+    }
+
+    return true;
+}
+
+struct ovn_unpaired_port_binding_map_callbacks switch_port_callbacks = {
+    .sb_is_valid = switch_port_sb_is_valid,
+};
+
+static void
+unpaired_logical_switch_port_map_destroy(
+    struct ovn_unpaired_port_binding_map *map)
+{
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &map->ports) {
+        struct ovn_unpaired_port_binding *upb = node->data;
+        logical_switch_port_cookie_free(upb->cookie);
+    }
+    ovn_unpaired_port_binding_map_destroy(map);
+}
+
+void *
+en_port_binding_logical_switch_port_init(struct engine_node *node OVS_UNUSED,
+                                         struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
+    ovn_unpaired_port_binding_map_init(map, &switch_port_callbacks);
+    return map;
+}
+
+void
+en_port_binding_logical_switch_port_run(struct engine_node *node, void *data)
+{
+    const struct ovn_synced_logical_switch_map *ls_map =
+        engine_get_input_data("datapath_synced_logical_switch", node);
+
+    struct ovn_unpaired_port_binding_map *map = data;
+
+    unpaired_logical_switch_port_map_destroy(map);
+    ovn_unpaired_port_binding_map_init(map, &switch_port_callbacks);
+
+    const struct ovn_synced_logical_switch *paired_ls;
+    HMAP_FOR_EACH (paired_ls, hmap_node, &ls_map->synced_switches) {
+        const struct nbrec_logical_switch_port *nbsp;
+        for (size_t i = 0; i < paired_ls->nb->n_ports; i++) {
+            nbsp = paired_ls->nb->ports[i];
+            uint32_t requested_tunnel_key = smap_get_int(&nbsp->options,
+                                                         "requested-tnl-key",
+                                                         0);
+            struct logical_switch_port_cookie *cookie =
+                logical_switch_port_cookie_alloc(nbsp, paired_ls);
+            struct ovn_unpaired_port_binding *upb;
+            upb = ovn_unpaired_port_binding_alloc(requested_tunnel_key,
+                                                  nbsp->name,
+                                                  LSP_TYPE_NAME,
+                                                  cookie,
+                                                  paired_ls->sb);
+            smap_clone(&upb->external_ids, &nbsp->external_ids);
+            const char *name = smap_get(&nbsp->external_ids,
+                                        "neutron:port_name");
+            if (name && name[0]) {
+                smap_add(&upb->external_ids, "name", name);
+            }
+            if (!shash_add_once(&map->ports, nbsp->name, upb)) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "duplicate logical port %s", nbsp->name);
+            }
+        }
+    }
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+void
+en_port_binding_logical_switch_port_cleanup(void *data)
+{
+    struct ovn_unpaired_port_binding_map *map = data;
+    unpaired_logical_switch_port_map_destroy(map);
+}
+
+static void
+paired_logical_switch_port_map_init(
+    struct ovn_paired_logical_switch_port_map *switch_port_map)
+{
+    shash_init(&switch_port_map->paired_switch_ports);
+}
+
+static void
+paired_logical_switch_port_map_destroy(
+    struct ovn_paired_logical_switch_port_map *switch_port_map)
+{
+    shash_destroy_free_data(&switch_port_map->paired_switch_ports);
+}
+
+void *
+en_port_binding_paired_logical_switch_port_init(
+    struct engine_node *node OVS_UNUSED, struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_paired_logical_switch_port_map *switch_port_map;
+    switch_port_map = xzalloc(sizeof *switch_port_map);
+    paired_logical_switch_port_map_init(switch_port_map);
+
+    return switch_port_map;
+}
+
+void
+en_port_binding_paired_logical_switch_port_run(struct engine_node *node,
+                                               void *data)
+{
+    const struct ovn_paired_port_bindings *pbs =
+        engine_get_input_data("port_binding_pair", node);
+    struct ovn_paired_logical_switch_port_map *switch_port_map = data;
+
+    paired_logical_switch_port_map_destroy(switch_port_map);
+    paired_logical_switch_port_map_init(switch_port_map);
+
+    struct ovn_paired_port_binding *spb;
+    LIST_FOR_EACH (spb, list_node, &pbs->paired_pbs) {
+        if (strcmp(spb->type, LSP_TYPE_NAME)) {
+            continue;
+        }
+        const struct logical_switch_port_cookie *cookie = spb->cookie;
+        struct ovn_paired_logical_switch_port *lsw = xzalloc(sizeof *lsw);
+        lsw->nb = cookie->nbsp;
+        lsw->sw = cookie->sw;
+        lsw->sb = spb->sb_pb;
+        shash_add(&switch_port_map->paired_switch_ports, lsw->nb->name, lsw);
+
+        /* This deals with a special case where a logical switch port is
+         * removed and added back very quickly. The sequence of events is as
+         * follows:
+         * 1) NB Logical_Switch_Port "lsp" is added to the NB DB.
+         * 2) en-port-binding-pair creates a corresponding SB Port_Binding.
+         * 3) The user binds the port to a hypervisor.
+         * 4) ovn-controller on the hypervisor sets the SB Port_Binding "up"
+         *    column to "true".
+         * 5) ovn-northd sets the Logical_Switch_Port "up" column to "true".
+         * 6) A user deletes and then re-adds "lsp" back to the NB
+         *    Logical_Switch_Port column very quickly, so quickly that we
+         *    do not detect the deletion at all.
+         * 7) The new northbound Logical_Switch_Port has its "up" column
+         *    empty (i.e. not "true") since it is new.
+         * 8) The pairing code matches the new Logical_Switch_Port with the
+         *    existing Port_Binding for "lsp" since the pairing code matches
+         *    using the name of the Logical_Switch_Port.
+         *
+         * At this point, the SB Port_Binding's "up" column is set "true",
+         * but the NB Logical_Switch_Port's "up" column is not. We need to
+         * ensure the NB Logical_Switch_Port's "up" column is set to "true"
+         * as well.
+         *
+         * In most cases, setting the NB Logical_Switch_Port "up" column to
+         * true is accomplished when changes on the SB Port_Binding are
+         * detected. But in this rare case, there is no SB Port_Binding
+         * change, so the "up" column is unserviced.
+         */
+        lsp_set_up(lsw->sb, lsw->nb);
+    }
+
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+void en_port_binding_paired_logical_switch_port_cleanup(void *data)
+{
+    struct ovn_paired_logical_switch_port_map *map = data;
+    paired_logical_switch_port_map_destroy(map);
+}
diff --git a/northd/en-port-binding-logical-switch-port.h 
b/northd/en-port-binding-logical-switch-port.h
new file mode 100644
index 000000000..6227b04d1
--- /dev/null
+++ b/northd/en-port-binding-logical-switch-port.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2025, 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 EN_PORT_BINDING_LOGICAL_SWITCH_PORT_H
+#define EN_PORT_BINDING_LOGICAL_SWITCH_PORT_H
+
+#include "lib/inc-proc-eng.h"
+#include "openvswitch/shash.h"
+
+
+void *en_port_binding_logical_switch_port_init(struct engine_node *,
+                                           struct engine_arg *);
+
+void en_port_binding_logical_switch_port_run(struct engine_node *,
+                                         void *data);
+void en_port_binding_logical_switch_port_cleanup(void *data);
+
+struct ovn_paired_logical_switch_port {
+    const struct nbrec_logical_switch_port *nb;
+    const struct sbrec_port_binding *sb;
+    const struct ovn_synced_logical_switch *sw;
+};
+
+struct ovn_paired_logical_switch_port_map {
+    struct shash paired_switch_ports;
+};
+
+void *en_port_binding_paired_logical_switch_port_init(struct engine_node *,
+                                                      struct engine_arg *);
+
+void en_port_binding_paired_logical_switch_port_run(struct engine_node *,
+                                                    void *data);
+void en_port_binding_paired_logical_switch_port_cleanup(void *data);
+
+#endif /* EN_PORT_BINDING_LOGICAL_SWITCH_PORT_H */
diff --git a/northd/en-port-binding-mirror.c b/northd/en-port-binding-mirror.c
new file mode 100644
index 000000000..e5bc8396c
--- /dev/null
+++ b/northd/en-port-binding-mirror.c
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <config.h>
+#include "ovn-util.h"
+#include "lib/inc-proc-eng.h"
+#include "ovn-nb-idl.h"
+#include "en-datapath-logical-switch.h"
+#include "en-port-binding-mirror.h"
+#include "port_binding_pair.h"
+#include "northd.h"
+#include "openvswitch/vlog.h"
+
+#define MIRROR_PORT_TYPE "mirror"
+
+VLOG_DEFINE_THIS_MODULE(en_port_binding_mirror);
+
+struct mirror_port {
+    char *name;
+    const char *sink;
+    const struct nbrec_logical_switch_port *nbsp;
+};
+
+static struct mirror_port *
+mirror_port_alloc(const struct sbrec_datapath_binding *sb, const char *sink,
+                  const struct nbrec_logical_switch_port *nbsp)
+{
+    struct mirror_port *mp = xzalloc(sizeof *mp);
+    mp->name = ovn_mirror_port_name(ovn_datapath_name(sb), sink);
+    mp->sink = sink;
+    mp->nbsp = nbsp;
+
+    return mp;
+}
+
+static void
+mirror_port_free(struct mirror_port *mp)
+{
+    free(mp->name);
+    free(mp);
+}
+
+static void
+unpaired_mirror_map_destroy(struct ovn_unpaired_port_binding_map *map)
+{
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &map->ports) {
+        struct ovn_unpaired_port_binding *upb = node->data;
+        mirror_port_free(upb->cookie);
+    }
+    ovn_unpaired_port_binding_map_destroy(map);
+}
+
+void *en_port_binding_mirror_init(struct engine_node *node OVS_UNUSED,
+                                  struct engine_arg *arg OVS_UNUSED)
+{
+    struct ovn_unpaired_port_binding_map *map = xzalloc(sizeof *map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+    return map;
+}
+
+void en_port_binding_mirror_run(struct engine_node *node,
+                                void *data)
+{
+    const struct ovn_synced_logical_switch_map *ls_map =
+        engine_get_input_data("datapath_synced_logical_switch", node);
+    struct ovn_unpaired_port_binding_map *map = data;
+
+    unpaired_mirror_map_destroy(map);
+    ovn_unpaired_port_binding_map_init(map, NULL);
+
+    /* Typically, we'd use an ovsdb_idl_index to search for a specific record
+     * based on a column value. However, we currently are not monitoring
+     * the Logical_Switch_Port table at all in ovn-northd. Introducing
+     * this monitoring is likely more computationally intensive than
+     * making an on-the-fly sset of logical switch port names.
+     */
+    struct sset all_switch_ports = SSET_INITIALIZER(&all_switch_ports);
+    const struct ovn_synced_logical_switch *ls;
+    HMAP_FOR_EACH (ls, hmap_node, &ls_map->synced_switches) {
+        for (size_t i = 0; i < ls->nb->n_ports; i++) {
+            sset_add(&all_switch_ports, ls->nb->ports[i]->name);
+        }
+    }
+
+    HMAP_FOR_EACH (ls, hmap_node, &ls_map->synced_switches) {
+        for (size_t i = 0; i < ls->nb->n_ports; i++) {
+            const struct nbrec_logical_switch_port *nbsp = ls->nb->ports[i];
+            for (size_t j = 0; j < nbsp->n_mirror_rules; j++) {
+                struct nbrec_mirror *nb_mirror = nbsp->mirror_rules[j];
+                if (strcmp(nb_mirror->type, "lport")) {
+                    continue;
+                }
+                if (!sset_find(&all_switch_ports, nb_mirror->sink)) {
+                    continue;
+                }
+                struct mirror_port *mp = mirror_port_alloc(ls->sb,
+                                                           nb_mirror->sink,
+                                                           nbsp);
+                struct ovn_unpaired_port_binding *upb;
+                upb = ovn_unpaired_port_binding_alloc(0, mp->name,
+                                                      MIRROR_PORT_TYPE, mp,
+                                                      ls->sb);
+                shash_add(&map->ports, mp->name, upb);
+            }
+        }
+    }
+    sset_destroy(&all_switch_ports);
+
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+void en_port_binding_mirror_cleanup(void *data)
+{
+    struct ovn_unpaired_port_binding_map *map = data;
+    unpaired_mirror_map_destroy(map);
+}
+
+static void
+ovn_paired_mirror_map_init(
+    struct ovn_paired_mirror_map *map)
+{
+    shash_init(&map->paired_mirror_ports);
+}
+
+static void
+ovn_paired_mirror_map_destroy(
+    struct ovn_paired_mirror_map *map)
+{
+    shash_destroy_free_data(&map->paired_mirror_ports);
+}
+
+void *
+en_port_binding_paired_mirror_init(struct engine_node *node OVS_UNUSED,
+                                   struct engine_arg *arg OVS_UNUSED)
+{
+    struct ovn_paired_mirror_map *map = xzalloc(sizeof *map);
+    ovn_paired_mirror_map_init(map);
+    return map;
+}
+
+void
+en_port_binding_paired_mirror_run(struct engine_node *node,
+                                  void *data)
+{
+    const struct ovn_paired_port_bindings *pbs =
+        engine_get_input_data("port_binding_pair", node);
+    struct ovn_paired_mirror_map *map = data;
+
+    ovn_paired_mirror_map_destroy(map);
+    ovn_paired_mirror_map_init(map);
+
+    struct ovn_paired_port_binding *port;
+    LIST_FOR_EACH (port, list_node, &pbs->paired_pbs) {
+        if (strcmp(port->type, MIRROR_PORT_TYPE)) {
+            continue;
+        }
+        const struct mirror_port *mp = port->cookie;
+        struct ovn_paired_mirror *opm = xmalloc(sizeof *opm);
+        opm->name = mp->name;
+        opm->sink = mp->sink;
+        opm->sb = port->sb_pb;
+        opm->nbsp = mp->nbsp;
+        shash_add(&map->paired_mirror_ports, opm->name, opm);
+    }
+
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+void
+en_port_binding_paired_mirror_cleanup(void *data)
+{
+    struct ovn_paired_mirror_map *map = data;
+
+    ovn_paired_mirror_map_destroy(map);
+}
+
diff --git a/northd/en-port-binding-mirror.h b/northd/en-port-binding-mirror.h
new file mode 100644
index 000000000..36ff8e1db
--- /dev/null
+++ b/northd/en-port-binding-mirror.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2025, 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 EN_PORT_BINDING_MIRROR_H
+#define EN_PORT_BINDING_MIRROR_H
+
+#include "lib/inc-proc-eng.h"
+#include "openvswitch/shash.h"
+
+void *en_port_binding_mirror_init(struct engine_node *,
+                                           struct engine_arg *);
+
+void en_port_binding_mirror_run(struct engine_node *,
+                                         void *data);
+void en_port_binding_mirror_cleanup(void *data);
+
+struct ovn_paired_mirror {
+    const char *name;
+    const char *sink;
+    const struct nbrec_logical_switch_port *nbsp;
+    const struct sbrec_port_binding *sb;
+};
+
+struct ovn_paired_mirror_map {
+    struct shash paired_mirror_ports;
+};
+
+void *en_port_binding_paired_mirror_init(struct engine_node *,
+                                                      struct engine_arg *);
+
+void en_port_binding_paired_mirror_run(struct engine_node *,
+                                                    void *data);
+void en_port_binding_paired_mirror_cleanup(void *data);
+
+#endif /* EN_PORT_BINDING_MIRROR_H */
diff --git a/northd/en-port-binding-pair.c b/northd/en-port-binding-pair.c
new file mode 100644
index 000000000..af8d3e295
--- /dev/null
+++ b/northd/en-port-binding-pair.c
@@ -0,0 +1,467 @@
+/*
+ * Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "en-port-binding-pair.h"
+#include "en-global-config.h"
+#include "port_binding_pair.h"
+#include "ovn-sb-idl.h"
+#include "mcast-group-index.h"
+
+#include "openvswitch/vlog.h"
+
+VLOG_DEFINE_THIS_MODULE(port_binding_pair);
+
+void *
+en_port_binding_pair_init(struct engine_node *node OVS_UNUSED,
+                      struct engine_arg *args OVS_UNUSED)
+{
+    struct ovn_paired_port_bindings *paired_port_bindings
+        = xzalloc(sizeof *paired_port_bindings);
+    ovs_list_init(&paired_port_bindings->paired_pbs);
+    hmap_init(&paired_port_bindings->tunnel_key_maps);
+
+    return paired_port_bindings;
+}
+
+static struct ovn_unpaired_port_binding *
+find_unpaired_port_binding(const struct ovn_unpaired_port_binding_map **maps,
+                           size_t n_maps,
+                           const struct sbrec_port_binding *sb_pb)
+{
+    const struct ovn_unpaired_port_binding_map *map;
+
+    for (size_t i = 0; i < n_maps; i++) {
+        map = maps[i];
+        struct ovn_unpaired_port_binding *upb;
+        upb = shash_find_data(&map->ports, sb_pb->logical_port);
+        if (upb && map->cb->sb_is_valid(sb_pb, upb)) {
+            return upb;
+        }
+    }
+
+    return NULL;
+}
+
+struct tunnel_key_map {
+    struct hmap_node hmap_node;
+    uint32_t datapath_tunnel_key;
+    struct hmap port_tunnel_keys;
+};
+
+static struct tunnel_key_map *
+find_tunnel_key_map(uint32_t datapath_tunnel_key,
+                    const struct hmap *tunnel_key_maps)
+{
+    uint32_t hash = hash_int(datapath_tunnel_key, 0);
+    struct tunnel_key_map *key_map;
+    HMAP_FOR_EACH_WITH_HASH (key_map, hmap_node, hash, tunnel_key_maps) {
+        if (key_map->datapath_tunnel_key == datapath_tunnel_key) {
+            return key_map;
+        }
+    }
+    return NULL;
+}
+
+static struct tunnel_key_map *
+alloc_tunnel_key_map(uint32_t datapath_tunnel_key,
+                     struct hmap *tunnel_key_maps)
+{
+    uint32_t hash = hash_int(datapath_tunnel_key, 0);
+    struct tunnel_key_map *key_map;
+
+    key_map = xzalloc(sizeof *key_map);
+    key_map->datapath_tunnel_key = datapath_tunnel_key;
+    hmap_init(&key_map->port_tunnel_keys);
+    hmap_insert(tunnel_key_maps, &key_map->hmap_node, hash);
+
+    return key_map;
+
+}
+
+static struct tunnel_key_map *
+find_or_alloc_tunnel_key_map(const struct sbrec_datapath_binding *sb_dp,
+                             struct hmap *tunnel_key_maps)
+{
+    struct tunnel_key_map *key_map = find_tunnel_key_map(sb_dp->tunnel_key,
+                                                         tunnel_key_maps);
+    if (!key_map) {
+        key_map = alloc_tunnel_key_map(sb_dp->tunnel_key, tunnel_key_maps);
+    }
+    return key_map;
+}
+
+static void
+tunnel_key_maps_destroy(struct hmap *tunnel_key_maps)
+{
+    struct tunnel_key_map *key_map;
+    HMAP_FOR_EACH_POP (key_map, hmap_node, tunnel_key_maps) {
+        hmap_destroy(&key_map->port_tunnel_keys);
+        free(key_map);
+    }
+    hmap_destroy(tunnel_key_maps);
+}
+
+struct candidate_spb {
+    struct ovs_list list_node;
+    struct ovn_paired_port_binding *spb;
+    uint32_t requested_tunnel_key;
+    uint32_t existing_tunnel_key;
+    struct tunnel_key_map *tunnel_key_map;
+};
+
+static void
+reset_port_binding_pair_data(
+    struct ovn_paired_port_bindings *paired_port_bindings)
+{
+    /* Free the old paired port_bindings */
+    struct ovn_paired_port_binding *spb;
+    LIST_FOR_EACH_POP (spb, list_node, &paired_port_bindings->paired_pbs) {
+        free(spb);
+    }
+    tunnel_key_maps_destroy(&paired_port_bindings->tunnel_key_maps);
+
+    hmap_init(&paired_port_bindings->tunnel_key_maps);
+    ovs_list_init(&paired_port_bindings->paired_pbs);
+}
+
+static struct candidate_spb *
+candidate_spb_alloc(const struct ovn_unpaired_port_binding *upb,
+                    const struct sbrec_port_binding *sb_pb,
+                    struct hmap *tunnel_key_maps)
+{
+    struct ovn_paired_port_binding *spb;
+    spb = xzalloc(sizeof *spb);
+    spb->sb_pb = sb_pb;
+    spb->cookie = upb->cookie;
+    spb->type = upb->type;
+    sbrec_port_binding_set_external_ids(sb_pb, &upb->external_ids);
+    sbrec_port_binding_set_logical_port(sb_pb, upb->name);
+
+    struct candidate_spb *candidate;
+    candidate = xzalloc(sizeof *candidate);
+    candidate->spb = spb;
+    candidate->requested_tunnel_key = upb->requested_tunnel_key;
+    candidate->existing_tunnel_key = spb->sb_pb->tunnel_key;
+    candidate->tunnel_key_map = find_or_alloc_tunnel_key_map(upb->sb_dp,
+                                                             tunnel_key_maps);
+
+    return candidate;
+}
+
+static void
+get_candidate_pbs_from_sb(
+    const struct sbrec_port_binding_table *sb_pb_table,
+    const struct ovn_unpaired_port_binding_map **input_maps,
+    size_t n_input_maps, struct hmap *tunnel_key_maps,
+    struct ovs_list *candidate_spbs, struct smap *visited)
+{
+    const struct sbrec_port_binding *sb_pb;
+    const struct ovn_unpaired_port_binding *upb;
+    SBREC_PORT_BINDING_TABLE_FOR_EACH_SAFE (sb_pb, sb_pb_table) {
+        upb = find_unpaired_port_binding(input_maps, n_input_maps, sb_pb);
+        if (!upb) {
+            sbrec_port_binding_delete(sb_pb);
+            continue;
+        }
+
+        if (!uuid_equals(&upb->sb_dp->header_.uuid,
+            &sb_pb->datapath->header_.uuid)) {
+            /* A matching unpaired port was found for this port binding, but it
+             * has moved to a different datapath. Delete the old SB port
+             * binding so that a new one will be created later when we traverse
+             * unpaired port bindings later.
+             */
+            sbrec_port_binding_delete(sb_pb);
+            continue;
+        }
+
+        if (!smap_add_once(visited, sb_pb->logical_port, upb->type)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_INFO_RL(
+                &rl, "deleting port_binding "UUID_FMT" with "
+                "duplicate name %s",
+                UUID_ARGS(&sb_pb->header_.uuid), sb_pb->logical_port);
+            sbrec_port_binding_delete(sb_pb);
+            continue;
+        }
+        struct candidate_spb *candidate;
+        candidate = candidate_spb_alloc(upb, sb_pb, tunnel_key_maps);
+        ovs_list_push_back(candidate_spbs, &candidate->list_node);
+    }
+}
+
+static void
+get_candidate_pbs_from_nb(
+    struct ovsdb_idl_txn *ovnsb_idl_txn,
+    const struct ovn_unpaired_port_binding_map **input_maps,
+    uint32_t n_input_maps,
+    struct hmap *tunnel_key_maps,
+    struct ovs_list *candidate_spbs,
+    struct smap *visited)
+{
+    for (size_t i = 0; i < n_input_maps; i++) {
+        const struct ovn_unpaired_port_binding_map *map = input_maps[i];
+        struct shash_node *shash_node;
+        SHASH_FOR_EACH (shash_node, &map->ports) {
+            const struct ovn_unpaired_port_binding *upb = shash_node->data;
+            const char *visited_type = smap_get(visited, upb->name);
+            if (visited_type) {
+                if (strcmp(upb->type, visited_type)) {
+                    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
+                                                                            1);
+                    VLOG_WARN_RL(&rl, "duplicate logical port %s", upb->name);
+                }
+                continue;
+            } else {
+                /* Add the port to "visited" to help with detection of
+                 * duplicated port names across different types of ports.
+                 */
+                smap_add_once(visited, upb->name, upb->type);
+            }
+            const struct sbrec_port_binding *sb_pb;
+            sb_pb = sbrec_port_binding_insert(ovnsb_idl_txn);
+
+            struct candidate_spb *candidate;
+            candidate = candidate_spb_alloc(upb, sb_pb, tunnel_key_maps);
+            ovs_list_push_back(candidate_spbs, &candidate->list_node);
+        }
+    }
+}
+
+static void
+pair_requested_tunnel_keys(struct ovs_list *candidate_spbs,
+                           struct ovs_list *paired_pbs)
+{
+    struct candidate_spb *candidate;
+    LIST_FOR_EACH_SAFE (candidate, list_node, candidate_spbs) {
+        if (!candidate->requested_tunnel_key) {
+            continue;
+        }
+        if (candidate->requested_tunnel_key >= OVN_VXLAN_MIN_MULTICAST) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+            VLOG_WARN_RL(&rl, "Tunnel key %"PRIu32" for port %s"
+                         " is incompatible with VXLAN",
+                         candidate->requested_tunnel_key,
+                         candidate->spb->sb_pb->logical_port);
+            continue;
+        }
+
+        if (ovn_add_tnlid(&candidate->tunnel_key_map->port_tunnel_keys,
+                          candidate->requested_tunnel_key)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+            VLOG_WARN_RL(&rl, "Logical port_binding %s requests same "
+                         "tunnel key %"PRIu32" as another logical "
+                         "port_binding on the same datapath",
+                         candidate->spb->sb_pb->logical_port,
+                         candidate->requested_tunnel_key);
+        }
+        sbrec_port_binding_set_tunnel_key(candidate->spb->sb_pb,
+                                          candidate->requested_tunnel_key);
+        ovs_list_remove(&candidate->list_node);
+        ovs_list_push_back(paired_pbs, &candidate->spb->list_node);
+        free(candidate);
+    }
+}
+
+static void
+pair_existing_tunnel_keys(struct ovs_list *candidate_spbs,
+                          struct ovs_list *paired_pbs)
+{
+    struct candidate_spb *candidate;
+    LIST_FOR_EACH_SAFE (candidate, list_node, candidate_spbs) {
+        if (!candidate->existing_tunnel_key) {
+            continue;
+        }
+        /* Existing southbound pb. If this key is available,
+         * reuse it.
+         */
+        if (ovn_add_tnlid(&candidate->tunnel_key_map->port_tunnel_keys,
+                          candidate->existing_tunnel_key)) {
+            ovs_list_remove(&candidate->list_node);
+            ovs_list_push_back(paired_pbs, &candidate->spb->list_node);
+            free(candidate);
+        }
+    }
+}
+
+static void
+pair_new_tunnel_keys(struct ovs_list *candidate_spbs,
+                     struct ovs_list *paired_pbs,
+                     uint32_t max_pb_tunnel_id)
+{
+    uint32_t hint = 0;
+    struct candidate_spb *candidate;
+    LIST_FOR_EACH_SAFE (candidate, list_node, candidate_spbs) {
+        uint32_t tunnel_key =
+            ovn_allocate_tnlid(&candidate->tunnel_key_map->port_tunnel_keys,
+                               "port", 1, max_pb_tunnel_id,
+                               &hint);
+        if (!tunnel_key) {
+            continue;
+        }
+        sbrec_port_binding_set_tunnel_key(candidate->spb->sb_pb,
+                                              tunnel_key);
+        ovs_list_remove(&candidate->list_node);
+        ovs_list_push_back(paired_pbs, &candidate->spb->list_node);
+        free(candidate);
+    }
+
+}
+
+static void
+free_unpaired_candidates(struct ovs_list *candidate_spbs)
+{
+    struct candidate_spb *candidate;
+    /* Anything from this list represents a port_binding where a tunnel ID
+     * could not be allocated. Delete the SB port_binding binding for these.
+     */
+    LIST_FOR_EACH_POP (candidate, list_node, candidate_spbs) {
+        sbrec_port_binding_delete(candidate->spb->sb_pb);
+        free(candidate->spb);
+        free(candidate);
+    }
+}
+
+static void
+cleanup_stale_fdb_entries(const struct sbrec_fdb_table *sbrec_fdb_table,
+                          struct hmap *tunnel_key_maps)
+{
+    const struct sbrec_fdb *fdb_e;
+    SBREC_FDB_TABLE_FOR_EACH_SAFE (fdb_e, sbrec_fdb_table) {
+        bool delete = true;
+        struct tunnel_key_map *map = find_tunnel_key_map(fdb_e->dp_key,
+                                                         tunnel_key_maps);
+        if (map) {
+            if (ovn_tnlid_present(&map->port_tunnel_keys, fdb_e->port_key)) {
+                delete = false;
+            }
+        }
+
+        if (delete) {
+            sbrec_fdb_delete(fdb_e);
+        }
+    }
+}
+
+void
+en_port_binding_pair_run(struct engine_node *node , void *data)
+{
+    const struct sbrec_port_binding_table *sb_pb_table =
+        EN_OVSDB_GET(engine_get_input("SB_port_binding", node));
+    const struct sbrec_fdb_table *sb_fdb_table =
+        EN_OVSDB_GET(engine_get_input("SB_fdb", node));
+    const struct ed_type_global_config *global_config =
+        engine_get_input_data("global_config", node);
+    /* The inputs are:
+     * * Some number of input maps.
+     * * Southbound Port Binding table.
+     * * Global config data.
+     * * FDB Table.
+     *
+     * Therefore, the number of inputs - 3 is the number of input
+     * maps from the port_binding-specific nodes.
+     */
+    size_t n_input_maps = node->n_inputs - 3;
+    const struct ovn_unpaired_port_binding_map **input_maps =
+        xmalloc(n_input_maps *sizeof *input_maps);
+    struct ovn_paired_port_bindings *paired_port_bindings = data;
+
+    for (size_t i = 0; i < n_input_maps; i++) {
+        input_maps[i] = engine_get_data(node->inputs[i].node);
+    }
+
+    reset_port_binding_pair_data(paired_port_bindings);
+
+    struct smap visited = SMAP_INITIALIZER(&visited);
+    struct ovs_list candidate_spbs = OVS_LIST_INITIALIZER(&candidate_spbs);
+    get_candidate_pbs_from_sb(sb_pb_table, input_maps, n_input_maps,
+                              &paired_port_bindings->tunnel_key_maps,
+                              &candidate_spbs, &visited);
+
+    const struct engine_context *eng_ctx = engine_get_context();
+    get_candidate_pbs_from_nb(eng_ctx->ovnsb_idl_txn, input_maps,
+                              n_input_maps,
+                              &paired_port_bindings->tunnel_key_maps,
+                              &candidate_spbs, &visited);
+
+    smap_destroy(&visited);
+
+    pair_requested_tunnel_keys(&candidate_spbs,
+                               &paired_port_bindings->paired_pbs);
+    pair_existing_tunnel_keys(&candidate_spbs,
+                              &paired_port_bindings->paired_pbs);
+    pair_new_tunnel_keys(&candidate_spbs, &paired_port_bindings->paired_pbs,
+                         global_config->max_pb_tunnel_id);
+
+    cleanup_stale_fdb_entries(sb_fdb_table,
+                              &paired_port_bindings->tunnel_key_maps);
+
+    free_unpaired_candidates(&candidate_spbs);
+    free(input_maps);
+
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+void
+en_port_binding_pair_cleanup(void *data)
+{
+    struct ovn_paired_port_bindings *paired_port_bindings = data;
+    struct ovn_paired_port_binding *spb;
+
+    LIST_FOR_EACH_POP (spb, list_node, &paired_port_bindings->paired_pbs) {
+        free(spb);
+    }
+    tunnel_key_maps_destroy(&paired_port_bindings->tunnel_key_maps);
+}
+
+bool
+port_binding_fdb_change_handler(struct engine_node *node, void *data)
+{
+    struct ovn_paired_port_bindings *paired_port_bindings = data;
+    const struct sbrec_fdb_table *sbrec_fdb_table =
+        EN_OVSDB_GET(engine_get_input("SB_fdb", node));
+
+    /* check if changed rows are stale and delete them */
+    const struct sbrec_fdb *fdb_e, *fdb_prev_del = NULL;
+    SBREC_FDB_TABLE_FOR_EACH_TRACKED (fdb_e, sbrec_fdb_table) {
+        if (sbrec_fdb_is_deleted(fdb_e)) {
+            continue;
+        }
+
+        if (fdb_prev_del) {
+            sbrec_fdb_delete(fdb_prev_del);
+        }
+
+        fdb_prev_del = fdb_e;
+        struct tunnel_key_map *tunnel_key_map =
+            find_tunnel_key_map(fdb_e->dp_key,
+                                &paired_port_bindings->tunnel_key_maps);
+        if (tunnel_key_map) {
+            if (ovn_tnlid_present(&tunnel_key_map->port_tunnel_keys,
+                                  fdb_e->port_key)) {
+                fdb_prev_del = NULL;
+            }
+        }
+    }
+
+    if (fdb_prev_del) {
+        sbrec_fdb_delete(fdb_prev_del);
+    }
+
+    return true;
+}
diff --git a/northd/en-port-binding-pair.h b/northd/en-port-binding-pair.h
new file mode 100644
index 000000000..4bcb5f671
--- /dev/null
+++ b/northd/en-port-binding-pair.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2025, 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 EN_PORT_BINDING_PAIR_H
+#define EN_PORT_BINDING_PAIR_H
+
+#include "inc-proc-eng.h"
+
+void *en_port_binding_pair_init(struct engine_node *node,
+                            struct engine_arg *args);
+
+
+void en_port_binding_pair_run(struct engine_node *node , void *data);
+
+void en_port_binding_pair_cleanup(void *data);
+
+bool port_binding_fdb_change_handler(struct engine_node *, void *data);
+
+#endif /* EN_PORT_BINDING_PAIR_H */
diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
index f96dd006d..b2cf29684 100644
--- a/northd/inc-proc-northd.c
+++ b/northd/inc-proc-northd.c
@@ -50,6 +50,11 @@
 #include "en-datapath-logical-router.h"
 #include "en-datapath-logical-switch.h"
 #include "en-datapath-sync.h"
+#include "en-port-binding-logical-router-port.h"
+#include "en-port-binding-logical-switch-port.h"
+#include "en-port-binding-chassisredirect.h"
+#include "en-port-binding-mirror.h"
+#include "en-port-binding-pair.h"
 #include "unixctl.h"
 #include "util.h"
 
@@ -190,6 +195,23 @@ static ENGINE_NODE(datapath_synced_logical_router,
 static ENGINE_NODE(datapath_synced_logical_switch,
                    "datapath_synced_logical_switch");
 static ENGINE_NODE(datapath_sync, "datapath_sync");
+static ENGINE_NODE(port_binding_logical_router_port,
+                   "port_binding_logical_router");
+static ENGINE_NODE(port_binding_logical_switch_port,
+                   "port_binding_logical_switch");
+static ENGINE_NODE(port_binding_chassisredirect_port,
+                   "port_binding_chassisredirect_port");
+static ENGINE_NODE(port_binding_mirror,
+                   "port_binding_mirror");
+static ENGINE_NODE(port_binding_paired_logical_router_port,
+                   "port_binding_paired_logical_router_port");
+static ENGINE_NODE(port_binding_paired_logical_switch_port,
+                   "port_binding_paired_logical_switch_port");
+static ENGINE_NODE(port_binding_paired_chassisredirect_port,
+                   "port_binding_paired_chassisredirect_port");
+static ENGINE_NODE(port_binding_paired_mirror,
+                   "port_binding_paired_mirror");
+static ENGINE_NODE(port_binding_pair, "port_binding_pair");
 
 void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
                           struct ovsdb_idl_loop *sb)
@@ -235,6 +257,36 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_datapath_synced_logical_switch, &en_datapath_sync,
                      NULL);
 
+    engine_add_input(&en_port_binding_logical_switch_port,
+                     &en_datapath_synced_logical_switch, NULL);
+    engine_add_input(&en_port_binding_logical_router_port,
+                     &en_datapath_synced_logical_router, NULL);
+    engine_add_input(&en_port_binding_chassisredirect_port,
+                     &en_datapath_synced_logical_switch, NULL);
+    engine_add_input(&en_port_binding_chassisredirect_port,
+                     &en_datapath_synced_logical_router, NULL);
+    engine_add_input(&en_port_binding_mirror,
+                     &en_datapath_synced_logical_switch, NULL);
+    engine_add_input(&en_port_binding_pair,
+                     &en_port_binding_logical_switch_port, NULL);
+    engine_add_input(&en_port_binding_pair,
+                     &en_port_binding_logical_router_port, NULL);
+    engine_add_input(&en_port_binding_pair,
+                     &en_port_binding_chassisredirect_port, NULL);
+    engine_add_input(&en_port_binding_pair, &en_port_binding_mirror, NULL);
+    engine_add_input(&en_port_binding_pair, &en_sb_port_binding, NULL);
+    engine_add_input(&en_port_binding_pair, &en_global_config, NULL);
+    engine_add_input(&en_port_binding_pair, &en_sb_fdb,
+                     port_binding_fdb_change_handler);
+    engine_add_input(&en_port_binding_paired_logical_router_port,
+                     &en_port_binding_pair, NULL);
+    engine_add_input(&en_port_binding_paired_logical_switch_port,
+                     &en_port_binding_pair, NULL);
+    engine_add_input(&en_port_binding_paired_chassisredirect_port,
+                     &en_port_binding_pair, NULL);
+    engine_add_input(&en_port_binding_paired_mirror, &en_port_binding_pair,
+                     NULL);
+
     engine_add_input(&en_northd, &en_nb_mirror, NULL);
     engine_add_input(&en_northd, &en_nb_mirror_rule, NULL);
     engine_add_input(&en_northd, &en_nb_static_mac_binding, NULL);
@@ -250,7 +302,7 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_northd, &en_sb_service_monitor, NULL);
     engine_add_input(&en_northd, &en_sb_static_mac_binding, NULL);
     engine_add_input(&en_northd, &en_sb_chassis_template_var, NULL);
-    engine_add_input(&en_northd, &en_sb_fdb, northd_sb_fdb_change_handler);
+    engine_add_input(&en_northd, &en_sb_fdb, engine_noop_handler);
     engine_add_input(&en_northd, &en_global_config,
                      northd_global_config_handler);
 
@@ -289,6 +341,14 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
                      engine_noop_handler);
     engine_add_input(&en_northd, &en_datapath_synced_logical_switch,
                      engine_noop_handler);
+    engine_add_input(&en_northd, &en_port_binding_paired_logical_router_port,
+                     engine_noop_handler);
+    engine_add_input(&en_northd, &en_port_binding_paired_logical_switch_port,
+                     engine_noop_handler);
+    engine_add_input(&en_northd, &en_port_binding_paired_chassisredirect_port,
+                     engine_noop_handler);
+    engine_add_input(&en_northd, &en_port_binding_paired_mirror,
+                     engine_noop_handler);
 
     engine_add_input(&en_lr_nat, &en_northd, lr_nat_northd_handler);
 
diff --git a/northd/northd.c b/northd/northd.c
index 7ab69c308..4e49e6843 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -54,6 +54,10 @@
 #include "en-sampling-app.h"
 #include "en-datapath-logical-switch.h"
 #include "en-datapath-logical-router.h"
+#include "en-port-binding-logical-switch-port.h"
+#include "en-port-binding-logical-router-port.h"
+#include "en-port-binding-chassisredirect.h"
+#include "en-port-binding-mirror.h"
 #include "lib/ovn-parallel-hmap.h"
 #include "ovn/actions.h"
 #include "ovn/features.h"
@@ -464,7 +468,7 @@ od_has_lb_vip(const struct ovn_datapath *od)
     }
 }
 
-static const char *
+const char *
 ovn_datapath_name(const struct sbrec_datapath_binding *sb)
 {
     return smap_get_def(&sb->external_ids, "name", "");
@@ -495,8 +499,6 @@ ovn_datapath_create(struct hmap *datapaths, const struct 
uuid *key,
     od->sb = sb;
     od->nbs = nbs;
     od->nbr = nbr;
-    hmap_init(&od->port_tnlids);
-    od->port_key_hint = 0;
     hmap_insert(datapaths, &od->key_node, uuid_hash(&od->key));
     od->lr_group = NULL;
     hmap_init(&od->ports);
@@ -523,7 +525,6 @@ ovn_datapath_destroy(struct hmap *datapaths, struct 
ovn_datapath *od)
          * private list and once we've exited that function it is not safe to
          * use it. */
         hmap_remove(datapaths, &od->key_node);
-        ovn_destroy_tnlids(&od->port_tnlids);
         destroy_ipam_info(&od->ipam_info);
         free(od->router_ports);
         free(od->ls_peers);
@@ -1003,6 +1004,7 @@ ovn_port_create(struct hmap *ports, const char *key,
     op->sb = sb;
     ovn_port_set_nb(op, nbsp, nbrp);
     op->primary_port = op->cr_port = NULL;
+    op->tunnel_key = sb->tunnel_key;
     hmap_insert(ports, &op->key_node, hash_string(op->key, 0));
 
     op->lflow_ref = lflow_ref_create();
@@ -1014,10 +1016,6 @@ ovn_port_create(struct hmap *ports, const char *key,
 static void
 ovn_port_cleanup(struct ovn_port *port)
 {
-    if (port->tunnel_key) {
-        ovs_assert(port->od);
-        ovn_free_tnlid(&port->od->port_tnlids, port->tunnel_key);
-    }
     for (int i = 0; i < port->n_lsp_addrs; i++) {
         destroy_lport_addresses(&port->lsp_addrs[i]);
     }
@@ -1094,12 +1092,6 @@ ovn_port_find(const struct hmap *ports, const char *name)
     return ovn_port_find__(ports, name, false);
 }
 
-static struct ovn_port *
-ovn_port_find_bound(const struct hmap *ports, const char *name)
-{
-    return ovn_port_find__(ports, name, true);
-}
-
 static bool
 lsp_is_clone_to_unknown(const struct nbrec_logical_switch_port *nbsp)
 {
@@ -1164,7 +1156,7 @@ lsp_disable_arp_nd_rsp(const struct 
nbrec_logical_switch_port *nbsp)
     return smap_get_bool(&nbsp->options, "disable_arp_nd_rsp", false);
 }
 
-static bool
+bool
 lsp_is_type_changed(const struct sbrec_port_binding *sb,
                 const struct nbrec_logical_switch_port *nbsp,
                 bool *update_sbrec)
@@ -1900,104 +1892,35 @@ parse_lsp_addrs(struct ovn_port *op)
     }
 }
 
-static void
-create_mirror_port(struct ovn_port *op, struct hmap *ports,
-                   struct ovs_list *both_dbs, struct ovs_list *nb_only,
-                   const struct nbrec_mirror *nb_mirror)
-{
-    char *mp_name = ovn_mirror_port_name(ovn_datapath_name(op->od->sb),
-                                         nb_mirror->sink);
-    struct ovn_port *mp = ovn_port_find(ports, mp_name);
-    struct ovn_port *target_port = ovn_port_find(ports, nb_mirror->sink);
-
-    if (!target_port) {
-        goto clear;
-    }
-
-    if (!mp) {
-        mp = ovn_port_create(ports, mp_name, op->nbsp, NULL, NULL);
-        ovs_list_push_back(nb_only, &mp->list);
-    } else if (mp->sb) {
-        ovn_port_set_nb(mp, op->nbsp, NULL);
-        ovs_list_remove(&mp->list);
-        ovs_list_push_back(both_dbs, &mp->list);
-    } else {
-        goto clear;
-    }
-
-    mp->mirror_target_port = target_port;
-    mp->od = op->od;
+static struct ovn_port *
+create_mirror_port(const struct ovn_port *source,
+                   struct ovn_port *sink, const char *mirror_port_name,
+                   struct hmap *ports,
+                   const struct sbrec_port_binding *sb_pb)
+{
+    struct ovn_port *mp = ovn_port_create(ports, mirror_port_name,
+                                          source->nbsp, NULL, sb_pb);
+    ovn_port_set_nb(mp, source->nbsp, NULL);
+    mp->mirror_target_port = sink;
+    mp->od = source->od;
 
-clear:
-    free(mp_name);
+    return mp;
 }
 
 static struct ovn_port *
 join_logical_ports_lsp(struct hmap *ports,
-                       struct ovs_list *nb_only, struct ovs_list *both,
                        struct ovn_datapath *od,
                        const struct nbrec_logical_switch_port *nbsp,
+                       const struct sbrec_port_binding *sb_pb,
                        const char *name,
                        unsigned long *queue_id_bitmap,
-                       struct hmap *tag_alloc_table,
-                       struct hmapx *mirror_attached_ports)
-{
-    struct ovn_port *op = ovn_port_find_bound(ports, name);
-    if (op && (op->od || op->nbsp || op->nbrp)) {
-        static struct vlog_rate_limit rl
-            = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "duplicate logical port %s", name);
-        return NULL;
-    } else if (op && (!op->sb || op->sb->datapath == od->sb)) {
-        /*
-         * Handle cases where lport type was explicitly changed
-         * in the NBDB, in such cases:
-         * 1. remove the current sbrec of the affected lport from
-         *    the port_binding table.
-         *
-         * 2. create a new sbrec with the same logical_port as the
-         *    deleted lport and add it to the nb_only list which
-         *    will make the northd handle this lport as a new
-         *    created one and recompute everything that is needed
-         *    for this lport.
-         *
-         * This change will affect container/virtual lport type
-         * changes only for now, this change is needed in
-         * contaier/virtual lport cases to avoid port type
-         * conflicts in the ovn-controller when the user clears
-         * the parent_port field in the container lport or updated
-         * the lport type.
-         *
-         */
-        bool update_sbrec = false;
-        if (op->sb && lsp_is_type_changed(op->sb, nbsp,
-                                          &update_sbrec)
-                       && update_sbrec) {
-            ovs_list_remove(&op->list);
-            sbrec_port_binding_delete(op->sb);
-            ovn_port_destroy(ports, op);
-            op = ovn_port_create(ports, name, nbsp,
-                                 NULL, NULL);
-            ovs_list_push_back(nb_only, &op->list);
-        } else {
-            ovn_port_set_nb(op, nbsp, NULL);
-            ovs_list_remove(&op->list);
-
-            uint32_t queue_id = smap_get_int(&op->sb->options,
-                                             "qdisc_queue_id", 0);
-            if (queue_id) {
-                bitmap_set1(queue_id_bitmap, queue_id);
-            }
-
-            ovs_list_push_back(both, &op->list);
-
-            /* This port exists due to a SB binding, but should
-             * not have been initialized fully. */
-            ovs_assert(!op->n_lsp_addrs && !op->n_ps_addrs);
-        }
-    } else {
-        op = ovn_port_create(ports, name, nbsp, NULL, NULL);
-        ovs_list_push_back(nb_only, &op->list);
+                       struct hmap *tag_alloc_table)
+{
+    struct ovn_port *op = ovn_port_create(ports, name, nbsp, NULL, sb_pb);
+    uint32_t queue_id = smap_get_int(&op->sb->options,
+                                     "qdisc_queue_id", 0);
+    if (queue_id) {
+        bitmap_set1(queue_id_bitmap, queue_id);
     }
 
     if (lsp_is_localnet(nbsp)) {
@@ -2022,47 +1945,23 @@ join_logical_ports_lsp(struct hmap *ports,
     hmap_insert(&od->ports, &op->dp_node,
                 hmap_node_hash(&op->key_node));
 
-    if (nbsp->n_mirror_rules) {
-        hmapx_add(mirror_attached_ports, op);
-    }
-
     tag_alloc_add_existing_tags(tag_alloc_table, nbsp);
     return op;
 }
 
 static struct ovn_port*
 join_logical_ports_lrp(struct hmap *ports,
-                       struct ovs_list *nb_only, struct ovs_list *both,
                        struct hmapx *dgps,
                        struct ovn_datapath *od,
                        const struct nbrec_logical_router_port *nbrp,
+                       const struct sbrec_port_binding *sb_pb,
                        const char *name, struct lport_addresses *lrp_networks)
 {
     if (!lrp_networks->n_ipv4_addrs && !lrp_networks->n_ipv6_addrs) {
       return NULL;
     }
 
-    struct ovn_port *op = ovn_port_find_bound(ports, name);
-    if (op && (op->od || op->nbsp || op->nbrp)) {
-        static struct vlog_rate_limit rl
-            = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "duplicate logical router port %s",
-                     name);
-        destroy_lport_addresses(lrp_networks);
-        return NULL;
-    } else if (op && (!op->sb || op->sb->datapath == od->sb)) {
-        ovn_port_set_nb(op, NULL, nbrp);
-        ovs_list_remove(&op->list);
-        ovs_list_push_back(both, &op->list);
-
-        /* This port exists but should not have been
-         * initialized fully. */
-        ovs_assert(!op->lrp_networks.n_ipv4_addrs
-                   && !op->lrp_networks.n_ipv6_addrs);
-    } else {
-        op = ovn_port_create(ports, name, NULL, nbrp, NULL);
-        ovs_list_push_back(nb_only, &op->list);
-    }
+    struct ovn_port *op = ovn_port_create(ports, name, NULL, nbrp, sb_pb);
 
     op->lrp_networks = *lrp_networks;
     op->od = od;
@@ -2116,128 +2015,125 @@ join_logical_ports_lrp(struct hmap *ports,
 
 
 static struct ovn_port *
-create_cr_port(struct ovn_port *op, struct hmap *ports,
-               struct ovs_list *both_dbs, struct ovs_list *nb_only)
+create_cr_port(struct ovn_port *op, const char *name, struct hmap *ports,
+               const struct sbrec_port_binding *sb_pb)
 {
-    char *redirect_name = ovn_chassis_redirect_name(
-        op->nbsp ? op->nbsp->name : op->nbrp->name);
-
-    struct ovn_port *crp = ovn_port_find(ports, redirect_name);
-    if (crp && crp->sb && crp->sb->datapath == op->od->sb) {
-        ovn_port_set_nb(crp, op->nbsp, op->nbrp);
-        ovs_list_remove(&crp->list);
-        ovs_list_push_back(both_dbs, &crp->list);
-    } else {
-        crp = ovn_port_create(ports, redirect_name,
-                              op->nbsp, op->nbrp, NULL);
-        ovs_list_push_back(nb_only, &crp->list);
-    }
+    struct ovn_port *crp = ovn_port_create(ports, name, op->nbsp, op->nbrp,
+                                           sb_pb);
 
     crp->primary_port = op;
     op->cr_port = crp;
     crp->od = op->od;
-    free(redirect_name);
 
     return crp;
 }
 
-/* Returns true if chassis resident port needs to be created for
- * op's peer logical switch.  False otherwise.
- *
- * Chassis resident port needs to be created if the following
- * conditionsd are met:
- *   - op is a distributed gateway port
- *   - op is the only distributed gateway port attached to its
- *     router
- *   - op's peer logical switch has no localnet ports.
- */
-static bool
-peer_needs_cr_port_creation(struct ovn_port *op)
-{
-    if ((op->nbrp->n_gateway_chassis || op->nbrp->ha_chassis_group)
-        && op->od->n_l3dgw_ports == 1 && op->peer && op->peer->nbsp
-        && !op->peer->od->n_localnet_ports) {
-        return true;
-    }
-
-    return false;
-}
-
 static void
-join_mirror_ports(struct ovn_port *op,
-                  const struct nbrec_logical_switch_port *nbsp,
-                  struct hmap *ports, struct ovs_list *both,
-                  struct ovs_list *nb_only)
+join_logical_ports(
+    struct hmap *ls_datapaths, struct hmap *lr_datapaths,
+    const struct ovn_paired_logical_switch_port_map *paired_lsps,
+    const struct ovn_paired_logical_router_port_map *paired_lrps,
+    const struct ovn_paired_chassisredirect_port_map *paired_crps,
+    const struct ovn_paired_mirror_map *paired_mirrors,
+    struct hmap *ls_ports, struct hmap *lr_ports,
+    unsigned long *queue_id_bitmap,
+    struct hmap *tag_alloc_table)
 {
-    /* Create mirror targets port bindings if there any mirror
-     * with lport type attached to this port. */
-    for (size_t j = 0; j < op->nbsp->n_mirror_rules; j++) {
-        struct nbrec_mirror *mirror = nbsp->mirror_rules[j];
-        if (!strcmp(mirror->type, "lport")) {
-            create_mirror_port(op, ports, both, nb_only, mirror);
+    struct ovn_datapath *od;
+    struct hmapx dgps = HMAPX_INITIALIZER(&dgps);
+
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &paired_lrps->paired_router_ports) {
+        struct ovn_paired_logical_router_port *slrp = node->data;
+        od = ovn_datapath_from_sbrec(ls_datapaths, lr_datapaths,
+                                     slrp->router->sb);
+        if (!od) {
+            /* This can happen if the router is not enabled */
+            continue;
         }
+        struct lport_addresses lrp_networks;
+        if (!extract_lrp_networks(slrp->nb, &lrp_networks)) {
+            static struct vlog_rate_limit rl
+                = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "bad 'mac' %s", slrp->nb->mac);
+            continue;
+        }
+
+        join_logical_ports_lrp(lr_ports, &dgps, od, slrp->nb, slrp->sb,
+                               slrp->nb->name, &lrp_networks);
     }
-}
 
-static void
-join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table,
-                   struct hmap *ls_datapaths, struct hmap *lr_datapaths,
-                   struct hmap *ports, unsigned long *queue_id_bitmap,
-                   struct hmap *tag_alloc_table, struct ovs_list *sb_only,
-                   struct ovs_list *nb_only, struct ovs_list *both)
-{
-    ovs_list_init(sb_only);
-    ovs_list_init(nb_only);
-    ovs_list_init(both);
+    SHASH_FOR_EACH (node, &paired_lsps->paired_switch_ports) {
+        struct ovn_paired_logical_switch_port *slsp = node->data;
+        od = ovn_datapath_from_sbrec(ls_datapaths, lr_datapaths,
+                                     slsp->sw->sb);
+        if (!od) {
+            /* This should not happen, but we'll be defensive just in case */
+            continue;
+        }
+        join_logical_ports_lsp(ls_ports, od, slsp->nb, slsp->sb,
+                               slsp->nb->name, queue_id_bitmap,
+                               tag_alloc_table);
+    }
 
-    const struct sbrec_port_binding *sb;
-    SBREC_PORT_BINDING_TABLE_FOR_EACH (sb, sbrec_pb_table) {
-        struct ovn_port *op = ovn_port_create(ports, sb->logical_port,
-                                              NULL, NULL, sb);
-        ovs_list_push_back(sb_only, &op->list);
+    SHASH_FOR_EACH (node, &paired_crps->paired_chassisredirect_router_ports) {
+        struct ovn_paired_chassisredirect_router_port *crp = node->data;
+        struct ovn_port *primary_port =
+            ovn_port_find(lr_ports, crp->primary_port->name);
+        create_cr_port(primary_port, crp->name, lr_ports, crp->sb);
     }
 
-    struct ovn_datapath *od;
-    struct hmapx dgps = HMAPX_INITIALIZER(&dgps);
-    struct hmapx mirror_attached_ports =
-                    HMAPX_INITIALIZER(&mirror_attached_ports);
-    HMAP_FOR_EACH (od, key_node, lr_datapaths) {
-        ovs_assert(od->nbr);
-        for (size_t i = 0; i < od->nbr->n_ports; i++) {
-            const struct nbrec_logical_router_port *nbrp
-                = od->nbr->ports[i];
-
-            struct lport_addresses lrp_networks;
-            if (!extract_lrp_networks(nbrp, &lrp_networks)) {
-                static struct vlog_rate_limit rl
-                    = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "bad 'mac' %s", nbrp->mac);
-                continue;
-            }
-            join_logical_ports_lrp(ports, nb_only, both, &dgps,
-                                   od, nbrp,
-                                   nbrp->name, &lrp_networks);
-        }
+    SHASH_FOR_EACH (node, &paired_crps->paired_chassisredirect_switch_ports) {
+        struct ovn_paired_chassisredirect_switch_port *crp = node->data;
+        struct ovn_port *primary_port =
+            ovn_port_find(ls_ports, crp->primary_port->name);
+        create_cr_port(primary_port, crp->name, ls_ports, crp->sb);
     }
 
-    HMAP_FOR_EACH (od, key_node, ls_datapaths) {
-        ovs_assert(od->nbs);
-        for (size_t i = 0; i < od->nbs->n_ports; i++) {
-            const struct nbrec_logical_switch_port *nbsp
-                = od->nbs->ports[i];
-            join_logical_ports_lsp(ports, nb_only, both, od, nbsp,
-                                   nbsp->name, queue_id_bitmap,
-                                   tag_alloc_table, &mirror_attached_ports);
+    SHASH_FOR_EACH (node, &paired_mirrors->paired_mirror_ports) {
+        struct ovn_paired_mirror *mirror = node->data;
+        struct ovn_port *source_port =
+            ovn_port_find(ls_ports, mirror->nbsp->name);
+        struct ovn_port *sink_port =
+            ovn_port_find(ls_ports, mirror->sink);
+        if (!sink_port) {
+            continue;
         }
+        create_mirror_port(source_port, sink_port, mirror->name, ls_ports, 
mirror->sb);
     }
 
     /* Connect logical router ports, and logical switch ports of type "router",
      * to their peers.  As well as logical switch ports of type "switch" to
      * theirs. */
+
     struct ovn_port *op;
-    HMAP_FOR_EACH (op, key_node, ports) {
-        if (op->nbsp && lsp_is_router(op->nbsp) && !op->primary_port) {
-            struct ovn_port *peer = ovn_port_get_peer(ports, op);
+    HMAP_FOR_EACH (op, key_node, lr_ports) {
+        if (op->nbrp->peer && !is_cr_port(op)) {
+            struct ovn_port *peer = ovn_port_find(lr_ports, op->nbrp->peer);
+            if (peer) {
+                if (peer->nbrp && peer->nbrp->peer &&
+                        !strcmp(op->nbrp->name, peer->nbrp->peer)) {
+                    /* We only configure LRP peers if each LRP has the other as
+                     * its peer. */
+                    op->peer = peer;
+                } else if (peer->nbsp) {
+                    /* An ovn_port for a switch port of type "router" does have
+                     * a router port as its peer (see the case above for
+                     * "router" ports), but this is set via options:router-port
+                     * in Logical_Switch_Port and does not involve the
+                     * Logical_Router_Port's 'peer' column. */
+                    static struct vlog_rate_limit rl =
+                            VLOG_RATE_LIMIT_INIT(5, 1);
+                    VLOG_WARN_RL(&rl, "Bad configuration: The peer of router "
+                                 "port %s is a switch port", op->key);
+                }
+            }
+        }
+    }
+
+    HMAP_FOR_EACH (op, key_node, ls_ports) {
+        if (lsp_is_router(op->nbsp) && !op->primary_port) {
+            struct ovn_port *peer = ovn_port_get_peer(lr_ports, op);
             if (!peer || !peer->nbrp) {
                 continue;
             }
@@ -2293,21 +2189,21 @@ join_logical_ports(const struct 
sbrec_port_binding_table *sbrec_pb_table,
                         arp_proxy, op->nbsp->name);
                 }
             }
-        } else if (op->nbsp && op->nbsp->peer && lsp_is_switch(op->nbsp)) {
+        } else if (op->nbsp->peer && lsp_is_switch(op->nbsp)) {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            struct ovn_port *peer = ovn_port_find(ports, op->nbsp->peer);
+            struct ovn_port *peer = ovn_port_find(ls_ports, op->nbsp->peer);
 
             if (!peer) {
                 continue;
             }
 
-            if (peer->nbrp || (peer->nbsp && lsp_is_router(peer->nbsp))) {
+            if (lsp_is_router(peer->nbsp)) {
                 VLOG_WARN_RL(&rl, "Bad configuration: The peer of switch "
                                   "port %s is a router port", op->key);
                 continue;
             }
 
-            if (!peer->nbsp || !lsp_is_switch(peer->nbsp)) {
+            if (!lsp_is_switch(peer->nbsp)) {
                 /* Common case.  Likely the manual configuration is not
                  * finished yet. */
                 continue;
@@ -2322,26 +2218,6 @@ join_logical_ports(const struct sbrec_port_binding_table 
*sbrec_pb_table,
             }
 
             op->peer = peer;
-        } else if (op->nbrp && op->nbrp->peer && !is_cr_port(op)) {
-            struct ovn_port *peer = ovn_port_find(ports, op->nbrp->peer);
-            if (peer) {
-                if (peer->nbrp && peer->nbrp->peer &&
-                        !strcmp(op->nbrp->name, peer->nbrp->peer)) {
-                    /* We only configure LRP peers if each LRP has the other as
-                     * its peer. */
-                    op->peer = peer;
-                } else if (peer->nbsp) {
-                    /* An ovn_port for a switch port of type "router" does have
-                     * a router port as its peer (see the case above for
-                     * "router" ports), but this is set via options:router-port
-                     * in Logical_Switch_Port and does not involve the
-                     * Logical_Router_Port's 'peer' column. */
-                    static struct vlog_rate_limit rl =
-                            VLOG_RATE_LIMIT_INIT(5, 1);
-                    VLOG_WARN_RL(&rl, "Bad configuration: The peer of router "
-                                 "port %s is a switch port", op->key);
-                }
-            }
         }
     }
 
@@ -2352,11 +2228,6 @@ join_logical_ports(const struct sbrec_port_binding_table 
*sbrec_pb_table,
         ovs_assert(op->nbrp);
         ovs_assert(op->nbrp->ha_chassis_group || op->nbrp->n_gateway_chassis);
 
-        /* Additional "derived" ovn_port crp represents the instance of op on
-         * the gateway chassis. */
-        struct ovn_port *crp = create_cr_port(op, ports, both, nb_only);
-        ovs_assert(crp);
-
         /* Add to l3dgw_ports in od, for later use during flow creation. */
         if (od->n_l3dgw_ports == od->n_allocated_l3dgw_ports) {
             od->l3dgw_ports = x2nrealloc(od->l3dgw_ports,
@@ -2372,41 +2243,16 @@ join_logical_ports(const struct 
sbrec_port_binding_table *sbrec_pb_table,
         }
     }
 
-
-    /* Create chassisredirect port for the distributed gateway port's (DGP)
-     * peer if
-     *  - DGP's router has only one DGP and
-     *  - Its peer is a logical switch port and
-     *  - Its peer's logical switch has no localnet ports
-     *
-     * This is required to support
-     *   - NAT via geneve (for the overlay provider networks) and
-     *   - to centralize routing on the gateway chassis for the traffic
-     *     destined to the DGP's networks.
-     *
-     * Future enhancement: Support 'centralizerouting' for all the DGP's
-     * of a logical router.
-     * */
-    HMAPX_FOR_EACH (hmapx_node, &dgps) {
-        op = hmapx_node->data;
-        if (peer_needs_cr_port_creation(op)) {
-            create_cr_port(op->peer, ports, both, nb_only);
-        }
-    }
     hmapx_destroy(&dgps);
 
-    HMAPX_FOR_EACH (hmapx_node, &mirror_attached_ports) {
-        op = hmapx_node->data;
-        if (op && op->nbsp) {
-            join_mirror_ports(op, op->nbsp, ports, both, nb_only);
-        }
-    }
-    hmapx_destroy(&mirror_attached_ports);
-
     /* Wait until all ports have been connected to add to IPAM since
      * it relies on proper peers to be set
      */
-    HMAP_FOR_EACH (op, key_node, ports) {
+    HMAP_FOR_EACH (op, key_node, ls_ports) {
+        ipam_add_port_addresses(op->od, op);
+    }
+
+    HMAP_FOR_EACH (op, key_node, lr_ports) {
         ipam_add_port_addresses(op->od, op);
     }
 }
@@ -2809,15 +2655,6 @@ copy_gw_chassis_from_nbrp_to_sbpb(
     free(sb_ha_chassis);
 }
 
-static const char*
-op_get_name(const struct ovn_port *op)
-{
-    ovs_assert(op->nbsp || op->nbrp);
-    const char *name = op->nbsp ? op->nbsp->name
-                                : op->nbrp->name;
-    return name;
-}
-
 static void
 ovn_update_ipv6_prefix(struct hmap *lr_ports)
 {
@@ -3078,8 +2915,6 @@ ovn_port_update_sbrec(struct ovsdb_idl_txn *ovnsb_txn,
         const char *addresses = ds_cstr(&s);
         sbrec_port_binding_set_mac(op->sb, &addresses, 1);
         ds_destroy(&s);
-
-        sbrec_port_binding_set_external_ids(op->sb, &op->nbrp->external_ids);
     } else {
         if (op->mirror_target_port) {
             /* In case of using a lport mirror, we establish a port binding
@@ -3287,15 +3122,6 @@ ovn_port_update_sbrec(struct ovsdb_idl_txn *ovnsb_txn,
             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");
-        if (name && name[0]) {
-            smap_add(&ids, "name", name);
-        }
-        sbrec_port_binding_set_external_ids(op->sb, &ids);
-        smap_destroy(&ids);
-
         if (!op->nbsp->n_mirror_rules) {
             /* Nothing is set. Clear mirror_rules from pb. */
             sbrec_port_binding_set_mirror_rules(op->sb, NULL, 0);
@@ -3353,27 +3179,6 @@ cleanup_sb_ha_chassis_groups(
     }
 }
 
-static void
-cleanup_stale_fdb_entries(const struct sbrec_fdb_table *sbrec_fdb_table,
-                          struct hmap *ls_datapaths)
-{
-    const struct sbrec_fdb *fdb_e;
-    SBREC_FDB_TABLE_FOR_EACH_SAFE (fdb_e, sbrec_fdb_table) {
-        bool delete = true;
-        struct ovn_datapath *od
-            = ovn_datapath_find_by_key(ls_datapaths, fdb_e->dp_key);
-        if (od) {
-            if (ovn_tnlid_present(&od->port_tnlids, fdb_e->port_key)) {
-                delete = false;
-            }
-        }
-
-        if (delete) {
-            sbrec_fdb_delete(fdb_e);
-        }
-    }
-}
-
 static void
 delete_fdb_entries(struct ovsdb_idl_index *sbrec_fdb_by_dp_and_port,
                  uint32_t dp_key, uint32_t port_key)
@@ -4137,64 +3942,6 @@ sync_pbs_for_northd_changed_ovn_ports(
     return true;
 }
 
-static bool
-ovn_port_add_tnlid(struct ovn_port *op, uint32_t tunnel_key)
-{
-    bool added = ovn_add_tnlid(&op->od->port_tnlids, tunnel_key);
-    if (added) {
-        op->tunnel_key = tunnel_key;
-        if (tunnel_key > op->od->port_key_hint) {
-            op->od->port_key_hint = tunnel_key;
-        }
-    }
-    return added;
-}
-
-/* Returns false if the requested key is confict with another allocated key, so
- * that the I-P engine can fallback to recompute if needed; otherwise return
- * true (even if the key is not allocated). */
-static bool
-ovn_port_assign_requested_tnl_id(struct ovn_port *op)
-{
-    const struct smap *options = (op->nbsp
-                                  ? &op->nbsp->options
-                                  : &op->nbrp->options);
-    uint32_t tunnel_key = smap_get_int(options, "requested-tnl-key", 0);
-    if (tunnel_key) {
-        if (vxlan_mode && tunnel_key >= OVN_VXLAN_MIN_MULTICAST) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-            VLOG_WARN_RL(&rl, "Tunnel key %"PRIu32" for port %s "
-                         "is incompatible with VXLAN",
-                         tunnel_key, op_get_name(op));
-            return true;
-        }
-        if (!ovn_port_add_tnlid(op, tunnel_key)) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-            VLOG_WARN_RL(&rl, "Logical %s port %s requests same tunnel key "
-                         "%"PRIu32" as another LSP or LRP",
-                         op->nbsp ? "switch" : "router",
-                         op_get_name(op), tunnel_key);
-            return false;
-        }
-    }
-    return true;
-}
-
-static bool
-ovn_port_allocate_key(struct ovn_port *op)
-{
-    if (!op->tunnel_key) {
-        uint8_t key_bits = vxlan_mode ? 12 : 16;
-        op->tunnel_key = ovn_allocate_tnlid(&op->od->port_tnlids, "port",
-                                            1, (1u << (key_bits - 1)) - 1,
-                                            &op->od->port_key_hint);
-        if (!op->tunnel_key) {
-            return false;
-        }
-    }
-    return true;
-}
-
 /* Updates the southbound Port_Binding table so that it contains the logical
  * switch ports specified by the northbound database.
  *
@@ -4203,17 +3950,19 @@ ovn_port_allocate_key(struct ovn_port *op)
  * datapaths. */
 static void
 build_ports(struct ovsdb_idl_txn *ovnsb_txn,
-    const struct sbrec_port_binding_table *sbrec_port_binding_table,
     const struct sbrec_mirror_table *sbrec_mirror_table,
     const struct sbrec_mac_binding_table *sbrec_mac_binding_table,
     const struct sbrec_ha_chassis_group_table *sbrec_ha_chassis_group_table,
     struct ovsdb_idl_index *sbrec_chassis_by_name,
     struct ovsdb_idl_index *sbrec_chassis_by_hostname,
     struct ovsdb_idl_index *sbrec_ha_chassis_grp_by_name,
+    const struct ovn_paired_logical_switch_port_map *paired_lsps,
+    const struct ovn_paired_logical_router_port_map *paired_lrps,
+    const struct ovn_paired_chassisredirect_port_map *paired_crps,
+    const struct ovn_paired_mirror_map *paired_mirrors,
     struct hmap *ls_datapaths, struct hmap *lr_datapaths,
     struct hmap *ls_ports, struct hmap *lr_ports)
 {
-    struct ovs_list sb_only, nb_only, both;
     /* XXX: Add tag_alloc_table and queue_id_bitmap as part of northd_data
      * to improve I-P. */
     struct hmap tag_alloc_table = HMAP_INITIALIZER(&tag_alloc_table);
@@ -4224,107 +3973,37 @@ build_ports(struct ovsdb_idl_txn *ovnsb_txn,
     struct sset active_ha_chassis_grps =
         SSET_INITIALIZER(&active_ha_chassis_grps);
 
-    /* Borrow ls_ports for joining NB and SB for both LSPs and LRPs.
-     * We will split them later. */
-    struct hmap *ports = ls_ports;
-    join_logical_ports(sbrec_port_binding_table, ls_datapaths, lr_datapaths,
-                       ports, queue_id_bitmap,
-                       &tag_alloc_table, &sb_only, &nb_only, &both);
+    join_logical_ports(ls_datapaths, lr_datapaths,
+                       paired_lsps, paired_lrps, paired_crps,
+                       paired_mirrors, ls_ports, lr_ports, queue_id_bitmap,
+                       &tag_alloc_table);
 
-    /* Purge stale Mac_Bindings if ports are deleted. */
-    bool remove_mac_bindings = !ovs_list_is_empty(&sb_only);
-
-    /* Assign explicitly requested tunnel ids first. */
     struct ovn_port *op;
-    LIST_FOR_EACH (op, list, &both) {
-        ovn_port_assign_requested_tnl_id(op);
-    }
-    LIST_FOR_EACH (op, list, &nb_only) {
-        ovn_port_assign_requested_tnl_id(op);
-    }
-
-    /* Keep nonconflicting tunnel IDs that are already assigned. */
-    LIST_FOR_EACH (op, list, &both) {
-        if (!op->tunnel_key) {
-            ovn_port_add_tnlid(op, op->sb->tunnel_key);
-        }
-    }
-
-    /* Assign new tunnel ids where needed. */
-    LIST_FOR_EACH_SAFE (op, list, &both) {
-        if (!ovn_port_allocate_key(op)) {
-            sbrec_port_binding_delete(op->sb);
-            ovs_list_remove(&op->list);
-            ovn_port_destroy(ports, op);
-        }
-    }
-    LIST_FOR_EACH_SAFE (op, list, &nb_only) {
-        if (!ovn_port_allocate_key(op)) {
-            ovs_list_remove(&op->list);
-            ovn_port_destroy(ports, op);
-        }
-    }
-
-    /* For logical ports that are in both databases, update the southbound
-     * record based on northbound data.
-     * For logical ports that are in NB database, do any tag allocation
-     * needed. */
-    LIST_FOR_EACH_SAFE (op, list, &both) {
-        /* When reusing stale Port_Bindings, make sure that stale
-         * Mac_Bindings are purged.
-         */
-        if (op->od->sb != op->sb->datapath) {
-            remove_mac_bindings = true;
-        }
-        if (op->nbsp) {
-            tag_alloc_create_new_tag(&tag_alloc_table, op->nbsp);
-        }
+    /* For logical ports, update the southbound record based on northbound
+     * data.
+     * For logical switch ports, do any tag allocation needed.
+     */
+    HMAP_FOR_EACH (op, key_node, ls_ports) {
+        tag_alloc_create_new_tag(&tag_alloc_table, op->nbsp);
         ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
                               sbrec_chassis_by_hostname,
                               sbrec_ha_chassis_grp_by_name,
                               sbrec_mirror_table,
                               op, queue_id_bitmap,
                               &active_ha_chassis_grps);
-        op->od->is_transit_router |= is_transit_router_port(op);
-        ovs_list_remove(&op->list);
     }
 
-    /* Add southbound record for each unmatched northbound record. */
-    LIST_FOR_EACH_SAFE (op, list, &nb_only) {
-        op->sb = sbrec_port_binding_insert(ovnsb_txn);
+    HMAP_FOR_EACH (op, key_node, lr_ports) {
         ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
                               sbrec_chassis_by_hostname,
                               sbrec_ha_chassis_grp_by_name,
                               sbrec_mirror_table,
                               op, queue_id_bitmap,
                               &active_ha_chassis_grps);
-        sbrec_port_binding_set_logical_port(op->sb, op->key);
         op->od->is_transit_router |= is_transit_router_port(op);
-        ovs_list_remove(&op->list);
-    }
-
-    /* Delete southbound records without northbound matches. */
-    if (!ovs_list_is_empty(&sb_only)) {
-        LIST_FOR_EACH_SAFE (op, list, &sb_only) {
-            ovs_list_remove(&op->list);
-            sbrec_port_binding_delete(op->sb);
-            ovn_port_destroy(ports, op);
-        }
     }
 
-    /* Move logical router ports to lr_ports, and logical switch ports will
-     * remain in ports/ls_ports. */
-    HMAP_FOR_EACH_SAFE (op, key_node, ports) {
-        if (!op->nbrp) {
-            continue;
-        }
-        hmap_remove(ports, &op->key_node);
-        hmap_insert(lr_ports, &op->key_node, op->key_node.hash);
-    }
-
-    if (remove_mac_bindings) {
-        cleanup_mac_bindings(sbrec_mac_binding_table, lr_datapaths, lr_ports);
-    }
+    cleanup_mac_bindings(sbrec_mac_binding_table, lr_datapaths, lr_ports);
 
     tag_alloc_destroy(&tag_alloc_table);
     bitmap_free(queue_id_bitmap);
@@ -4468,67 +4147,39 @@ ovn_port_find_in_datapath(struct ovn_datapath *od,
     return NULL;
 }
 
-static bool
+static void
 ls_port_init(struct ovn_port *op, struct ovsdb_idl_txn *ovnsb_txn,
              struct ovn_datapath *od,
-             const struct sbrec_port_binding *sb,
              const struct sbrec_mirror_table *sbrec_mirror_table,
              struct ovsdb_idl_index *sbrec_chassis_by_name,
              struct ovsdb_idl_index *sbrec_chassis_by_hostname)
 {
     op->od = od;
     parse_lsp_addrs(op);
-    /* Assign explicitly requested tunnel ids first. */
-    if (!ovn_port_assign_requested_tnl_id(op)) {
-        return false;
-    }
-    /* Keep nonconflicting tunnel IDs that are already assigned. */
-    if (sb) {
-        if (!op->tunnel_key) {
-            ovn_port_add_tnlid(op, sb->tunnel_key);
-        }
-    }
-    /* Assign new tunnel ids where needed. */
-    if (!ovn_port_allocate_key(op)) {
-        return false;
-    }
-    /* Create new binding, if needed. */
-    if (sb) {
-        op->sb = sb;
-    } else {
-        /* XXX: the new SB port_binding will change in IDL, so need to handle
-         * SB port_binding updates incrementally to achieve end-to-end
-         * incremental processing. */
-        op->sb = sbrec_port_binding_insert(ovnsb_txn);
-        sbrec_port_binding_set_logical_port(op->sb, op->key);
-    }
     ovn_port_update_sbrec(ovnsb_txn, sbrec_chassis_by_name,
                           sbrec_chassis_by_hostname, NULL, sbrec_mirror_table,
                           op, NULL, NULL);
-    return true;
 }
 
 static struct ovn_port *
 ls_port_create(struct ovsdb_idl_txn *ovnsb_txn, struct hmap *ls_ports,
                const char *key, const struct nbrec_logical_switch_port *nbsp,
                struct ovn_datapath *od,
+               const struct sbrec_port_binding *sb,
                const struct sbrec_mirror_table *sbrec_mirror_table,
                struct ovsdb_idl_index *sbrec_chassis_by_name,
                struct ovsdb_idl_index *sbrec_chassis_by_hostname)
 {
     struct ovn_port *op = ovn_port_create(ls_ports, key, nbsp, NULL,
-                                          NULL);
+                                          sb);
     hmap_insert(&od->ports, &op->dp_node, hmap_node_hash(&op->key_node));
-    if (!ls_port_init(op, ovnsb_txn, od, NULL, sbrec_mirror_table,
-                      sbrec_chassis_by_name, sbrec_chassis_by_hostname)) {
-        ovn_port_destroy(ls_ports, op);
-        return NULL;
-    }
+    ls_port_init(op, ovnsb_txn, od, sbrec_mirror_table,
+                 sbrec_chassis_by_name, sbrec_chassis_by_hostname);
 
     return op;
 }
 
-static bool
+static void
 ls_port_reinit(struct ovn_port *op, struct ovsdb_idl_txn *ovnsb_txn,
                 const struct nbrec_logical_switch_port *nbsp,
                 struct ovn_datapath *od,
@@ -4539,10 +4190,11 @@ ls_port_reinit(struct ovn_port *op, struct 
ovsdb_idl_txn *ovnsb_txn,
 {
     ovn_port_cleanup(op);
     op->sb = sb;
+    op->tunnel_key = sb->tunnel_key;
     ovn_port_set_nb(op, nbsp, NULL);
     op->primary_port = op->cr_port = NULL;
-    return ls_port_init(op, ovnsb_txn, od, sb, sbrec_mirror_table,
-                        sbrec_chassis_by_name, sbrec_chassis_by_hostname);
+    ls_port_init(op, ovnsb_txn, od, sbrec_mirror_table,
+                 sbrec_chassis_by_name, sbrec_chassis_by_hostname);
 }
 
 /* Returns true if the logical switch has changes which can be
@@ -4559,6 +4211,7 @@ ls_changes_can_be_handled(
 {
     /* Check if the columns are changed in this row. */
     enum nbrec_logical_switch_column_id col;
+
     for (col = 0; col < NBREC_LOGICAL_SWITCH_N_COLUMNS; col++) {
         if (nbrec_logical_switch_is_updated(ls, col)) {
             if (col == NBREC_LOGICAL_SWITCH_COL_ACLS ||
@@ -4708,15 +4361,18 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn 
*ovnsb_idl_txn,
 
     /* Compare the individual ports in the old and new Logical Switches */
     for (size_t j = 0; j < changed_ls->n_ports; ++j) {
-        struct nbrec_logical_switch_port *new_nbsp = changed_ls->ports[j];
-        op = ovn_port_find_in_datapath(od, new_nbsp);
+        const struct ovn_paired_logical_switch_port *paired_lsp =
+            shash_find_data(&ni->paired_lsps->paired_switch_ports,
+                            changed_ls->ports[j]->name);
+        op = ovn_port_find_in_datapath(od, paired_lsp->nb);
 
         if (!op) {
-            if (!lsp_can_be_inc_processed(new_nbsp)) {
+            if (!lsp_can_be_inc_processed(paired_lsp->nb)) {
                 goto fail;
             }
             op = ls_port_create(ovnsb_idl_txn, &nd->ls_ports,
-                                new_nbsp->name, new_nbsp, od,
+                                paired_lsp->nb->name, paired_lsp->nb, od,
+                                paired_lsp->sb,
                                 ni->sbrec_mirror_table,
                                 ni->sbrec_chassis_by_name,
                                 ni->sbrec_chassis_by_hostname);
@@ -4724,28 +4380,27 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn 
*ovnsb_idl_txn,
                 goto fail;
             }
             add_op_to_northd_tracked_ports(&trk_lsps->created, op);
-        } else if (ls_port_has_changed(new_nbsp)) {
+        } else if (ls_port_has_changed(paired_lsp->nb)) {
             /* Existing port updated */
             bool temp = false;
-            if (lsp_is_type_changed(op->sb, new_nbsp, &temp) ||
+            if (lsp_is_type_changed(op->sb, paired_lsp->nb, &temp) ||
                 !op->lsp_can_be_inc_processed ||
-                !lsp_can_be_inc_processed(new_nbsp)) {
+                !lsp_can_be_inc_processed(paired_lsp->nb)) {
                 goto fail;
             }
-            const struct sbrec_port_binding *sb = op->sb;
-            if (sset_contains(&nd->svc_monitor_lsps, new_nbsp->name)) {
+            if (sset_contains(&nd->svc_monitor_lsps, paired_lsp->nb->name)) {
                 /* This port is used for svc monitor, which may be impacted
                  * by this change. Fallback to recompute. */
                 goto fail;
             }
-            if (!lsp_handle_mirror_rules_changes(new_nbsp) ||
+            if (!lsp_handle_mirror_rules_changes(paired_lsp->nb) ||
                  is_lsp_mirror_target_port(ni->nbrec_mirror_by_type_and_sink,
                                            op)) {
                 /* Fallback to recompute. */
                 goto fail;
             }
             if (!check_lsp_is_up &&
-                !check_lsp_changes_other_than_up(new_nbsp)) {
+                !check_lsp_changes_other_than_up(paired_lsp->nb)) {
                 /* If the only change is the "up" column while the
                  * "ignore_lsp_down" is set to true, just ignore this
                  * change. */
@@ -4754,17 +4409,11 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn 
*ovnsb_idl_txn,
             }
 
             uint32_t old_tunnel_key = op->tunnel_key;
-            if (!ls_port_reinit(op, ovnsb_idl_txn,
-                                new_nbsp,
-                                od, sb, ni->sbrec_mirror_table,
-                                ni->sbrec_chassis_by_name,
-                                ni->sbrec_chassis_by_hostname)) {
-                if (sb) {
-                    sbrec_port_binding_delete(sb);
-                }
-                ovn_port_destroy(&nd->ls_ports, op);
-                goto fail;
-            }
+            ls_port_reinit(op, ovnsb_idl_txn,
+                           paired_lsp->nb,
+                           od, paired_lsp->sb, ni->sbrec_mirror_table,
+                           ni->sbrec_chassis_by_name,
+                           ni->sbrec_chassis_by_hostname);
             add_op_to_northd_tracked_ports(&trk_lsps->updated, op);
 
             if (old_tunnel_key != op->tunnel_key) {
@@ -4789,7 +4438,6 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
             add_op_to_northd_tracked_ports(&trk_lsps->deleted, op);
             hmap_remove(&nd->ls_ports, &op->key_node);
             hmap_remove(&od->ports, &op->dp_node);
-            sbrec_port_binding_delete(op->sb);
             delete_fdb_entries(ni->sbrec_fdb_by_dp_and_port, od->tunnel_key,
                                 op->tunnel_key);
             if (is_lsp_mirror_target_port(ni->nbrec_mirror_by_type_and_sink,
@@ -19043,13 +18691,16 @@ ovnnb_db_run(struct northd_input *input_data,
                        &data->ls_datapaths, &data->lr_datapaths,
                        &data->lb_datapaths_map, &data->lb_group_datapaths_map);
     build_ports(ovnsb_txn,
-                input_data->sbrec_port_binding_table,
                 input_data->sbrec_mirror_table,
                 input_data->sbrec_mac_binding_table,
                 input_data->sbrec_ha_chassis_group_table,
                 input_data->sbrec_chassis_by_name,
                 input_data->sbrec_chassis_by_hostname,
                 input_data->sbrec_ha_chassis_grp_by_name,
+                input_data->paired_lsps,
+                input_data->paired_lrps,
+                input_data->paired_crps,
+                input_data->paired_mirrors,
                 &data->ls_datapaths.datapaths, &data->lr_datapaths.datapaths,
                 &data->ls_ports, &data->lr_ports);
     build_lb_port_related_data(ovnsb_txn,
@@ -19083,10 +18734,7 @@ ovnnb_db_run(struct northd_input *input_data,
     sync_template_vars(ovnsb_txn, input_data->nbrec_chassis_template_var_table,
                        input_data->sbrec_chassis_template_var_table);
 
-    cleanup_stale_fdb_entries(input_data->sbrec_fdb_table,
-                              &data->ls_datapaths.datapaths);
     stopwatch_stop(CLEAR_LFLOWS_CTX_STOPWATCH_NAME, time_msec());
-
 }
 
 /* Stores the set of chassis which references an ha_chassis_group.
@@ -19277,6 +18925,25 @@ handle_cr_port_binding_changes(const struct 
sbrec_port_binding *sb,
     }
 }
 
+void
+lsp_set_up(const struct sbrec_port_binding *pb,
+           const struct nbrec_logical_switch_port *lsp)
+{
+    bool up = false;
+
+    if (lsp_is_router(lsp) || lsp_is_switch(lsp)) {
+        up = true;
+    } else if (pb->chassis) {
+        up = !smap_get_bool(&pb->chassis->other_config, "is-remote", false)
+             ? pb->n_up && pb->up[0]
+             : true;
+    }
+
+    if (!lsp->up || *lsp->up != up) {
+        nbrec_logical_switch_port_set_up(lsp, &up, 1);
+    }
+}
+
 /* Handle changes to the 'chassis' column of the 'Port_Binding' table.  When
  * this column is not empty, it means we need to set the corresponding logical
  * port as 'up' in the northbound DB. */
@@ -19325,25 +18992,13 @@ handle_port_binding_changes(struct ovsdb_idl_txn 
*ovnsb_txn,
             continue;
         }
 
-        bool up = false;
-
-        if (lsp_is_router(op->nbsp) || lsp_is_switch(op->nbsp)) {
-            up = true;
-        } else if (sb->chassis) {
-            up = !smap_get_bool(&sb->chassis->other_config, "is-remote", false)
-                 ? sb->n_up && sb->up[0]
-                 : true;
-        }
-
-        if (!op->nbsp->up || *op->nbsp->up != up) {
-            nbrec_logical_switch_port_set_up(op->nbsp, &up, 1);
-        }
+        lsp_set_up(sb, op->nbsp);
 
         /* ovn-controller will update 'Port_Binding.up' only if it was
          * explicitly set to 'false'.
          */
         if (!op->sb->n_up) {
-            up = false;
+            bool up = false;
             sbrec_port_binding_set_up(op->sb, &up, 1);
         }
 
diff --git a/northd/northd.h b/northd/northd.h
index 3f904e697..45b941ce5 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -73,6 +73,12 @@ struct northd_input {
     const struct ovn_synced_logical_switch_map *synced_lses;
     const struct ovn_synced_logical_router_map *synced_lrs;
 
+    /* Paired port binding inputs. */
+    const struct ovn_paired_logical_switch_port_map *paired_lsps;
+    const struct ovn_paired_logical_router_port_map *paired_lrps;
+    const struct ovn_paired_chassisredirect_port_map *paired_crps;
+    const struct ovn_paired_mirror_map *paired_mirrors;
+
     /* Indexes */
     struct ovsdb_idl_index *sbrec_chassis_by_name;
     struct ovsdb_idl_index *sbrec_chassis_by_hostname;
@@ -379,9 +385,6 @@ struct ovn_datapath {
     size_t n_router_ports;
     size_t n_allocated_router_ports;
 
-    struct hmap port_tnlids;
-    uint32_t port_key_hint;
-
     bool has_unknown;
     bool has_vtep_lports;
     bool has_arp_proxy_port;
@@ -834,6 +837,8 @@ void ovnsb_db_run(struct ovsdb_idl_txn *ovnnb_txn,
                   const struct sbrec_ha_chassis_group_table *,
                   struct hmap *ls_ports,
                   struct hmap *lr_ports);
+void lsp_set_up(const struct sbrec_port_binding *pb,
+                const struct nbrec_logical_switch_port *lsp);
 bool northd_handle_ls_changes(struct ovsdb_idl_txn *,
                               const struct northd_input *,
                               struct northd_data *);
@@ -1044,4 +1049,10 @@ struct ovn_port_routable_addresses get_op_addresses(
 
 void destroy_routable_addresses(struct ovn_port_routable_addresses *ra);
 
+bool lsp_is_type_changed(const struct sbrec_port_binding *sb,
+                         const struct nbrec_logical_switch_port *nbsp,
+                         bool *update_sbrec);
+
+const char *
+ovn_datapath_name(const struct sbrec_datapath_binding *sb);
 #endif /* NORTHD_H */
diff --git a/northd/port_binding_pair.c b/northd/port_binding_pair.c
new file mode 100644
index 000000000..bfd3d0b42
--- /dev/null
+++ b/northd/port_binding_pair.c
@@ -0,0 +1,81 @@
+/* Copyright (c) 2025, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "port_binding_pair.h"
+
+struct ovn_unpaired_port_binding *
+ovn_unpaired_port_binding_alloc(uint32_t requested_tunnel_key,
+                                const char *name,
+                                const char *type,
+                                void *cookie,
+                                const struct sbrec_datapath_binding *sb_dp)
+{
+    struct ovn_unpaired_port_binding *pb = xzalloc(sizeof *pb);
+    pb->requested_tunnel_key = requested_tunnel_key;
+    pb->name = name;
+    pb->type = type;
+    pb->cookie = cookie;
+    pb->sb_dp = sb_dp;
+    smap_init(&pb->external_ids);
+
+    return pb;
+}
+
+void
+ovn_unpaired_port_binding_destroy(struct ovn_unpaired_port_binding *pb)
+{
+    smap_destroy(&pb->external_ids);
+}
+
+static bool
+default_sb_is_valid(const struct sbrec_port_binding *sb_pb OVS_UNUSED,
+                    const struct ovn_unpaired_port_binding *upb OVS_UNUSED)
+{
+    return true;
+}
+
+static struct ovn_unpaired_port_binding_map_callbacks default_callbacks = {
+    .sb_is_valid = default_sb_is_valid,
+};
+
+void
+ovn_unpaired_port_binding_map_init(
+    struct ovn_unpaired_port_binding_map *map,
+    const struct ovn_unpaired_port_binding_map_callbacks *cb)
+{
+    shash_init(&map->ports);
+    if (cb) {
+        map->cb = cb;
+    } else {
+        map->cb = &default_callbacks;
+    }
+}
+
+void
+ovn_unpaired_port_binding_map_destroy(
+    struct ovn_unpaired_port_binding_map *map)
+{
+    struct ovn_unpaired_port_binding *pb;
+    struct shash_node *node;
+    SHASH_FOR_EACH_SAFE (node, &map->ports) {
+        pb = node->data;
+        shash_delete(&map->ports, node);
+        ovn_unpaired_port_binding_destroy(pb);
+        free(pb);
+    }
+    shash_destroy(&map->ports);
+}
diff --git a/northd/port_binding_pair.h b/northd/port_binding_pair.h
new file mode 100644
index 000000000..c76d30ca1
--- /dev/null
+++ b/northd/port_binding_pair.h
@@ -0,0 +1,117 @@
+/* Copyright (c) 2025, 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 PORT_BINDING_PAIR_H
+#define PORT_BINDING_PAIR_H 1
+
+#include "openvswitch/hmap.h"
+#include "openvswitch/list.h"
+#include "openvswitch/shash.h"
+#include "smap.h"
+
+/* Port Binding pairing API. This file consists of utility functions
+ * that can be used when pairing northbound port types (e.g.
+ * Logical_Router_Port and Logical_Switch_Port) to southbound Port_Bindings.
+ *
+ * The basic flow of data is as such.
+ * 1. A northbound type is converted into an ovn_unpaired_port_binding.
+ * All ovn_unpaired_port_bindings are placed into an ovn_unpaired_datapath_map.
+ * 2. The en_port_binding_pair node takes all of the maps in as input and
+ * pairs them with southbound port bindings. This includes allocating
+ * tunnel keys across all ports. The output of this node is
+ * ovn_paired_port_bindings, which contains a list of all paired port bindings.
+ * 3. A northbound type-aware node then takes the ovn_paired_port_bindings,
+ * and decodes the generic paired port bindings back into a type-specific
+ * version (e.g. ovn_paired_logical_router_port). Later nodes can then consume
+ * these type-specific paired port binding types in order to perform
+ * further processing.
+ *
+ * It is important to note that this code pairs northbound ports to southbound
+ * port bindings, but it does not 100% sync them. The following fields are
+ * synced between the northbound port and the southbound Port_Binding:
+ * - logical_port
+ * - tunnel_key
+ * - external_ids
+ *
+ * Two later incremental engine nodes sync the rest of the fields on the Port
+ * Binding. en_northd syncs the vast majority of the data. Then finally,
+ * en_sync_to_sb syncs the nat_addresses of the Port_Binding.
+ */
+
+struct ovn_unpaired_port_binding {
+    uint32_t requested_tunnel_key;
+    struct smap external_ids;
+    void *cookie;
+    const char *name;
+    const char *type;
+    const struct sbrec_datapath_binding *sb_dp;
+};
+
+struct sbrec_port_binding;
+struct ovn_unpaired_port_binding_map_callbacks {
+    bool (*sb_is_valid)(const struct sbrec_port_binding *sp_pb,
+                        const struct ovn_unpaired_port_binding *upb);
+};
+
+struct ovn_unpaired_port_binding_map {
+    struct shash ports;
+    const struct ovn_unpaired_port_binding_map_callbacks *cb;
+};
+
+struct sbrec_port_binding;
+struct unpaired_port_data;
+
+struct unpaired_port_data_callbacks {
+    bool (*is_valid)(const struct unpaired_port_data *unpaired,
+                     const struct sbrec_port_binding *sp_pb);
+    struct ovn_unpaired_port_binding *
+        (*find)(const struct unpaired_port_data *unpaired,
+                const struct sbrec_port_binding *sb_pb);
+    void (*get_ports)(const struct unpaired_port_data *unpaired,
+                      struct shash *returned_ports);
+};
+
+struct unpaired_port_data {
+    void *private_data;
+    struct unpaired_port_data_callbacks *cb;
+};
+
+struct ovn_paired_port_binding {
+    struct ovs_list list_node;
+    const void *cookie;
+    const char *type;
+    const struct sbrec_port_binding *sb_pb;
+};
+
+struct ovn_paired_port_bindings {
+    struct ovs_list paired_pbs;
+    struct hmap tunnel_key_maps;
+};
+
+struct ovn_unpaired_port_binding *ovn_unpaired_port_binding_alloc(
+        uint32_t requested_tunnel_key, const char *name,
+        const char *type,
+        void *cookie,
+        const struct sbrec_datapath_binding *sb_dp);
+
+void ovn_unpaired_port_binding_destroy(struct ovn_unpaired_port_binding *pb);
+
+void ovn_unpaired_port_binding_map_init(
+    struct ovn_unpaired_port_binding_map *map,
+    const struct ovn_unpaired_port_binding_map_callbacks *cb);
+void ovn_unpaired_port_binding_map_destroy(
+    struct ovn_unpaired_port_binding_map *map);
+
+#endif /* PORT_BINDING_PAIR_H */
-- 
2.47.0

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to