When an LRP carries dynamic-routing-redistribute=lb or =nat, northd
already enumerates neighbouring LRs' LB VIPs and NAT external IPs and
emits Advertised_Route entries for them. The advertising LR, however,
has no route to those addresses through the peer, so traffic forwarded
via the advertising LR's VRF cannot reach the backend.

Add a forwarding route on the advertising LR for each such IP: install
a parsed_route pointing at the peer LRP as nexthop. The peer LRP is
always passed as tracked_port, including for distributed NAT, whose
backend LSP is tracked separately in the Advertised_Route row.
Forwarding routes use a dedicated route source (ROUTE_SOURCE_LB or
ROUTE_SOURCE_NAT) and lose to operator-installed static routes.

Treat LB and NAT routes for the same prefix as ECMP-compatible. This
keeps both paths when an LB VIP and a NAT external IP overlap.

The advertising LRP may be unnumbered (no IP in the nexthop's family),
in which case lrp_addr_s is passed through as NULL. The emitted route
omits REG_SRC_IPV{4,6} but ARP resolution still works: the LS-level
ls_in_arp_rsp responder matches on arp.tpa alone.

This patch adds route construction and tests. Incremental change
tracking will be added in a separate change.

Signed-off-by: Dmitrii Shcherbakov <[email protected]>
---
 northd/en-advertised-route-sync.c | 160 +++++++-
 northd/en-advertised-route-sync.h |  12 +-
 northd/en-group-ecmp-route.c      |  37 +-
 northd/inc-proc-northd.c          |   4 +
 northd/northd.c                   |  20 +-
 tests/ovn-inc-proc-graph-dump.at  |   7 +-
 tests/ovn-northd.at               | 636 +++++++++++++++++++++++++++++-
 7 files changed, 840 insertions(+), 36 deletions(-)

diff --git a/northd/en-advertised-route-sync.c 
b/northd/en-advertised-route-sync.c
index 0343a021f..cafb3ca0c 100644
--- a/northd/en-advertised-route-sync.c
+++ b/northd/en-advertised-route-sync.c
@@ -166,16 +166,94 @@ dynamic_routes_track_od(struct dynamic_routes_data *data,
     uuidset_insert(od->nbr ? &data->nb_lr : &data->nb_ls, &od->key);
 }
 
+/* Install a parsed_route on advertising_od that forwards ip_address (a
+ * LB VIP or NAT external IP) through advertising_op toward tracked_port,
+ * where tracked_port must be a peer LRP on the shared LS so that its
+ * first matching-family network address is a valid nexthop.
+ *
+ * Used by the connected-neighbour redistribution paths
+ * (build_{lb,nat}_connected_routes) so the advertising LR can
+ * forward to the peer's VIPs and external IPs, not just advertise
+ * reachability for them.  The caller always passes the peer LRP as
+ * tracked_port, including for distributed NAT (whose backend LSP
+ * is tracked separately in the Advertised_Route row).
+ *
+ * Silently no-ops when:
+ *   - tracked_port is not an LRP (defensive because callers should not
+ *     pass a non-LRP), or
+ *   - the prefix string fails to parse, or
+ *   - the peer LRP carries no address of the prefix's IP family.
+ *
+ * When advertising_op is unnumbered for the nexthop's family, lrp_addr_s
+ * is NULL. */
+static void
+add_redistribute_parsed_route(struct hmap *parsed_routes_out,
+                              const struct ovn_datapath *advertising_od,
+                              const struct ovn_port *advertising_op,
+                              const struct ovn_port *tracked_port,
+                              const char *ip_address,
+                              enum route_source source,
+                              const struct ovsdb_idl_row *source_hint)
+{
+    if (!tracked_port || !tracked_port->nbrp) {
+        /* Defensive: callers must pass an LRP. */
+        return;
+    }
+
+    /* Parse the prefix (the VIP/FIP). */
+    struct in6_addr prefix;
+    if (!ip46_parse(ip_address, &prefix)) {
+        return;
+    }
+    bool is_v6 = !IN6_IS_ADDR_V4MAPPED(&prefix);
+    unsigned int plen = is_v6 ? 128 : 32;
+
+    /* Choose the nexthop from the peer LRP's first matching-family address. */
+    const char *nexthop_s = NULL;
+    if (!is_v6 && tracked_port->lrp_networks.n_ipv4_addrs) {
+        nexthop_s = tracked_port->lrp_networks.ipv4_addrs[0].addr_s;
+    } else if (is_v6 && tracked_port->lrp_networks.n_ipv6_addrs) {
+        nexthop_s = tracked_port->lrp_networks.ipv6_addrs[0].addr_s;
+    }
+    if (!nexthop_s) {
+        return;
+    }
+
+    /* If advertising_op has an address in the nexthop's family, use it as
+     * eth.src. Otherwise (unnumbered LRP) leave lrp_addr_s NULL so the
+     * emitted route omits REG_SRC_IPV{4,6}. ARP resolution still works:
+     * the LS-level ls_in_arp_rsp responder matches on arp.tpa alone. */
+    const char *lrp_addr_s = lrp_find_member_ip(advertising_op, nexthop_s);
+
+    struct in6_addr *nexthop = xmalloc(sizeof *nexthop);
+    if (!ip46_parse(nexthop_s, nexthop)) {
+        free(nexthop);
+        return;
+    }
+
+    parsed_route_add(advertising_od, nexthop, &prefix, plen, false,
+                     lrp_addr_s, advertising_op, 0, false, false, false, NULL,
+                     source, false, source_hint, tracked_port,
+                     parsed_routes_out);
+}
+
 /* This function adds a new route for each entry in lr_nat record
  * to "routes". Logical port of the route is set to "advertising_op" and
  * tracked port is set to NAT's distributed gw port. If NAT doesn't have
  * DGP (for example if it's set on gateway router), no tracked port will
- * be set.*/
+ * be set.
+ *
+ * If forwarding_port and parsed_routes_out are non-NULL, also installs a
+ * local forwarding parsed_route on advertising_op->od.  forwarding_port is
+ * the peer LRP on the shared network.  tracked_port can be a distributed
+ * NAT's backend LSP. */
 static void
 build_nat_route_for_port(const struct ovn_port *advertising_op,
                          const struct lr_nat_record *lr_nat,
                          const struct hmap *ls_ports,
-                         struct hmap *routes)
+                         struct hmap *routes,
+                         const struct ovn_port *forwarding_port,
+                         struct hmap *parsed_routes_out)
 {
     const struct ovn_datapath *advertising_od = advertising_op->od;
 
@@ -203,11 +281,22 @@ build_nat_route_for_port(const struct ovn_port 
*advertising_op,
                          nat->nb->external_ip, tracked_port,
                          ROUTE_SOURCE_NAT);
         }
