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]> --- 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); + } + + 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 { + 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); + } } } 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" +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 + +# 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
