From 1366869a813ea1724fa40b99b591ccf46a8416db Mon Sep 17 00:00:00 2001 From: Andrea Panattoni Date: Fri, 26 Jun 2026 15:03:53 +0200 Subject: [PATCH 1/2] frr: add VXLAN interface redirect for IPv6 nexthops The IPv4 nexthop path redirects iface_id to the VXLAN tunnel when a remote RMAC is found, but the IPv6 path was missing this. Fixes: 45afc2b00cbf ("frr: only redirect to VXLAN for remote L3 nexthops") Signed-off-by: Andrea Panattoni Reviewed-by: Robin Jarry --- frr/rt_grout.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frr/rt_grout.c b/frr/rt_grout.c index 3fa719371..52a617e28 100644 --- a/frr/rt_grout.c +++ b/frr/rt_grout.c @@ -745,6 +745,8 @@ grout_add_nexthop(uint32_t nh_id, gr_nh_origin_t origin, const struct nexthop *n if (rmac != NULL) { memcpy(&l3->mac, rmac, sizeof(l3->mac)); l3->flags |= GR_NH_F_REMOTE; + if (vxlan_iface_id != GR_IFACE_ID_UNDEF) + req->nh.iface_id = vxlan_iface_id; } break; case NEXTHOP_TYPE_IFINDEX: From e9507171946542c84bd14699fd53e03a3c0039bd Mon Sep 17 00:00:00 2001 From: Andrea Panattoni Date: Thu, 25 Jun 2026 18:45:37 +0200 Subject: [PATCH 2/2] frr: fix EVPN type-5 forwarding for IPv6 prefixes EVPN type-5 routes for IPv6 prefixes use v4-mapped nexthop addresses (::ffff:X.X.X.X) when the VTEP uses IPv4. These failed for two reasons: grout_add_nexthop() looked up the RMAC cache with an IPADDR_V6 key but grout_neigh_update_ctx() stored it under IPADDR_V4, so the hash never matched; and the nexthop carried GR_AF_IP6, causing vxlan_output to look for an IPv6 underlay route that does not exist. Fix both by detecting v4-mapped addresses: extract the embedded IPv4, store the nexthop as GR_AF_IP4, and look up the RMAC with the matching IPADDR_V4 key. Add the VXLAN interface redirect that was missing from the IPv6 path. In ip6_output, derive the VTEP address family from l3->af instead of hardcoding GR_AF_IP6. Signed-off-by: Andrea Panattoni Reviewed-by: Robin Jarry --- frr/rt_grout.c | 21 +++++++-- modules/ip/datapath/ip_output.c | 7 ++- modules/ip6/datapath/ip6_output.c | 7 ++- smoke/evpn_l3vpn_frr_test.sh | 77 +++++++++++++++++++++++++++---- 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/frr/rt_grout.c b/frr/rt_grout.c index 52a617e28..82462a5ca 100644 --- a/frr/rt_grout.c +++ b/frr/rt_grout.c @@ -736,11 +736,24 @@ grout_add_nexthop(uint32_t nh_id, gr_nh_origin_t origin, const struct nexthop *n case NEXTHOP_TYPE_IPV6: case NEXTHOP_TYPE_IPV6_IFINDEX: l3 = (struct gr_nexthop_info_l3 *)req->nh.info; - l3->af = GR_AF_IP6; - memcpy(&l3->ipv6, &nh->gate.ipv6, sizeof(l3->ipv6)); + // EVPN type-5 IPv6 prefixes use v4-mapped nexthops + // (::ffff:X.X.X.X) because the VTEP is always IPv4. + // Store as native IPv4 so vxlan_output uses IPv4 + // encapsulation to reach the remote VTEP. + if (IS_MAPPED_IPV6(&nh->gate.ipv6)) { + struct in_addr v4; + ipv4_mapped_ipv6_to_ipv4(&nh->gate.ipv6, &v4); + l3->af = GR_AF_IP4; + l3->ipv4 = v4.s_addr; + vtep.ipa_type = IPADDR_V4; + vtep.ipaddr_v4 = v4; + } else { + l3->af = GR_AF_IP6; + memcpy(&l3->ipv6, &nh->gate.ipv6, sizeof(l3->ipv6)); + vtep.ipa_type = IPADDR_V6; + vtep.ipaddr_v6 = nh->gate.ipv6; + } // Apply cached RMAC from EVPN NEIGH install if available. - vtep.ipa_type = IPADDR_V6; - vtep.ipaddr_v6 = nh->gate.ipv6; rmac = l3vni_rmac_get(req->nh.vrf_id, &vtep); if (rmac != NULL) { memcpy(&l3->mac, rmac, sizeof(l3->mac)); diff --git a/modules/ip/datapath/ip_output.c b/modules/ip/datapath/ip_output.c index 78214edc7..9a3bcc24a 100644 --- a/modules/ip/datapath/ip_output.c +++ b/modules/ip/datapath/ip_output.c @@ -135,8 +135,11 @@ ip_output_process(struct rte_graph *graph, struct rte_node *node, void **objs, u eth_data->dst = l3->mac; eth_data->ether_type = RTE_BE16(RTE_ETHER_TYPE_IPV4); if (iface->type == GR_IFACE_TYPE_VXLAN) { - eth_data->vtep.af = GR_AF_IP4; - eth_data->vtep.ipv4 = l3->ipv4; + eth_data->vtep.af = l3->af; + if (l3->af == GR_AF_IP4) + eth_data->vtep.ipv4 = l3->ipv4; + else + eth_data->vtep.ipv6 = l3->ipv6; } else { eth_data->vtep.af = GR_AF_UNSPEC; } diff --git a/modules/ip6/datapath/ip6_output.c b/modules/ip6/datapath/ip6_output.c index 63137862c..c352bc30c 100644 --- a/modules/ip6/datapath/ip6_output.c +++ b/modules/ip6/datapath/ip6_output.c @@ -117,8 +117,11 @@ ip6_output_process(struct rte_graph *graph, struct rte_node *node, void **objs, eth_data->dst = l3->mac; eth_data->ether_type = RTE_BE16(RTE_ETHER_TYPE_IPV6); if (iface->type == GR_IFACE_TYPE_VXLAN) { - eth_data->vtep.af = GR_AF_IP6; - eth_data->vtep.ipv6 = l3->ipv6; + eth_data->vtep.af = l3->af; + if (l3->af == GR_AF_IP4) + eth_data->vtep.ipv4 = l3->ipv4; + else + eth_data->vtep.ipv6 = l3->ipv6; } else { eth_data->vtep.af = GR_AF_UNSPEC; } diff --git a/smoke/evpn_l3vpn_frr_test.sh b/smoke/evpn_l3vpn_frr_test.sh index aebd07f29..22423e0c3 100755 --- a/smoke/evpn_l3vpn_frr_test.sh +++ b/smoke/evpn_l3vpn_frr_test.sh @@ -7,11 +7,13 @@ # a standalone FRR+Linux peer. # # Each side has a VRF with an L3 VNI (1000) and a host connected to a local -# port. BGP EVPN advertises IP prefixes (type-5 routes) and RMAC entries -# (type-2 routes with GR_NH_F_REMOTE nexthops) across the VXLAN overlay. +# port. BGP EVPN advertises IPv4 and IPv6 prefixes (type-5 routes) and RMAC +# entries (type-2 routes with GR_NH_F_REMOTE nexthops) across the VXLAN overlay. +# IPv6 type-5 routes use v4-mapped IPv6 nexthops (::ffff:X.X.X.X) because the +# VTEP is always IPv4. # # Success criteria: -# - Both sides exchange EVPN type-5 routes (IP prefixes installed). +# - Both sides exchange EVPN type-5 routes (IPv4 and IPv6 prefixes installed). # - Host-A and Host-B can ping each other through the L3 VXLAN overlay. # - RMACs are installed as remote nexthops on the grout side. # @@ -37,9 +39,10 @@ # '--------|---------------|------' '----|--------------|---------' # | | | | # | | <------- BGP ----> | | -# 16.0.0.0/24 '---------------------' 48.0.0.0/24 -# | underlay | -# .-------|-----------. 172.16.0.0/24 .----------|--------. +# 16.0.0.0/24 '---------------------' 48.0.0.0/24 +# fd00:16::/64 underlay fd00:48::/64 +# | 172.16.0.0/24 | +# .-------|-----------. .----------|--------. # | +---+----+ | | +---+----+ | # | | x-p1 | | | | x-p1 | | # | +--------+ | <= = = = = = = = = = = = => | +--------+ | @@ -61,6 +64,7 @@ start_frr evpn-peer 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 +ip netns exec evpn-peer sysctl -qw net.ipv6.conf.all.forwarding=1 move_to_netns x-p0 evpn-peer ip -n evpn-peer addr add 172.16.0.1/24 dev x-p0 @@ -83,12 +87,15 @@ ip -n evpn-peer link add p1 type veth peer name x-p1 ip -n evpn-peer link set p1 master tenant ip -n evpn-peer link set p1 up ip -n evpn-peer addr add 16.0.0.1/24 dev p1 +ip -n evpn-peer addr add fd00:16::1/64 dev p1 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 16.0.0.2/24 dev x-p1 ip -n host-a route add default via 16.0.0.1 +ip -n host-a addr add fd00:16::2/64 dev x-p1 +ip -n host-a -6 route add default via fd00:16::1 # FRR config on the Linux peer vtysh -N evpn-peer <<-EOF @@ -117,8 +124,13 @@ router bgp 65000 vrf tenant redistribute connected exit-address-family + address-family ipv6 unicast + redistribute connected + exit-address-family + address-family l2vpn evpn advertise ipv4 unicast + advertise ipv6 unicast exit-address-family exit EOF @@ -131,12 +143,15 @@ grcli interface add vxlan vxlan-l3 vni 1000 local 172.16.0.2 vrf tenant create_interface p1 vrf tenant set_ip_address p1 48.0.0.1/24 +set_ip_address p1 fd00:48::1/64 netns_add host-b move_to_netns x-p1 host-b ip -n host-b addr add 48.0.0.2/24 dev x-p1 ip -n host-b addr add 10.0.0.1/24 dev lo ip -n host-b route add default via 48.0.0.1 +ip -n host-b addr add fd00:48::2/64 dev x-p1 +ip -n host-b -6 route add default via fd00:48::1 mark_events @@ -167,8 +182,13 @@ router bgp 65000 vrf tenant redistribute connected exit-address-family + address-family ipv6 unicast + redistribute connected + exit-address-family + address-family l2vpn evpn advertise ipv4 unicast + advertise ipv6 unicast exit-address-family exit EOF @@ -194,7 +214,7 @@ while ! vtysh -N evpn-peer -c "show evpn vni 1000" | grep -qF "L3"; do attempts=$((attempts + 1)) done -# -- Wait for EVPN type-5 route exchange --------------------------------------- +# -- Wait for EVPN type-5 route exchange (IPv4) -------------------------------- attempts=0 while ! vtysh -c "show bgp l2vpn evpn route type 5" | grep -qF "16.0.0.0"; do if [ "$attempts" -ge 5 ]; then @@ -217,8 +237,32 @@ while ! vtysh -N evpn-peer -c "show bgp l2vpn evpn route type 5" | grep -qF "48. attempts=$((attempts + 1)) done +# -- Wait for EVPN type-5 route exchange (IPv6) -------------------------------- +attempts=0 +while ! vtysh -c "show bgp l2vpn evpn route type 5" | grep -qF "fd00:16::"; do + if [ "$attempts" -ge 5 ]; then + vtysh -c "show bgp l2vpn evpn route type 5" + fail "Grout FRR did not learn type-5 route for fd00:16::/64" + fi + sleep 1 + attempts=$((attempts + 1)) +done + +attempts=0 +while ! vtysh -N evpn-peer -c "show bgp l2vpn evpn route type 5" | grep -qF "fd00:48::"; do + if [ "$attempts" -ge 5 ]; then + vtysh -c "show bgp vrf tenant ipv6 unicast" + vtysh -c "show bgp l2vpn evpn route" + vtysh -N evpn-peer -c "show bgp l2vpn evpn route type 5" + fail "Linux peer did not learn type-5 route for fd00:48::/64" + fi + sleep 1 + attempts=$((attempts + 1)) +done + # -- Wait for routes to be installed in VRF ------------------------------------ wait_event 'route4 add: vrf=tenant 16.0.0.0/24' +wait_event 'route6 add: vrf=tenant fd00:16::/64' attempts=0 while ! ip -n evpn-peer route show vrf tenant | grep -qF "48.0.0.0/24"; do @@ -230,7 +274,17 @@ while ! ip -n evpn-peer route show vrf tenant | grep -qF "48.0.0.0/24"; do attempts=$((attempts + 1)) done -# -- Check RMAC is set on the route nexthop ------------------------------------ +attempts=0 +while ! ip -n evpn-peer -6 route show vrf tenant | grep -qF "fd00:48::"; do + if [ "$attempts" -ge 5 ]; then + ip -n evpn-peer -6 route show vrf tenant + fail "Route fd00:48::/64 not installed in peer VRF tenant" + fi + sleep 1 + attempts=$((attempts + 1)) +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" @@ -239,13 +293,16 @@ vtysh -c "show bgp l2vpn evpn route type 5" grcli route show vrf tenant grcli nexthop show vrf tenant -# -- Verify L3 connectivity through VXLAN overlay ------------------------------ +# -- Verify L3 connectivity through VXLAN overlay (IPv4) ----------------------- ip netns exec host-b ping -i0.1 -c3 -W1 16.0.0.2 ip netns exec host-a ping -i0.1 -c3 -W1 48.0.0.2 +# -- Verify L3 connectivity through VXLAN overlay (IPv6) ----------------------- +ip netns exec host-b ping -6 -i0.1 -c3 -W1 fd00:16::2 +ip netns exec host-a ping -6 -i0.1 -c3 -W1 fd00:48::2 + # -- Verify local nexthop uses port, not VXLAN --------------------------------- # Route to 10.0.0.0/24 (behind host-b) via local gateway 48.0.0.2. The nexthop # for 48.0.0.2 must use port p1, not the VXLAN interface. set_ip_route 10.0.0.0/24 48.0.0.2 tenant - grcli ping 10.0.0.1 vrf tenant count 3 delay 10