Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/gr_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#include <stdlib.h>

// Must be bumped when making non-backward compatible changes in API headers
#define GR_API_VERSION 2
#define GR_API_VERSION 3

// API request header.
struct gr_api_request {
Expand Down
16 changes: 16 additions & 0 deletions frr/if_grout.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "if_grout.h"
#include "if_map.h"
#include "l3vni_map.h"
#include "log_grout.h"
#include "zebra_dplane_grout.h"

Expand Down Expand Up @@ -51,6 +52,7 @@ void grout_link_change(struct gr_iface *gr_if, bool new, bool startup) {
const struct gr_iface_info_vlan *gr_vlan = NULL;
const struct gr_iface_info_port *gr_port = NULL;
const struct gr_iface_info_bond *gr_bond = NULL;
const struct gr_iface_info_vrf *gr_vrf = NULL;
ifindex_t bridge_ifindex = IFINDEX_INTERNAL;
ifindex_t link_ifindex = IFINDEX_INTERNAL;
ifindex_t bond_ifindex = IFINDEX_INTERNAL;
Expand Down Expand Up @@ -86,6 +88,8 @@ void grout_link_change(struct gr_iface *gr_if, bool new, bool startup) {
link_type = ZEBRA_LLT_IPIP;
break;
case GR_IFACE_TYPE_VRF:
gr_vrf = (const struct gr_iface_info_vrf *)&gr_if->info;
mac = &gr_vrf->mac;
link_type = ZEBRA_LLT_ETHER;
zif_type = ZEBRA_IF_VRF;
break;
Expand Down Expand Up @@ -151,6 +155,16 @@ void grout_link_change(struct gr_iface *gr_if, bool new, bool startup) {
dplane_ctx_set_ifp_table_id(
ctx, vrf_grout_to_frr(gr_if->base.vrf_id)
);

// For VXLAN in VRF mode, present it as a bridge slave
// of the VRF interface. FRR requires an SVI (derived
// from the bridge master) to bring the L3VNI up and
// compute the Router MAC for EVPN type-5 routes.
if (zif_type == ZEBRA_IF_VXLAN) {
bridge_ifindex = ifindex_grout_to_frr(gr_if->base.vrf_id);
slave_type = ZEBRA_IF_SLAVE_BRIDGE;
l3vni_set(gr_if->base.vrf_id, gr_if->id);
}
break;
case GR_IFACE_MODE_BOND:
bond_ifindex = ifindex_grout_to_frr(gr_if->domain_id);
Expand Down Expand Up @@ -201,6 +215,8 @@ void grout_link_change(struct gr_iface *gr_if, bool new, bool startup) {
} else {
dplane_ctx_set_op(ctx, DPLANE_OP_INTF_DELETE);
dplane_ctx_set_status(ctx, ZEBRA_DPLANE_REQUEST_QUEUED);
if (gr_vxlan != NULL && gr_if->mode == GR_IFACE_MODE_VRF)
l3vni_del(gr_if->base.vrf_id);
remove_mapping_by_grout_ifindex(gr_if->id);
}

Expand Down
119 changes: 119 additions & 0 deletions frr/l3vni_map.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2026 Robin Jarry

#include "if_map.h"
#include "l3vni_map.h"

#include <gr_infra.h>

#include <lib/jhash.h>
#include <lib/typesafe.h>

// All functions in this file run exclusively on the dplane thread
// (grout_link_change, grout_add_nexthop, grout_neigh_update_ctx).
// No locking required.

// VRF -> VXLAN iface mapping ///////////////////////////////////////////////////

PREDECL_HASH(l3vni_hash);

struct l3vni_entry {
struct l3vni_hash_item item;
uint16_t vrf_id;
uint16_t vxlan_iface_id;
};

static int l3vni_cmp(const struct l3vni_entry *a, const struct l3vni_entry *b) {
return numcmp(a->vrf_id, b->vrf_id);
}

static uint32_t l3vni_hashfn(const struct l3vni_entry *e) {
return e->vrf_id;
}

DECLARE_HASH(l3vni_hash, struct l3vni_entry, item, l3vni_cmp, l3vni_hashfn);
static struct l3vni_hash_head l3vni_entries = INIT_HASH(l3vni_entries);

void l3vni_set(uint16_t vrf_id, uint16_t vxlan_iface_id) {
struct l3vni_entry *e, key = {.vrf_id = vrf_id};

e = l3vni_hash_find(&l3vni_entries, &key);
if (e != NULL) {
e->vxlan_iface_id = vxlan_iface_id;
return;
}
e = XCALLOC(MTYPE_GROUT_MEM, sizeof(*e));
e->vrf_id = vrf_id;
e->vxlan_iface_id = vxlan_iface_id;
l3vni_hash_add(&l3vni_entries, e);
}

void l3vni_del(uint16_t vrf_id) {
struct l3vni_entry key = {.vrf_id = vrf_id};
struct l3vni_entry *e = l3vni_hash_find(&l3vni_entries, &key);

if (e != NULL) {
l3vni_hash_del(&l3vni_entries, e);
XFREE(MTYPE_GROUT_MEM, e);
}
}

uint16_t l3vni_get_vxlan(uint16_t vrf_id) {
struct l3vni_entry key = {.vrf_id = vrf_id};
struct l3vni_entry *e = l3vni_hash_find(&l3vni_entries, &key);
return e ? e->vxlan_iface_id : GR_IFACE_ID_UNDEF;
}

// (VRF, VTEP) -> RMAC cache ///////////////////////////////////////////////////

PREDECL_HASH(rmac_hash);

struct rmac_entry {
struct rmac_hash_item item;
uint16_t vrf_id;
ip4_addr_t vtep;
struct ethaddr mac;
};

static int rmac_cmp(const struct rmac_entry *a, const struct rmac_entry *b) {
int r = numcmp(a->vrf_id, b->vrf_id);
return r ? r : numcmp(a->vtep, b->vtep);
}

static uint32_t rmac_hashfn(const struct rmac_entry *e) {
return jhash_2words(e->vrf_id, e->vtep, 0);
}

DECLARE_HASH(rmac_hash, struct rmac_entry, item, rmac_cmp, rmac_hashfn);
static struct rmac_hash_head rmac_entries = INIT_HASH(rmac_entries);

void l3vni_rmac_set(uint16_t vrf_id, ip4_addr_t vtep, const struct ethaddr *mac) {
struct rmac_entry *e, key = {.vrf_id = vrf_id, .vtep = vtep};

e = rmac_hash_find(&rmac_entries, &key);
if (e != NULL) {
e->mac = *mac;
return;
}
e = XCALLOC(MTYPE_GROUT_MEM, sizeof(*e));
e->vrf_id = vrf_id;
e->vtep = vtep;
e->mac = *mac;
rmac_hash_add(&rmac_entries, e);
}

void l3vni_rmac_del(uint16_t vrf_id, ip4_addr_t vtep) {
struct rmac_entry key = {.vrf_id = vrf_id, .vtep = vtep};
struct rmac_entry *e = rmac_hash_find(&rmac_entries, &key);

if (e != NULL) {
rmac_hash_del(&rmac_entries, e);
XFREE(MTYPE_GROUT_MEM, e);
}
}

const struct ethaddr *l3vni_rmac_get(uint16_t vrf_id, ip4_addr_t vtep) {
struct rmac_entry key = {.vrf_id = vrf_id, .vtep = vtep};
struct rmac_entry *e = rmac_hash_find(&rmac_entries, &key);
return e ? &e->mac : NULL;
}
45 changes: 45 additions & 0 deletions frr/l3vni_map.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2026 Robin Jarry

// L3VNI dplane-thread state for EVPN symmetric IRB (Integrated Routing and
// Bridging).
//
// FRR's EVPN type-5 (IP prefix) routes use a per-VRF L3 VNI with a VXLAN
// interface. Two mappings are maintained on the dplane thread (no locking):
//
// VRF -> VXLAN iface
//
// grout_add_nexthop() redirects nexthops from the VRF (FRR's SVI model) to
// the VXLAN interface so that ip_output routes packets into the tunnel.
//
// (VRF, VTEP) -> RMAC
//
// DPLANE_OP_NEIGH_INSTALL delivers the remote router MAC before
// DPLANE_OP_NH_INSTALL creates the nexthop. The RMAC is cached here and
// applied by grout_add_nexthop() when the nexthop arrives.

#pragma once

#include "lib/prefix.h"

#include <gr_net_types.h>

#include <stdint.h>

// Register vrf_id -> vxlan_iface_id mapping.
void l3vni_set(uint16_t vrf_id, uint16_t vxlan_iface_id);

// Remove mapping for vrf_id.
void l3vni_del(uint16_t vrf_id);

// Return vxlan iface id for vrf_id, or GR_IFACE_ID_UNDEF.
uint16_t l3vni_get_vxlan(uint16_t vrf_id);

// Cache remote VTEP router MAC for (vrf_id, vtep).
void l3vni_rmac_set(uint16_t vrf_id, ip4_addr_t vtep, const struct ethaddr *mac);

// Remove cached RMAC for (vrf_id, vtep).
void l3vni_rmac_del(uint16_t vrf_id, ip4_addr_t vtep);

// Look up cached RMAC for (vrf_id, vtep), or NULL.
const struct ethaddr *l3vni_rmac_get(uint16_t vrf_id, ip4_addr_t vtep);
1 change: 1 addition & 0 deletions frr/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ frr_plugin = shared_module(
files(
'if_grout.c',
'if_map.c',
'l3vni_map.c',
'rt_grout.c',
'zebra_dplane_grout.c',
) + grout_header,
Expand Down
42 changes: 42 additions & 0 deletions frr/rt_grout.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright (c) 2025 Maxime Leroy, Free Mobile

#include "if_map.h"
#include "l3vni_map.h"
#include "log_grout.h"
#include "rt_grout.h"

Expand Down Expand Up @@ -658,7 +659,9 @@ grout_add_nexthop(uint32_t nh_id, gr_nh_origin_t origin, const struct nexthop *n
struct gr_nexthop_info_srv6 *sr6;
struct gr_nh_add_req *req = NULL;
struct gr_nexthop_info_l3 *l3;
const struct ethaddr *rmac;
size_t len = sizeof(*req);
uint16_t vxlan_iface_id;
gr_nh_type_t type;

switch (nh->type) {
Expand Down Expand Up @@ -706,12 +709,25 @@ grout_add_nexthop(uint32_t nh_id, gr_nh_origin_t origin, const struct nexthop *n

switch (type) {
case GR_NH_T_L3:
// For L3 nexthops in VRFs with an L3VNI, redirect the iface from
// the VRF (SVI in FRR's model) to the VXLAN interface. Grout
// routes packets directly through the VXLAN tunnel.
vxlan_iface_id = l3vni_get_vxlan(req->nh.vrf_id);
if (vxlan_iface_id != GR_IFACE_ID_UNDEF)
req->nh.iface_id = vxlan_iface_id;

switch (nh->type) {
case NEXTHOP_TYPE_IPV4:
case NEXTHOP_TYPE_IPV4_IFINDEX:
l3 = (struct gr_nexthop_info_l3 *)req->nh.info;
l3->af = GR_AF_IP4;
memcpy(&l3->ipv4, &nh->gate.ipv4, sizeof(l3->ipv4));
// Apply cached RMAC from EVPN NEIGH install if available.
rmac = l3vni_rmac_get(req->nh.vrf_id, l3->ipv4);
if (rmac != NULL) {
memcpy(&l3->mac, rmac, sizeof(l3->mac));
l3->flags |= GR_NH_F_REMOTE;
}
break;
case NEXTHOP_TYPE_IPV6:
case NEXTHOP_TYPE_IPV6_IFINDEX:
Expand Down Expand Up @@ -1012,6 +1028,32 @@ enum zebra_dplane_result grout_macfdb_update_ctx(struct zebra_dplane_ctx *ctx) {
return ret == 0 ? ZEBRA_DPLANE_REQUEST_SUCCESS : ZEBRA_DPLANE_REQUEST_FAILURE;
}

enum zebra_dplane_result grout_neigh_update_ctx(struct zebra_dplane_ctx *ctx) {
const struct ipaddr *addr = dplane_ctx_neigh_get_ipaddr(ctx);
bool add = dplane_ctx_get_op(ctx) != DPLANE_OP_NEIGH_DELETE;
uint16_t vrf_id = vrf_frr_to_grout(dplane_ctx_get_vrf(ctx));

if (addr->ipa_type != IPADDR_V4) {
gr_log_debug("only IPv4 VTEP addresses supported, skip");
return ZEBRA_DPLANE_REQUEST_SUCCESS;
}

// Cache the RMAC for later use by grout_add_nexthop. We cannot
// create a separate nexthop here because grout's L3 nexthop hash
// keys on (vrf, addr) without iface_id, so it would collide with
// the route nexthop that FRR installs right after.
if (add) {
const struct ethaddr *mac = dplane_ctx_neigh_get_mac(ctx);
gr_log_debug("cache rmac vrf=%u %pIA %pEA", vrf_id, addr, mac);
l3vni_rmac_set(vrf_id, addr->ipaddr_v4.s_addr, mac);
} else {
gr_log_debug("uncache rmac vrf=%u %pIA", vrf_id, addr);
l3vni_rmac_del(vrf_id, addr->ipaddr_v4.s_addr);
}

return ZEBRA_DPLANE_REQUEST_SUCCESS;
}

enum zebra_dplane_result grout_vxlan_flood_update_ctx(struct zebra_dplane_ctx *ctx) {
const struct ipaddr *addr = dplane_ctx_neigh_get_ipaddr(ctx);
bool add = dplane_ctx_get_op(ctx) == DPLANE_OP_VTEP_ADD;
Expand Down
1 change: 1 addition & 0 deletions frr/rt_grout.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ void grout_nexthop_change(bool new, struct gr_nexthop *gr_nh, bool startup);
void grout_macfdb_change(const struct gr_fdb_entry *fdb, bool new);
enum zebra_dplane_result grout_macfdb_update_ctx(struct zebra_dplane_ctx *ctx);

enum zebra_dplane_result grout_neigh_update_ctx(struct zebra_dplane_ctx *ctx);
enum zebra_dplane_result grout_vxlan_flood_update_ctx(struct zebra_dplane_ctx *ctx);
5 changes: 5 additions & 0 deletions frr/zebra_dplane_grout.c
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,11 @@ static enum zebra_dplane_result zd_grout_process_update(struct zebra_dplane_ctx
case DPLANE_OP_MAC_DELETE:
return grout_macfdb_update_ctx(ctx);

case DPLANE_OP_NEIGH_INSTALL:
case DPLANE_OP_NEIGH_UPDATE:
case DPLANE_OP_NEIGH_DELETE:
return grout_neigh_update_ctx(ctx);

case DPLANE_OP_VTEP_ADD:
case DPLANE_OP_VTEP_DELETE:
return grout_vxlan_flood_update_ctx(ctx);
Expand Down
2 changes: 2 additions & 0 deletions modules/infra/api/gr_infra.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ struct gr_iface_info_port {

// VRF reconfiguration attribute flags.
#define GR_VRF_SET_FIB GR_BIT64(32)
#define GR_VRF_SET_MAC GR_BIT64(33)

// Per-AF FIB configuration.
struct gr_iface_info_vrf_fib {
Expand All @@ -138,6 +139,7 @@ struct gr_iface_info_vrf_fib {
struct gr_iface_info_vrf {
struct gr_iface_info_vrf_fib ipv4;
struct gr_iface_info_vrf_fib ipv6;
struct rte_ether_addr mac; // Used as Router MAC for EVPN L3VNI.
};

// VLAN reconfiguration attribute flags.
Expand Down
3 changes: 3 additions & 0 deletions modules/infra/api/gr_nexthop.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ typedef enum : uint8_t {
GR_NH_F_GATEWAY = GR_BIT8(2), // Gateway route.
GR_NH_F_LINK = GR_BIT8(3), // Connected link route.
GR_NH_F_MCAST = GR_BIT8(4), // Multicast address.
GR_NH_F_REMOTE = GR_BIT8(5), // Remote VTEP nexthop (EVPN).
} gr_nh_flags_t;

// Nexthop types for different forwarding behaviors.
Expand Down Expand Up @@ -176,6 +177,8 @@ static inline const char *gr_nh_flag_name(const gr_nh_flags_t flag) {
return "link";
case GR_NH_F_MCAST:
return "multicast";
case GR_NH_F_REMOTE:
return "remote";
}
return "?";
}
Expand Down
7 changes: 5 additions & 2 deletions modules/infra/cli/nexthop.c
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,8 @@ static cmd_status_t nh_l3_add(struct gr_api_client *c, const struct ec_pnode *p)
goto out;
if (arg_eth_addr(p, "MAC", &l3->mac) < 0 && errno != ENOENT)
goto out;
if (arg_str(p, "remote"))
l3->flags |= GR_NH_F_REMOTE;

if (gr_api_client_send_recv(c, GR_NH_ADD, len, req, NULL) < 0)
goto out;
Expand Down Expand Up @@ -619,13 +621,14 @@ static int ctx_init(struct ec_node *root) {

ret = CLI_COMMAND(
NEXTHOP_ADD_CTX(root),
"l3 iface IFACE [(id ID),(address IP),(mac MAC)]",
"l3 iface IFACE [(id ID),(address IP),(mac MAC),(remote)]",
nh_l3_add,
"Add a new L3 nexthop.",
with_help("IPv4/6 address.", ec_node_re("IP", IP_ANY_RE)),
with_help("Ethernet address.", ec_node_re("MAC", ETH_ADDR_RE)),
with_help("Nexthop ID.", ec_node_uint("ID", 1, UINT32_MAX - 1, 10)),
with_help("Output interface.", ec_node_dyn("IFACE", complete_iface_names, NULL))
with_help("Output interface.", ec_node_dyn("IFACE", complete_iface_names, NULL)),
with_help("Mark as remote (EVPN).", ec_node_str("remote", "remote"))
);
if (ret < 0)
return ret;
Expand Down
Loading
Loading