+
+        if (forwarding_port && parsed_routes_out) {
+            add_redistribute_parsed_route(parsed_routes_out, advertising_od,
+                                          advertising_op, forwarding_port,
+                                          nat->nb->external_ip,
+                                          ROUTE_SOURCE_NAT,
+                                          &nat->nb->header_);
+        }
     }
 }
 
 /* Generate routes for NAT external IPs in lr_nat, for each ovn port
- * in "od" that has enabled redistribution of NAT adresses.*/
+ * in "od" that has enabled redistribution of NAT addresses.
+ *
+ * No forwarding route is needed because the LR owns the NAT and
+ * its own NAT pipeline handles ingress for the external IP. */
 static void
 build_nat_routes(const struct ovn_datapath *od,
                  const struct lr_nat_record *lr_nat,
@@ -220,15 +309,15 @@ build_nat_routes(const struct ovn_datapath *od,
             continue;
         }
 
-        build_nat_route_for_port(op, lr_nat, ls_ports, routes);
+        build_nat_route_for_port(op, lr_nat, ls_ports, routes, NULL, NULL);
     }
 }
 
 /* Similar to build_nat_routes, this function generates routes for nat records
  * in neighboring routers. For each ovn port in "od" that has enabled
- * redistribution of NAT adresses, look up their neighbors (either directly
+ * redistribution of NAT addresses, look up their neighbors (either directly
  * connected routers, or routers connected through common LS) and advertise
- * thier external NAT IPs too.*/
+ * their external NAT IPs too.*/
 static void
 build_nat_connected_routes(
     const struct ovn_datapath *od,
@@ -260,9 +349,13 @@ build_nat_connected_routes(
                 continue;
             }
 
-            /* Advertise peer's NAT routes via the local port too. */
+            /* Advertise peer's NAT routes via the local port too, and
+             * install forwarding routes so we can reach the
+             * peer's external IPs. */
             build_nat_route_for_port(op, peer_lr_stateful->lrnat_rec,
-                                     ls_ports, &data->routes);
+                                     ls_ports, &data->routes,
+                                     op->peer,
+                                     &data->parsed_routes);
             continue;
         }
 
@@ -282,9 +375,13 @@ build_nat_connected_routes(
                 continue;
             }
 
-            /* Advertise peer's NAT routes via the local port too. */
+            /* Advertise peer's NAT routes via the local port too, and
+             * install forwarding routes so we can reach the
+             * peer's external IPs. */
             build_nat_route_for_port(op, peer_lr_stateful->lrnat_rec,
-                                     ls_ports, &data->routes);
+                                     ls_ports, &data->routes,
+                                     rp->peer,
+                                     &data->parsed_routes);
             /* Track the LR datapath on the other side of LS
              * for any changes. */
             dynamic_routes_track_od(data, rp->peer->od);
@@ -292,12 +389,15 @@ build_nat_connected_routes(
     }
 }
 
-/* This function adds a new route for each IP in lb_ips to "routes".*/
+/* This function adds a new route for each IP in lb_ips to "routes".
+ * If parsed_routes_out is non-NULL, also installs a local forwarding
+ * parsed_route per VIP. */
 static void
 build_lb_route_for_port(const struct ovn_port *advertising_op,
                         const struct ovn_port *tracked_port,
                         const struct ovn_lb_ip_set *lb_ips,
-                        struct hmap *routes)
+                        struct hmap *routes,
+                        struct hmap *parsed_routes_out)
 {
     const struct ovn_datapath *advertising_od = advertising_op->od;
 
@@ -305,10 +405,22 @@ build_lb_route_for_port(const struct ovn_port 
*advertising_op,
     SSET_FOR_EACH (ip_address, &lb_ips->ips_v4_adv) {
         ar_entry_add(routes, advertising_od, advertising_op,
                      ip_address, tracked_port, ROUTE_SOURCE_LB);
+        if (parsed_routes_out) {
+            add_redistribute_parsed_route(parsed_routes_out, advertising_od,
+                                          advertising_op, tracked_port,
+                                          ip_address, ROUTE_SOURCE_LB,
+                                          &advertising_op->nbrp->header_);
+        }
     }
     SSET_FOR_EACH (ip_address, &lb_ips->ips_v6_adv) {
         ar_entry_add(routes, advertising_od, advertising_op,
                      ip_address, tracked_port, ROUTE_SOURCE_LB);
+        if (parsed_routes_out) {
+            add_redistribute_parsed_route(parsed_routes_out, advertising_od,
+                                          advertising_op, tracked_port,
+                                          ip_address, ROUTE_SOURCE_LB,
+                                          &advertising_op->nbrp->header_);
+        }
     }
 }
 
@@ -343,7 +455,7 @@ build_lb_connected_routes(const struct ovn_datapath *od,
             lr_stateful_rec = lr_stateful_table_find_by_uuid(
                 lr_stateful_table, peer_od->key);
             build_lb_route_for_port(op, op->peer, lr_stateful_rec->lb_ips,
-                                    &data->routes);
+                                    &data->routes, &data->parsed_routes);
             continue;
         }
 
