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

Reply via email to