On Tue, Jul 7, 2026 at 12:30 PM Matteo Perin via dev < [email protected]> wrote:
> 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]> > --- > Hi Matoe, thank you for the patch, I have a few comments down below. Should we also add a NEWS entry for this? > This is a reworked version of this RFC: > https://mail.openvswitch.org/pipermail/ovs-dev/2025-December/428792.html > moving the veth peer ifindex discovery to the OVS side, as suggested. > > Now that we can rely on the peer_ifindex in the status column we can > get all the required info with a simple query without involving ETHTOOL > from the controller code. > > This will allow to remove the need of additional configuration when using > routing-protocol-redirect with one of the most common network setups used > along routing daemons. > > Best Regards, > Matteo > > controller/ovn-controller.8.xml | 8 ++ > controller/route.c | 140 ++++++++++++++++++++++++++++--- > northd/northd.c | 5 ++ > ovn-nb.xml | 26 ++++++ > tests/atlocal.in | 3 + > tests/system-ovn.at | 143 ++++++++++++++++++++++++++++++++ > 6 files changed, 315 insertions(+), 10 deletions(-) > > 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..1e1cdc786 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,46 @@ 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_INFO_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); > + } > nit: Let's reduce this to DBG. > + > + return peer_iface; > } > > static void > @@ -275,11 +361,7 @@ 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 (port_name) { > /* 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 > @@ -292,6 +374,44 @@ route_run(struct route_ctx_in *r_ctx_in, > ifname); > } > sset_add(r_ctx_out->filtered_ports, port_name); > + } else { > Could we avoid this deep nesting? > + const char *redirect_port = smap_get(&repb->options, > + > "routing-protocol-redirect"); > + if (redirect_port) { > + /* routing-protocol-redirect points to a LSP. If that > LSP > + * is bound locally and connected through a veth > pair, we > + * can auto-discover its peer interface and use it to > + * scope route learning, without requiring > + * dynamic-routing-port-name/port-mapping to be > manually > + * configured. Track 'redirect_port' so that we > + * recompute if its binding changes. */ > + sset_add(r_ctx_out->filtered_ports, redirect_port); > + > + char *peer_iface = find_veth_peer_for_redirect_port( > + r_ctx_in->local_bindings, r_ctx_in->chassis, > + redirect_port); > + if (peer_iface) { > + /* Auto-discovery succeeded: this LRP now filters > on > + * a specific interface, just like an explicit > + * dynamic-routing-port-name would. */ > + lr_has_port_name_filter = true; > + smap_add(&ad->bound_ports, > local_peer->logical_port, > + peer_iface); + free(peer_iface); > + } else { > + /* Auto-discovery failed (redirect port not bound > + * locally, not a veth device, or its peer cannot > be > + * resolved): fall back to learning routes from > all > + * interfaces on this LRP. */ > + smap_add_nocopy(&ad->bound_ports, > + xstrdup(local_peer->logical_port), > + NULL); + } > + } else { > + /* No port-name set, so we learn routes from all > ports. */ > + smap_add_nocopy(&ad->bound_ports, > + xstrdup(local_peer->logical_port), > NULL); > + } > This whole section can be greatly simplified with a few modifications: diff --git a/controller/route.c b/controller/route.c index 1e1cdc786..6802f82ea 100644 --- a/controller/route.c +++ b/controller/route.c @@ -377,6 +377,7 @@ route_run(struct route_ctx_in *r_ctx_in, } else { const char *redirect_port = smap_get(&repb->options, "routing-protocol-redirect"); + char *peer_iface = NULL; if (redirect_port) { /* routing-protocol-redirect points to a LSP. If that LSP * is bound locally and connected through a veth pair, we @@ -387,31 +388,15 @@ route_run(struct route_ctx_in *r_ctx_in, * recompute if its binding changes. */ sset_add(r_ctx_out->filtered_ports, redirect_port); - char *peer_iface = find_veth_peer_for_redirect_port( + peer_iface = find_veth_peer_for_redirect_port( r_ctx_in->local_bindings, r_ctx_in->chassis, redirect_port); - if (peer_iface) { - /* Auto-discovery succeeded: this LRP now filters on - * a specific interface, just like an explicit - * dynamic-routing-port-name would. */ - lr_has_port_name_filter = true; - smap_add(&ad->bound_ports, local_peer->logical_port, - peer_iface); - free(peer_iface); - } else { - /* Auto-discovery failed (redirect port not bound - * locally, not a veth device, or its peer cannot be - * resolved): fall back to learning routes from all - * interfaces on this LRP. */ - smap_add_nocopy(&ad->bound_ports, - xstrdup(local_peer->logical_port), - NULL); - } - } else { - /* No port-name set, so we learn routes from all ports. */ - smap_add_nocopy(&ad->bound_ports, - xstrdup(local_peer->logical_port), NULL); } + + lr_has_port_name_filter = !!peer_iface; + smap_add_nocopy(&ad->bound_ports, + xstrdup(local_peer->logical_port), + peer_iface); } } > } > } > > diff --git a/northd/northd.c b/northd/northd.c > index aecfb2ec6..3edecad72 100644 > --- a/northd/northd.c > +++ b/northd/northd.c > @@ -4190,6 +4190,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 33a6dc676..03fd7a70b 100644 > --- a/ovn-nb.xml > +++ b/ovn-nb.xml > @@ -4487,6 +4487,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 ed5d63fd3..7cd44ee69 100644 > --- a/tests/system-ovn.at > +++ b/tests/system-ovn.at > @@ -20970,6 +20970,149 @@ 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 > +# Delete if already exists to avoid "File exists" errors > +ip link del bgp-ovn 2>/dev/null || true > +check ip link add bgp-ovn type veth peer name bgp-peer > +on_exit "ip link del bgp-ovn 2>/dev/null || true" > The on_exit should be enough here, there is no need to remove that interface before the test. > +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 > + > +wait_for_ports_up bgp-lsp > nit: Missing ovn-nbctl --wait=hv sync. > + > +# 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 > > Thanks, Ales _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
