On Wed, Aug 19, 2026 at 4:44 AM Dumitru Ceara <[email protected]> wrote:
>
> On 7/29/26 6:53 AM, Han Zhou wrote:
> > For EVPN L3 (type-5) routes, the advertised L3 VNI may differ from the
> > destination logical switch's dynamic-routing-vni.  Previously OVN always
> > used the switch's VNI when encapsulating traffic towards a learned
> > route, so it could not honor a per-route VNI (e.g. symmetric IRB).
> >
> > FRR installs such routes with the L3 VNI attached as lightweight-tunnel
> > (LWT) encapsulation.  ovn-controller reads this VNI from the route and
> > stores it in the SB Learned_Route's external_ids:vni.  ovn-northd reads
> > it into the parsed route and, in the logical router IP routing stage,
> > loads it into tun_id (\"evpn_l3_vni\") with a validity bit.  tun_id is
> > not part of the generic logical register range (reg0..reg9) that is
> > zeroed on the router-to-switch transition, so it survives to the egress,
> > and it is available in every OVS version.  ovn-controller's EVPN tunnel
> > egress then matches the validity bit and takes the VNI from tun_id to
> > override the tunnel VNI, falling back to the switch's
dynamic-routing-vni
> > when it is not set.
> >
> > The validity bit is tun_id[31], which is above the 24-bit tunnel-key
> > range: a tunnel-received packet only populates tun_id[0..23] (the
on-wire
> > VNI is 24 bits), so the bit is guaranteed 0 unless the route flow sets
> > it.
> >
>
> Hi Han,
>
> I only had a brief look at this patch (so not an in-depth review) as
> it's marked as RFC.  In my opinion the approach you chose here makes
sense.
>
> > This depends on the OVS route-table library change that parses the LWT
> > tunnel id (VNI) from kernel routes, so that ovn-controller can read it.
> >
>
> I'll try to review the OVS patch first and maybe you can repost the OVN
> one once the OVS bits are merged, what do you think?

Of course. Thanks Dumitru!

>
> Thanks,
> Dumitru
>
> > Assisted-by: Claude Opus 4.8, Cursor
> > Signed-off-by: Han Zhou <[email protected]>
> > ---
> >  .../topics/dynamic-routing/architecture.rst   |  19 +++
> >  controller/physical.c                         | 139 +++++++++++++-----
> >  controller/route-exchange-netlink.c           |   2 +
> >  controller/route-exchange-netlink.h           |   5 +
> >  controller/route-exchange.c                   |  40 ++++-
> >  lib/logical-fields.c                          |  13 ++
> >  northd/en-learned-route-sync.c                |  15 +-
> >  northd/northd.c                               |  57 ++++++-
> >  northd/northd.h                               |   7 +
> >  ovn-sb.xml                                    |  10 ++
> >  tests/ovn-northd.at                           |  46 ++++++
> >  tests/system-ovn.at                           |  18 +++
> >  12 files changed, 320 insertions(+), 51 deletions(-)
> >
> > diff --git a/Documentation/topics/dynamic-routing/architecture.rst
b/Documentation/topics/dynamic-routing/architecture.rst
> > index 0ea1755626de..906b0d6ac2e8 100644
> > --- a/Documentation/topics/dynamic-routing/architecture.rst
> > +++ b/Documentation/topics/dynamic-routing/architecture.rst
> > @@ -364,6 +364,25 @@ For each qualifying route, ``ovn-controller``
creates a ``Learned_Route``
> >  record in the Southbound database containing the datapath, logical
port,
> >  IP prefix, and nexthop.
> >
> > +EVPN L3 VNI (Type-5 Routes)
> > +~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> > +
> > +An EVPN type-5 (IP prefix) route may be advertised with an L3 VNI that
> > +differs from the destination logical switch's
``dynamic-routing-vni``.  When
> > +the routing daemon installs such a route it attaches the L3 VNI to the
kernel
> > +route as lightweight tunnel (LWT) encapsulation metadata (for example
> > +``ip route add ... encap ip id <vni> ...``, as programmed by FRR for
> > +symmetric-IRB type-5 routes).  ``ovn-controller`` reads this VNI from
the
> > +route's LWT encapsulation and stores it in the ``Learned_Route``
record's
> > +``external_ids:vni`` key.
> > +
> > +``ovn-northd`` propagates this VNI into the logical router IP routing
pipeline
> > +so that, when a packet matches the learned route, the EVPN tunnel
egress
> > +encapsulates it with the route's VNI instead of the logical switch's
> > +``dynamic-routing-vni``.  If a learned route carries no VNI (the
kernel route
> > +has no LWT encapsulation), OVN falls back to the logical switch's
> > +``dynamic-routing-vni``.
> > +
> >  Flow Generation by ovn-northd
> >  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> >
> > diff --git a/controller/physical.c b/controller/physical.c
> > index 452256e7ab41..85b3d73faa97 100644
> > --- a/controller/physical.c
> > +++ b/controller/physical.c
> > @@ -85,6 +85,23 @@ static struct uuid *hc_uuid = NULL;
> >
> >  #define CHASSIS_MAC_TO_ROUTER_MAC_CONJID        100
> >
> > +/* tun_id is used to carry the EVPN L3 VNI of a learned (type-5) route
from the
> > + * logical router pipeline into the EVPN tunnel egress of the peer
logical
> > + * switch pipeline.  Must match "evpn_l3_vni" (REG_EVPN_L3_VNI) in
northd.c.
> > + * tun_id is not part of the generic logical register range
(reg0..reg9) that
> > + * is zeroed on the router-to-switch transition, so it survives to the
egress
> > + * without special handling, and it is available in every OVS
version.  The low
> > + * 24 bits hold the VNI and bit 31 marks it as valid.
> > + *
> > + * Bit 31 is chosen deliberately: it sits above the 24-bit tunnel-key
range.  A
> > + * tunnel-received packet carries the ingress VNI in tun_id[0..23]
(the on-wire
> > + * VXLAN/Geneve VNI is 24 bits by protocol) and OVN only ever loads
tun_id via
> > + * its low 24 bits (see put_encapsulation()), so tun_id[31] is
guaranteed 0 for
> > + * any received or locally-originated packet.  Only the logical router
route
> > + * flow ever sets it, hence a stale ingress tun_id (non-zero in the
low bits)
> > + * cannot falsely trigger the egress override match on this bit. */
> > +#define OVN_EVPN_VNI_VALID_BIT 31
> > +
> >  void
> >  physical_register_ovs_idl(struct ovsdb_idl *ovs_idl)
> >  {
> > @@ -3407,6 +3424,74 @@ evpn_local_ip_map_destroy(struct
evpn_local_ip_map *map)
> >      hmap_destroy(&map->vni_ip6);
> >  }
> >
> > +/* Adds the pair of EVPN tunnel egress flows (a normal flow at
'base_prio' and
> > + * a loopback variant at 'base_prio + 5') for 'binding' to
> > + * OFTABLE_REMOTE_VTEP_OUTPUT.  All flows set tun_src/tun_dst from the
binding.
> > + *
> > + * When 'vni_from_route' is false the tunnel VNI is the binding's own
VNI.
> > + * When true, the flows additionally match on the EVPN L3 VNI validity
bit and
> > + * take the VNI from tun_id instead (used for a learned type-5 route
advertised
> > + * with a VNI different from the switch's dynamic-routing-vni). */
> > +static void
> > +add_evpn_binding_egress_flows(const struct evpn_binding *binding,
> > +                              ovs_be32 local_ip4,
> > +                              const struct in6_addr *local_ip6,
> > +                              bool vni_from_route, uint16_t base_prio,
> > +                              struct ofpbuf *ofpacts, struct match
*match,
> > +                              struct ovn_desired_flow_table
*flow_table)
> > +{
> > +    ofpbuf_clear(ofpacts);
> > +    match_init_catchall(match);
> > +    match_outport_dp_and_port_keys(match, binding->dp_key,
> > +                                   binding->binding_key);
> > +    if (vni_from_route) {
> > +        match_set_tun_id_masked(match,
> > +                                htonll(1ULL << OVN_EVPN_VNI_VALID_BIT),
> > +                                htonll(1ULL <<
OVN_EVPN_VNI_VALID_BIT));
> > +    }
> > +
> > +    if (local_ip4) {
> > +        put_load_bytes(&local_ip4, sizeof local_ip4, MFF_TUN_SRC, 0,
32,
> > +                       ofpacts);
> > +        ovs_be32 ip4 = in6_addr_get_mapped_ipv4(&binding->remote_ip);
> > +        put_load_bytes(&ip4, sizeof ip4, MFF_TUN_DST, 0, 32, ofpacts);
> > +    } else {
> > +        put_load_bytes(local_ip6, sizeof *local_ip6, MFF_TUN_IPV6_SRC,
> > +                       0, 128, ofpacts);
> > +        put_load_bytes(&binding->remote_ip, sizeof binding->remote_ip,
> > +                       MFF_TUN_IPV6_DST, 0, 128, ofpacts);
> > +    }
> > +
> > +    if (vni_from_route) {
> > +        /* The VNI is already in tun_id[0..23] (loaded by the logical
router
> > +         * pipeline).  Clear the upper bits, including the validity
bit, so
> > +         * that only the 24-bit VNI is left for encapsulation. */
> > +        put_load(0, MFF_TUN_ID, 24, 40, ofpacts);
> > +    } else {
> > +        put_load(binding->vni, MFF_TUN_ID, 0, 24, ofpacts);
> > +    }
> > +
> > +    size_t ofpacts_size = ofpacts->size;
> > +    ofpact_put_OUTPUT(ofpacts)->port = binding->tunnel_ofport;
> > +
> > +    ofctrl_add_flow(flow_table, OFTABLE_REMOTE_VTEP_OUTPUT, base_prio,
> > +                    binding->flow_uuid.parts[0],
> > +                    match, ofpacts, &binding->flow_uuid);
> > +
> > +    /* Loopback variant: match on the LOOPBACK flag and set in_port to
none,
> > +     * otherwise the hairpin traffic would be rejected by ovs. */
> > +    match_set_reg_masked(match, MFF_LOG_FLAGS - MFF_REG0,
> > +                         MLF_ALLOW_LOOPBACK, MLF_ALLOW_LOOPBACK);
> > +
> > +    ofpbuf_truncate(ofpacts, ofpacts_size);
> > +    put_load(ofp_to_u16(OFPP_NONE), MFF_IN_PORT, 0, 16, ofpacts);
> > +    ofpact_put_OUTPUT(ofpacts)->port = binding->tunnel_ofport;
> > +
> > +    ofctrl_add_flow(flow_table, OFTABLE_REMOTE_VTEP_OUTPUT, base_prio
+ 5,
> > +                    binding->flow_uuid.parts[0],
> > +                    match, ofpacts, &binding->flow_uuid);
> > +}
> > +
> >  static void
> >  physical_consider_evpn_binding(const struct evpn_binding *binding,
> >                                 const struct evpn_local_ip_map
*vni_ip_map,
> > @@ -3454,46 +3539,22 @@ physical_consider_evpn_binding(const struct
evpn_binding *binding,
> >                      binding->flow_uuid.parts[0],
> >                      match, ofpacts, &binding->flow_uuid);
> >
> > -    /* Egress flows. */
> > -    ofpbuf_clear(ofpacts);
> > -    match_init_catchall(match);
> > -
> > -    match_outport_dp_and_port_keys(match, binding->dp_key,
> > -                                   binding->binding_key);
> > -
> > -    if (local_ip4) {
> > -        put_load_bytes(&local_ip4, sizeof local_ip4, MFF_TUN_SRC, 0,
32,
> > -                       ofpacts);
> > -        ovs_be32 ip4 = in6_addr_get_mapped_ipv4(&binding->remote_ip);
> > -        put_load_bytes(&ip4, sizeof ip4, MFF_TUN_DST, 0, 32, ofpacts);
> > -    } else {
> > -        put_load_bytes(local_ip6, sizeof *local_ip6, MFF_TUN_IPV6_SRC,
> > -                       0, 128, ofpacts);
> > -        put_load_bytes(&binding->remote_ip, sizeof binding->remote_ip,
> > -                       MFF_TUN_IPV6_DST, 0, 128, ofpacts);
> > -    }
> > -    put_load(binding->vni, MFF_TUN_ID, 0, 24, ofpacts);
> > -
> > -    size_t ofpacts_size = ofpacts->size;
> > -    ofpact_put_OUTPUT(ofpacts)->port = binding->tunnel_ofport;
> > -
> > -    ofctrl_add_flow(flow_table, OFTABLE_REMOTE_VTEP_OUTPUT, 50,
> > -                    binding->flow_uuid.parts[0],
> > -                    match, ofpacts, &binding->flow_uuid);
> > -
> > -    /* Add flow that will match on LOOPBACK flag, in that case set
> > -     * in_port to none otherwise the hairpin traffic would be rejected
> > -     * by ovs. */
> > -    match_set_reg_masked(match, MFF_LOG_FLAGS - MFF_REG0,
> > -                         MLF_ALLOW_LOOPBACK, MLF_ALLOW_LOOPBACK);
> > +    /* Egress flows using the binding's own VNI. */
> > +    add_evpn_binding_egress_flows(binding, local_ip4, local_ip6,
false, 50,
> > +                                  ofpacts, match, flow_table);
> >
> > -    ofpbuf_truncate(ofpacts, ofpacts_size);
> > -    put_load(ofp_to_u16(OFPP_NONE), MFF_IN_PORT, 0, 16, ofpacts);
> > -    ofpact_put_OUTPUT(ofpacts)->port = binding->tunnel_ofport;
> > -
> > -    ofctrl_add_flow(flow_table, OFTABLE_REMOTE_VTEP_OUTPUT, 55,
> > -                    binding->flow_uuid.parts[0],
> > -                    match, ofpacts, &binding->flow_uuid);
> > +    /* EVPN L3 VNI override egress flows.
> > +     *
> > +     * A learned (type-5) route may need to egress with an L3 VNI that
differs
> > +     * from this binding's VNI (the destination logical switch's
> > +     * dynamic-routing-vni).  The logical router pipeline stores the
desired
> > +     * VNI in tun_id (REG_EVPN_L3_VNI) with the validity bit set;
tun_id
> > +     * survives the LR->LS transition (it is not part of the
reg0..reg9 range
> > +     * zeroed there).  The higher-priority flows added below match on
that
> > +     * validity bit and take the VNI from tun_id; tun_src/tun_dst
still come
> > +     * from the binding (the remote VTEP is the route's nexthop). */
> > +    add_evpn_binding_egress_flows(binding, local_ip4, local_ip6, true,
60,
> > +                                  ofpacts, match, flow_table);
> >
> >      /* Dynamic FDB learn flows. */
> >      ofpbuf_clear(ofpacts);
> > diff --git a/controller/route-exchange-netlink.c
b/controller/route-exchange-netlink.c
> > index 8f1615c4ecaf..00b9ea4dd2b1 100644
> > --- a/controller/route-exchange-netlink.c
> > +++ b/controller/route-exchange-netlink.c
> > @@ -248,6 +248,8 @@ handle_route_msg(const struct route_table_msg *msg,
> >                  .prefix = rd->rta_dst,
> >                  .plen = rd->rtm_dst_len,
> >                  .nexthop = nexthop->addr,
> > +                .vni_present = rd->vni_present,
> > +                .vni = rd->vni,
> >              };
> >              memcpy(rr.ifname, nexthop->ifname, IFNAMSIZ);
> >              rr.ifname[IFNAMSIZ] = '\0';
> > diff --git a/controller/route-exchange-netlink.h
b/controller/route-exchange-netlink.h
> > index c137b5119b36..2e00daf25646 100644
> > --- a/controller/route-exchange-netlink.h
> > +++ b/controller/route-exchange-netlink.h
> > @@ -18,6 +18,7 @@
> >  #ifndef ROUTE_EXCHANGE_NETLINK_H
> >  #define ROUTE_EXCHANGE_NETLINK_H 1
> >
> > +#include <stdbool.h>
> >  #include <stdint.h>
> >  #include <linux/rtnetlink.h>
> >  #include <netinet/in.h>
> > @@ -45,6 +46,10 @@ struct re_nl_received_route_node {
> >      struct in6_addr nexthop;
> >      /* Adding 1 to this to be sure we actually have a terminating '\0'
*/
> >      char ifname[IFNAMSIZ + 1];
> > +    /* EVPN L3 VNI learned from the route's LWT encapsulation.  Valid
only if
> > +     * 'vni_present'. */
> > +    bool vni_present;
> > +    uint32_t vni;
> >  };
> >
> >  int re_nl_create_vrf(const char *ifname, uint32_t table_id);
> > diff --git a/controller/route-exchange.c b/controller/route-exchange.c
> > index b86eb43bf259..ad0a7b3a6da0 100644
> > --- a/controller/route-exchange.c
> > +++ b/controller/route-exchange.c
> > @@ -18,6 +18,8 @@
> >  #include <config.h>
> >
> >  #include <errno.h>
> > +#include <inttypes.h>
> > +#include <limits.h>
> >  #include <net/if.h>
> >  #include <stdbool.h>
> >
> > @@ -104,11 +106,31 @@ route_add_entry(struct hmap *routes,
> >      hmap_insert(routes, &route_e->hmap_node, hash);
> >  }
> >
> > +/* Returns true if the VNI stored in 'sb_route' (external_ids:vni)
matches the
> > + * '(vni_present, vni)' pair learned from the system.  A learned route
without
> > + * a VNI matches only an SB route without a VNI, so that a route which
gains or
> > + * loses its VNI is detected as changed. */
> > +static bool
> > +learned_route_vni_matches(const struct sbrec_learned_route *sb_route,
> > +                          bool vni_present, uint32_t vni)
> > +{
> > +    /* A valid VNI is at most 24 bits, so UINT_MAX is a safe marker
for a
> > +     * missing (or unparseable) external_ids:vni. */
> > +    unsigned int existing_vni =
> > +        smap_get_uint(&sb_route->external_ids, "vni", UINT_MAX);
> > +
> > +    if (!vni_present) {
> > +        return existing_vni == UINT_MAX;
> > +    }
> > +    return existing_vni == vni;
> > +}
> > +
> >  static struct route_entry *
> >  route_lookup(struct hmap *route_map,
> >               const struct sbrec_datapath_binding *sb_db,
> >               const struct sbrec_port_binding *logical_port,
> > -             const char *ip_prefix, const char *nexthop)
> > +             const char *ip_prefix, const char *nexthop,
> > +             bool vni_present, uint32_t vni)
> >  {
> >      struct route_entry *route_e;
> >      uint32_t hash;
> > @@ -130,6 +152,9 @@ route_lookup(struct hmap *route_map,
> >          if (strcmp(route_e->sb_route->nexthop, nexthop)) {
> >              continue;
> >          }
> > +        if (!learned_route_vni_matches(route_e->sb_route, vni_present,
vni)) {
> > +            continue;
> > +        }
> >
> >          return route_e;
> >      }
> > @@ -196,7 +221,9 @@ sb_sync_learned_routes(const struct vector
*learned_routes,
> >              }
> >
> >              route_e = route_lookup(&sync_routes, datapath,
> > -                                   logical_port, ip_prefix, nexthop);
> > +                                   logical_port, ip_prefix, nexthop,
> > +                                   learned_route->vni_present,
> > +                                   learned_route->vni);
> >              if (route_e) {
> >                  route_e->stale = false;
> >              } else {
> > @@ -209,6 +236,15 @@ sb_sync_learned_routes(const struct vector
*learned_routes,
> >                  sbrec_learned_route_set_logical_port(sb_route,
logical_port);
> >                  sbrec_learned_route_set_ip_prefix(sb_route, ip_prefix);
> >                  sbrec_learned_route_set_nexthop(sb_route, nexthop);
> > +                if (learned_route->vni_present) {
> > +                    struct smap external_ids =
> > +                        SMAP_INITIALIZER(&external_ids);
> > +                    smap_add_format(&external_ids, "vni", "%"PRIu32,
> > +                                    learned_route->vni);
> > +                    sbrec_learned_route_set_external_ids(sb_route,
> > +
&external_ids);
> > +                    smap_destroy(&external_ids);
> > +                }
> >
> >                  route_add_entry(&sync_routes, sb_route, false);
> >              }
> > diff --git a/lib/logical-fields.c b/lib/logical-fields.c
> > index 35d9a2e229c6..e5a64969fc4f 100644
> > --- a/lib/logical-fields.c
> > +++ b/lib/logical-fields.c
> > @@ -135,6 +135,19 @@ ovn_init_symtab(struct shash *symtab)
> >          free(name);
> >      }
> >
> > +    /* EVPN L3 VNI.  Carries the L3 VNI of a learned (type-5) route
from the
> > +     * logical router pipeline to the EVPN tunnel egress in the peer
logical
> > +     * switch pipeline.  Backed by tun_id, which is not part of the
generic
> > +     * logical register range (reg0..reg9) that is zeroed on the
> > +     * router-to-switch transition, so it survives to the egress.
 tun_id is a
> > +     * 64-bit tunnel-metadata field available in every OVS version, so
this
> > +     * needs no special register capability.  The low 24 bits hold the
VNI
> > +     * (which is also what gets encapsulated on the wire) and bit 31
marks the
> > +     * VNI as valid.  Bit 31 is above the 24-bit tunnel-key range, so
it is
> > +     * guaranteed 0 on a tunnel-received packet (whose ingress VNI only
> > +     * occupies tun_id[0..23]) and is only ever set by the route flow.
*/
> > +    expr_symtab_add_field(symtab, "evpn_l3_vni", MFF_TUN_ID, NULL,
false);
> > +
> >      /* Flags used in logical to physical transformation. */
> >      expr_symtab_add_field(symtab, "flags", MFF_LOG_FLAGS, NULL, false);
> >      char flags_str[16];
> > diff --git a/northd/en-learned-route-sync.c
b/northd/en-learned-route-sync.c
> > index cbd516b6874b..ffba8390a858 100644
> > --- a/northd/en-learned-route-sync.c
> > +++ b/northd/en-learned-route-sync.c
> > @@ -15,6 +15,7 @@
> >   */
> >
> >  #include <config.h>
> > +#include <limits.h>
> >  #include <stdbool.h>
> >
> >  #include "openvswitch/vlog.h"
> > @@ -199,10 +200,20 @@ parse_route_from_sbrec_route(struct hmap
*parsed_routes_out,
> >          return NULL;
> >      }
> >
> > +    /* An EVPN (type-5) route may be learned with an L3 VNI that
differs from
> > +     * the local switch's dynamic-routing-vni.  When present, it is
stored in
> > +     * the SB Learned_Route's external_ids:vni.  A valid VNI is at
most 24
> > +     * bits, so UINT_MAX marks a missing (or, an unexpectedly
malformed)
> > +     * value. */
> > +    unsigned int raw_vni = smap_get_uint(&route->external_ids, "vni",
> > +                                         UINT_MAX);
> > +    bool vni_present = raw_vni != UINT_MAX;
> > +    uint32_t vni = vni_present ? raw_vni : 0;
> > +
> >      return parsed_route_add(od, nexthop, &prefix, plen, false,
lrp_addr_s,
> >                              out_port, 0, false, false, false, NULL,
> > -                            ROUTE_SOURCE_LEARNED, true,
&route->header_, NULL,
> > -                            parsed_routes_out);
> > +                            ROUTE_SOURCE_LEARNED, vni_present, vni,
true,
> > +                            &route->header_, NULL, parsed_routes_out);
> >  }
> >
> >  static void
> > diff --git a/northd/northd.c b/northd/northd.c
> > index 4ca9820d4833..090eeb854853 100644
> > --- a/northd/northd.c
> > +++ b/northd/northd.c
> > @@ -231,6 +231,21 @@ BUILD_ASSERT_DECL(ACL_OBS_STAGE_MAX < (1 << 2));
> >  #define REG_POLICY_CHAIN_ID "reg9[16..31]"
> >  #define REG_ROUTE_TABLE_ID "reg7"
> >
> > +/* Field carrying the EVPN L3 VNI of a learned (type-5) route towards
the EVPN
> > + * tunnel egress in the peer logical switch pipeline.  This is used
when the
> > + * route was advertised with an L3 VNI different from the destination
logical
> > + * switch's dynamic-routing-vni.  It is backed by tun_id (see
"evpn_l3_vni" in
> > + * lib/logical-fields.c), which is not part of the generic logical
register
> > + * range (reg0..reg9) that is zeroed on the LR->LS transition, so it
survives
> > + * to the egress without any special handling.  tun_id is available in
every
> > + * OVS version, so no register capability is required.  The low 24
bits hold
> > + * the VNI and bit 31 marks it as valid.  Bit 31 is above the 24-bit
tunnel-key
> > + * range: a tunnel-received packet only populates tun_id[0..23] (the
on-wire
> > + * VNI is 24 bits), so this validity bit is guaranteed 0 unless this
route flow
> > + * sets it. */
> > +#define REG_EVPN_L3_VNI "evpn_l3_vni[0..23]"
> > +#define REG_EVPN_L3_VNI_VALID "evpn_l3_vni[31]"
> > +
> >  /* Registers used for pasing observability information for switches:
> >   * domain and point ID. */
> >  #define REG_OBS_POINT_ID_NEW "reg3"
> > @@ -12433,6 +12448,11 @@ parsed_route_lookup(struct hmap *routes,
size_t hash,
> >              continue;
> >          }
> >
> > +        if (pr->vni_present != new_pr->vni_present ||
> > +            pr->vni != new_pr->vni) {
> > +            continue;
> > +        }
> > +
> >          return pr;
> >      }
> >
> > @@ -12453,6 +12473,8 @@ parsed_route_init(const struct ovn_datapath *od,
> >                    bool override_connected,
> >                    const struct sset *ecmp_selection_fields,
> >                    enum route_source source,
> > +                  bool vni_present,
> > +                  uint32_t vni,
> >                    bool dynamic_routing_advertise,
> >                    const struct ovn_port *tracked_port,
> >                    const struct ovsdb_idl_row *source_hint)
> > @@ -12464,6 +12486,8 @@ parsed_route_init(const struct ovn_datapath *od,
> >      new_pr->plen = plen;
> >      new_pr->nexthop = nexthop;
> >      new_pr->route_table_id = route_table_id;
> > +    new_pr->vni_present = vni_present;
> > +    new_pr->vni = vni;
> >      new_pr->is_src_route = is_src_route;
> >      new_pr->od = od;
> >      new_pr->ecmp_symmetric_reply = ecmp_symmetric_reply;
> > @@ -12498,8 +12522,8 @@ parsed_route_clone(const struct parsed_route
*pr)
> >          pr->od, nexthop, pr->prefix, pr->plen, pr->is_discard_route,
> >          pr->lrp_addr_s, pr->out_port, pr->route_table_id,
pr->is_src_route,
> >          pr->ecmp_symmetric_reply, pr->override_connected,
> > -        &pr->ecmp_selection_fields, pr->source,
pr->dynamic_routing_advertise,
> > -        pr->tracked_port, pr->source_hint);
> > +        &pr->ecmp_selection_fields, pr->source, pr->vni_present,
pr->vni,
> > +        pr->dynamic_routing_advertise, pr->tracked_port,
pr->source_hint);
> >
> >      new_pr->hash = pr->hash;
> >      return new_pr;
> > @@ -12561,6 +12585,8 @@ parsed_route_add(const struct ovn_datapath *od,
> >                   bool override_connected,
> >                   const struct sset *ecmp_selection_fields,
> >                   enum route_source source,
> > +                 bool vni_present,
> > +                 uint32_t vni,
> >                   bool dynamic_routing_advertise,
> >                   const struct ovsdb_idl_row *source_hint,
> >                   const struct ovn_port *tracked_port,
> > @@ -12572,7 +12598,8 @@ parsed_route_add(const struct ovn_datapath *od,
> >                              lrp_addr_s, out_port, route_table_id,
> >                              is_src_route, ecmp_symmetric_reply,
> >                              override_connected, ecmp_selection_fields,
> > -                            source, dynamic_routing_advertise,
> > +                            source, vni_present, vni,
> > +                            dynamic_routing_advertise,
> >                              tracked_port, source_hint);
> >
> >      new_pr->hash = route_hash(new_pr);
> > @@ -12723,6 +12750,7 @@ parsed_routes_add_static(const struct
ovn_datapath *od,
> >                                                 ecmp_symmetric_reply,
> >                                                 override_connected,
> >                                                 &ecmp_selection_fields,
source,
> > +                                               false, 0,
> >
dynamic_routing_advertise,
> >                                                 &route->header_, NULL,
routes);
> >      sset_destroy(&ecmp_selection_fields);
> > @@ -12742,7 +12770,7 @@ parsed_routes_add_connected(const struct
ovn_datapath *od,
> >          parsed_route_add(od, NULL, &prefix, addr->plen,
> >                           false, addr->addr_s, op, 0, false, false,
> >                           false, NULL, ROUTE_SOURCE_CONNECTED,
> > -                         true, &op->nbrp->header_, NULL, routes);
> > +                         false, 0, true, &op->nbrp->header_, NULL,
routes);
> >      }
> >
> >      for (size_t i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
> > @@ -12750,7 +12778,7 @@ parsed_routes_add_connected(const struct
ovn_datapath *od,
> >
> >          parsed_route_add(od, NULL, &addr->network, addr->plen, false,
> >                           addr->addr_s, op, 0, false, false, false,
> > -                         NULL, ROUTE_SOURCE_CONNECTED, true,
> > +                         NULL, ROUTE_SOURCE_CONNECTED, false, 0, true,
> >                           &op->nbrp->header_, NULL, routes);
> >      }
> >  }
> > @@ -13168,7 +13196,8 @@ add_route(struct lflow_table *lflows, const
struct ovn_datapath *od,
> >            const struct ovsdb_idl_row *stage_hint, bool
is_discard_route,
> >            enum route_source source, struct lflow_ref *lflow_ref,
> >            bool is_ipv4_prefix, bool is_ipv4_nexthop,
> > -          bool override_connected)
> > +          bool override_connected,
> > +          bool evpn_vni_present, uint32_t evpn_vni)
> >  {
> >      struct ds match = DS_EMPTY_INITIALIZER;
> >      uint16_t priority = calc_priority(plen, source, override_connected,
> > @@ -13203,6 +13232,15 @@ add_route(struct lflow_table *lflows, const
struct ovn_datapath *od,
> >          ds_put_cstr(&actions, debug_drop_action());
> >      } else {
> >          ds_put_format(&common_actions, REG_ECMP_GROUP_ID" = 0; ");
> > +        if (evpn_vni_present) {
> > +            /* Carry the route's EVPN L3 VNI towards the EVPN tunnel
egress so
> > +             * that it is used (instead of the destination logical
switch's
> > +             * dynamic-routing-vni) when encapsulating this traffic.
It is
> > +             * stored in tun_id, which survives to the egress. */
> > +            ds_put_format(&common_actions,
> > +                          REG_EVPN_L3_VNI" = %"PRIu32"; "
> > +                          REG_EVPN_L3_VNI_VALID" = 1; ", evpn_vni);
> > +        }
> >          if (gateway) {
> >              ds_put_format(&common_actions, "%s = ",
> >                            is_ipv4_nexthop ? REG_NEXT_HOP_IPV4 :
> > @@ -13262,7 +13300,8 @@ build_route_flow(struct lflow_table *lflows,
const struct ovn_datapath *od,
> >                route->route_table_id, bfd_ports,
> >                route->source_hint,
> >                route->is_discard_route, route->source, lflow_ref,
> > -              is_ipv4_prefix, is_ipv4_nexthop,
route->override_connected);
> > +              is_ipv4_prefix, is_ipv4_nexthop,
route->override_connected,
> > +              route->vni_present, route->vni);
> >
> >      free(prefix_s);
> >  }
> > @@ -18999,7 +19038,8 @@ build_routable_flows_for_router_port(
> >                                bfd_ports, &router_port->nbrp->header_,
> >                                false, ROUTE_SOURCE_CONNECTED,
> >                                lrp->stateful_lflow_ref,
> > -                              true, is_ipv4_nexthop ? true : false,
false);
> > +                              true, is_ipv4_nexthop ? true : false,
false,
> > +                              false, 0);
> >                  }
> >              }
> >          }
> > @@ -20738,6 +20778,7 @@ lflow_handle_northd_lr_changes(struct
ovsdb_idl_txn *ovnsb_txn,
> >          .route_data = lflow_input->route_data,
> >          .route_tables = lflow_input->route_tables,
> >          .route_policies = lflow_input->route_policies,
> > +        .features = lflow_input->features,
> >          .match = DS_EMPTY_INITIALIZER,
> >          .actions = DS_EMPTY_INITIALIZER,
> >      };
> > diff --git a/northd/northd.h b/northd/northd.h
> > index d27f519d6e33..1a6b8960bcad 100644
> > --- a/northd/northd.h
> > +++ b/northd/northd.h
> > @@ -878,6 +878,11 @@ struct parsed_route {
> >      const struct ovn_port *out_port;
> >      const struct ovn_port *tracked_port; /* May be NULL. */
> >      bool dynamic_routing_advertise;
> > +    /* EVPN L3 VNI for this route (e.g. a type-5 route learned with a
> > +     * different VNI than the local switch's dynamic-routing-vni).
Valid only
> > +     * if 'vni_present'. */
> > +    bool vni_present;
> > +    uint32_t vni;
> >  };
> >
> >  struct parsed_route *parsed_route_clone(const struct parsed_route *);
> > @@ -901,6 +906,8 @@ struct parsed_route *parsed_route_add(
> >      bool override_connected,
> >      const struct sset *ecmp_selection_fields,
> >      enum route_source source,
> > +    bool vni_present,
> > +    uint32_t vni,
> >      bool dynamic_routing_advertise,
> >      const struct ovsdb_idl_row *source_hint,
> >      const struct ovn_port *tracked_port,
> > diff --git a/ovn-sb.xml b/ovn-sb.xml
> > index e403eb36031f..a8834963f6a8 100644
> > --- a/ovn-sb.xml
> > +++ b/ovn-sb.xml
> > @@ -5518,6 +5518,16 @@ tcp.flags = RST;
> >        This is the nexthop ip we learned from outside of OVN.
> >      </column>
> >
> > +    <column name="external_ids" key="vni">
> > +      For routes learned on an EVPN (type-5) enabled datapath, this is
the
> > +      L3 VNI associated with the route, as learned from the route's
> > +      lightweight tunnel (LWT) encapsulation.  When set,
> > +      <code>ovn-controller</code> uses this VNI (rather than the
logical
> > +      switch's <code>dynamic-routing-vni</code>) when encapsulating
traffic
> > +      that egresses towards this route's nexthop.  When not set, the
local
> > +      <code>dynamic-routing-vni</code> is used.
> > +    </column>
> > +
> >      <column name="external_ids">
> >        See <em>External IDs</em> at the beginning of this document.
> >      </column>
> > diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> > index 810e9f2d7419..63960524cd7b 100644
> > --- a/tests/ovn-northd.at
> > +++ b/tests/ovn-northd.at
> > @@ -16701,6 +16701,52 @@ OVN_CLEANUP_NORTHD
> >  AT_CLEANUP
> >  ])
> >
> > +OVN_FOR_EACH_NORTHD_NO_HV([
> > +AT_SETUP([dynamic-routing - learned route with EVPN L3 VNI])
> > +AT_KEYWORDS([dynamic-routing])
> > +ovn_start
> > +
> > +check ovn-nbctl lr-add lr0
> > +check ovn-nbctl --wait=sb set Logical_Router lr0
option:dynamic-routing=true \
> > +    option:dynamic-routing-redistribute="connected,static"
> > +check ovn-nbctl --wait=sb lrp-add lr0 lr0-sw0 00:00:00:00:ff:01
10.0.0.1/24
> > +sw0=$(fetch_column port_binding _uuid logical_port=lr0-sw0)
> > +datapath=$(fetch_column datapath_binding _uuid external_ids:name=lr0)
> > +
> > +# A type-5 route learned with an L3 VNI (external_ids:vni) that
differs from
> > +# the local switch's dynamic-routing-vni.  northd should carry the VNI
towards
> > +# the EVPN tunnel egress via reg16 (REG_EVPN_L3_VNI) with the validity
bit set.
> > +check_uuid ovn-sbctl create Learned_Route \
> > +    datapath=$datapath                    \
> > +    logical_port=$sw0                     \
> > +    ip_prefix=172.16.0.0/24               \
> > +    nexthop=10.0.0.11                     \
> > +    external_ids:vni=100
> > +check ovn-nbctl --wait=sb sync
> > +check_row_count Learned_Route 1
> > +ovn-sbctl dump-flows lr0 > lr0flows
> > +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | grep 'ip4.dst ==
172.16.0.0/24' | ovn_strip_lflows], [0], [dnl
> > +  table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 &&
ip4.dst == 172.16.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0;
evpn_l3_vni[[0..23]] = 100; evpn_l3_vni[[31]] = 1; reg0 = 10.0.0.11; reg5 =
10.0.0.1; eth.src = 00:00:00:00:ff:01; outport = "lr0-sw0"; flags.loopback
= 1; reg9[[9]] = 1; next;)
> > +])
> > +
> > +# A learned route without a VNI must not set the EVPN VNI register
(fallback
> > +# to the destination switch's dynamic-routing-vni at egress).
> > +check_uuid ovn-sbctl create Learned_Route \
> > +    datapath=$datapath                    \
> > +    logical_port=$sw0                     \
> > +    ip_prefix=172.16.1.0/24               \
> > +    nexthop=10.0.0.12
> > +check ovn-nbctl --wait=sb sync
> > +check_row_count Learned_Route 2
> > +ovn-sbctl dump-flows lr0 > lr0flows
> > +AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | grep 'ip4.dst ==
172.16.1.0/24' | ovn_strip_lflows], [0], [dnl
> > +  table=??(lr_in_ip_routing   ), priority=1838 , match=(reg7 == 0 &&
ip4.dst == 172.16.1.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 =
10.0.0.12; reg5 = 10.0.0.1; eth.src = 00:00:00:00:ff:01; outport =
"lr0-sw0"; flags.loopback = 1; reg9[[9]] = 1; next;)
> > +])
> > +
> > +OVN_CLEANUP_NORTHD
> > +AT_CLEANUP
> > +])
> > +
> >  OVN_FOR_EACH_NORTHD_NO_HV([
> >  AT_SETUP([dynamic-routing - route learning ecmp])
> >  AT_KEYWORDS([dynamic-routing])
> > diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> > index fdb2c0a5df3f..18e92c511478 100644
> > --- a/tests/system-ovn.at
> > +++ b/tests/system-ovn.at
> > @@ -20798,6 +20798,24 @@ AT_CHECK([ip route del 10.10.3.1 via 20.0.0.25
vrf vrf-$vni])
> >  OVS_WAIT_FOR_OUTPUT([ovn-sbctl list Learned_Route | grep ip_prefix |
sort], [0], [dnl
> >  ])
> >
> > +# A route learned with a lightweight-tunnel (LWT) encap ID - as FRR
installs
> > +# for EVPN type-5 routes carrying an L3 VNI - records that VNI in the
> > +# Learned_Route's external_ids:vni.  A route without an encap ID has
no VNI.
> > +AS_BOX([$(date +%H:%M:%S.%03N) Learned route with EVPN L3 VNI])
> > +AT_CHECK([ip route add 10.10.5.1 encap ip id 5000 dst 20.0.0.25 \
> > +    via 20.0.0.25 vrf vrf-$vni proto zebra])
> > +wait_row_count Learned_Route 1 ip_prefix=10.10.5.1
external_ids:vni=5000
> > +
> > +AT_CHECK([ip route add 10.10.6.1 via 20.0.0.25 vrf vrf-$vni proto
zebra])
> > +wait_row_count Learned_Route 1 ip_prefix=10.10.6.1
> > +# Only the encap route carries a VNI; the plain route does not.
> > +check_row_count Learned_Route 1 external_ids:vni=5000
> > +
> > +AT_CHECK([ip route del 10.10.5.1 encap ip id 5000 dst 20.0.0.25 \
> > +    via 20.0.0.25 vrf vrf-$vni])
> > +AT_CHECK([ip route del 10.10.6.1 via 20.0.0.25 vrf vrf-$vni])
> > +wait_row_count Learned_Route 0
> > +
> >  # Disable learning on router
> >  AS_BOX([$(date +%H:%M:%S.%03N) Disable dynamic-route learning])
> >
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to