@@ -360,7 +472,7 @@ build_lb_connected_routes(const struct ovn_datapath *od,
                 lr_stateful_table, rp->peer->od->key);
 
             build_lb_route_for_port(op, rp->peer, lr_stateful_rec->lb_ips,
-                                    &data->routes);
+                                    &data->routes, &data->parsed_routes);
             /* Track the LR datapath on the other side of LS
              * for any changes. */
             dynamic_routes_track_od(data, rp->peer->od);
@@ -385,14 +497,16 @@ build_lb_routes(const struct ovn_datapath *od,
          * - always redirected to a distributed gateway router port
          *
          * Advertise the LB IPs via all 'op' if this is a gateway router or
-         * throuh all DGPs of this distributed router otherwise. */
+         * through all DGPs of this distributed router otherwise. */
 
         if (od->is_gw_router) {
-            build_lb_route_for_port(op, NULL, lb_ips, routes);
+            build_lb_route_for_port(op, NULL, lb_ips, routes,
+                                    NULL);
         } else {
             struct ovn_port *dgp;
             VECTOR_FOR_EACH (&od->l3dgw_ports, dgp) {
-                build_lb_route_for_port(op, dgp, lb_ips, routes);
+                build_lb_route_for_port(op, dgp, lb_ips, routes,
+                                        NULL);
             }
         }
     }
@@ -528,6 +642,7 @@ en_dynamic_routes_init(struct engine_node *node OVS_UNUSED,
     struct dynamic_routes_data *data = xmalloc(sizeof *data);
     *data = (struct dynamic_routes_data) {
         .routes = HMAP_INITIALIZER(&data->routes),
+        .parsed_routes = HMAP_INITIALIZER(&data->parsed_routes),
         .nb_lr = UUIDSET_INITIALIZER(&data->nb_lr),
         .nb_ls = UUIDSET_INITIALIZER(&data->nb_ls),
     };
@@ -543,6 +658,11 @@ en_dynamic_routes_clear(struct dynamic_routes_data *data)
         ar_entry_free(ar);
     }
 
+    struct parsed_route *pr;
+    HMAP_FOR_EACH_POP (pr, key_node, &data->parsed_routes) {
+        parsed_route_free(pr);
+    }
+
     uuidset_clear(&data->nb_lr);
     uuidset_clear(&data->nb_ls);
 }
@@ -554,6 +674,7 @@ en_dynamic_routes_cleanup(void *data_)
 
     en_dynamic_routes_clear(data);
     hmap_destroy(&data->routes);
+    hmap_destroy(&data->parsed_routes);
     uuidset_destroy(&data->nb_lr);
     uuidset_destroy(&data->nb_ls);
 }
@@ -566,7 +687,7 @@ en_dynamic_routes_run(struct engine_node *node, void *data)
     struct ed_type_lr_stateful *lr_stateful_data =
         engine_get_input_data("lr_stateful", node);
 
-    en_dynamic_routes_clear(data);
+    en_dynamic_routes_clear(dynamic_routes_data);
 
     const struct ovn_datapath *od;
     HMAP_FOR_EACH (od, key_node, &northd_data->lr_datapaths.datapaths) {
@@ -598,6 +719,7 @@ en_dynamic_routes_run(struct engine_node *node, void *data)
         build_lb_connected_routes(od, &lr_stateful_data->table,
                                   dynamic_routes_data);
     }
+
     return EN_UPDATED;
 }
 
