From 207ebb4fd87ac24e5b88c5d3567fff7b2128053e Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Thu, 16 Jul 2026 12:01:05 +0200 Subject: [PATCH 01/12] clang-format: add missing foreach macros Some foreach macros were missing from the clang-format configuration file. Add them. The complete list was generated with the following command: git grep -i _foreach | sed -En 's/.*\<([a-z0-9_]*_foreach[a-z0-9_]*)\>.*/ - \1/Ip' | LC_ALL=C sort -u Signed-off-by: Robin Jarry --- .clang-format | 10 ++++++---- cli/complete.c | 4 ++-- cli/exec.c | 2 +- frr/zebra_dplane_grout.c | 4 ++-- modules/infra/control/port.c | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.clang-format b/.clang-format index 5c4051e2f..de4f163a0 100644 --- a/.clang-format +++ b/.clang-format @@ -71,9 +71,13 @@ EmptyLineBeforeAccessModifier: LogicalBlock ExperimentalAutoDetectBinPacking: false FixNamespaceComments: false ForEachMacros: + - EC_COMP_FOREACH - LIST_FOREACH - LIST_FOREACH_SAFE + - RB_FOREACH + - RNODE_FOREACH_RE - RTE_ETH_FOREACH_DEV + - RTE_ETH_FOREACH_MATCHING_DEV - RTE_LCORE_FOREACH - SLIST_FOREACH - SLIST_FOREACH_SAFE @@ -81,13 +85,11 @@ ForEachMacros: - STAILQ_FOREACH_SAFE - TAILQ_FOREACH - TAILQ_FOREACH_SAFE - - rte_graph_foreach_node - gr_api_client_stream_foreach + - gr_nh_flags_foreach + - rte_graph_foreach_node - vec_foreach - vec_foreach_ref - - gr_nh_flags_foreach -IfMacros: - - KJ_IF_MAYBE IncludeBlocks: Regroup IncludeCategories: - Regex: '^".*' diff --git a/cli/complete.c b/cli/complete.c index 05e6fa8ce..5efaaa57b 100644 --- a/cli/complete.c +++ b/cli/complete.c @@ -115,7 +115,7 @@ int bash_complete(struct ec_node *cmdlist) { count = 0; comp_width = 0; - EC_COMP_FOREACH(item, cmpl, EC_COMP_FULL | EC_COMP_PARTIAL) { + EC_COMP_FOREACH (item, cmpl, EC_COMP_FULL | EC_COMP_PARTIAL) { int w = strlen(ec_comp_item_get_str(item)); if (w > comp_width) comp_width = w; @@ -131,7 +131,7 @@ int bash_complete(struct ec_node *cmdlist) { colon_prefix = last_colon - comp_word + 1; comp_width -= colon_prefix; - EC_COMP_FOREACH(item, cmpl, EC_COMP_FULL | EC_COMP_PARTIAL) { + EC_COMP_FOREACH (item, cmpl, EC_COMP_FULL | EC_COMP_PARTIAL) { const char *choice = ec_comp_item_get_str(item) + colon_prefix; const char *help = find_help(item); if (count > 1 && help != NULL) { diff --git a/cli/exec.c b/cli/exec.c index effccbd22..50af9cbb7 100644 --- a/cli/exec.c +++ b/cli/exec.c @@ -98,7 +98,7 @@ get_suggestions(const struct ec_node *cmdlist, const char *cmdline, unsigned *po goto out; struct ec_comp_item *i; - EC_COMP_FOREACH(i, c, EC_COMP_FULL) { + EC_COMP_FOREACH (i, c, EC_COMP_FULL) { if (ec_strvec_add(sug, ec_comp_item_get_str(i)) < 0) goto out; } diff --git a/frr/zebra_dplane_grout.c b/frr/zebra_dplane_grout.c index 2410c7f59..52eec1eee 100644 --- a/frr/zebra_dplane_grout.c +++ b/frr/zebra_dplane_grout.c @@ -244,7 +244,7 @@ static void grout_sync_poll_marker(struct event *e) { if (table) { rn = route_node_lookup(table, &grout_sync_marker_prefix); if (rn) { - RNODE_FOREACH_RE(rn, re) { + RNODE_FOREACH_RE (rn, re) { if (CHECK_FLAG(re->status, ROUTE_ENTRY_REMOVED)) continue; if (re->tag == GROUT_SYNC_MARKER_TAG @@ -944,7 +944,7 @@ static void grout_ns_reset(void) { struct vrf *default_vrf, *vrf; struct interface *ifp; - RB_FOREACH(vrf, vrf_id_head, &vrfs_by_id) { + RB_FOREACH (vrf, vrf_id_head, &vrfs_by_id) { FOR_ALL_INTERFACES(vrf, ifp) { if_down(ifp); } diff --git a/modules/infra/control/port.c b/modules/infra/control/port.c index 7fe67fea2..afcd3269e 100644 --- a/modules/infra/control/port.c +++ b/modules/infra/control/port.c @@ -586,7 +586,7 @@ static int iface_port_init(struct iface *iface, const void *api_info) { struct rte_eth_dev_info info; int ret; - RTE_ETH_FOREACH_MATCHING_DEV(port_id, api->devargs, &iterator) { + RTE_ETH_FOREACH_MATCHING_DEV (port_id, api->devargs, &iterator) { rte_eth_iterator_cleanup(&iterator); return errno_set(EEXIST); } @@ -594,7 +594,7 @@ static int iface_port_init(struct iface *iface, const void *api_info) { if ((ret = rte_dev_probe(api->devargs)) < 0) return errno_set(-ret); - RTE_ETH_FOREACH_MATCHING_DEV(port_id, api->devargs, &iterator) { + RTE_ETH_FOREACH_MATCHING_DEV (port_id, api->devargs, &iterator) { rte_eth_iterator_cleanup(&iterator); break; } From 50db4a75a32ec27465b276a9c009a9c3f9135531 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 19:40:07 +0200 Subject: [PATCH 02/12] ip,ip6: allow learning neighbors from unsolicited ARP/NDP Add a per-interface flag to control neighbor snooping. Configurable via grcli: interface set port p0 neigh_snoop on/off. When GR_IFACE_F_NEIGH_SNOOP is enabled on an interface, forward unsolicited ARP replies and Neighbor Advertisements to the control plane instead of dropping them. The existing arp_probe_input_cb and ndp_probe_input_cb callbacks already handle creating nexthops for unknown senders, so no control plane changes are needed. This allows grout to passively learn IP+MAC bindings from ARP/NDP traffic observed on an interface, similar to Linux's arp_accept sysctl. Signed-off-by: Robin Jarry --- modules/infra/api/gr_infra.h | 1 + modules/infra/cli/cli_iface.h | 7 ++++++- modules/infra/cli/iface.c | 9 +++++++++ modules/ip/datapath/arp_input_reply.c | 2 +- modules/ip6/datapath/ndp_na_input.c | 2 +- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/infra/api/gr_infra.h b/modules/infra/api/gr_infra.h index af3b637c8..e93c25206 100644 --- a/modules/infra/api/gr_infra.h +++ b/modules/infra/api/gr_infra.h @@ -34,6 +34,7 @@ typedef enum : uint16_t { GR_IFACE_F_PACKET_TRACE = GR_BIT16(2), GR_IFACE_F_SNAT_STATIC = GR_BIT16(3), GR_IFACE_F_SNAT_DYNAMIC = GR_BIT16(4), + GR_IFACE_F_NEIGH_SNOOP = GR_BIT16(5), } gr_iface_flags_t; // Interface state flags. diff --git a/modules/infra/cli/cli_iface.h b/modules/infra/cli/cli_iface.h index 4bb12d677..10baa868f 100644 --- a/modules/infra/cli/cli_iface.h +++ b/modules/infra/cli/cli_iface.h @@ -74,7 +74,8 @@ int arg_iface( CLI_CONTEXT(root, INTERFACE_ARG, CTX_ARG("set", "Modify an existing interface.")) #define IFACE_ATTRS_CMD \ - "(up|down),(promisc PROMISC),(mtu MTU),((vrf VRF)|(domain DOMAIN)),(description DESCR)" + "(up|down),(promisc PROMISC),(neigh_snoop NEIGH_SNOOP),(mtu MTU)," \ + "((vrf VRF)|(domain DOMAIN)),(description DESCR)" #define IFACE_ATTRS_ARGS \ with_help("Set the interface UP.", ec_node_str("up", "up")), \ @@ -83,6 +84,10 @@ int arg_iface( EC_NODE_OR("PROMISC", ec_node_str("", "on"), ec_node_str("", "off")) \ ), \ with_help("Set the interface DOWN.", ec_node_str("down", "down")), \ + with_help( \ + "Enable/disable neighbor snooping (learn from unsolicited ARP/NDP).", \ + EC_NODE_OR("NEIGH_SNOOP", ec_node_str("", "on"), ec_node_str("", "off")) \ + ), \ with_help( \ "Maximum transmission unit size.", \ ec_node_uint("MTU", 1280, UINT16_MAX - 1, 10) \ diff --git a/modules/infra/cli/iface.c b/modules/infra/cli/iface.c index 09c0e00eb..cc1a05834 100644 --- a/modules/infra/cli/iface.c +++ b/modules/infra/cli/iface.c @@ -261,6 +261,15 @@ uint64_t parse_iface_args( set_attrs |= GR_IFACE_SET_FLAGS; } + const char *neigh_snoop = arg_str(p, "NEIGH_SNOOP"); + if (neigh_snoop != NULL && strcmp(neigh_snoop, "on") == 0) { + iface->flags |= GR_IFACE_F_NEIGH_SNOOP; + set_attrs |= GR_IFACE_SET_FLAGS; + } else if (neigh_snoop != NULL && strcmp(neigh_snoop, "off") == 0) { + iface->flags &= ~GR_IFACE_F_NEIGH_SNOOP; + set_attrs |= GR_IFACE_SET_FLAGS; + } + if (arg_u16(p, "MTU", &iface->mtu) == 0) set_attrs |= GR_IFACE_SET_MTU; diff --git a/modules/ip/datapath/arp_input_reply.c b/modules/ip/datapath/arp_input_reply.c index 365c20164..191338cbe 100644 --- a/modules/ip/datapath/arp_input_reply.c +++ b/modules/ip/datapath/arp_input_reply.c @@ -37,7 +37,7 @@ static uint16_t arp_input_reply_process( iface = mbuf_data(mbuf)->iface; remote = nh4_lookup(iface->vrf_id, arp->arp_data.arp_sip); - if (remote != NULL) { + if (remote != NULL || (iface->flags & GR_IFACE_F_NEIGH_SNOOP)) { control_output_set_cb(mbuf, arp_probe_input_cb, 0); rte_node_enqueue_x1(graph, node, CONTROL, mbuf); } else { diff --git a/modules/ip6/datapath/ndp_na_input.c b/modules/ip6/datapath/ndp_na_input.c index bfc8b844c..89e4cb424 100644 --- a/modules/ip6/datapath/ndp_na_input.c +++ b/modules/ip6/datapath/ndp_na_input.c @@ -77,7 +77,7 @@ static uint16_t ndp_na_input_process( // recipient has apparently not initiated any communication with the // target. remote = nh6_lookup(iface->vrf_id, iface->id, &na->target); - if (remote == NULL) { + if (remote == NULL && !(iface->flags & GR_IFACE_F_NEIGH_SNOOP)) { edge = DROP; goto next; } From ea45a68ea26b800ea3a55cb4fdf13a5b2a276a4f Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 19:42:00 +0200 Subject: [PATCH 03/12] nexthop: add GR_NH_ORIGIN_NEIGH for learned neighbors ARP/NDP-learned nexthops were created with GR_NH_ORIGIN_INTERNAL which suppresses event generation. Add GR_NH_ORIGIN_NEIGH so these nexthops produce GR_EVENT_NEXTHOP_NEW and GR_EVENT_NEXTHOP_UPDATE events that the FRR plugin can receive and translate into neighbor entries for zebra. The associated host routes (/32, /128) remain GR_NH_ORIGIN_INTERNAL since they are internal plumbing and should not be visible to FRR. The neighbor are not synced yet in zebra. This will be done in the next commit. Signed-off-by: Robin Jarry --- frr/rt_grout.c | 14 +++++++++++--- modules/infra/api/gr_nexthop.h | 3 +++ modules/infra/control/nexthop.c | 5 +++-- modules/ip/control/nexthop.c | 2 +- modules/ip6/control/nexthop.c | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/frr/rt_grout.c b/frr/rt_grout.c index c7adaa830..1c9670dcd 100644 --- a/frr/rt_grout.c +++ b/frr/rt_grout.c @@ -936,11 +936,19 @@ void grout_nexthop_change(bool new, struct gr_nexthop *gr_nh, bool startup) { afi_t afi = AFI_UNSPEC; int family, type; - gr_log_debug("%s nh_id %u", new ? "add" : "del", gr_nh->nh_id); + gr_log_debug( + "%s nh_id %u origin %s", + new ? "add" : "del", + gr_nh->nh_id, + gr_nh_origin_name(gr_nh->origin) + ); - // XXX: grout is optional to have an ID for nexthop - // but in FRR, it's mandatory + if (gr_nh->origin == GR_NH_ORIGIN_NEIGH) { + gr_log_debug("dynamic neighbor learn, skip"); + return; + } if (gr_nh->nh_id == 0) { + // Nexthop IDs are optional in grout, but mandatory in FRR. gr_log_debug("nexthop without ID, skip"); return; } diff --git a/modules/infra/api/gr_nexthop.h b/modules/infra/api/gr_nexthop.h index cbc4e7939..6b362dadb 100644 --- a/modules/infra/api/gr_nexthop.h +++ b/modules/infra/api/gr_nexthop.h @@ -75,6 +75,7 @@ typedef enum : uint8_t { GR_NH_ORIGIN_ZSTATIC = 196, // (RTPROT_ZSTATIC from zebra) GR_NH_ORIGIN_OPENFABRIC = 197, // (RTPROT_OPENFABIC from zebra) GR_NH_ORIGIN_SRTE = 198, // (RTPROT_SRTE from zebra) + GR_NH_ORIGIN_NEIGH = 254, // Learned from ARP/NDP traffic. GR_NH_ORIGIN_INTERNAL = 255, // Reserved for internal use (no events, no ID allocation). } gr_nh_origin_t; @@ -269,6 +270,8 @@ static inline const char *gr_nh_origin_name(gr_nh_origin_t origin) { return "openfabric"; case GR_NH_ORIGIN_SRTE: return "srte"; + case GR_NH_ORIGIN_NEIGH: + return "neigh"; case GR_NH_ORIGIN_INTERNAL: return "INTERNAL"; } diff --git a/modules/infra/control/nexthop.c b/modules/infra/control/nexthop.c index 1dcd6c0e2..da6e60cd1 100644 --- a/modules/infra/control/nexthop.c +++ b/modules/infra/control/nexthop.c @@ -100,8 +100,8 @@ static void nexthop_id_put(struct nexthop *nh) { static int nexthop_id_get(struct nexthop *nh) { int ret; - // no id for internal, as we should not let user manipulate it - if (nh->origin == GR_NH_ORIGIN_INTERNAL) { + // no id for internal/neigh, as we should not let user manipulate them + if (nh->origin == GR_NH_ORIGIN_INTERNAL || nh->origin == GR_NH_ORIGIN_NEIGH) { nh->nh_id = 0; return 0; } @@ -274,6 +274,7 @@ bool nexthop_origin_valid(gr_nh_origin_t origin) { case GR_NH_ORIGIN_ZSTATIC: case GR_NH_ORIGIN_OPENFABRIC: case GR_NH_ORIGIN_SRTE: + case GR_NH_ORIGIN_NEIGH: case GR_NH_ORIGIN_INTERNAL: return true; } diff --git a/modules/ip/control/nexthop.c b/modules/ip/control/nexthop.c index 3ef0c3e94..a5c3508d6 100644 --- a/modules/ip/control/nexthop.c +++ b/modules/ip/control/nexthop.c @@ -162,7 +162,7 @@ void arp_probe_input_cb(void *obj, uintptr_t, const struct control_queue_drain * nh = nexthop_new( &(struct gr_nexthop_base) { .type = GR_NH_T_L3, - .origin = GR_NH_ORIGIN_INTERNAL, + .origin = GR_NH_ORIGIN_NEIGH, .iface_id = iface->id, .vrf_id = iface->vrf_id, }, diff --git a/modules/ip6/control/nexthop.c b/modules/ip6/control/nexthop.c index 5e53cc523..5f8643033 100644 --- a/modules/ip6/control/nexthop.c +++ b/modules/ip6/control/nexthop.c @@ -201,7 +201,7 @@ void ndp_probe_input_cb(void *obj, uintptr_t, const struct control_queue_drain * .type = GR_NH_T_L3, .iface_id = iface->id, .vrf_id = iface->vrf_id, - .origin = GR_NH_ORIGIN_INTERNAL, + .origin = GR_NH_ORIGIN_NEIGH, }, &(struct gr_nexthop_info_l3) { .af = GR_AF_IP6, From 605d9dbee58962e64009efd27d7c4c8eb23e4f30 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 21:40:49 +0200 Subject: [PATCH 04/12] nexthop: allow deleting by key instead of ID Change GR_NH_DEL to accept a full struct gr_nexthop instead of just an ID. The handler uses nexthop_lookup() which first tries the ID if specified or falls back to the type-specific key-based lookup (e.g. vrf+addr for L3). This will allow the FRR plugin to delete remote nexthops installed from EVPN neighbor entries without tracking their auto-allocated IDs. The nexthop type ids must be changed to avoid an error when deleting a nexthop from its ID which has already been deleted indirectly: ZEBRA: GROUT: grout_del_nexthop: nh_id 16 ERROR: AddressSanitizer: heap-buffer-overflow, READ of size 1 #0 in l3_lookup ../modules/infra/control/l3_nexthop.c:112 #1 in nexthop_lookup ../modules/infra/control/nexthop.c:306 #2 in nh_del ../modules/infra/api/nexthop.c:56 #3 in read_cb ../main/api.c:315 ZEBRA: GROUT: grout_client_send_recv: ERROR: GR_NH_DEL: Connection reset by peer Indeed, since nh->type=0 is both the unset value and GR_NH_T_L3, the nexthop_lookup() code calls l3_lookup() with nh->info pointing to outside the API message buffer. The only reliable to guard against this is to make GR_NH_T_L3 != 0. Shift all enum values up by one and adjust code accordingly. Signed-off-by: Robin Jarry --- .clang-format | 1 + api/gr_api.h | 2 +- frr/rt_grout.c | 2 +- modules/infra/api/gr_nexthop.h | 4 ++-- modules/infra/api/nexthop.c | 2 +- modules/infra/cli/nexthop.c | 2 +- modules/infra/control/nexthop.c | 24 ++++++++++++++++-------- 7 files changed, 23 insertions(+), 14 deletions(-) diff --git a/.clang-format b/.clang-format index de4f163a0..a02c15207 100644 --- a/.clang-format +++ b/.clang-format @@ -87,6 +87,7 @@ ForEachMacros: - TAILQ_FOREACH_SAFE - gr_api_client_stream_foreach - gr_nh_flags_foreach + - nexthop_type_ops_foreach - rte_graph_foreach_node - vec_foreach - vec_foreach_ref diff --git a/api/gr_api.h b/api/gr_api.h index a7a1082d2..6dd6246e9 100644 --- a/api/gr_api.h +++ b/api/gr_api.h @@ -11,7 +11,7 @@ #include // Must be bumped when making non-backward compatible changes in API headers -#define GR_API_VERSION 5 +#define GR_API_VERSION 6 // API request header. struct gr_api_request { diff --git a/frr/rt_grout.c b/frr/rt_grout.c index 1c9670dcd..9ffb19e8c 100644 --- a/frr/rt_grout.c +++ b/frr/rt_grout.c @@ -654,7 +654,7 @@ static enum zebra_dplane_result grout_add_nexthop_group(struct zebra_dplane_ctx static enum zebra_dplane_result grout_del_nexthop(uint32_t nh_id) { gr_log_debug("nh_id %u", nh_id); - struct gr_nh_del_req req = {.missing_ok = true, .nh_id = nh_id}; + struct gr_nh_del_req req = {.missing_ok = true, .nh = {.nh_id = nh_id}}; if (grout_client_send_recv(GR_NH_DEL, sizeof(req), &req, NULL) < 0) return ZEBRA_DPLANE_REQUEST_FAILURE; diff --git a/modules/infra/api/gr_nexthop.h b/modules/infra/api/gr_nexthop.h index 6b362dadb..c97e46247 100644 --- a/modules/infra/api/gr_nexthop.h +++ b/modules/infra/api/gr_nexthop.h @@ -30,7 +30,7 @@ typedef enum : uint8_t { // Nexthop types for different forwarding behaviors. typedef enum : uint8_t { - GR_NH_T_L3 = 0, // Standard L3 forwarding. + GR_NH_T_L3 = 1, // Standard L3 forwarding. GR_NH_T_SR6_OUTPUT, // SRv6 output nexthop. GR_NH_T_SR6_LOCAL, // SRv6 local nexthop. GR_NH_T_DNAT, // DNAT transformation. @@ -326,8 +326,8 @@ GR_REQ(GR_NH_ADD, struct gr_nh_add_req, struct gr_empty); // Automatically removes all routes referencing this nexthop. // Protected nexthops (Local+Static, Link origin) cannot be deleted. struct gr_nh_del_req { - uint32_t nh_id; uint8_t missing_ok; + struct gr_nexthop nh; }; GR_REQ(GR_NH_DEL, struct gr_nh_del_req, struct gr_empty); diff --git a/modules/infra/api/nexthop.c b/modules/infra/api/nexthop.c index fad53e6a7..68d65d719 100644 --- a/modules/infra/api/nexthop.c +++ b/modules/infra/api/nexthop.c @@ -53,7 +53,7 @@ static struct api_out nh_del(const void *request, struct api_ctx *) { const struct gr_nh_del_req *req = request; struct nexthop *nh; - nh = nexthop_lookup_id(req->nh_id); + nh = nexthop_lookup(&req->nh.base, req->nh.info); if (nh == NULL) { if (req->missing_ok) return api_out(0, 0, NULL); diff --git a/modules/infra/cli/nexthop.c b/modules/infra/cli/nexthop.c index 162595bb4..9bcb2ae50 100644 --- a/modules/infra/cli/nexthop.c +++ b/modules/infra/cli/nexthop.c @@ -417,7 +417,7 @@ static cmd_status_t nh_blackhole_add(struct gr_api_client *c, const struct ec_pn static cmd_status_t nh_del(struct gr_api_client *c, const struct ec_pnode *p) { struct gr_nh_del_req req = {.missing_ok = true}; - if (arg_u32(p, "ID", &req.nh_id) < 0) + if (arg_u32(p, "ID", &req.nh.nh_id) < 0) return CMD_ERROR; if (gr_api_client_send_recv(c, GR_NH_DEL, sizeof(req), &req, NULL) < 0) diff --git a/modules/infra/control/nexthop.c b/modules/infra/control/nexthop.c index da6e60cd1..36c9b68af 100644 --- a/modules/infra/control/nexthop.c +++ b/modules/infra/control/nexthop.c @@ -28,7 +28,11 @@ LOG_TYPE("nexthop"); static struct rte_mempool *pool; static struct id_pool *pool_id; static struct rte_hash *hash_by_id; -static const struct nexthop_type_ops *type_ops[256]; +static const struct nexthop_type_ops *type_ops[UINT_NUM_VALUES(gr_nh_type_t)]; + +#define nexthop_type_ops_foreach(t, ops) \ + for (t = GR_NH_T_L3; nexthop_type_valid(t); t++) \ + for (ops = type_ops[t]; ops != NULL; ops = NULL) struct gr_nexthop_config nh_conf = { .max_count = DEFAULT_MAX_COUNT, @@ -179,9 +183,10 @@ static int nexthop_config_allocate(const struct gr_nexthop_config *c) { if (pid == NULL) goto fail; - for (gr_nh_type_t t = 0; nexthop_type_valid(t); t++) { - const struct nexthop_type_ops *ops = type_ops[t]; - if (ops == NULL || ops->reconfig == NULL) + const struct nexthop_type_ops *ops; + gr_nh_type_t t; + nexthop_type_ops_foreach (t, ops) { + if (ops->reconfig == NULL) continue; LOG(INFO, "%s: %u nexthops", gr_nh_type_name(t), c->max_count); if (ops->reconfig(c) < 0) @@ -305,6 +310,9 @@ struct nexthop *nexthop_lookup(const struct gr_nexthop_base *base, const void *i if (nh == NULL && ops != NULL && ops->lookup != NULL) nh = ops->lookup(base, info); + if (nh == NULL) + return errno_set_null(ENOENT); + return nh; } @@ -489,9 +497,9 @@ void nexthop_destroy(struct nexthop *nh) { assert(nh->ref_count == 0); - for (gr_nh_type_t t = 0; nexthop_type_valid(t); t++) { - ops = type_ops[t]; - if (ops != NULL && ops->remove_references != NULL) + gr_nh_type_t t; + nexthop_type_ops_foreach (t, ops) { + if (ops->remove_references != NULL) ops->remove_references(nh); } nexthop_id_put(nh); @@ -567,7 +575,7 @@ static void nexthop_metrics_collect(struct metrics_writer *w) { memset(counts, 0, sizeof(counts)); nexthop_iter(count_types, counts); - for (gr_nh_type_t t = 0; nexthop_type_valid(t); t++) { + for (gr_nh_type_t t = GR_NH_T_L3; nexthop_type_valid(t); t++) { metrics_ctx_init(&ctx, w, "type", gr_nh_type_name(t), NULL); metric_emit(&ctx, &m_count, counts[t]); } From 20d9b9d2a9cb895d366f29d7a547aba0858bae40 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 19:43:38 +0200 Subject: [PATCH 05/12] frr: sync resolved nexthops as neighbor entries to zebra When a grout nexthop event arrives for an L3 nexthop with a resolved MAC, push a DPLANE_OP_NEIGH_INSTALL to zebra. On deletion, push DPLANE_OP_NEIGH_DELETE. This makes FRR aware of locally learned neighbors so it can advertise them as Type-2 MAC+IP routes via BGP EVPN. The notification is called before the nh_id check in grout_nexthop_change so that nexthops with nh_id=0 (auto-allocated by the ARP/NDP control plane) still produce neighbor entries in zebra even though they are not registered as zebra nexthop groups. Signed-off-by: Robin Jarry --- frr/rt_grout.c | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/frr/rt_grout.c b/frr/rt_grout.c index 9ffb19e8c..430312068 100644 --- a/frr/rt_grout.c +++ b/frr/rt_grout.c @@ -931,6 +931,60 @@ void grout_nexthop_group_add(struct gr_nexthop *gr_nh, bool startup) { ); } +static void grout_neigh_notify(bool new, struct gr_nexthop *gr_nh) { + const struct gr_nexthop_info_l3 *l3; + static const struct ethaddr zero_mac = {}; + struct zebra_dplane_ctx *ctx; + struct ethaddr mac; + struct ipaddr ip; + + if (gr_nh->type != GR_NH_T_L3) + return; + + l3 = (const struct gr_nexthop_info_l3 *)gr_nh->info; + + if (l3->af != GR_AF_IP4 && l3->af != GR_AF_IP6) + return; + if (l3->flags & (GR_NH_F_LOCAL | GR_NH_F_LINK | GR_NH_F_MCAST | GR_NH_F_REMOTE)) + return; + if (new && memcmp(&l3->mac, &zero_mac, sizeof(zero_mac)) == 0) + return; + + memset(&ip, 0, sizeof(ip)); + if (l3->af == GR_AF_IP4) { + ip.ipa_type = IPADDR_V4; + memcpy(&ip.ipaddr_v4, &l3->ipv4, sizeof(ip.ipaddr_v4)); + } else { + ip.ipa_type = IPADDR_V6; + memcpy(&ip.ipaddr_v6, &l3->ipv6, sizeof(ip.ipaddr_v6)); + } + memcpy(&mac, &l3->mac, sizeof(mac)); + + gr_log_debug( + "%s neigh iface=%u %pIA %pEA", new ? "add" : "del", gr_nh->iface_id, &ip, &mac + ); + + ctx = dplane_ctx_alloc(); + dplane_ctx_set_op(ctx, new ? DPLANE_OP_NEIGH_INSTALL : DPLANE_OP_NEIGH_DELETE); + dplane_ctx_set_ns_id(ctx, GROUT_NS); + dplane_ctx_set_ifindex(ctx, ifindex_grout_to_frr(gr_nh->iface_id)); + dplane_ctx_neigh_set_ipaddr(ctx, &ip); + dplane_ctx_neigh_set_mac(ctx, &mac); + dplane_ctx_neigh_set_ndm_state(ctx, NUD_REACHABLE); + dplane_ctx_neigh_set_ndm_family(ctx, l3->af == GR_AF_IP4 ? AF_INET : AF_INET6); + dplane_ctx_neigh_set_l2_len(ctx, ETH_ALEN); +#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 6, 0) + dplane_ctx_neigh_set_is_own(ctx, false); +#else + dplane_ctx_neigh_set_is_ext(ctx, false); +#endif + dplane_ctx_neigh_set_is_router(ctx, false); + dplane_ctx_neigh_set_dp_static(ctx, false); + dplane_ctx_neigh_set_local_inactive(ctx, false); + + dplane_provider_enqueue_to_zebra(ctx); +} + void grout_nexthop_change(bool new, struct gr_nexthop *gr_nh, bool startup) { struct nexthop *nh = NULL; afi_t afi = AFI_UNSPEC; @@ -943,6 +997,8 @@ void grout_nexthop_change(bool new, struct gr_nexthop *gr_nh, bool startup) { gr_nh_origin_name(gr_nh->origin) ); + grout_neigh_notify(new, gr_nh); + if (gr_nh->origin == GR_NH_ORIGIN_NEIGH) { gr_log_debug("dynamic neighbor learn, skip"); return; From 33c2fdd2b450e3ff80ce33d8c89c6f90a4996db7 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 19:44:25 +0200 Subject: [PATCH 06/12] frr: install nexthops from EVPN neighbor entries When the dplane plugin receives DPLANE_OP_NEIGH_INSTALL for a remote MAC/IP (from EVPN Type-2 routes), install the entry as a grout nexthop with GR_NH_F_REMOTE. The iface_id is resolved from the FRR ifindex which points to the bridge (SVI) interface. This allows the bridge suppress node to look up the nexthop and answer ARP/ND requests locally. Both IPv4 and IPv6 neighbors are supported. The existing L3VNI RMAC caching for Type-5 routes is preserved alongside. Signed-off-by: Robin Jarry --- frr/rt_grout.c | 87 +++++++++++++++++++++++++++++ smoke/evpn_l2l3vni_frr_test.sh | 2 +- smoke/evpn_l3vpn_frr_test.sh | 2 +- smoke/evpn_l3vpn_vxlan6_frr_test.sh | 2 +- 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/frr/rt_grout.c b/frr/rt_grout.c index 430312068..ddf9c59fc 100644 --- a/frr/rt_grout.c +++ b/frr/rt_grout.c @@ -1159,6 +1159,90 @@ enum zebra_dplane_result grout_macfdb_update_ctx(struct zebra_dplane_ctx *ctx) { return ret == 0 ? ZEBRA_DPLANE_REQUEST_SUCCESS : ZEBRA_DPLANE_REQUEST_FAILURE; } +static void neigh_install_nexthop(struct zebra_dplane_ctx *ctx, const struct ipaddr *addr) { + uint16_t iface_id = ifindex_frr_to_grout(dplane_ctx_get_ifindex(ctx)); + const struct ethaddr *mac = dplane_ctx_neigh_get_mac(ctx); + struct gr_nexthop_info_l3 *l3; + struct gr_nh_add_req *req; + size_t len; + + if (iface_id == GR_IFACE_ID_UNDEF) + return; + + len = sizeof(*req) + sizeof(*l3); + req = calloc(1, len); + if (req == NULL) { + gr_log_err("calloc: %s", strerror(errno)); + return; + } + + req->exist_ok = true; + req->nh.type = GR_NH_T_L3; + req->nh.origin = zebra2origin(dplane_ctx_get_type(ctx)); + req->nh.iface_id = iface_id; + l3 = (struct gr_nexthop_info_l3 *)req->nh.info; + l3->flags = GR_NH_F_REMOTE; + + switch (addr->ipa_type) { + case IPADDR_V4: + l3->af = GR_AF_IP4; + memcpy(&l3->ipv4, &addr->ipaddr_v4, sizeof(l3->ipv4)); + break; + case IPADDR_V6: + l3->af = GR_AF_IP6; + memcpy(&l3->ipv6, &addr->ipaddr_v6, sizeof(l3->ipv6)); + break; + default: + free(req); + return; + } + memcpy(&l3->mac, mac, sizeof(l3->mac)); + + gr_log_debug("install remote nh iface=%u %pIA %pEA", iface_id, addr, mac); + grout_client_send_recv(GR_NH_ADD, len, req, NULL); + free(req); +} + +static void neigh_delete_nexthop(struct zebra_dplane_ctx *ctx, const struct ipaddr *addr) { + uint16_t iface_id = ifindex_frr_to_grout(dplane_ctx_get_ifindex(ctx)); + struct gr_nexthop_info_l3 *l3; + struct gr_nh_del_req *req; + size_t len; + + if (iface_id == GR_IFACE_ID_UNDEF) + return; + + len = sizeof(*req) + sizeof(*l3); + req = calloc(1, len); + if (req == NULL) { + gr_log_err("calloc: %s", strerror(errno)); + return; + } + + req->missing_ok = true; + req->nh.type = GR_NH_T_L3; + req->nh.iface_id = iface_id; + l3 = (struct gr_nexthop_info_l3 *)req->nh.info; + + switch (addr->ipa_type) { + case IPADDR_V4: + l3->af = GR_AF_IP4; + memcpy(&l3->ipv4, &addr->ipaddr_v4, sizeof(l3->ipv4)); + break; + case IPADDR_V6: + l3->af = GR_AF_IP6; + memcpy(&l3->ipv6, &addr->ipaddr_v6, sizeof(l3->ipv6)); + break; + default: + free(req); + return; + } + + gr_log_debug("delete remote nh iface=%u %pIA", iface_id, addr); + grout_client_send_recv(GR_NH_DEL, len, req, NULL); + free(req); +} + 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; @@ -1179,9 +1263,12 @@ enum zebra_dplane_result grout_neigh_update_ctx(struct zebra_dplane_ctx *ctx) { 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, mac); + // Install a remote nexthop for ARP/ND suppression. + neigh_install_nexthop(ctx, addr); } else { gr_log_debug("uncache rmac vrf=%u %pIA", vrf_id, addr); l3vni_rmac_del(vrf_id, addr); + neigh_delete_nexthop(ctx, addr); } return ZEBRA_DPLANE_REQUEST_SUCCESS; diff --git a/smoke/evpn_l2l3vni_frr_test.sh b/smoke/evpn_l2l3vni_frr_test.sh index 162949b49..11e07cddd 100755 --- a/smoke/evpn_l2l3vni_frr_test.sh +++ b/smoke/evpn_l2l3vni_frr_test.sh @@ -260,7 +260,7 @@ done # -- Check RMAC is set on route nexthops and uses L3 VNI ---------------------- rmac=$(ip netns exec evpn-peer cat /sys/class/net/vni-l3/address) -wait_event "nh new: type=L3 id=[0-9]+ iface=vni-l3 vrf=tenant origin=zebra family=ipv4 addr=172.16.0.1 mac=$rmac flags=static remote" +wait_event "nh update: type=L3 id=[0-9]+ iface=vni-l3 vrf=tenant origin=zebra family=ipv4 addr=172.16.0.1 mac=$rmac flags=static remote" # -- Verify L3 connectivity through L3 VNI VXLAN overlay ---------------------- ip netns exec host-b ping -i0.1 -c3 -W1 16.0.0.2 diff --git a/smoke/evpn_l3vpn_frr_test.sh b/smoke/evpn_l3vpn_frr_test.sh index e3d5af191..63fab2e5b 100755 --- a/smoke/evpn_l3vpn_frr_test.sh +++ b/smoke/evpn_l3vpn_frr_test.sh @@ -235,7 +235,7 @@ done # -- Check RMAC is set on route nexthops --------------------------------------- rmac=$(ip netns exec evpn-peer cat /sys/class/net/vxlan-l3/address) -wait_event "nh new: type=L3 id=[0-9]+ iface=vxlan-l3 vrf=tenant origin=zebra family=ipv4 addr=172.16.0.1 mac=$rmac flags=static remote" +wait_event "nh update: type=L3 id=[0-9]+ iface=vxlan-l3 vrf=tenant origin=zebra family=ipv4 addr=172.16.0.1 mac=$rmac flags=static remote" vtysh -c "show bgp l2vpn evpn route type 5" grcli route show vrf tenant diff --git a/smoke/evpn_l3vpn_vxlan6_frr_test.sh b/smoke/evpn_l3vpn_vxlan6_frr_test.sh index 0c113a816..e52eead27 100755 --- a/smoke/evpn_l3vpn_vxlan6_frr_test.sh +++ b/smoke/evpn_l3vpn_vxlan6_frr_test.sh @@ -242,7 +242,7 @@ done # -- Check RMAC is set on route nexthops --------------------------------------- rmac=$(ip netns exec evpn-peer cat /sys/class/net/vxlan-l3/address) -wait_event "nh new: type=L3 id=[0-9]+ iface=vxlan-l3 vrf=tenant origin=zebra family=ipv6 addr=3fff::1 mac=$rmac flags=static remote" +wait_event "nh update: type=L3 id=[0-9]+ iface=vxlan-l3 vrf=tenant origin=zebra family=ipv6 addr=3fff::1 mac=$rmac flags=static remote" vtysh -c "show bgp l2vpn evpn route type 5" grcli route show vrf tenant From d4fe50d3ec2bb96e1738e5321d2f560ade65aee2 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 21:50:56 +0200 Subject: [PATCH 07/12] cli: add nexthop flush command Add GR_NH_FLUSH API request and grcli command to delete all nexthops matching a given origin. This is useful for clearing ARP/NDP-learned neighbors (origin neigh) which don't have IDs. Signed-off-by: Robin Jarry --- modules/infra/api/gr_nexthop.h | 8 ++++++++ modules/infra/api/nexthop.c | 24 +++++++++++++++++++++++ modules/infra/cli/nexthop.c | 36 ++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/modules/infra/api/gr_nexthop.h b/modules/infra/api/gr_nexthop.h index c97e46247..439338554 100644 --- a/modules/infra/api/gr_nexthop.h +++ b/modules/infra/api/gr_nexthop.h @@ -129,6 +129,7 @@ enum gr_nexthop_requests : uint32_t { GR_NH_DEL, GR_NH_LIST, GR_NH_GET, + GR_NH_FLUSH, }; // Nexthop events (not generated for GR_NH_ORIGIN_INTERNAL nexthops). @@ -348,3 +349,10 @@ struct gr_nh_get_req { }; GR_REQ(GR_NH_GET, struct gr_nh_get_req, struct gr_nexthop); + +// Flush nexthops matching the given origin. +struct gr_nh_flush_req { + gr_nh_origin_t origin; +}; + +GR_REQ(GR_NH_FLUSH, struct gr_nh_flush_req, struct gr_empty); diff --git a/modules/infra/api/nexthop.c b/modules/infra/api/nexthop.c index 68d65d719..df6b3e599 100644 --- a/modules/infra/api/nexthop.c +++ b/modules/infra/api/nexthop.c @@ -127,6 +127,29 @@ static struct api_out nh_get(const void *request, struct api_ctx *) { return api_out(0, len, pub); } +static struct api_out nh_flush(const void *request, struct api_ctx *) { + const struct gr_nh_flush_req *req = request; + struct nexthop *nh = NULL; + + for (;;) { + nh = nexthop_next(nh); + if (nh == NULL) + break; + if (nh->origin != req->origin) + continue; + if (nh->type == GR_NH_T_L3) { + struct nexthop_info_l3 *l3 = nexthop_info_l3(nh); + if ((l3->flags & NH_LOCAL_ADDR_FLAGS) == NH_LOCAL_ADDR_FLAGS) + continue; + } + nexthop_routes_cleanup(nh); + while (nh->ref_count > 0) + nexthop_decref(nh); + } + + return api_out(0, 0, NULL); +} + RTE_INIT(_init) { api_handler(GR_NH_CONFIG_GET, nh_config_get); api_handler(GR_NH_CONFIG_SET, nh_config_set); @@ -134,4 +157,5 @@ RTE_INIT(_init) { api_handler(GR_NH_DEL, nh_del); api_handler(GR_NH_LIST, nh_list); api_handler(GR_NH_GET, nh_get); + api_handler(GR_NH_FLUSH, nh_flush); } diff --git a/modules/infra/cli/nexthop.c b/modules/infra/cli/nexthop.c index 9bcb2ae50..fc010b004 100644 --- a/modules/infra/cli/nexthop.c +++ b/modules/infra/cli/nexthop.c @@ -426,6 +426,26 @@ static cmd_status_t nh_del(struct gr_api_client *c, const struct ec_pnode *p) { return CMD_SUCCESS; } +static cmd_status_t nh_flush(struct gr_api_client *c, const struct ec_pnode *p) { + struct gr_nh_flush_req req = {}; + const char *origin = arg_str(p, "ORIGIN"); + + if (origin == NULL) + return CMD_ERROR; + + if (strcmp(origin, "neigh") == 0) + req.origin = GR_NH_ORIGIN_NEIGH; + else if (strcmp(origin, "static") == 0) + req.origin = GR_NH_ORIGIN_STATIC; + else + return CMD_ERROR; + + if (gr_api_client_send_recv(c, GR_NH_FLUSH, sizeof(req), &req, NULL) < 0) + return CMD_ERROR; + + return CMD_SUCCESS; +} + static cmd_status_t nh_group_add(struct gr_api_client *c, const struct ec_pnode *p) { struct gr_nexthop_info_group *group; struct gr_nh_add_req *req = NULL; @@ -669,6 +689,22 @@ static int ctx_init(struct ec_node *root) { "Delete a next hop.", with_help("Nexthop ID.", ec_node_uint("ID", 1, UINT32_MAX - 1, 10)) ); + if (ret < 0) + return ret; + ret = CLI_COMMAND( + NEXTHOP_CTX(root), + "flush origin ORIGIN", + nh_flush, + "Flush all nexthops with the given origin.", + with_help( + "Nexthop origin.", + EC_NODE_OR( + "ORIGIN", + ec_node_str("neigh", "neigh"), + ec_node_str("static", "static") + ) + ) + ); if (ret < 0) return ret; ret = CLI_COMMAND( From 4d1b47e8509219c81c5338d0d1f19285ce04240b Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 23:13:10 +0200 Subject: [PATCH 08/12] bridge: homogenize flag names All interface/nexthop flags use "presence" semantics. Having flags to signify the absence of a feature is confusing. Change the semantics of bridge flags to follow the same style. Bump the API version since it changes the semantics of the bridge flag values. Signed-off-by: Robin Jarry --- modules/l2/api/gr_l2.h | 5 +-- modules/l2/cli/bridge.c | 56 +++++++++++++++--------------- modules/l2/control/bridge.c | 3 ++ modules/l2/datapath/bridge_input.c | 5 ++- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/modules/l2/api/gr_l2.h b/modules/l2/api/gr_l2.h index 071ec4090..3a711664c 100644 --- a/modules/l2/api/gr_l2.h +++ b/modules/l2/api/gr_l2.h @@ -15,8 +15,9 @@ // Bridge configuration flags. typedef enum : uint16_t { - GR_BRIDGE_F_NO_FLOOD = GR_BIT16(0), - GR_BRIDGE_F_NO_LEARN = GR_BIT16(1), + GR_BRIDGE_F_FLOOD = GR_BIT16(0), // Flood BUM traffic. + GR_BRIDGE_F_LEARN = GR_BIT16(1), // Dynamic MAC learning in FDB. +#define GR_BRIDGE_F_VALID (GR_BRIDGE_F_FLOOD | GR_BRIDGE_F_LEARN) } gr_bridge_flags_t; #define GR_BRIDGE_MAX_MEMBERS 64 diff --git a/modules/l2/cli/bridge.c b/modules/l2/cli/bridge.c index 6a4c134e6..5f9639e4f 100644 --- a/modules/l2/cli/bridge.c +++ b/modules/l2/cli/bridge.c @@ -12,6 +12,7 @@ #include #include +#include #include static void @@ -22,9 +23,9 @@ bridge_show(struct gr_api_client *c, const struct gr_iface *iface, struct gr_obj o, "bridge_flags", GR_DISP_STR_ARRAY, - "%sflood %slearn", - (bridge->flags & GR_BRIDGE_F_NO_FLOOD) ? "no_" : "", - (bridge->flags & GR_BRIDGE_F_NO_LEARN) ? "no_" : "" + "flood %s learn %s", + (bridge->flags & GR_BRIDGE_F_FLOOD) ? "on" : "off", + (bridge->flags & GR_BRIDGE_F_LEARN) ? "on" : "off" ); gr_object_field(o, "ageing_time", GR_DISP_INT, "%u", bridge->ageing_time); gr_object_field(o, "mac", 0, ETH_F, &bridge->mac); @@ -40,10 +41,10 @@ bridge_list_info(struct gr_api_client *, const struct gr_iface *iface, char *buf snprintf( buf, len, - "members=%u %sflood %slearn", + "members=%u flood %s learn %s", bridge->n_members, - (bridge->flags & GR_BRIDGE_F_NO_FLOOD) ? "no_" : "", - (bridge->flags & GR_BRIDGE_F_NO_LEARN) ? "no_" : "" + (bridge->flags & GR_BRIDGE_F_FLOOD) ? "on" : "off", + (bridge->flags & GR_BRIDGE_F_LEARN) ? "on" : "off" ); } @@ -64,18 +65,21 @@ static uint64_t parse_bridge_args( set_attrs = parse_iface_args(c, p, iface, sizeof(*bridge), update); - if (arg_str(p, "flood")) { - bridge->flags &= ~GR_BRIDGE_F_NO_FLOOD; + const char *on_off = arg_str(p, "FLOOD"); + if (on_off != NULL && strcmp(on_off, "on") == 0) { + bridge->flags |= GR_BRIDGE_F_FLOOD; set_attrs |= GR_BRIDGE_SET_FLAGS; - } else if (arg_str(p, "no_flood")) { - bridge->flags |= GR_BRIDGE_F_NO_FLOOD; + } else if (on_off != NULL && strcmp(on_off, "off") == 0) { + bridge->flags &= ~GR_BRIDGE_F_FLOOD; set_attrs |= GR_BRIDGE_SET_FLAGS; } - if (arg_str(p, "learn")) { - bridge->flags &= ~GR_BRIDGE_F_NO_LEARN; + + on_off = arg_str(p, "LEARN"); + if (on_off != NULL && strcmp(on_off, "on") == 0) { + bridge->flags |= GR_BRIDGE_F_LEARN; set_attrs |= GR_BRIDGE_SET_FLAGS; - } else if (arg_str(p, "no_learn")) { - bridge->flags |= GR_BRIDGE_F_NO_LEARN; + } else if (on_off != NULL && strcmp(on_off, "off") == 0) { + bridge->flags &= ~GR_BRIDGE_F_LEARN; set_attrs |= GR_BRIDGE_SET_FLAGS; } @@ -98,6 +102,7 @@ static uint64_t parse_bridge_args( static cmd_status_t bridge_add(struct gr_api_client *c, const struct ec_pnode *p) { const struct gr_iface_add_resp *resp; struct gr_iface_add_req *req = NULL; + struct gr_iface_info_bridge *br; void *resp_ptr = NULL; size_t len; @@ -107,6 +112,8 @@ static cmd_status_t bridge_add(struct gr_api_client *c, const struct ec_pnode *p req->iface.type = GR_IFACE_TYPE_BRIDGE; req->iface.flags = GR_IFACE_F_UP; + br = PAYLOAD(req); + br->flags = GR_BRIDGE_F_FLOOD | GR_BRIDGE_F_LEARN; if (parse_bridge_args(c, p, &req->iface, false) == 0) goto err; @@ -145,7 +152,7 @@ static cmd_status_t bridge_set(struct gr_api_client *c, const struct ec_pnode *p return ret; } -#define BRIDGE_ATTRS_CMD IFACE_ATTRS_CMD ",(ageing_time AGE),(mac MAC),FLOOD,LEARN" +#define BRIDGE_ATTRS_CMD IFACE_ATTRS_CMD ",(ageing_time AGE),(mac MAC),(flood FLOOD),(learn LEARN)" #define BRIDGE_ATTRS_ARGS \ IFACE_ATTRS_ARGS, \ @@ -154,20 +161,13 @@ static cmd_status_t bridge_set(struct gr_api_client *c, const struct ec_pnode *p ec_node_uint("AGE", 0, UINT16_MAX, 10) \ ), \ with_help("Bridge ethernet address.", ec_node_re("MAC", ETH_ADDR_RE)), \ - EC_NODE_OR( \ - "FLOOD", \ - with_help( \ - "Enable flooding of BUM traffic.", ec_node_str("flood", "flood") \ - ), \ - with_help( \ - "Disable flooding of BUM traffic.", \ - ec_node_str("no_flood", "no_flood") \ - ) \ + with_help( \ + "Enable/disable flooding of BUM traffic.", \ + EC_NODE_OR("FLOOD", ec_node_str("", "on"), ec_node_str("", "off")) \ ), \ - EC_NODE_OR( \ - "LEARN", \ - with_help("Enable MAC learning.", ec_node_str("learn", "learn")), \ - with_help("Disable MAC learning.", ec_node_str("no_learn", "no_learn")) \ + with_help( \ + "Enable/disable dynamic MAC learning.", \ + EC_NODE_OR("LEARN", ec_node_str("", "on"), ec_node_str("", "off")) \ ) static int ctx_init(struct ec_node *root) { diff --git a/modules/l2/control/bridge.c b/modules/l2/control/bridge.c index a7d5b9851..8337e1f15 100644 --- a/modules/l2/control/bridge.c +++ b/modules/l2/control/bridge.c @@ -45,6 +45,9 @@ static int bridge_reconfig( struct iface_info_bridge *cur = iface_info_bridge(iface); const struct gr_iface_info_bridge *next = api_info; + if (next->flags & ~GR_BRIDGE_F_VALID) + return errno_set(EINVAL); + if (set_attrs & GR_BRIDGE_SET_MAC) iface_set_eth_addr(iface, &next->mac); if (set_attrs & GR_BRIDGE_SET_FLAGS) diff --git a/modules/l2/datapath/bridge_input.c b/modules/l2/datapath/bridge_input.c index e9ea266f4..e90660205 100644 --- a/modules/l2/datapath/bridge_input.c +++ b/modules/l2/datapath/bridge_input.c @@ -58,8 +58,7 @@ static uint16_t bridge_input_process( } br = iface_info_bridge(bridge); - if (rte_is_unicast_ether_addr(ð->src_addr) - && !(br->flags & GR_BRIDGE_F_NO_LEARN)) { + if (rte_is_unicast_ether_addr(ð->src_addr) && (br->flags & GR_BRIDGE_F_LEARN)) { struct l3_addr vtep = {0}; if (d->iface->type == GR_IFACE_TYPE_VXLAN) vtep = d->vtep; @@ -97,7 +96,7 @@ static uint16_t bridge_input_process( edge = FLOOD; } next: - if (edge == FLOOD && (br->flags & GR_BRIDGE_F_NO_FLOOD)) + if (edge == FLOOD && !(br->flags & GR_BRIDGE_F_FLOOD)) edge = FLOOD_DISABLED; rte_node_enqueue_x1(graph, node, edge, m); From 5593c428a39add5aba111f304e5f40e8240b674b Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 23:11:52 +0200 Subject: [PATCH 09/12] api: generalize gr_flags_foreach This macro will be useful to format all flag masks. Signed-off-by: Robin Jarry --- .clang-format | 2 +- api/gr_macro.h | 5 +++++ modules/infra/api/gr_nexthop.h | 7 +------ modules/infra/cli/nexthop.c | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.clang-format b/.clang-format index a02c15207..2db983f1e 100644 --- a/.clang-format +++ b/.clang-format @@ -86,7 +86,7 @@ ForEachMacros: - TAILQ_FOREACH - TAILQ_FOREACH_SAFE - gr_api_client_stream_foreach - - gr_nh_flags_foreach + - gr_flags_foreach - nexthop_type_ops_foreach - rte_graph_foreach_node - vec_foreach diff --git a/api/gr_macro.h b/api/gr_macro.h index f9225feda..8a3e3229e 100644 --- a/api/gr_macro.h +++ b/api/gr_macro.h @@ -5,6 +5,7 @@ #include #include +#include // Get number of elements in a static array. #define ARRAY_DIM(array) (sizeof(array) / sizeof(array[0])) @@ -48,3 +49,7 @@ #define GR_SYMBOL_FORBIDDEN(func, new_func) \ sorry_##func##_is_a_banned_function_use_##new_func##_instead + +#define gr_flags_foreach(f, flags) \ + for (uint64_t __bit = 0, f = 1ULL; __bit < sizeof(flags) * CHAR_BIT; f = 1ULL << ++__bit) \ + if (flags & f) diff --git a/modules/infra/api/gr_nexthop.h b/modules/infra/api/gr_nexthop.h index 439338554..b4fcd73e4 100644 --- a/modules/infra/api/gr_nexthop.h +++ b/modules/infra/api/gr_nexthop.h @@ -143,11 +143,6 @@ GR_EVENT(GR_EVENT_NEXTHOP_NEW, struct gr_nexthop); GR_EVENT(GR_EVENT_NEXTHOP_DELETE, struct gr_nexthop); GR_EVENT(GR_EVENT_NEXTHOP_UPDATE, struct gr_nexthop); -#define gr_nh_flags_foreach(f, flags) \ - for (gr_nh_flags_t __i = 0, f = GR_BIT8(0); __i < sizeof(gr_nh_flags_t) * CHAR_BIT; \ - f = GR_BIT8(++__i)) \ - if (flags & f) - // Convert nexthop state enum to string representation. static inline const char *gr_nh_state_name(const gr_nh_state_t state) { switch (state) { @@ -166,7 +161,7 @@ static inline const char *gr_nh_state_name(const gr_nh_state_t state) { } // Convert nexthop flag enum to string representation. -// For flag masks, iterate individual flags using gr_nh_flags_foreach. +// For flag masks, iterate individual flags using gr_flags_foreach. static inline const char *gr_nh_flag_name(const gr_nh_flags_t flag) { switch (flag) { case GR_NH_F_STATIC: diff --git a/modules/infra/cli/nexthop.c b/modules/infra/cli/nexthop.c index fc010b004..6e47204c1 100644 --- a/modules/infra/cli/nexthop.c +++ b/modules/infra/cli/nexthop.c @@ -120,7 +120,7 @@ static void add_columns_l3(struct gr_table *table) { static void format_nh_flags(char *buf, size_t len, gr_nh_flags_t flags) { ssize_t n = 0; buf[0] = 0; - gr_nh_flags_foreach (fl, flags) { + gr_flags_foreach (fl, flags) { if (n > 0) SAFE_BUF(snprintf, len, " "); SAFE_BUF(snprintf, len, "%s", gr_nh_flag_name(fl)); From 0cccd8950245dd507ecc84f4c04855ba1264478c Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 23:19:16 +0200 Subject: [PATCH 10/12] l2: add bridge and fdb flag names in API header Use gr_flags_foreach to simplify formatting them. Signed-off-by: Robin Jarry --- modules/l2/api/gr_l2.h | 26 ++++++++++++++++++++++++++ modules/l2/cli/bridge.c | 30 ++++++++++++++++-------------- modules/l2/cli/fdb.c | 12 ++++++------ 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/modules/l2/api/gr_l2.h b/modules/l2/api/gr_l2.h index 3a711664c..b296071d5 100644 --- a/modules/l2/api/gr_l2.h +++ b/modules/l2/api/gr_l2.h @@ -20,6 +20,18 @@ typedef enum : uint16_t { #define GR_BRIDGE_F_VALID (GR_BRIDGE_F_FLOOD | GR_BRIDGE_F_LEARN) } gr_bridge_flags_t; +// Convert bridge flag enum to string representation. +// For flag masks, iterate individual flags using gr_flags_foreach. +static inline const char *gr_bridge_flag_name(gr_bridge_flags_t f) { + switch (f) { + case GR_BRIDGE_F_FLOOD: + return "flood"; + case GR_BRIDGE_F_LEARN: + return "learn"; + } + return "?"; +} + #define GR_BRIDGE_MAX_MEMBERS 64 #define GR_BRIDGE_DEFAULT_AGEING 300 @@ -68,6 +80,20 @@ typedef enum : uint8_t { GR_FDB_F_EXTERN = GR_BIT8(2), // Programmed by external control plane. } gr_fdb_flags_t; +// Convert FDB flag enum to string representation. +// For flag masks, iterate individual flags using gr_flags_foreach. +static inline const char *gr_fdb_flag_name(gr_fdb_flags_t f) { + switch (f) { + case GR_FDB_F_STATIC: + return "static"; + case GR_FDB_F_LEARN: + return "learn"; + case GR_FDB_F_EXTERN: + return "extern"; + } + return "?"; +} + // Forwarding database entry associating a MAC+VLAN to a bridge member interface. struct gr_fdb_entry { uint16_t bridge_id; diff --git a/modules/l2/cli/bridge.c b/modules/l2/cli/bridge.c index 5f9639e4f..d0228e681 100644 --- a/modules/l2/cli/bridge.c +++ b/modules/l2/cli/bridge.c @@ -15,17 +15,26 @@ #include #include +static const char *format_bridge_flags(gr_bridge_flags_t flags) { + static char buf[128]; // grcli is single threaded, this is safe + size_t n = 0; + buf[0] = 0; + + gr_flags_foreach (f, flags) { + if (n > 0) + SAFE_BUF(snprintf, sizeof(buf), " "); + SAFE_BUF(snprintf, sizeof(buf), "%s", gr_bridge_flag_name(f)); + } +err: + return buf; +} + static void bridge_show(struct gr_api_client *c, const struct gr_iface *iface, struct gr_object *o) { const struct gr_iface_info_bridge *bridge = PAYLOAD(iface); gr_object_field( - o, - "bridge_flags", - GR_DISP_STR_ARRAY, - "flood %s learn %s", - (bridge->flags & GR_BRIDGE_F_FLOOD) ? "on" : "off", - (bridge->flags & GR_BRIDGE_F_LEARN) ? "on" : "off" + o, "bridge_flags", GR_DISP_STR_ARRAY, "%s", format_bridge_flags(bridge->flags) ); gr_object_field(o, "ageing_time", GR_DISP_INT, "%u", bridge->ageing_time); gr_object_field(o, "mac", 0, ETH_F, &bridge->mac); @@ -38,14 +47,7 @@ bridge_show(struct gr_api_client *c, const struct gr_iface *iface, struct gr_obj static void bridge_list_info(struct gr_api_client *, const struct gr_iface *iface, char *buf, size_t len) { const struct gr_iface_info_bridge *bridge = PAYLOAD(iface); - snprintf( - buf, - len, - "members=%u flood %s learn %s", - bridge->n_members, - (bridge->flags & GR_BRIDGE_F_FLOOD) ? "on" : "off", - (bridge->flags & GR_BRIDGE_F_LEARN) ? "on" : "off" - ); + snprintf(buf, len, "members=%u %s", bridge->n_members, format_bridge_flags(bridge->flags)); } static struct cli_iface_type bridge_type = { diff --git a/modules/l2/cli/fdb.c b/modules/l2/cli/fdb.c index 48d90b16a..4dc4ba030 100644 --- a/modules/l2/cli/fdb.c +++ b/modules/l2/cli/fdb.c @@ -79,12 +79,12 @@ static cmd_status_t fdb_flush(struct gr_api_client *c, const struct ec_pnode *p) static size_t fdb_format_flags(char *buf, size_t len, gr_fdb_flags_t flags) { size_t n = 0; buf[0] = 0; - if (flags & GR_FDB_F_LEARN) - SAFE_BUF(snprintf, len, "%slearn", n ? " " : ""); - if (flags & GR_FDB_F_STATIC) - SAFE_BUF(snprintf, len, "%sstatic", n ? " " : ""); - if (flags & GR_FDB_F_EXTERN) - SAFE_BUF(snprintf, len, "%sextern", n ? " " : ""); + + gr_flags_foreach (f, flags) { + if (n > 0) + SAFE_BUF(snprintf, len, " "); + SAFE_BUF(snprintf, len, "%s", gr_fdb_flag_name(f)); + } err: return n; } From 0a7194e980d47cfd9f01a3a5747684af9718123f Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 19:45:21 +0200 Subject: [PATCH 11/12] l2: add ARP/ND suppression in bridges Add flag to allow enabling ARP/ND suppression on bridge interfaces. When a bridge has GR_BRIDGE_F_NEIGH_SUPPRESS enabled, broadcast and multicast traffic is routed through bridge_neigh_suppress before flooding. The node intercepts ARP requests and IPv6 Neighbor Solicitations, looks up the target IP in the bridge's VRF nexthop table, and replies on behalf of known remote hosts (GR_NH_F_REMOTE with a reachable MAC). For ARP, the Ethernet and ARP headers are rewritten in-place. For ND, the packet is rebuilt from scratch since a Neighbor Advertisement has a different structure than a Neighbor Solicitation. In both cases, the reply is sent directly to iface_output, bypassing eth_output to avoid double Ethernet header prepending. Packets that cannot be suppressed (unknown target, non-ARP/ND, or non-remote nexthop) fall through to bridge_flood as usual. Also add a "remote" flag to the nexthop add l3 CLI command so remote nexthops can be configured manually for testing. Signed-off-by: Robin Jarry --- docs/graph.svg | 772 ++++++++++---------- modules/l2/api/gr_l2.h | 5 +- modules/l2/cli/bridge.c | 19 +- modules/l2/datapath/bridge_input.c | 5 +- modules/l2/datapath/bridge_neigh_suppress.c | 218 ++++++ modules/l2/datapath/meson.build | 1 + 6 files changed, 642 insertions(+), 378 deletions(-) create mode 100644 modules/l2/datapath/bridge_neigh_suppress.c diff --git a/docs/graph.svg b/docs/graph.svg index 538dd604e..c9613d3ed 100644 --- a/docs/graph.svg +++ b/docs/graph.svg @@ -1,948 +1,972 @@ - - - - + + + bond_output - -bond_output + +bond_output port_output - -port_output + +port_output bond_output->port_output - - + + iface_input - -iface_input + +iface_input xconnect - -xconnect + +xconnect iface_input->xconnect - - + + eth_input - -eth_input + +eth_input iface_input->eth_input - - + + bridge_input - -bridge_input + +bridge_input iface_input->bridge_input - - + + iface_output - -iface_output + +iface_output iface_output->bond_output - - + + iface_output->port_output - - + + iface_output->bridge_input - - + + - + vxlan_output - -vxlan_output + +vxlan_output iface_output->vxlan_output - - + + port_tx - -port_tx + +port_tx port_output->port_tx - - + + port_rx - -port_rx + +port_rx port_rx->iface_input - - + + xconnect->port_output - - + + lacp_input - -lacp_input + +lacp_input eth_input->lacp_input - - + + snap_input - -snap_input + +snap_input eth_input->snap_input - - + + arp_input - -arp_input + +arp_input eth_input->arp_input - - + + - + ip_input - -ip_input + +ip_input eth_input->ip_input - - + + - + ip6_input - -ip6_input + +ip6_input eth_input->ip6_input - - + + eth_output - -eth_output + +eth_output eth_output->iface_output - - + + l2_redirect - -l2_redirect + +l2_redirect lacp_output - -lacp_output + +lacp_output lacp_output->eth_output - - + + snap_input->l2_redirect - - + + arp_input_reply - -arp_input_reply + +arp_input_reply arp_input->arp_input_reply - - + + arp_input_request - -arp_input_request + +arp_input_request arp_input->arp_input_request - - + + arp_output_reply - -arp_output_reply + +arp_output_reply arp_output_reply->eth_output - - + + arp_output_request - -arp_output_request + +arp_output_request arp_output_request->eth_output - - + + bridge_flood - -bridge_flood + +bridge_flood bridge_flood->iface_input - - + + bridge_flood->iface_output - - + + - + vxlan_flood - -vxlan_flood + +vxlan_flood bridge_flood->vxlan_flood - - + + bridge_input->iface_input - - + + bridge_input->iface_output - - + + bridge_input->bridge_flood - - + + - + + +bridge_neigh_suppress + +bridge_neigh_suppress + + +bridge_input->bridge_neigh_suppress + + + + + +bridge_neigh_suppress->iface_output + + + + + +bridge_neigh_suppress->bridge_flood + + + + + vxlan_flood->iface_output - - + + - + ospf_redirect - -ospf_redirect + +ospf_redirect - + ospf_redirect->l2_redirect - - + + - + loopback_input - -loopback_input + +loopback_input loopback_input->ip_input - - + + loopback_input->ip6_input - - + + - + loopback_output - -loopback_output + +loopback_output - + xvrf - -xvrf + +xvrf xvrf->ip_input - - + + xvrf->ip6_input - - + + - + ip_forward - -ip_forward + +ip_forward - + ip_output - -ip_output + +ip_output ip_forward->ip_output - - + + - + ip_fragment - -ip_fragment + +ip_fragment ip_fragment->ip_output - - + + - + ip_hold - -ip_hold + +ip_hold ip_input->ip_forward - - + + - + ip_input_local - -ip_input_local + +ip_input_local ip_input->ip_input_local - - + + ip_input->ip_output - - + + - + dnat44_dynamic - -dnat44_dynamic + +dnat44_dynamic ip_input->dnat44_dynamic - - + + - + dnat44_static - -dnat44_static + +dnat44_static ip_input->dnat44_static - - + + - + ip_loadbalance - -ip_loadbalance + +ip_loadbalance ip_loadbalance->ip_output - - + + ip_input_local->ospf_redirect - - + + - + ipip_input - -ipip_input + +ipip_input ip_input_local->ipip_input - - + + - + icmp_input - -icmp_input + +icmp_input ip_input_local->icmp_input - - + + - + l4_input_local - -l4_input_local + +l4_input_local ip_input_local->l4_input_local - - + + ip_output->eth_output - - + + ip_output->xvrf - - + + ip_output->ip_fragment - - + + ip_output->ip_hold - - + + ip_output->ip_loadbalance - - + + - + ipip_output - -ipip_output + +ipip_output ip_output->ipip_output - - + + - + sr6_output - -sr6_output + +sr6_output ip_output->sr6_output - - + + - + ip6_forward - -ip6_forward + +ip6_forward - + ip6_output - -ip6_output + +ip6_output ip6_forward->ip6_output - - + + - + ip6_hold - -ip6_hold + +ip6_hold ip6_input->ip6_forward - - + + - + ip6_input_local - -ip6_input_local + +ip6_input_local ip6_input->ip6_input_local - - + + ip6_input->ip6_output - - + + - + sr6_local - -sr6_local + +sr6_local ip6_input->sr6_local - - + + - + ip6_loadbalance - -ip6_loadbalance + +ip6_loadbalance ip6_loadbalance->ip6_output - - + + ip6_input_local->ospf_redirect - - + + - + icmp6_input - -icmp6_input + +icmp6_input ip6_input_local->icmp6_input - - + + ip6_input_local->l4_input_local - - + + ip6_output->eth_output - - + + ip6_output->xvrf - - + + ip6_output->ip6_hold - - + + ip6_output->ip6_loadbalance - - + + ip6_output->sr6_output - - + + ipip_input->ip_input - - + + ipip_output->ip_output - - + + - + vxlan_input - -vxlan_input + +vxlan_input - + vxlan_input->iface_input - - + + - + vxlan_output->ip_output - - + + - + vxlan_output->ip6_output - - + + - + dnat44_dynamic->ip_forward - - + + - + dnat44_dynamic->ip_input_local - - + + - + dnat44_static->ip_forward - - + + - + dnat44_static->ip_input_local - - + + - + sr6_local->ip_input - - + + - + sr6_local->ip6_input - - + + - + sr6_local->ip6_input_local - - + + - + sr6_output->ip6_output - - + + - + icmp_output - -icmp_output + +icmp_output icmp_input->icmp_output - - + + - + icmp_local_send - -icmp_local_send + +icmp_local_send icmp_local_send->icmp_output - - + + icmp_output->ip_output - - + + - + icmp6_output - -icmp6_output + +icmp6_output icmp6_input->icmp6_output - - + + - + ndp_na_input - -ndp_na_input + +ndp_na_input icmp6_input->ndp_na_input - - + + - + ndp_ns_input - -ndp_ns_input + +ndp_ns_input icmp6_input->ndp_ns_input - - + + - + ndp_rs_input - -ndp_rs_input + +ndp_rs_input icmp6_input->ndp_rs_input - - + + - + icmp6_local_send - -icmp6_local_send + +icmp6_local_send icmp6_local_send->icmp6_output - - + + icmp6_output->ip6_output - - + + - + ndp_na_output - -ndp_na_output + +ndp_na_output ndp_na_output->icmp6_output - - + + - + ndp_ns_output - -ndp_ns_output + +ndp_ns_output ndp_ns_output->icmp6_output - - + + - + l4_input_local->vxlan_input - - + + - + l4_loopback_output - -l4_loopback_output + +l4_loopback_output - + l4_input_local->l4_loopback_output - - + + - + dhcp_input - -dhcp_input + +dhcp_input - + l4_input_local->dhcp_input - - + + - + l4_loopback_output->loopback_output - - + + diff --git a/modules/l2/api/gr_l2.h b/modules/l2/api/gr_l2.h index b296071d5..18ef14dd3 100644 --- a/modules/l2/api/gr_l2.h +++ b/modules/l2/api/gr_l2.h @@ -17,7 +17,8 @@ typedef enum : uint16_t { GR_BRIDGE_F_FLOOD = GR_BIT16(0), // Flood BUM traffic. GR_BRIDGE_F_LEARN = GR_BIT16(1), // Dynamic MAC learning in FDB. -#define GR_BRIDGE_F_VALID (GR_BRIDGE_F_FLOOD | GR_BRIDGE_F_LEARN) + GR_BRIDGE_F_NEIGH_SUPPRESS = GR_BIT16(2), // Intercept ARP/NDP requests. +#define GR_BRIDGE_F_VALID (GR_BRIDGE_F_FLOOD | GR_BRIDGE_F_LEARN | GR_BRIDGE_F_NEIGH_SUPPRESS) } gr_bridge_flags_t; // Convert bridge flag enum to string representation. @@ -28,6 +29,8 @@ static inline const char *gr_bridge_flag_name(gr_bridge_flags_t f) { return "flood"; case GR_BRIDGE_F_LEARN: return "learn"; + case GR_BRIDGE_F_NEIGH_SUPPRESS: + return "neigh_suppress"; } return "?"; } diff --git a/modules/l2/cli/bridge.c b/modules/l2/cli/bridge.c index d0228e681..fcc9d22c5 100644 --- a/modules/l2/cli/bridge.c +++ b/modules/l2/cli/bridge.c @@ -84,6 +84,14 @@ static uint64_t parse_bridge_args( bridge->flags &= ~GR_BRIDGE_F_LEARN; set_attrs |= GR_BRIDGE_SET_FLAGS; } + on_off = arg_str(p, "NEIGH_SUPPRESS"); + if (on_off != NULL && strcmp(on_off, "on") == 0) { + bridge->flags |= GR_BRIDGE_F_NEIGH_SUPPRESS; + set_attrs |= GR_BRIDGE_SET_FLAGS; + } else if (on_off != NULL && strcmp(on_off, "off") == 0) { + bridge->flags &= ~GR_BRIDGE_F_NEIGH_SUPPRESS; + set_attrs |= GR_BRIDGE_SET_FLAGS; + } if (arg_u16(p, "AGE", &bridge->ageing_time) == 0) set_attrs |= GR_BRIDGE_SET_AGEING_TIME; @@ -154,8 +162,9 @@ static cmd_status_t bridge_set(struct gr_api_client *c, const struct ec_pnode *p return ret; } -#define BRIDGE_ATTRS_CMD IFACE_ATTRS_CMD ",(ageing_time AGE),(mac MAC),(flood FLOOD),(learn LEARN)" - +#define BRIDGE_ATTRS_CMD \ + IFACE_ATTRS_CMD ",(ageing_time AGE),(mac MAC)" \ + ",(flood FLOOD),(learn LEARN),(neigh_suppress NEIGH_SUPPRESS)" #define BRIDGE_ATTRS_ARGS \ IFACE_ATTRS_ARGS, \ with_help( \ @@ -167,6 +176,12 @@ static cmd_status_t bridge_set(struct gr_api_client *c, const struct ec_pnode *p "Enable/disable flooding of BUM traffic.", \ EC_NODE_OR("FLOOD", ec_node_str("", "on"), ec_node_str("", "off")) \ ), \ + with_help( \ + "Enable/disable ARP/NDP requests suppression.", \ + EC_NODE_OR( \ + "NEIGH_SUPPRESS", ec_node_str("", "on"), ec_node_str("", "off") \ + ) \ + ), \ with_help( \ "Enable/disable dynamic MAC learning.", \ EC_NODE_OR("LEARN", ec_node_str("", "on"), ec_node_str("", "off")) \ diff --git a/modules/l2/datapath/bridge_input.c b/modules/l2/datapath/bridge_input.c index e90660205..ac2465cdc 100644 --- a/modules/l2/datapath/bridge_input.c +++ b/modules/l2/datapath/bridge_input.c @@ -13,6 +13,7 @@ enum edges { OUTPUT = 0, INPUT, FLOOD, + NEIGH_SUPPRESS, BRIDGE_INVAL, HAIRPIN, OUT_IFACE_INVAL, @@ -91,8 +92,9 @@ static uint16_t bridge_input_process( } else { edge = OUTPUT; } + } else if (br->flags & GR_BRIDGE_F_NEIGH_SUPPRESS) { + edge = NEIGH_SUPPRESS; } else { - // Broadcast, multicast edge = FLOOD; } next: @@ -131,6 +133,7 @@ static struct rte_node_register node = { [OUTPUT] = "iface_output", [INPUT] = "iface_input", [FLOOD] = "bridge_flood", + [NEIGH_SUPPRESS] = "bridge_neigh_suppress", [BRIDGE_INVAL] = "bridge_input_invalid_domain", [HAIRPIN] = "bridge_input_hairpin", [OUT_IFACE_INVAL] = "bridge_input_invalid_output", diff --git a/modules/l2/datapath/bridge_neigh_suppress.c b/modules/l2/datapath/bridge_neigh_suppress.c new file mode 100644 index 000000000..8c3cbc276 --- /dev/null +++ b/modules/l2/datapath/bridge_neigh_suppress.c @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 Robin Jarry + +#include "icmp6.h" +#include "iface.h" +#include "ip4.h" +#include "ip6.h" +#include "ip6_datapath.h" +#include "nexthop.h" +#include "rxtx.h" + +#include +#include +#include +#include + +enum edges { + IFACE_OUTPUT = 0, + FLOOD, + DROP, + EDGE_COUNT, +}; + +static inline bool is_suppressable(const struct nexthop *nh) { + const struct nexthop_info_l3 *l3; + + if (nh == NULL || nh->type != GR_NH_T_L3) + return false; + + l3 = nexthop_info_l3(nh); + if (!(l3->flags & GR_NH_F_REMOTE)) + return false; + if (l3->state != GR_NH_S_REACHABLE) + return false; + if (rte_is_zero_ether_addr(&l3->mac)) + return false; + + return true; +} + +static rte_edge_t +suppress_arp(struct rte_mbuf *m, struct rte_ether_hdr *eth, const struct iface *bridge) { + const struct nexthop_info_l3 *l3; + struct rte_ether_addr req_sha; + const struct nexthop *nh; + struct rte_arp_hdr *arp; + ip4_addr_t req_sip; + + if (rte_pktmbuf_pkt_len(m) < sizeof(*eth) + sizeof(*arp)) + return FLOOD; + + arp = PAYLOAD(eth); + if (arp->arp_opcode != RTE_BE16(RTE_ARP_OP_REQUEST)) + return FLOOD; + + nh = nh4_lookup(bridge->vrf_id, arp->arp_data.arp_tip); + if (!is_suppressable(nh)) + return FLOOD; + + l3 = nexthop_info_l3(nh); + + // Save requester info before overwriting. + req_sha = arp->arp_data.arp_sha; + req_sip = arp->arp_data.arp_sip; + + // Rewrite ARP payload in-place: request -> reply. + arp->arp_opcode = RTE_BE16(RTE_ARP_OP_REPLY); + arp->arp_data.arp_tha = req_sha; + arp->arp_data.arp_tip = req_sip; + arp->arp_data.arp_sha = l3->mac; + arp->arp_data.arp_sip = l3->ipv4; + + // Rewrite Ethernet header in-place. + eth->dst_addr = req_sha; + eth->src_addr = l3->mac; + + return IFACE_OUTPUT; +} + +static rte_edge_t +suppress_nd(struct rte_mbuf *m, struct rte_ether_hdr *eth, const struct iface *bridge) { + const struct nexthop_info_l3 *l3; + struct icmp6_neigh_solicit *ns; + struct icmp6_neigh_advert *na; + struct rte_ether_addr req_mac; + struct icmp6_opt_lladdr *ll; + struct rte_ipv6_addr req_ip; + const struct nexthop *nh; + struct rte_ipv6_hdr *ip6; + struct icmp6_opt *opt; + uint16_t payload_len; + struct icmp6 *icmp6; + + if (rte_pktmbuf_pkt_len(m) < sizeof(*eth) + sizeof(*ip6) + sizeof(*icmp6) + sizeof(*ns)) + return FLOOD; + + ip6 = PAYLOAD(eth); + if (ip6->proto != IPPROTO_ICMPV6) + return FLOOD; + + icmp6 = PAYLOAD(ip6); + if (icmp6->type != ICMP6_TYPE_NEIGH_SOLICIT) + return FLOOD; + + ns = PAYLOAD(icmp6); + nh = nh6_lookup(bridge->vrf_id, GR_IFACE_ID_UNDEF, &ns->target); + if (!is_suppressable(nh)) + return FLOOD; + + l3 = nexthop_info_l3(nh); + + // Save requester info. + req_mac = eth->src_addr; + req_ip = ip6->src_addr; + + // Trim entire packet and rebuild NA from scratch. + rte_pktmbuf_trim(m, rte_pktmbuf_pkt_len(m)); + + payload_len = sizeof(*icmp6) + sizeof(*na) + sizeof(*opt) + sizeof(*ll); + eth = (struct rte_ether_hdr *)rte_pktmbuf_append( + m, sizeof(*eth) + sizeof(*ip6) + payload_len + ); + if (eth == NULL) + return DROP; + + // Ethernet header. + eth->dst_addr = req_mac; + eth->src_addr = l3->mac; + eth->ether_type = RTE_BE16(RTE_ETHER_TYPE_IPV6); + + // IPv6 header. + ip6 = PAYLOAD(eth); + ip6_set_fields(ip6, payload_len, IPPROTO_ICMPV6, &l3->ipv6, &req_ip); + + // ICMPv6 NA. + icmp6 = PAYLOAD(ip6); + icmp6->type = ICMP6_TYPE_NEIGH_ADVERT; + icmp6->code = 0; + na = PAYLOAD(icmp6); + na->flags = ICMP6_NA_F_SOLICITED | ICMP6_NA_F_OVERRIDE; + na->__reserved = 0; + na->__reserved2 = 0; + na->target = l3->ipv6; + + // Target link-layer address option. + opt = PAYLOAD(na); + opt->type = ICMP6_OPT_TARGET_LLADDR; + opt->len = ICMP6_OPT_LEN(sizeof(*opt) + sizeof(*ll)); + ll = PAYLOAD(opt); + ll->mac = l3->mac; + + // Compute ICMPv6 checksum. + icmp6->cksum = 0; + icmp6->cksum = rte_ipv6_udptcp_cksum(ip6, icmp6); + + return IFACE_OUTPUT; +} + +static uint16_t bridge_neigh_suppress_process( + struct rte_graph *graph, + struct rte_node *node, + void **objs, + uint16_t nb_objs +) { + const struct iface *bridge; + struct iface_mbuf_data *d; + struct rte_ether_hdr *eth; + struct rte_mbuf *m; + rte_edge_t edge; + + for (uint16_t i = 0; i < nb_objs; i++) { + m = objs[i]; + d = iface_mbuf_data(m); + eth = rte_pktmbuf_mtod(m, struct rte_ether_hdr *); + + bridge = iface_from_id(d->iface->domain_id); + if (bridge == NULL) { + edge = DROP; + goto next; + } + + switch (eth->ether_type) { + case RTE_BE16(RTE_ETHER_TYPE_ARP): + edge = suppress_arp(m, eth, bridge); + break; + case RTE_BE16(RTE_ETHER_TYPE_IPV6): + edge = suppress_nd(m, eth, bridge); + break; + default: + edge = FLOOD; + break; + } +next: + rte_node_enqueue_x1(graph, node, edge, m); + } + + return nb_objs; +} + +static struct rte_node_register node = { + .name = "bridge_neigh_suppress", + .process = bridge_neigh_suppress_process, + .nb_edges = EDGE_COUNT, + .next_nodes = { + [IFACE_OUTPUT] = "iface_output", + [FLOOD] = "bridge_flood", + [DROP] = "bridge_neigh_suppress_drop", + }, +}; + +static struct gr_node_info info = { + .node = &node, + .type = GR_NODE_T_L2, +}; + +GR_NODE_REGISTER(info); + +GR_DROP_REGISTER(bridge_neigh_suppress_drop); diff --git a/modules/l2/datapath/meson.build b/modules/l2/datapath/meson.build index b6dc45fc5..6207085ea 100644 --- a/modules/l2/datapath/meson.build +++ b/modules/l2/datapath/meson.build @@ -4,6 +4,7 @@ src += files( 'bridge_flood.c', 'bridge_input.c', + 'bridge_neigh_suppress.c', 'vxlan_flood.c', 'vxlan_input.c', 'vxlan_output.c', From 5aee5b64c48abc6601bda02193dd609ee3cccad7 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 1 Apr 2026 19:53:56 +0200 Subject: [PATCH 12/12] smoke: add ARP/ND suppression tests bridge_neigh_suppress_test verifies suppression with a manually configured remote nexthop: an ARP request for a known remote IP gets answered locally by grout with the correct MAC, and is not flooded to other bridge ports. evpn_neigh_suppress_frr_test verifies end-to-end suppression with FRR EVPN. The Linux peer has neigh_suppress on the VXLAN interface and advertise-svi-ip in BGP. Both sides have bridge SVIs and hosts with default routes through them. When hosts ARP for their gateways, VTEPs learn the IP+MAC bindings and advertise Type-2 MAC+IP routes. The remote VTEP installs the nexthops and the suppress node answers ARPs locally. Signed-off-by: Robin Jarry --- .github/workflows/check.yml | 2 +- README.md | 4 +- smoke/bridge_neigh_suppress_test.sh | 71 ++++++++++ smoke/evpn_neigh_suppress_frr_test.sh | 179 ++++++++++++++++++++++++++ 4 files changed, 253 insertions(+), 3 deletions(-) create mode 100755 smoke/bridge_neigh_suppress_test.sh create mode 100755 smoke/evpn_neigh_suppress_frr_test.sh diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7c0425108..9783bcfd4 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -87,7 +87,7 @@ jobs: sudo apt-get install -qy --no-install-recommends \ make gcc gdb ccache ninja-build meson git scdoc inotify-tools \ libibverbs-dev libasan8 libcmocka-dev libedit-dev libarchive-dev \ - libevent-dev libmnl-dev libnuma-dev python3-pyelftools \ + libevent-dev libmnl-dev libnuma-dev python3-pyelftools arping \ socat tcpdump traceroute graphviz iproute2 iputils-ping ndisc6 jq \ dnsmasq systemd-coredump abigail-tools \ "linux-modules-extra-$(uname -r)" diff --git a/README.md b/README.md index 24d6a48cb..12ab9df8e 100644 --- a/README.md +++ b/README.md @@ -287,13 +287,13 @@ In order to run the `smoke-tests`, `lint`, `check-patches` and `update-graph` targets, you'll need additional packages: ```sh -dnf install gawk gdb clang-tools-extra jq codespell curl traceroute graphviz ndisc6 abidiff inotify-tools +dnf install gawk gdb clang-tools-extra iputils jq codespell curl traceroute graphviz ndisc6 abidiff inotify-tools ``` or ```sh -apt install gawk gdb clang-format jq codespell curl traceroute graphviz ndisc6 abigail-tools inotify-tools +apt install arping gawk gdb clang-format jq codespell curl traceroute graphviz ndisc6 abigail-tools inotify-tools ``` ### Build diff --git a/smoke/bridge_neigh_suppress_test.sh b/smoke/bridge_neigh_suppress_test.sh new file mode 100755 index 000000000..2344dd05e --- /dev/null +++ b/smoke/bridge_neigh_suppress_test.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Robin Jarry + +# Verify ARP/ND suppression with manually configured remote nexthops. +# +# A bridge with neigh_suppress has two ports. A remote nexthop (with a known +# MAC) is installed for a target IP. When a host sends an ARP request for that +# IP, grout replies locally using the remote nexthop's MAC instead of flooding +# the request to the other port. +# +# .-------------. .---------------------------. .-------------. +# | host-a | | grout | | host-b | +# | | | | | | +# | 10.0.0.2 | | 10.0.0.1 | | 10.0.0.3 | +# | +-------+ | | +----+ +------+ +----+ | | +-------+ | +# | | x-p0 + - - - - - + p0 +---+ br0 +--+ p1 + - - - - - + x-p1 | | +# | +-------+ | | +----+ +------+ +----+ | | +-------+ | +# | | | remote nh: 10.0.0.99 | | | +# | | | mac de:ad:be:ef:00:01 | | | +# '-------------' '---------------------------' '-------------' + +. $(dirname $0)/_init.sh + +grcli interface add bridge br0 neigh_suppress on neigh_snoop on +grcli address add 10.0.0.1/24 iface br0 + +port_add p0 domain br0 +port_add p1 domain br0 + +# Install a remote nexthop for the suppressed IP. +grcli nexthop add l3 iface br0 address 10.0.0.99 mac de:ad:be:ef:00:01 remote + +netns_add host-a +move_to_netns x-p0 host-a +ip -n host-a addr add 10.0.0.2/24 dev x-p0 + +netns_add host-b +move_to_netns x-p1 host-b +ip -n host-b addr add 10.0.0.3/24 dev x-p1 + +# Wait for initial ARP/ND traffic to settle before testing suppression. +sleep 2 + +# Capture on host-b filtering only for ARP targeting 10.0.0.99. +ip netns exec host-b timeout 3 \ + tcpdump -c1 -t -nn -i x-p1 'arp and arp[24:4] = 0x0a000063' \ + >$tmp/tcpdump.out 2>/dev/null & +tcpdump_pid=$! +sleep 0.5 + +# Send an ARP request from host-a for the suppressed IP. +ip netns exec host-a arping -c1 -w1 -I x-p0 10.0.0.99 >$tmp/arping.out + +# Wait for tcpdump to finish. +wait $tcpdump_pid 2>/dev/null || true + +if grep -q ARP $tmp/tcpdump.out; then + cat $tmp/tcpdump.out + fail "ARP request for 10.0.0.99 was flooded to host-b (should have been suppressed)" +fi + +# Verify the suppressed ARP reply has the correct remote MAC. +if ! grep -qi 'DE:AD:BE:EF:00:01' $tmp/arping.out; then + cat $tmp/arping.out + fail "ARP reply did not contain expected MAC de:ad:be:ef:00:01" +fi + +# Verify normal L2 connectivity still works (non-suppressed traffic). +ip netns exec host-a ping -i0.01 -c3 -W1 -n 10.0.0.3 || fail "L2 ping host-a->host-b failed" +ip netns exec host-b ping -i0.01 -c3 -W1 -n 10.0.0.2 || fail "L2 ping host-b->host-a failed" diff --git a/smoke/evpn_neigh_suppress_frr_test.sh b/smoke/evpn_neigh_suppress_frr_test.sh new file mode 100755 index 000000000..e7ab28c0b --- /dev/null +++ b/smoke/evpn_neigh_suppress_frr_test.sh @@ -0,0 +1,179 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Robin Jarry + +# Verify ARP suppression with FRR EVPN Type-2 MAC+IP routes. +# +# Both VTEPs have bridge SVIs. The Linux peer has neigh_suppress on the +# VXLAN interface and advertise-svi-ip in BGP. Grout has neigh_suppress on +# the bridge and neigh_snoop on the bridge to learn local neighbors. +# +# When hosts ARP for their gateways, the VTEPs learn the IP+MAC bindings +# and advertise them as Type-2 MAC+IP routes. Remote VTEPs install the +# neighbors and the suppress node answers ARP requests locally. +# +# .-------------------------. .-------------------------. +# | evpn-peer | | grout | +# | | | | +# | +----------+ | | +----------+ | +# | | vxlan100 | nh_suppr | | | vxlan100 | | +# | +----+-----+ | | +-----+----+ | +# | | | | | | +# | +---+---+ | | +---+---+ | +# | | br100 | | | nh_suppr | br100 | | +# | +---+---+ | | +---+---+ | +# | | .1 | | .2 | | +# | +---+---+ +-------+ | | +------+ +---+---+ | +# | | p1 | | x-p0 | | | | p0 | | p1 | | +# | +---+---+ +---+---+ | | +---+--+ +---+---+ | +# '------|------------|-----' '-----|------------|------' +# | | | | +# | | <----- BGP -----> | | +# .------|--------. `----------------------' .-------|------. +# | +---+----+ | underlay | +---+----+ | +# | | x-p1 | | 172.16.0.0/24 | | x-p1 | | +# | +--------+ | | +--------+ | +# | .2 | | .3 | +# | host-a | overlay | host-b | +# '---------------' 10.0.0.0/24 '--------------' + +. $(dirname $0)/_init_frr.sh + +# right side ------------------------------------------------------------------- +create_interface p0 +set_ip_address p0 172.16.0.2/24 + +# left side -------------------------------------------------------------------- +start_frr evpn-peer 0 + +ip netns exec evpn-peer sysctl -qw net.ipv4.conf.all.forwarding=1 +ip netns exec evpn-peer sysctl -qw net.ipv4.conf.all.rp_filter=0 +ip netns exec evpn-peer sysctl -qw net.ipv4.conf.default.rp_filter=0 + +move_to_netns x-p0 evpn-peer +ip -n evpn-peer addr add 172.16.0.1/24 dev x-p0 + +ip -n evpn-peer link add br100 type bridge +ip -n evpn-peer link set br100 up +# linux needs an IP address in order to learn neighbors +ip -n evpn-peer addr add 10.0.0.4/24 dev br100 + +ip -n evpn-peer link add vxlan100 type vxlan id 100 local 172.16.0.1 dstport 4789 nolearning +ip -n evpn-peer link set vxlan100 master br100 +ip -n evpn-peer link set vxlan100 type bridge_slave neigh_suppress on learning off +ip -n evpn-peer link set vxlan100 up + +ip -n evpn-peer link add p1 type veth peer name x-p1 +ip -n evpn-peer link set p1 master br100 +ip -n evpn-peer link set p1 up + +netns_add host-a +ip -n evpn-peer link set x-p1 netns host-a +ip -n host-a link set x-p1 up +ip -n host-a addr add 10.0.0.2/24 dev x-p1 +ip -n host-a route add default via 10.0.0.4 + +# BGP EVPN on peer with advertise-svi-ip for Type-2 MAC+IP routes. +vtysh -N evpn-peer <<-EOF +configure terminal + +router bgp 65000 + bgp router-id 172.16.0.1 + no bgp default ipv4-unicast + + neighbor 172.16.0.2 remote-as 65000 + + address-family l2vpn evpn + neighbor 172.16.0.2 activate + advertise-all-vni + advertise-svi-ip + exit-address-family +exit +EOF + +# BGP EVPN on grout. +vtysh <<-EOF +configure terminal + +router bgp 65000 + bgp router-id 172.16.0.2 + no bgp default ipv4-unicast + + neighbor 172.16.0.1 remote-as 65000 + + address-family l2vpn evpn + neighbor 172.16.0.1 activate + advertise-all-vni + advertise-svi-ip + exit-address-family +exit +EOF + +# Wait for advertise-all-vni to take effect before creating bridge members. +attempts=0 +while ! vtysh -c "show evpn" | grep -q "L2 VNIs"; do + if [ "$attempts" -ge 10 ]; then + vtysh -c "show evpn" + fail "EVPN not enabled in zebra" + fi + sleep 1 + attempts=$((attempts + 1)) +done + +grcli interface add bridge br100 neigh_suppress on neigh_snoop on +grcli address add 10.0.0.1/24 iface br100 +grcli interface add vxlan vxlan100 vni 100 local 172.16.0.2 domain br100 + +attempts=0 +while ! vtysh -c "show evpn vni 100" | grep -q "VNI: 100"; do + if [ "$attempts" -ge 10 ]; then + vtysh -c "show evpn vni 100" + fail "zebra did not learn VNI 100" + fi + sleep 1 + attempts=$((attempts + 1)) +done + +mark_events +create_interface p1 domain br100 + +netns_add host-b +move_to_netns x-p1 host-b +ip -n host-b addr add 10.0.0.3/24 dev x-p1 +ip -n host-b route add default via 10.0.0.1 + +# -- Wait for EVPN type-3 (flood VTEP) exchange ------------------------------- +wait_event -t 10 "flood add:.*172.16.0.1.*vni=100" + +# -- Trigger ARP learning on both sides --------------------------------------- +# Each host pings its gateway SVI. This makes the host ARP for the gateway, +# teaching the local VTEP the host's IP+MAC. FRR advertises the binding as +# a Type-2 MAC+IP route. +mark_events +ip netns exec host-a ping -c1 -W1 10.0.0.4 || true +ip netns exec host-b ping -c1 -W1 10.0.0.1 || true + +# Also establish overlay connectivity. +ip netns exec host-b ping -i0.1 -c3 -W1 10.0.0.2 +ip netns exec host-a ping -i0.1 -c3 -W1 10.0.0.3 + +# -- Wait for remote nexthop to be installed via DPLANE_OP_NEIGH_INSTALL ------ +mac_a=$(ip netns exec host-a cat /sys/class/net/x-p1/address) +wait_event -t 10 "nh new:.*addr=10.0.0.2.*flags=.*remote" + +# -- Verify ARP suppression --------------------------------------------------- +# Flush host-b's ARP cache to force a new ARP request. +ip -n host-b neigh flush dev x-p1 + +# Send ARP from host-b. Grout should reply from its nexthop cache. +ip netns exec host-b arping -c1 -w2 -I x-p1 10.0.0.2 >$tmp/arping.out + +# Verify host-b received reply with the correct MAC. +if ! grep -qi "$mac_a" $tmp/arping.out; then + cat $tmp/arping.out + fail "ARP reply did not contain expected MAC $mac_a" +fi + +# -- Final connectivity check through overlay ---------------------------------- +ip netns exec host-b ping -i0.1 -c3 -W1 10.0.0.2 +ip netns exec host-a ping -i0.1 -c3 -W1 10.0.0.3