On Mon, Jul 20, 2026 at 10:18 AM Ales Musil <[email protected]> wrote:
> > > 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, > I'm sorry for mispelling your name Matteo. > > 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