@@ -801,7 +923,7 @@ advertised_route_table_sync(
         sbrec_advertised_route_set_datapath(sr, route_e->od->sdp->sb_dp);
         sbrec_advertised_route_set_logical_port(sr, route_e->op->sb);
         sbrec_advertised_route_set_ip_prefix(sr, route_e->ip_prefix);
-        if (route_e->tracked_port) {
+        if (route_e->tracked_port && route_e->tracked_port->sb) {
             sbrec_advertised_route_set_tracked_port(sr,
                                                     route_e->tracked_port->sb);
         }
diff --git a/northd/en-advertised-route-sync.h 
b/northd/en-advertised-route-sync.h
index 3bb48fa0a..bebdcfddb 100644
--- a/northd/en-advertised-route-sync.h
+++ b/northd/en-advertised-route-sync.h
@@ -18,10 +18,20 @@
 
 #include "lib/inc-proc-eng.h"
 #include "lib/uuidset.h"
+#include "openvswitch/hmap.h"
 
 struct dynamic_routes_data {
-    /* Stores struct ar_entry, one for each dynamic route. */
+    /* Stores struct ar_entry, one for each dynamic route. Fed only to
+     * en_advertised_route_sync (SB Advertised_Route table). */
     struct hmap routes;
+    /* Stores struct parsed_route, one per VIP/NAT-external IP whose
+     * advertisement was synthesized from a *connected-neighbour* LR (i.e.
+     * dynamic-routing-redistribute=lb/nat for an LRP whose peer LS hosts
+     * another LR that owns the LB/NAT). Without these the advertising LR
+     * would claim reachability for a prefix it had no local forwarding
+     * route to. Fed to en_group_ecmp_route alongside en_routes and
+     * en_learned_route_sync. */
+    struct hmap parsed_routes;
     /* Contains the uuids of all NB Logical Routers where we used a
      * lr_stateful_record during computation. */
     struct uuidset nb_lr;
diff --git a/northd/en-group-ecmp-route.c b/northd/en-group-ecmp-route.c
index 87dade486..a1b121c56 100644
--- a/northd/en-group-ecmp-route.c
+++ b/northd/en-group-ecmp-route.c
@@ -21,6 +21,7 @@
 #include "openvswitch/vlog.h"
 #include "northd.h"
 
+#include "en-advertised-route-sync.h"
 #include "en-group-ecmp-route.h"
 #include "en-learned-route-sync.h"
 #include "openvswitch/hmap.h"
@@ -180,6 +181,24 @@ unique_routes_destroy(struct hmap *unique_routes)
     hmap_destroy(unique_routes);
 }
 
+/* Allow LB and NAT forwarding routes for the same prefix to coexist as
+ * ECMP members instead of one silently replacing the other.  This is an
+ * edge case: it arises only when two different peer LRs independently
+ * use the same IP as a VIP and a NAT external IP, and a CMS would
+ * typically prevent such overlap.  Without this guard, one route
+ * would silently win and traffic to the other service would be
+ * black-holed. */
+static bool
+route_sources_ecmp_compatible(enum route_source a, enum route_source b)
+{
+    if (a == b) {
+        return true;
+    }
+
+    return (a == ROUTE_SOURCE_NAT || a == ROUTE_SOURCE_LB) &&
+           (b == ROUTE_SOURCE_NAT || b == ROUTE_SOURCE_LB);
+}
+
 /* Remove the unique_routes_node from the group, and return the parsed_route
  * pointed by the removed node. */
 static const struct parsed_route *
@@ -191,7 +210,8 @@ unique_routes_remove(struct group_ecmp_datapath *gn,
         if (ipv6_addr_equals(&route->prefix, &ur->route->prefix) &&
             route->plen == ur->route->plen &&
             route->is_src_route == ur->route->is_src_route &&
-            route->source == ur->route->source &&
+            route_sources_ecmp_compatible(route->source,
+                                          ur->route->source) &&
             route->route_table_id == ur->route->route_table_id) {
             hmap_remove(&gn->unique_routes, &ur->hmap_node);
             const struct parsed_route *existed_route = ur->route;
@@ -304,7 +324,7 @@ ecmp_groups_find(struct group_ecmp_datapath *gn,
             eg->plen == route->plen &&
             eg->is_src_route == route->is_src_route &&
             eg->route_table_id == route->route_table_id &&
-            eg->source == route->source) {
+            route_sources_ecmp_compatible(eg->source, route->source)) {
             return eg;
         }
     }
@@ -356,7 +376,8 @@ add_route(struct group_ecmp_datapath *gn, const struct 
parsed_route *pr)
 static void
 group_ecmp_route(struct group_ecmp_route_data *data,
                  const struct routes_data *routes_data,
-                 const struct learned_route_sync_data *learned_route_data)
+                 const struct learned_route_sync_data *learned_route_data,
+                 const struct dynamic_routes_data *dynamic_routes_data)
 {
     struct group_ecmp_datapath *gn;
     const struct parsed_route *pr;
@@ -369,6 +390,11 @@ group_ecmp_route(struct group_ecmp_route_data *data,
         gn = group_ecmp_datapath_lookup_or_add(data, pr->od);
         add_route(gn, pr);
     }
+
+    HMAP_FOR_EACH (pr, key_node, &dynamic_routes_data->parsed_routes) {
+        gn = group_ecmp_datapath_lookup_or_add(data, pr->od);
+        add_route(gn, pr);
+    }
 }
 
 enum engine_node_state
@@ -381,8 +407,11 @@ en_group_ecmp_route_run(struct engine_node *node, void 
*_data)
         = engine_get_input_data("routes", node);
     struct learned_route_sync_data *learned_route_data
         = engine_get_input_data("learned_route_sync", node);
+    struct dynamic_routes_data *dynamic_routes_data
+        = engine_get_input_data("dynamic_routes", node);
 
-    group_ecmp_route(data, routes_data, learned_route_data);
+    group_ecmp_route(data, routes_data, learned_route_data,
+                     dynamic_routes_data);
 
     return EN_UPDATED;
 }
diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
index 33aecd445..25aa3bcf0 100644
--- a/northd/inc-proc-northd.c
+++ b/northd/inc-proc-northd.c
@@ -383,6 +383,10 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
                      group_ecmp_route_routes_change_handler);
     engine_add_input(&en_group_ecmp_route, &en_learned_route_sync,
                      group_ecmp_route_learned_route_change_handler);
+    /* Connected-neighbour redistribute={lb,nat} also emits forwarding
+     * parsed_routes. Consume those to compose ECMP groups alongside
+     * routes and learned_route_sync. */
+    engine_add_input(&en_group_ecmp_route, &en_dynamic_routes, NULL);
 
     engine_add_input(&en_sync_meters, &en_nb_acl, sync_meters_nb_acl_handler);
     engine_add_input(&en_sync_meters, &en_nb_meter, NULL);
