On Wed, Jul 22, 2026 at 12:03 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 Matteo, thank you for the v2, I have couple of small comments down below. > 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); > nit: Should be DBG too. > + } 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"); > nit: Wrong alignment. > + 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 > Do we actually need the ethtool userspace binary? AFAIK ovs is using the ioctl so the check for Linux should be enough as we don't use the ethtool directly in the test. + > 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 > nit: We usually aling the '\'. > + > +# 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" > nit: Let's actually keep the error if the cleanup is broken. > +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 > > I can take care of that during merge, I was just wondering about the ethtool question above. Regards, Ales _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
