The OVN native route learning code added the LRP dynamic-routing-port-name
and accompanying dynamic-routing-port-mapping key in the local OVS table.

When the routing-protocol-redirect option is in use on an LRP, the need to
manually set the dynamic-routing-port-name and dynamic-routing-port-mapping
options can be removed if OVN can support looking up the veth pair bound to
the LSP referred to, when using veth pairs to connect routing daemons to OVN.

This commit introduces this capability: when the routing-protocol-redirect
option is configured on a LRP to redirect routing protocol traffic to a LSP,
and that LSP is bound to a veth interface, ovn-controller can now automatically
discover the peer interface name and uses it for dynamic route learning.

It works by:
- Looking up the local binding when routing-protocol-redirect specifies a
logical switch port.
- If the bound interface is a veth device, it reads the peer_ifindex value
from the OVS interface status column.
- This peer interface name is used for route learning in the configured VRF.

If auto-discovery fails, the system falls back to the previous behavior of
learning routes from all interfaces on the logical router port.

This feature is, of course, available on Linux only and requires an updated
version of OVS which populates peer_ifindex in the interface status column.

This will simplify dynamic routing deployments by automating the interface
mapping configuration that was previously required for veth-based routing
daemon integrations.

Signed-off-by: Matteo Perin <[email protected]>
---
 NEWS                            |   8 ++
 controller/ovn-controller.8.xml |   8 ++
 controller/route.c              | 181 ++++++++++++++++++++++++++++----
 northd/northd.c                 |   5 +
 ovn-nb.xml                      |  26 +++++
 tests/atlocal.in                |   3 +
 tests/system-ovn.at             | 142 +++++++++++++++++++++++++
 7 files changed, 350 insertions(+), 23 deletions(-)

diff --git a/NEWS b/NEWS
index c7cdcc5a4..42d0082c8 100644
--- a/NEWS
+++ b/NEWS
@@ -46,6 +46,14 @@ Post v26.03.0
        port, so that they can be advertised as EVPN Type-2 routes.  This
        is gated on a new "nat" token of the Logical_Switch
        "dynamic-routing-redistribute" option, independent of "ip".
+     * ovn-controller now automatically discovers the veth peer interface
+       of a Logical Switch Port referenced by the "routing-protocol-redirect"
+       option, when that port is bound locally and connected through a veth
+       pair.  This removes the need to manually configure
+       "dynamic-routing-port-name" and/or the Open_vSwitch
+       "external_ids:dynamic-routing-port-mapping" for veth-based routing
+       daemon integrations.  This feature is Linux-only and requires
+       OVS version >= 4.0.
    - Added "override-connected" option to Logical Router Static Routes to mark
      static routes as higher-priority than connected routes, which in turn led
      to changes in administrative distance for specific route types. Please see
diff --git a/controller/ovn-controller.8.xml b/controller/ovn-controller.8.xml
index ec713ce4f..dc4d106e5 100644
--- a/controller/ovn-controller.8.xml
+++ b/controller/ovn-controller.8.xml
@@ -414,6 +414,14 @@
           dynamic-routing-port-name option on Logical_Router_Ports.
           See the <code>ovn-nb</code>(5) for more details.
         </p>
+
+        <p>
+          Note: When using the <code>routing-protocol-redirect</code> option
+          with veth pairs on Linux systems, this mapping may not be necessary
+          as <code>ovn-controller</code> can automatically discover the veth
+          peer interface name. See the <code>routing-protocol-redirect</code>
+          option documentation in <code>ovn-nb</code>(5) for details.
+        </p>
       </dd>
 
       <dt><code>external_ids:ovn-cleanup-on-exit</code></dt>
diff --git a/controller/route.c b/controller/route.c
index 13e6d3010..79a7b10b7 100644
--- a/controller/route.c
+++ b/controller/route.c
@@ -38,6 +38,42 @@ VLOG_DEFINE_THIS_MODULE(exchange);
 #define PRIORITY_DEFAULT 1000
 #define PRIORITY_LOCAL_BOUND 100
 