diff --git a/northd/northd.c b/northd/northd.c
index 25ae2dc78..43be834dd 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -382,13 +382,16 @@ static const char *reg_ct_state[] = {
  *  2. ic-learned connected routes with route_table set.
  *  3. connected routes, including ic-learned.
  *  4. static routes, including ic-learned.
- *  5. routes learned from the outside via ovn-controller (e.g. bgp)
- *  6. (lowest priority) src-ip routes */
+ *  5. routes synthesized from connected-neighbour
+ *     dynamic-routing-redistribute={lb,nat}.
+ *  6. routes learned from the outside via ovn-controller (e.g. bgp)
+ *  7. (lowest priority) src-ip routes */
 #define ROUTE_PRIO_OFFSET_MULTIPLIER 12
 #define ROUTE_PRIO_OFFSET_PRIORITY_STATIC 10
 #define ROUTE_PRIO_OFFSET_IC_LEARNED_CONNECTED_WITH_TABLEID 8
 #define ROUTE_PRIO_OFFSET_CONNECTED 6
 #define ROUTE_PRIO_OFFSET_STATIC 4
+#define ROUTE_PRIO_OFFSET_REDISTRIBUTE 3
 #define ROUTE_PRIO_OFFSET_LEARNED 2
 
 #define ROUTE_PRIO_BASE_SHIFT ((MAX_PREFIX_LEN + 1) * \
@@ -12934,12 +12937,19 @@ get_route_offset(enum route_source source,
                ? ROUTE_PRIO_OFFSET_PRIORITY_STATIC
                : ROUTE_PRIO_OFFSET_STATIC;
 
+    case ROUTE_SOURCE_NAT:
+    case ROUTE_SOURCE_LB:
+        /* Priority offset for forwarding routes installed by
+         * redistribute={lb,nat}. Placed above LEARNED so dynamically
+         * learned routes for the same prefix cannot displace the locally
+         * known nexthop, and below STATIC so operator-installed routes
+         * still win. */
+        return ROUTE_PRIO_OFFSET_REDISTRIBUTE;
+
     case ROUTE_SOURCE_LEARNED:
         return ROUTE_PRIO_OFFSET_LEARNED;
 
-    /* Dynamic route types (NAT, LB, and connected-as-host) are not used. */
-    case ROUTE_SOURCE_NAT:
-    case ROUTE_SOURCE_LB:
+    /* connected-as-host advertisements don't produce forwarding routes. */
     case ROUTE_SOURCE_CONNECTED_AS_HOST:
     default:
         OVS_NOT_REACHED();
diff --git a/tests/ovn-inc-proc-graph-dump.at b/tests/ovn-inc-proc-graph-dump.at
index dd3e01147..fc44a98cf 100644
--- a/tests/ovn-inc-proc-graph-dump.at
+++ b/tests/ovn-inc-proc-graph-dump.at
@@ -168,9 +168,13 @@ digraph "Incremental-Processing-Engine" {
        learned_route_sync [[style=filled, shape=box, fillcolor=white, 
label="learned_route_sync"]];
        SB_learned_route -> learned_route_sync 
[[label="learned_route_sync_sb_learned_route_change_handler"]];
        northd -> learned_route_sync 
[[label="learned_route_sync_northd_change_handler"]];
+       dynamic_routes [[style=filled, shape=box, fillcolor=white, 
label="dynamic_routes"]];
+       lr_stateful -> dynamic_routes 
[[label="dynamic_routes_lr_stateful_change_handler"]];
+       northd -> dynamic_routes 
[[label="dynamic_routes_northd_change_handler"]];
        group_ecmp_route [[style=filled, shape=box, fillcolor=white, 
label="group_ecmp_route"]];
        routes -> group_ecmp_route 
[[label="group_ecmp_route_routes_change_handler"]];
        learned_route_sync -> group_ecmp_route 
[[label="group_ecmp_route_learned_route_change_handler"]];
+       dynamic_routes -> group_ecmp_route [[label=""]];
        ls_stateful [[style=filled, shape=box, fillcolor=white, 
label="ls_stateful"]];
        northd -> ls_stateful [[label="ls_stateful_northd_handler"]];
        port_group -> ls_stateful [[label="ls_stateful_port_group_handler"]];
@@ -218,9 +222,6 @@ digraph "Incremental-Processing-Engine" {
        SB_ecmp_nexthop -> ecmp_nexthop [[label=""]];
        SB_port_binding -> ecmp_nexthop [[label=""]];
        SB_mac_binding -> ecmp_nexthop 
[[label="ecmp_nexthop_mac_binding_handler"]];
-       dynamic_routes [[style=filled, shape=box, fillcolor=white, 
label="dynamic_routes"]];
-       lr_stateful -> dynamic_routes 
[[label="dynamic_routes_lr_stateful_change_handler"]];
-       northd -> dynamic_routes 
[[label="dynamic_routes_northd_change_handler"]];
        SB_advertised_route [[style=filled, shape=box, fillcolor=white, 
label="SB_advertised_route"]];
        advertised_route_sync [[style=filled, shape=box, fillcolor=white, 
label="advertised_route_sync"]];
        routes -> advertised_route_sync [[label=""]];
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 1f5607bdc..c1f113d56 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -17305,8 +17305,8 @@ check_engine_compute northd incremental
 check_engine_compute routes incremental
 check_engine_compute advertised_route_sync recompute
 check_engine_compute learned_route_sync incremental
-check_engine_compute group_ecmp_route unchanged
-check_engine_compute lflow incremental
+check_engine_compute group_ecmp_route recompute
+check_engine_compute lflow recompute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
@@ -17329,8 +17329,8 @@ check_engine_compute northd incremental
 check_engine_compute routes incremental
 check_engine_compute advertised_route_sync recompute
 check_engine_compute learned_route_sync incremental
-check_engine_compute group_ecmp_route unchanged
-check_engine_compute lflow incremental
+check_engine_compute group_ecmp_route recompute
+check_engine_compute lflow recompute
 CHECK_NO_CHANGE_AFTER_RECOMPUTE
 
 check as northd ovn-appctl -t ovn-northd inc-engine/clear-stats
@@ -18110,6 +18110,634 @@ OVN_CLEANUP_NORTHD
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB redistribute installs local forwarding route])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# When dynamic-routing-redistribute=lb is set on an LRP whose peer LS
+# hosts a neighbouring LR with an attached load balancer, northd emits
+# both an SB Advertised_Route entry and a /32 forwarding parsed_route
+# on the advertising LR so it can forward traffic to the peer's VIP.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+
+# lr0's transit LRP toward 'up' is unnumbered (no IPv4 address).
+# dynamic-routing-redistribute=lb is set on this LRP.
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+# lr1 is the neighbouring router that owns the load balancer.
+# Its LRP on 'up' carries an IPv4 address (10.0.0.1) which becomes the
+# nexthop for the forwarding route synthesised on lr0.
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+check ovn-nbctl \
+    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+# An Advertised_Route entry for lb0's VIP is emitted on lr0 with
+# tracked_port pointing at lr1's LRP.
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
+pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0         \
+    logical_port=$pb_lr0_up        \
+    tracked_port=$pb_lr1_up
+
+# lr0 also gets a /32 forwarding flow with reg0 = lr1's LRP IP
+# (10.0.0.1) and outport = lr0-up. Because lr0-up is unnumbered
+# (no IPv4 address) there is no REG_SRC_IPV4 clause in the action.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 
172.16.1.10/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 10.0.0.1; eth.src 
= 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 1; reg9[[9]] = 1; 
next;)
+])
+
+# Removing the redistribution option also removes the forwarding route.
+check ovn-nbctl --wait=sb remove Logical_Router_Port lr0-up options 
dynamic-routing-redistribute
+ovn-sbctl lflow-list lr0 > lr0_flows_after_remove
+AT_CHECK([grep -c 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows_after_remove], 
[1], [0
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB redistribute forwarding route - numbered LRP])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# Same as "LB redistribute installs local forwarding route" but the
+# advertising LRP is numbered (has an IPv4 address), so the emitted
+# forwarding flow includes reg5 = <src-ip> (REG_SRC_IPV4).
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01 10.0.0.2/24
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+check ovn-nbctl \
+    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+# lr0-up is numbered (10.0.0.2) so reg5 = 10.0.0.2 appears in the action.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 
172.16.1.10/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 10.0.0.1; reg5 = 
10.0.0.2; eth.src = 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 1; 
reg9[[9]] = 1; next;)
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - NAT redistribute forwarding route IPv4])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# When dynamic-routing-redistribute=nat is set on an LRP whose peer LS
+# hosts a neighbouring LR with a NAT external IP, northd emits a /32
+# forwarding parsed_route on the advertising LR.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=nat
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lrp-set-gateway-chassis lr1-up hv1
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+check ovn-nbctl --add-route lr-nat-add lr1 dnat_and_snat 172.16.1.10 
192.168.1.10
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
+pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
+
+# SB Advertised_Route for lr1's NAT external IP is emitted on lr0.
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0          \
+    logical_port=$pb_lr0_up         \
+    tracked_port=$pb_lr1_up
+
+# lr0 also gets a /32 forwarding flow to lr1-up (10.0.0.1).
+# Grep for priority 1935 specifically to avoid matching the connected
+# host route (priority 1938) that --add-route also produces.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows | grep 
'priority=1935' | ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 
172.16.1.10/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 10.0.0.1; eth.src 
= 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 1; reg9[[9]] = 1; 
next;)
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - NAT redistribute forwarding route IPv6])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# IPv6 variant of the NAT redistribute forwarding route test.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=nat
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 2001:db8::1/64
+check ovn-nbctl lrp-set-gateway-chassis lr1-up hv1
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+check ovn-nbctl --add-route lr-nat-add lr1 dnat_and_snat 2001:db8:ffff::10 
2001:db8:1::10
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
+pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
+
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="2001\:db8\:ffff\:\:10"  \
+    datapath=$datapath_lr0              \
+    logical_port=$pb_lr0_up             \
+    tracked_port=$pb_lr1_up
+
+# /128 forwarding flow for v6 NAT external IP.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep 'lr_in_ip_routing.*2001:db8:ffff::10/128' lr0_flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=3087 , match=(ip6.dst == 
2001:db8:ffff::10/128), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = 
2001:db8::1; eth.src = 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 
1; reg9[[9]] = 0; next;)
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - NAT redistribute distributed NAT tracked_port])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# When a neighbouring LR has a distributed NAT (logical_port +
+# external_mac set), the connected-neighbour path must use the
+# backend LSP as tracked_port rather than the DGP.  The DGP must
+# not be the same port that peers with the advertising LR, because
+# that creates a cr_port on the peer LSP and disables distributed
+# NAT (en-lr-nat.c: lr_nat_entry_set_dgw_port).
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=nat
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+# DGP on a separate port (lr1-ext), so lr1-up's peer LSP does not
+# get a cr_port and the NAT stays distributed.
+check ovn-nbctl lrp-add lr1 lr1-ext 00:00:00:00:00:04 192.168.2.1/24
+check ovn-nbctl lrp-set-gateway-chassis lr1-ext hv1
+check ovn-nbctl ls-add ext
+check ovn-nbctl lsp-add-router-port ext ext-lr1 lr1-ext
+check ovn-nbctl lsp-add ext ln-ext
+check ovn-nbctl lsp-set-type ln-ext localnet
+check ovn-nbctl lsp-set-options ln-ext network_name=phys
+
+check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
+check ovn-nbctl ls-add be
+check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
+check ovn-nbctl lsp-add be be-vm1
+check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
+# Distributed NAT: logical_port and external_mac point to the backend LSP.
+check ovn-nbctl --add-route lr-nat-add lr1 dnat_and_snat 172.16.1.10 
192.168.1.10 be-vm1 00:00:00:00:01:01
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
+pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
+
+# The advertised route must carry the backend LSP as tracked_port,
+# not the DGP (lr1-ext).
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0          \
+    logical_port=$pb_lr0_up         \
+    tracked_port=$pb_be_vm1
+
+# The forwarding route uses the peer LRP even though Advertised_Route tracks
+# the backend LSP.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 
172.16.1.10/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 10.0.0.1; eth.src 
= 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 1; reg9[[9]] = 1; 
next;)
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - NAT redistribute own-LR distributed NAT])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# When a router with a distributed NAT advertises its own NAT routes
+# (build_nat_routes path), the Advertised_Route must use the backend
+# LSP as tracked_port.
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl set Logical_Router lr1 \
+    options:dynamic-routing=true
+
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:01 10.0.0.1/24
+check ovn-nbctl lrp-set-options lr1-up dynamic-routing-redistribute=nat
+check ovn-nbctl lrp-set-gateway-chassis lr1-up hv1
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+check ovn-nbctl lsp-add up ln-up
+check ovn-nbctl lsp-set-type ln-up localnet
+check ovn-nbctl lsp-set-options ln-up network_name=phys
+
+check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
+check ovn-nbctl ls-add be
+check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
+check ovn-nbctl lsp-add be be-vm1
+check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
+
+# Distributed NAT: logical_port and external_mac point to the backend LSP.
+check ovn-nbctl --add-route lr-nat-add lr1 dnat_and_snat 172.16.1.10 
192.168.1.10 be-vm1 00:00:00:00:01:01
+check ovn-nbctl --wait=sb sync
+
+datapath_lr1=$(fetch_column Datapath_Binding _uuid external_ids:name=lr1)
+pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
+pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
+
+# The own-LR advertised route must carry the backend LSP as tracked_port.
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr1          \
+    logical_port=$pb_lr1_up         \
+    tracked_port=$pb_be_vm1
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - NAT redistribute connected-neighbour distributed 
NAT on localnet LS])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# A provider LS backed by a localnet port connects two LRs.  The
+# neighbour LR has a distributed NAT whose DGP is on a separate
+# provider LS (also localnet-backed).  The advertising LR must emit
+# an Advertised_Route with the backend LSP as tracked_port and a forwarding
+# parsed_route through the peer LRP on the provider network.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01 10.0.0.2/24
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=nat
+check ovn-nbctl ls-add provider
+check ovn-nbctl lsp-add-router-port provider prov-lr0 lr0-up
+check ovn-nbctl lsp-add provider ln-prov
+check ovn-nbctl lsp-set-type ln-prov localnet
+check ovn-nbctl lsp-set-options ln-prov network_name=physnet
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-prov 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port provider prov-lr1 lr1-prov
+
+# DGP on a separate provider LS, so lr1-prov's peer LSP does not get
+# a cr_port and the NAT stays distributed.
+check ovn-nbctl lrp-add lr1 lr1-ext 00:00:00:00:00:04 192.168.2.1/24
+check ovn-nbctl lrp-set-gateway-chassis lr1-ext hv1
+check ovn-nbctl ls-add ext
+check ovn-nbctl lsp-add-router-port ext ext-lr1 lr1-ext
+check ovn-nbctl lsp-add ext ln-ext
+check ovn-nbctl lsp-set-type ln-ext localnet
+check ovn-nbctl lsp-set-options ln-ext network_name=physnet2
+
+check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
+check ovn-nbctl ls-add be
+check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
+check ovn-nbctl lsp-add be be-vm1
+check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
+
+# Distributed NAT: logical_port and external_mac point to the backend LSP.
+check ovn-nbctl --add-route lr-nat-add lr1 dnat_and_snat 172.16.1.10 
192.168.1.10 be-vm1 00:00:00:00:01:01
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
+pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
+
+# Advertised_Route uses the backend LSP as tracked_port.
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0          \
+    logical_port=$pb_lr0_up         \
+    tracked_port=$pb_be_vm1
+
+# The forwarding route uses lr1-prov as its nexthop while route locality is
+# still tracked through be-vm1.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 
172.16.1.10/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 10.0.0.1; reg5 = 
10.0.0.2; eth.src = 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 1; 
reg9[[9]] = 1; next;)
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB redistribute forwarding route IPv6])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# IPv6 variant of the LB forwarding route test.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 2001:db8::1/64
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+check ovn-nbctl \
+    -- lb-add lb0 [[2001:db8:ffff::10]]:80 
[[2001:db8:1::10]]:80,[[2001:db8:1::11]]:80 \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
+pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
+
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="2001\:db8\:ffff\:\:10"   \
+    datapath=$datapath_lr0               \
+    logical_port=$pb_lr0_up              \
+    tracked_port=$pb_lr1_up
+
+# /128 forwarding flow for v6 LB VIP.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep 'lr_in_ip_routing.*2001:db8:ffff::10/128' lr0_flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=3087 , match=(ip6.dst == 
2001:db8:ffff::10/128), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = 
2001:db8::1; eth.src = 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 
1; reg9[[9]] = 0; next;)
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB group and NAT overlap use ECMP via LS])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# Two LRs (lr1, lr2) connect to a shared LS "join".  lr1 owns an LB and lr2
+# owns a NAT.  lr0 has redistribute=lb,nat on its LRP toward "join". lr0's
+# peer on "join" is an LSP (not a direct LR-LR peer), so northd discovers
+# both routes through the LS's router_ports.
+#
+# This is an edge case: a VIP IP and a NAT external IP are unlikely to
+# overlap in practice and a CMS would typically prevent it.  The test
+# verifies that both paths survive as ECMP members rather than one
+# silently replacing the other and black-holing traffic.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-join 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-join \
+    dynamic-routing-redistribute=lb,nat
+check ovn-nbctl ls-add join
+check ovn-nbctl lsp-add-router-port join join-lr0 lr0-join
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-join 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port join join-lr1 lr1-join
+check ovn-nbctl \
+    -- lb-add lb1 172.16.1.10:80 192.168.1.10:80
+lb1=$(fetch_column nb:Load_Balancer _uuid name=lb1)
+lbg1=$(ovn-nbctl create Load_Balancer_Group name=lbg1 \
+    load_balancer=$lb1)
+check ovn-nbctl add Logical_Router lr1 load_balancer_group $lbg1
+
+check ovn-nbctl lr-add lr2
+check ovn-nbctl lrp-add lr2 lr2-join 00:00:00:00:00:03 10.0.0.2/24
+check ovn-nbctl lrp-set-gateway-chassis lr2-join hv1
+check ovn-nbctl lsp-add-router-port join join-lr2 lr2-join
+check ovn-nbctl --add-route lr-nat-add lr2 dnat_and_snat \
+    172.16.1.10 192.168.2.10
+
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_join=$(fetch_column Port_Binding _uuid logical_port=lr0-join)
+pb_lr1_join=$(fetch_column Port_Binding _uuid logical_port=lr1-join)
+pb_lr2_join=$(fetch_column Port_Binding _uuid logical_port=lr2-join)
+
+# The LB VIP and NAT external IP overlap.  Both neighboring paths must be
+# represented and combined into ECMP forwarding on lr0.
+check_row_count Advertised_Route 2
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"         \
+    datapath=$datapath_lr0           \
+    logical_port=$pb_lr0_join        \
+    tracked_port=$pb_lr1_join
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"         \
+    datapath=$datapath_lr0           \
+    logical_port=$pb_lr0_join        \
+    tracked_port=$pb_lr2_join
+
+# One ECMP selection flow covers the prefix and two member flows carry the
+# respective neighbor nexthops.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows | \
+          grep -c 'select(1, 2)'], [0], [1
+])
+AT_CHECK([grep 'lr_in_ip_routing_ecmp' lr0_flows | \
+          grep -E -c 'reg0 = 10\.0\.0\.[[12]];'], [0], [2
+])
+
+# Removing the group withdraws both the advertised and forwarding route for
+# lb1 without affecting the overlapping NAT path.
+check ovn-nbctl --wait=sb remove Logical_Router lr1 load_balancer_group $lbg1
+check_row_count Advertised_Route 1 ip_prefix="172.16.1.10"
+ovn-sbctl lflow-list lr0 > lr0_flows_after_remove
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' \
+          lr0_flows_after_remove | grep -c 'reg0 = 10.0.0.2;'], [0], [1
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB forwarding route updates on nexthop change])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# Regression test: parsed_route_lookup must treat routes with different
+# nexthops as distinct.  Create a forwarding route for an LB VIP with
+# nexthop 10.0.0.1, then change the peer LRP address to 10.0.0.42 and
+# verify the logical flow is updated.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01 10.0.0.2/24
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+check ovn-nbctl \
+    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80 \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+# Forwarding flow on lr0 points at 10.0.0.1 (lr1-up's address).
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 
172.16.1.10/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 10.0.0.1; reg5 = 
10.0.0.2; eth.src = 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 1; 
reg9[[9]] = 1; next;)
+])
+
+# Change lr1-up's address from 10.0.0.1 to 10.0.0.42.
+check ovn-nbctl --wait=sb set Logical_Router_Port lr1-up 
networks=\"10.0.0.42/24\"
+
+# The forwarding flow must now use 10.0.0.42 as nexthop.
+ovn-sbctl lflow-list lr0 > lr0_flows_after
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows_after | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 
172.16.1.10/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 10.0.0.42; reg5 = 
10.0.0.2; eth.src = 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 1; 
reg9[[9]] = 1; next;)
+])
+
+# The old nexthop (10.0.0.1) must no longer appear in the flow.
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows_after | grep -c 
'reg0 = 10.0.0.1' || true], [0], [0
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB redistribute advertise=false skips forwarding 
route])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# When dynamic-routing-redistribute=lb is set on an LRP but the LB has
+# options:dynamic-routing-advertise=false, both the Advertised_Route row
+# and the forwarding parsed route / logical flow are not installed.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01 10.0.0.2/24
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+check ovn-nbctl \
+    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80 \
+    -- set Load_Balancer lb0 options:dynamic-routing-advertise=false \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+# No Advertised_Route should be emitted.
+check_row_count Advertised_Route 0
+
+# No forwarding flow for the LB VIP.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep -c 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows || true], [0], 
[0
+])
+
+# Enabling advertise should produce both.
+check ovn-nbctl --wait=sb remove Load_Balancer lb0 options 
dynamic-routing-advertise
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
+pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0          \
+    logical_port=$pb_lr0_up         \
+    tracked_port=$pb_lr1_up
+
+ovn-sbctl lflow-list lr0 > lr0_flows_on
+AT_CHECK([grep 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows_on | 
ovn_strip_lflows], [0], [dnl
+  table=??(lr_in_ip_routing   ), priority=1935 , match=(ip4.dst == 
172.16.1.10/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 10.0.0.1; reg5 = 
10.0.0.2; eth.src = 00:00:00:00:00:01; outport = "lr0-up"; flags.loopback = 1; 
reg9[[9]] = 1; next;)
+])
+
+# Disabling again should withdraw both.
+check ovn-nbctl --wait=sb set Load_Balancer lb0 
options:dynamic-routing-advertise=false
+check_row_count Advertised_Route 0
+
+ovn-sbctl lflow-list lr0 > lr0_flows_off
+AT_CHECK([grep -c 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows_off || true], 
[0], [0
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([dynamic-routing - LB sync to sb IPv6])
 AT_KEYWORDS([dynamic-routing])
-- 
2.53.0


_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to