diff --git a/.clang-format b/.clang-format index 5c4051e2f..2db983f1e 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,12 @@ ForEachMacros: - STAILQ_FOREACH_SAFE - TAILQ_FOREACH - TAILQ_FOREACH_SAFE - - rte_graph_foreach_node - gr_api_client_stream_foreach + - gr_flags_foreach + - nexthop_type_ops_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/.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/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/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/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/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/frr/rt_grout.c b/frr/rt_grout.c index c7adaa830..ddf9c59fc 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; @@ -931,16 +931,80 @@ 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; 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) + ); + + grout_neigh_notify(new, gr_nh); - // 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; } @@ -1095,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; @@ -1115,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/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/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/api/gr_nexthop.h b/modules/infra/api/gr_nexthop.h index cbc4e7939..b4fcd73e4 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. @@ -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; @@ -128,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). @@ -141,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) { @@ -164,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: @@ -269,6 +266,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"; } @@ -323,8 +322,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); @@ -345,3 +344,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 fad53e6a7..df6b3e599 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); @@ -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/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/infra/cli/nexthop.c b/modules/infra/cli/nexthop.c index 162595bb4..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)); @@ -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) @@ -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( diff --git a/modules/infra/control/nexthop.c b/modules/infra/control/nexthop.c index 1dcd6c0e2..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, @@ -100,8 +104,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; } @@ -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) @@ -274,6 +279,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; } @@ -304,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; } @@ -488,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); @@ -566,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]); } 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; } 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/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/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, 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; } diff --git a/modules/l2/api/gr_l2.h b/modules/l2/api/gr_l2.h index 071ec4090..18ef14dd3 100644 --- a/modules/l2/api/gr_l2.h +++ b/modules/l2/api/gr_l2.h @@ -15,10 +15,26 @@ // 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. + 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. +// 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"; + case GR_BRIDGE_F_NEIGH_SUPPRESS: + return "neigh_suppress"; + } + return "?"; +} + #define GR_BRIDGE_MAX_MEMBERS 64 #define GR_BRIDGE_DEFAULT_AGEING 300 @@ -67,6 +83,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 6a4c134e6..fcc9d22c5 100644 --- a/modules/l2/cli/bridge.c +++ b/modules/l2/cli/bridge.c @@ -12,19 +12,29 @@ #include #include +#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, - "%sflood %slearn", - (bridge->flags & GR_BRIDGE_F_NO_FLOOD) ? "no_" : "", - (bridge->flags & GR_BRIDGE_F_NO_LEARN) ? "no_" : "" + 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); @@ -37,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 %sflood %slearn", - bridge->n_members, - (bridge->flags & GR_BRIDGE_F_NO_FLOOD) ? "no_" : "", - (bridge->flags & GR_BRIDGE_F_NO_LEARN) ? "no_" : "" - ); + snprintf(buf, len, "members=%u %s", bridge->n_members, format_bridge_flags(bridge->flags)); } static struct cli_iface_type bridge_type = { @@ -64,18 +67,29 @@ 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; + } + 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; } @@ -98,6 +112,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 +122,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,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,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( \ @@ -154,20 +172,19 @@ 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")) \ + ), \ + with_help( \ + "Enable/disable ARP/NDP requests suppression.", \ + EC_NODE_OR( \ + "NEIGH_SUPPRESS", 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/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; } 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..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, @@ -58,8 +59,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; @@ -92,12 +92,13 @@ 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: - 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); @@ -132,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', 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_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 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