+/* Discover the veth peer interface name of 'iface' using the
+ * status:peer_ifindex value that OVS populates for veth devices.
+ *
+ * Returns the peer interface name, or NULL if 'iface' is not a veth device
+ * or if the peer ifindex does not resolve to an interface in this
+ * namespace.
+ *
+ * Caller must free the returned string.
+ */
+static char *
+find_veth_peer(const struct ovsrec_interface *iface)
+{
+    if (!iface) {
+        return NULL;
+    }
+
+    /* Only veth devices have status:peer_ifindex set. */
+    const char *peer_ifindex_str = smap_get(&iface->status, "peer_ifindex");
+    if (!peer_ifindex_str) {
+        return NULL;
+    }
+
+    unsigned int peer_ifindex;
+    if (!str_to_uint(peer_ifindex_str, 10, &peer_ifindex) || !peer_ifindex) {
+        return NULL;
+    }
+
+    /* Resolve the peer ifindex in ovn-controller namespace. */
+    char peer_ifname[IFNAMSIZ];
+    if (!if_indextoname(peer_ifindex, peer_ifname)) {
+        return NULL;
+    }
+
+    return xstrdup(peer_ifname);
+}
+
 static bool
 route_exchange_relevant_port(const struct sbrec_port_binding *pb)
 {
@@ -150,6 +186,25 @@ build_port_mapping(struct smap *mapping, const char 
*port_mapping)
     free(orig);
 }
 
+/* Looks up the OVS interface locally bound to logical port 'port_name'.
+ * Returns NULL if 'port_name' has no local binding on this chassis or
+ * if the port binding is not resident on 'chassis'. */
+static const struct ovsrec_interface *
+local_iface_for_port_name(struct shash *local_bindings,
+                          const struct sbrec_chassis *chassis,
+                          const char *port_name)
+{
+    const struct binding_lport *b_lport =
+        local_binding_get_primary_lport(local_binding_find(local_bindings,
+                                                           port_name));
+
+    if (!b_lport || !lport_pb_is_chassis_resident(chassis, b_lport->pb)) {
+        return NULL;
+    }
+
+    return b_lport->lbinding->iface;
+}
+
 static const char *
 ifname_from_port_name(const struct smap *port_mapping,
                       struct shash *local_bindings,
@@ -161,15 +216,110 @@ ifname_from_port_name(const struct smap *port_mapping,
         return iface;
     }
 
-    const struct binding_lport *b_lport =
-        local_binding_get_primary_lport(local_binding_find(local_bindings,
-                                                           port_name));
+    const struct ovsrec_interface *ovs_iface =
+        local_iface_for_port_name(local_bindings, chassis, port_name);
 
-    if (!b_lport || !lport_pb_is_chassis_resident(chassis, b_lport->pb)) {
+    return ovs_iface ? ovs_iface->name : NULL;
+}
+
+/* Resolves the veth peer interface name for the Logical Switch Port referred
+ * to by the LRP 'routing-protocol-redirect' option ('redirect_port').
+ * Returns NULL, without logging, if 'redirect_port' is not bound locally,
+ * since some other ovn-controller is expected to handle it. Returns NULL,
+ * after logging, if 'redirect_port' is bound locally but its interface is
+ * not a veth device or its peer cannot be resolved.
+ *
+ * Caller must free the returned string.
+ */
+static char *
+find_veth_peer_for_redirect_port(struct shash *local_bindings,
+                                 const struct sbrec_chassis *chassis,
+                                 const char *redirect_port)
+{
+    const struct ovsrec_interface *iface =
+        local_iface_for_port_name(local_bindings, chassis, redirect_port);
+    if (!iface) {
         return NULL;
     }
 
-    return b_lport->lbinding->iface->name;
+    char *peer_iface = find_veth_peer(iface);
+
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
+    if (peer_iface) {
+        VLOG_INFO_RL(&rl, "Auto-discovered veth peer '%s' for port '%s' "
+                     "(bound to '%s')", peer_iface, redirect_port,
+                     iface->name);
+    } else {
+        VLOG_DBG_RL(&rl, "Cannot auto-discover veth peer for port '%s' "
+                    "(bound to '%s'), falling back to learning routes "
+                    "from all ports", redirect_port, iface->name);
+    }
+
+    return peer_iface;
+}
+
+/* Determines which local interface, if any, route learning for local_peer
+ * should be scoped to, and records it in ad->bound_ports (keyed by the
+ * peer logical port).  The value is:
+ *  - the resolved interface name, if learning must be filtered to a
+ *    specific interface (an explicit dynamic-routing-port-name, or a veth
+ *    peer auto-discovered from routing-protocol-redirect); or
+ *  - NULL, if routes should be learned from all interfaces on this LRP.
+ *
+ * A port referenced via dynamic-routing-port-name or via
+ * routing-protocol-redirect is added to filtered_ports so that changes to
+ * its binding trigger a recompute.
+ *
+ * Returns true if a specific interface filter was applied for this peer
+ * (i.e. the value stored in bound_ports is non-NULL), false otherwise. */
+static bool
+route_add_bound_port(struct route_ctx_in *r_ctx_in,
+                     struct route_ctx_out *r_ctx_out,
+                     struct advertise_datapath_entry *ad,
+                     const struct sbrec_port_binding *repb,
+                     const struct sbrec_port_binding *local_peer,
+                     const char *port_name,
+                     const struct smap *port_mapping)
+{
+    /* An explicit dynamic-routing-port-name filters learning to the name
+     * from the port-mapping or the interface name of the local binding. If
+     * the port is neither in the port-mapping nor bound locally we will not
+     * learn routes for this port. */
+    if (port_name) {
+        const char *ifname = ifname_from_port_name(
+            port_mapping, r_ctx_in->local_bindings,
+            r_ctx_in->chassis, port_name);
+        if (ifname) {
+            smap_add(&ad->bound_ports, local_peer->logical_port, ifname);
+        }
+        sset_add(r_ctx_out->filtered_ports, port_name);
+        return true;
+    }
+
+    /* No explicit port-name. If routing-protocol-redirect points to a LSP
+     * that is bound locally and connected through a veth pair, auto-discover
+     * its peer interface and use it to scope route learning, without
+     * requiring dynamic-routing-port-name/port-mapping to be configured
+     * manually. */
+    char *peer_iface = NULL;
+    const char *redirect_port = smap_get(&repb->options,
+                                          "routing-protocol-redirect");
+    if (redirect_port) {
+        /* Track redirect_port so that we recompute if its binding changes. */
+        sset_add(r_ctx_out->filtered_ports, redirect_port);
+
+        peer_iface = find_veth_peer_for_redirect_port(
+            r_ctx_in->local_bindings, r_ctx_in->chassis, redirect_port);
+    }
+
+    /* If auto-discovery succeeded, peer_iface scopes learning to that
+     * interface (like an explicit dynamic-routing-port-name would).
+     * Otherwise peer_iface is NULL and we learn routes from all
+     * interfaces on this LRP.  Either way bound_ports takes ownership of
+     * peer_iface. */
+    smap_add_nocopy(&ad->bound_ports, xstrdup(local_peer->logical_port),
+                    peer_iface);
+    return peer_iface != NULL;
 }
 
 static void
@@ -275,24 +425,9 @@ route_run(struct route_ctx_in *r_ctx_in,
                          route_get_table_id(ad->db));
             }
 
