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

Reply via email to