-            if (!port_name) {
-                /* No port-name set, so we learn routes from all ports. */
-                smap_add_nocopy(&ad->bound_ports,
-                                xstrdup(local_peer->logical_port), NULL);
-            } else {
-                /* If a port_name is set the we filter for the name as set in
-                 * the port-mapping or the interface name of the local
-                 * binding. If the port is not in the port_mappings and not
-                 * bound locally we will not learn routes for this port. */
-                const char *ifname = ifname_from_port_name(
-                    &port_mapping, r_ctx_in->local_bindings,
-                    r_ctx_in->chassis, port_name);
-                if (ifname) {
-                    smap_add(&ad->bound_ports, local_peer->logical_port,
-                             ifname);
-                }
-                sset_add(r_ctx_out->filtered_ports, port_name);
-            }
+            lr_has_port_name_filter |= route_add_bound_port(
+                r_ctx_in, r_ctx_out, ad, repb, local_peer, port_name,
+                &port_mapping);
         }
 
         if (ad) {
diff --git a/northd/northd.c b/northd/northd.c
index 22108dd82..a1f3be739 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -4196,6 +4196,11 @@ sync_pb_for_lrp(struct ovn_port *op,
             if (portname) {
                 smap_add(&new, "dynamic-routing-port-name", portname);
             }
+            const char *redirect_port = smap_get(&op->nbrp->options,
+                                                 "routing-protocol-redirect");
+            if (redirect_port) {
+                smap_add(&new, "routing-protocol-redirect", redirect_port);
+            }
         }
 
         const char *redistribute_local_only_name =
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 1901bbf26..abab35913 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -4549,6 +4549,32 @@ or
           Logical Switch and act as if they were listening on Logical Router
           Port's IP addresses.
         </p>
+
+        <p>
+          When used with dynamic routing (when <ref column="options"
+          key="dynamic-routing" table="Logical_Router"/> is set to
+          <code>true</code>), if the specified Logical Switch Port is bound
+          locally and connected to a veth pair, <code>ovn-controller</code>
+          is able to automatically discover the peer interface name and use
+          it for route learning. This removes the need to manually configure
+          <ref column="options" key="dynamic-routing-port-name"/> and/or
+          <ref key="dynamic-routing-port-mapping" table="Open_vSwitch"
+          column="external_ids" db="Open_vSwitch"/> for veth-based routing
+          daemon integrations. If those options are set they always take
+          precedence over auto-discovery.
+        </p>
+
+        <p>
+          The auto-discovery feature relies on the peer interface index
+          being reported in the <code>Open_vSwitch</code>
+          <code>Interface</code> table <code>status:peer_ifindex</code>
+          key, which is only populated for veth devices on Linux systems by
+          a sufficiently recent version of Open vSwitch. If the bound
+          interface is not a veth device, if the peer cannot be resolved, or
+          if OVS does not report <code>status:peer_ifindex</code>, the system
+          will fallback to learning routes from all interfaces on the Logical
+          Router Port.
+        </p>
       </column>
 
       <column name="options" key="routing-protocols" type='{"type": "string"}'>
diff --git a/tests/atlocal.in b/tests/atlocal.in
index 2683e9a2f..408ebb114 100644
--- a/tests/atlocal.in
+++ b/tests/atlocal.in
@@ -162,6 +162,9 @@ find_command scapy
 # Set HAVE_NFT
 find_command nft
 
+# Set HAVE_ETHTOOL
+find_command ethtool
+
 CURL_OPT="-g -v --max-time 1 --retry 2 --retry-delay 1 --connect-timeout 1"
 
 # Determine whether "diff" supports "normal" diffs.  (busybox diff does not.)
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 17b4dcb3d..1eccdd739 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -20976,6 +20976,148 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port 
patch-.*/d
 /Failed to acquire.*/d
 /connection dropped.*/d
 /Couldn't parse IPv6 prefix nexthop.*/d"])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([dynamic-routing - routing-protocol-redirect auto-discovery])
+AT_SKIP_IF([test "$(uname -s)" != "Linux"])
+AT_SKIP_IF([test $HAVE_ETHTOOL = "no"])
+
+vni=1337
+VRF_RESERVE([$vni])
+
+# This test validates that automatic veth peer discovery works with the
+# routing-protocol-redirect option, using the status:peer_ifindex key
+# populated by Open vSwitch for veth devices, and that routes can be
+# learned through the auto-discovered interface.
+# Note: This feature is Linux-only, as status:peer_ifindex is only
+# populated by OVS for veth devices on Linux.
+#
+# Topology:
+#  +----------+
+#  |    lr    | (learns routes from VRF 1337)
+#  +----+-----+
+#       |
+#  +----+----+
+#  |   ls    |
+#  +----+----+
+#       |
+#  +----+------+     +----------+
+#  | bgp-lsp   |-----| bgp-peer | (veth pair - auto-discovered, in VRF 1337)
+#  +-----------+     +----------+
+
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+
+ADD_BR([br-int])
+check ovs-vsctl \
+    -- set Open_vSwitch . external-ids:system-id=hv1 \
+    -- set Open_vSwitch . 
external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+    -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+    -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+    -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+start_daemon ovn-controller
+
+# Create VRF for route learning
+OVS_WAIT_WHILE([ip link | grep -q ovnvrf$vni:.*UP])
+check ip link add vrf-$vni type vrf table $vni
+on_exit "ip link del vrf-$vni"
+check ip link set vrf-$vni up
+
+# Create logical router with routing-protocol-redirect.
+# Note: neither dynamic-routing-port-name nor the Open_vSwitch
+# external_ids:dynamic-routing-port-mapping are configured, so route
+# learning can only work if ovn-controller successfully auto-discovers
+# the veth peer of "bgp-lsp".
+check ovn-nbctl \
+    -- lr-add lr \
+      -- set Logical_Router lr \
+          options:chassis=hv1 \
+          options:dynamic-routing=true \
+          options:dynamic-routing-vrf-id=$vni \
+          options:dynamic-routing-maintain-vrf=false \
+    -- lrp-add lr lr-ext 00:00:00:01:00:10 1.1.1.1/24 \
+      -- lrp-set-options lr-ext dynamic-routing=true \
+                                routing-protocol-redirect=bgp-lsp \
+    -- ls-add ls \
+      -- lsp-add-router-port ls ls-lr-ext lr-ext \
+      -- lsp-add ls bgp-lsp \
+        -- lsp-set-options bgp-lsp dynamic-routing=true \
+        -- lsp-set-addresses bgp-lsp unknown
+
+# Create veth pair: one end bound to OVN (bgp-ovn), other end for BGP daemon 
(bgp-peer)
+# The auto-discovery will find bgp-peer from bgp-ovn
+check ip link add bgp-ovn type veth peer name bgp-peer
+on_exit "ip link del bgp-ovn 2>/dev/null || true"
+check ip link set bgp-ovn up
+check ip link set bgp-peer master vrf-$vni
+check ip link set bgp-peer up
+check ip addr add 1.1.1.100/24 dev bgp-peer
+
+# Bind bgp-ovn to OVN
+check ovs-vsctl add-port br-int bgp-ovn \
+    -- set interface bgp-ovn external_ids:iface-id=bgp-lsp
+
+check ovn-nbctl --wait=hv sync
+wait_for_ports_up bgp-lsp
+
+# Verify OVS itself reports the correct peer_ifindex for "bgp-ovn" before
+# checking that ovn-controller consumed it.
+bgp_peer_ifindex=$(cat /sys/class/net/bgp-peer/ifindex)
+OVS_WAIT_UNTIL_EQUAL([ovs-vsctl get interface bgp-ovn status:peer_ifindex],
+                     ["\"$bgp_peer_ifindex\""])
+
+# Verify veth peer auto-discovery happened in ovn-controller.
+OVS_WAIT_UNTIL([grep -q "Auto-discovered veth peer 'bgp-peer' for port "\
+"'bgp-lsp'" ovn-controller.log])
+
+# Add a route to the VRF (simulating BGP learning a route via bgp-peer)
+AT_CHECK([ip route add 10.10.1.1 via 1.1.1.2 vrf vrf-$vni proto zebra])
+
+# Verify learned route appears in SB database
+OVS_WAIT_UNTIL([ovn-sbctl list Learned_Route | grep ip_prefix | grep -Fe 
10.10.1.1])
+
+# Add a second route
+AT_CHECK([ip route add 10.10.2.1 via 1.1.1.2 vrf vrf-$vni proto zebra])
+
+# Verify both routes appear in SB database
+OVS_WAIT_FOR_OUTPUT([ovn-sbctl list Learned_Route | grep ip_prefix | sort], 
[0], [dnl
+ip_prefix           : "10.10.1.1"
+ip_prefix           : "10.10.2.1"
+])
+
+# Remove one route
+AT_CHECK([ip route del 10.10.2.1 via 1.1.1.2 vrf vrf-$vni])
+
+# Verify only one route remains
+OVS_WAIT_FOR_OUTPUT([ovn-sbctl list Learned_Route | grep ip_prefix | sort], 
[0], [dnl
+ip_prefix           : "10.10.1.1"
+])
+
+# Remove second route
+AT_CHECK([ip route del 10.10.1.1 via 1.1.1.2 vrf vrf-$vni])
+
+# Verify all routes removed
+OVS_WAIT_FOR_OUTPUT([ovn-sbctl list Learned_Route | grep ip_prefix | sort], 
[0], [dnl
+])
+
+# Delete logical objects before cleanup
+check ovn-nbctl --wait=hv ls-del ls
+check ovn-nbctl --wait=hv lr-del lr
+
+OVN_CLEANUP_CONTROLLER([hv1])
+
+OVN_CLEANUP_NORTHD
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
+/failed to query port patch-.*/d
+/.*terminating with signal 15.*/d
+/could not open network device bgp-ovn.*/d"])
+
 AT_CLEANUP
 ])
 
-- 
2.43.0

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

Reply via email to