From 3c3f554cdbd7f7c04d5716cba965322e2fcfaca6 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Fri, 10 Jul 2026 22:41:10 +0200 Subject: [PATCH 1/4] srv6: add per-nexthop encap source address Allow SRv6 output nexthops to carry an explicit encapsulation source address. When set, the datapath uses it directly as the outer IPv6 source instead of the global tunnel source or the interface preferred address. An internal SR_ENCAP_F_SRC flag avoids a 16-byte address comparison in the datapath hot path. Signed-off-by: Robin Jarry Reviewed-by: Maxime Leroy --- api/gr_api.h | 2 +- api/gr_net_compat.h | 8 ++++++++ modules/srv6/api/gr_srv6.h | 1 + modules/srv6/cli/route.c | 13 +++++++++++-- modules/srv6/control/route.c | 6 ++++++ modules/srv6/control/srv6.h | 6 ++++++ modules/srv6/datapath/srv6_output.c | 18 ++++++++++-------- 7 files changed, 43 insertions(+), 11 deletions(-) diff --git a/api/gr_api.h b/api/gr_api.h index 0713d7bf9..eaa8c80e1 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 3 +#define GR_API_VERSION 4 // API request header. struct gr_api_request { diff --git a/api/gr_net_compat.h b/api/gr_net_compat.h index bcd5b1e35..2b3d95864 100644 --- a/api/gr_net_compat.h +++ b/api/gr_net_compat.h @@ -15,6 +15,14 @@ struct rte_ipv6_addr { uint8_t a[RTE_IPV6_ADDR_SIZE]; }; +static inline bool rte_ipv6_addr_is_unspec(const struct rte_ipv6_addr *ip) { + for (unsigned i = 0; i < RTE_IPV6_ADDR_SIZE; i++) { + if (ip->a[i] != 0) + return false; + } + return true; +} + static inline void rte_ipv6_addr_mask(struct rte_ipv6_addr *ip, uint8_t depth) { if (depth < RTE_IPV6_MAX_DEPTH) { unsigned int d = depth / CHAR_BIT; diff --git a/modules/srv6/api/gr_srv6.h b/modules/srv6/api/gr_srv6.h index c64dcf44e..3e0bd5b7d 100644 --- a/modules/srv6/api/gr_srv6.h +++ b/modules/srv6/api/gr_srv6.h @@ -27,6 +27,7 @@ typedef enum : uint8_t { // Used with GR_NH_T_SR6_OUTPUT nexthops via GR_NH_ADD from gr_infra.h. struct gr_nexthop_info_srv6 { gr_srv6_encap_behavior_t encap_behavior; + struct rte_ipv6_addr encap_src; uint8_t n_seglist; struct rte_ipv6_addr seglist[]; }; diff --git a/modules/srv6/cli/route.c b/modules/srv6/cli/route.c index cf7e7f1ab..a052ce3e0 100644 --- a/modules/srv6/cli/route.c +++ b/modules/srv6/cli/route.c @@ -60,6 +60,9 @@ static cmd_status_t srv6_nh_add(struct gr_api_client *c, const struct ec_pnode * else sr6->encap_behavior = SR_H_ENCAPS; + if (arg_ip6(p, "SRC", &sr6->encap_src) < 0 && errno != ENOENT) + goto out; + if (gr_api_client_send_recv(c, GR_NH_ADD, len, req, NULL) < 0) goto out; @@ -108,6 +111,7 @@ static cmd_status_t srv6_tunsrc_show(struct gr_api_client *c, const struct ec_pn static void add_columns_srv6(struct gr_table *table) { gr_table_column(table, "ENCAP", GR_DISP_LEFT); + gr_table_column(table, "ENCAP_SRC", GR_DISP_LEFT); gr_table_column(table, "SEGLIST", GR_DISP_STR_ARRAY); } @@ -122,6 +126,8 @@ static void fill_table_srv6(struct gr_table *table, unsigned start_col, const vo "%s", sr6->encap_behavior == SR_H_ENCAPS_RED ? "h.encaps.red" : "h.encaps" ); + if (!rte_ipv6_addr_is_unspec(&sr6->encap_src)) + gr_table_cell(table, start_col + 1, IP6_F, &sr6->encap_src); for (unsigned i = 0; i < sr6->n_seglist; i++) { SAFE_BUF(snprintf, sizeof(buf), "%s" IP6_F, i > 0 ? " " : "", &sr6->seglist[i]); if (sizeof(buf) - n < 50) { @@ -131,7 +137,7 @@ static void fill_table_srv6(struct gr_table *table, unsigned start_col, const vo } err: if (n > 0) - gr_table_cell(table, start_col + 1, "%s", buf); + gr_table_cell(table, start_col + 2, "%s", buf); } static void fill_object_srv6(struct gr_object *o, const void *info) { @@ -144,6 +150,8 @@ static void fill_object_srv6(struct gr_object *o, const void *info) { "%s", sr6->encap_behavior == SR_H_ENCAPS_RED ? "h.encaps.red" : "h.encaps" ); + if (!rte_ipv6_addr_is_unspec(&sr6->encap_src)) + gr_object_field(o, "encap_src", 0, IP6_F, &sr6->encap_src); gr_object_array_open(o, "seglist"); for (unsigned i = 0; i < sr6->n_seglist; i++) gr_object_array_item(o, 0, IP6_F, &sr6->seglist[i]); @@ -165,12 +173,13 @@ static int ctx_init(struct ec_node *root) { ret = CLI_COMMAND( NEXTHOP_ADD_CTX(root), - "srv6 seglist SEGLIST+ [(encap h.encaps|h.encaps.red),(vrf VRF),(id ID)]", + "srv6 seglist SEGLIST+ [(encap h.encaps|h.encaps.red),(src SRC),(vrf VRF),(id ID)]", srv6_nh_add, "Add SRv6 encap nexthop.", with_help("Encaps.", ec_node_str("h.encaps", "h.encaps")), with_help("Encaps Reduced.", ec_node_str("h.encaps.red", "h.encaps.red")), with_help("Next SID to visit.", ec_node_re("SEGLIST", IPV6_RE)), + with_help("Encap source address.", ec_node_re("SRC", IPV6_RE)), with_help("Nexthop ID.", ec_node_uint("ID", 1, UINT32_MAX - 1, 10)), with_help("L3 routing domain name.", ec_node_dyn("VRF", complete_vrf_names, NULL)) ); diff --git a/modules/srv6/control/route.c b/modules/srv6/control/route.c index a0ae585c9..38c44f386 100644 --- a/modules/srv6/control/route.c +++ b/modules/srv6/control/route.c @@ -17,6 +17,9 @@ static bool srv6_output_nh_equal(const struct nexthop *a, const struct nexthop * if (ad->encap != bd->encap) return false; + if (!rte_ipv6_addr_eq(&ad->encap_src, &bd->encap_src)) + return false; + if (ad->n_seglist != bd->n_seglist) return false; @@ -40,6 +43,8 @@ static int srv6_output_nh_import_info(struct nexthop *nh, const void *info) { memcpy(seglist, pub->seglist, sizeof(*seglist) * pub->n_seglist); priv->encap = pub->encap_behavior; + priv->encap_src = pub->encap_src; + priv->flags = rte_ipv6_addr_is_unspec(&pub->encap_src) ? 0 : SR_ENCAP_F_SRC; priv->n_seglist = pub->n_seglist; tmp = priv->seglist; priv->seglist = seglist; @@ -63,6 +68,7 @@ static struct gr_nexthop *srv6_output_nh_to_api(const struct nexthop *nh, size_t sr6_pub = (struct gr_nexthop_info_srv6 *)pub->info; sr6_pub->encap_behavior = sr6_priv->encap; + sr6_pub->encap_src = sr6_priv->encap_src; sr6_pub->n_seglist = sr6_priv->n_seglist; memcpy(sr6_pub->seglist, sr6_priv->seglist, diff --git a/modules/srv6/control/srv6.h b/modules/srv6/control/srv6.h index 32ada5358..1c96eb84a 100644 --- a/modules/srv6/control/srv6.h +++ b/modules/srv6/control/srv6.h @@ -13,14 +13,20 @@ // GR_NH_TYPE_INFO(GR_NH_T_SR6_LOCAL, nexthop_info_srv6_local, { BASE(gr_nexthop_info_srv6_local); }); +typedef enum : uint8_t { + SR_ENCAP_F_SRC = GR_BIT8(0), +} sr_encap_flags_t; + // // srv6 encap data is allocated dynamically. // A pointer to it is stored in nexthop priv. // GR_NH_TYPE_INFO(GR_NH_T_SR6_OUTPUT, nexthop_info_srv6_output, { gr_srv6_encap_behavior_t encap; + sr_encap_flags_t flags; uint16_t n_seglist; struct rte_ipv6_addr *seglist; + struct rte_ipv6_addr encap_src; }); extern struct nexthop *tunsrc_nh; diff --git a/modules/srv6/datapath/srv6_output.c b/modules/srv6/datapath/srv6_output.c index 5789892d1..3d40b94c9 100644 --- a/modules/srv6/datapath/srv6_output.c +++ b/modules/srv6/datapath/srv6_output.c @@ -137,15 +137,17 @@ srv6_output_process(struct rte_graph *graph, struct rte_node *node, void **objs, } l3_mbuf_data(m)->nh = nh; - nh = sr_tunsrc_get(nh->iface_id, &d->seglist[0]); - if (nh == NULL) { - // cannot output packet on interface that does not have ip6 addr - edge = NO_ROUTE; - goto next; + if (d->flags & SR_ENCAP_F_SRC) { + ip6_set_fields(outer_ip6, plen, proto, &d->encap_src, &d->seglist[0]); + } else { + nh = sr_tunsrc_get(nh->iface_id, &d->seglist[0]); + if (nh == NULL) { + edge = NO_ROUTE; + goto next; + } + l3 = nexthop_info_l3(nh); + ip6_set_fields(outer_ip6, plen, proto, &l3->ipv6, &d->seglist[0]); } - l3 = nexthop_info_l3(nh); - - ip6_set_fields(outer_ip6, plen, proto, &l3->ipv6, &d->seglist[0]); edge = IP6_OUTPUT; next: From 12446ca9c7ba603979c23d6a4c8b2a2b96792ad0 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Fri, 10 Jul 2026 22:44:09 +0200 Subject: [PATCH 2/4] frr: sync per-nexthop SRv6 encap source with FRR 10.8 FRR 10.8 added a per-nexthop encap source address to nexthop_add_srv6_seg6(). Forward the encap_src field from grout SRv6 output nexthops to FRR, and copy it back when importing nexthops from FRR. Signed-off-by: Robin Jarry Reviewed-by: Maxime Leroy --- frr/rt_grout.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frr/rt_grout.c b/frr/rt_grout.c index 600afa8bf..becc18ded 100644 --- a/frr/rt_grout.c +++ b/frr/rt_grout.c @@ -306,7 +306,7 @@ static int grout_gr_nexthop_to_frr_nexthop( encap_behavior #if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 8, 0) , - NULL + rte_ipv6_addr_is_unspec(&sr6->encap_src) ? NULL : (void *)&sr6->encap_src #endif ); nh->type = nh->ifindex ? NEXTHOP_TYPE_IPV6_IFINDEX : NEXTHOP_TYPE_IPV6; @@ -850,6 +850,11 @@ grout_add_nexthop(uint32_t nh_id, gr_nh_origin_t origin, const struct nexthop *n memcpy(&sr6->seglist[i], &nh->nh_srv6->seg6_segs->seg[i], sizeof(sr6->seglist[i])); +#if CURRENT_FRR_VERSION >= MAKE_FRRVERSION(10, 8, 0) + memcpy(&sr6->encap_src, + &nh->nh_srv6->seg6_segs->encap_source, + sizeof(sr6->encap_src)); +#endif break; case GR_NH_T_BLACKHOLE: From c0553750eceebe1fc5cc34264418cf6cce23b0ae Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Fri, 10 Jul 2026 22:45:02 +0200 Subject: [PATCH 3/4] smoke: test per-nexthop SRv6 encap source address Add a test case that creates an SRv6 output nexthop with an explicit source address and verifies the encapsulated packets use it as the outer IPv6 source with tcpdump. Signed-off-by: Robin Jarry Reviewed-by: Maxime Leroy --- smoke/srv6_test.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/smoke/srv6_test.sh b/smoke/srv6_test.sh index 24b1b3696..26dd3b1ec 100755 --- a/smoke/srv6_test.sh +++ b/smoke/srv6_test.sh @@ -84,3 +84,27 @@ ip -n n1 route replace 192.168.61.0/24 encap seg6 mode encap segs fd00:202:0300: # test: ping goes through the NEXT-CSID transit node ip netns exec n0 ping -i0.01 -c3 -n 192.168.60.1 + +# +# Per-nexthop encap source test +# +# Create a new SRv6 output nexthop with an explicit source address and +# verify the encapsulated packets use it as the outer IPv6 source. +# + +encap_src=fd00:102::42 +grcli address add $encap_src/128 iface p1 +grcli nexthop add srv6 seglist fd00:202:200:: src $encap_src id 43 +grcli route add 192.168.0.0/16 via id 43 + +# capture a few packets on x-p1 in n1 +ip netns exec n1 timeout 5 tcpdump -c1 -pnn -l \ + "ip6 src $encap_src" -i x-p1 > $tmp/tcpdump.out 2>&1 & +tcpdump_pid=$! +sleep 1 + +ip netns exec n0 ping -i0.01 -c3 -n 192.168.60.1 +wait $tcpdump_pid || true + +grep -q "$encap_src" $tmp/tcpdump.out \ + || fail "encapsulated packet did not use per-nexthop encap_src $encap_src" From fda82158c01deeee3fe8f58f4cb228d9e57319ee Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Fri, 10 Jul 2026 22:46:24 +0200 Subject: [PATCH 4/4] smoke: add SRv6 per-nexthop encap source FRR test Test that FRR 10.8 per-nexthop encap source addresses are synced through the dplane plugin. The test configures an SRv6 route with encap-source in FRR and verifies grout encapsulates packets with the correct outer IPv6 source address. Requires FRR >= 10.8, skipped on older versions. Signed-off-by: Robin Jarry Reviewed-by: Maxime Leroy --- smoke/_init_frr.sh | 9 +++- smoke/srv6_encapsrc_frr_test.sh | 76 +++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100755 smoke/srv6_encapsrc_frr_test.sh diff --git a/smoke/_init_frr.sh b/smoke/_init_frr.sh index 23f3e237e..341bebac6 100644 --- a/smoke/_init_frr.sh +++ b/smoke/_init_frr.sh @@ -170,7 +170,7 @@ set_srv6_localsid() { exit" } -# set_srv6_route [--persist] +# set_srv6_route [--persist] [--encap-src ] # [ []] [sid2 sid3 ...] # # EXAMPLES @@ -185,6 +185,8 @@ exit" set_srv6_route() { local persist=0 [ "$1" = "--persist" ] && { persist=1; shift; } + local encap_src="" + [ "$1" = "--encap-src" ] && { encap_src=$2; shift 2; } local prefix="$1" local nhop="$2" shift 2 @@ -232,9 +234,12 @@ set_srv6_route() { local nh_vrf_clause="" [ -n "$nexthop_vrf_name" ] && nh_vrf_clause=" nexthop-vrf ${nexthop_vrf_name}" + local encap_src_clause="" + [ -n "$encap_src" ] && encap_src_clause=" encap-source ${encap_src}" + _apply_frr_config "$persist" \ "$route add: vrf=$gr_vrf_name $prefix origin=zebra_static via type=SRv6 .*${sids[0]}" \ - "${frr_ip} route ${prefix} ${nhop} segments ${seg_frr} vrf ${vrf_name}${nh_vrf_clause}" + "${frr_ip} route ${prefix} ${nhop} segments ${seg_frr}${encap_src_clause} vrf ${vrf_name}${nh_vrf_clause}" } # kill_frr_daemons [...] diff --git a/smoke/srv6_encapsrc_frr_test.sh b/smoke/srv6_encapsrc_frr_test.sh new file mode 100755 index 000000000..0a156833f --- /dev/null +++ b/smoke/srv6_encapsrc_frr_test.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Robin Jarry + +set -e +zebra=$(PATH="$1/frr_install/sbin:$1/frr_install/bin:$PATH" command -v zebra) +frr_version=$($zebra --version | sed -En 's/zebra version //p') +min_version=$(printf '%s\n%s\n' "$frr_version" "10.8.0" | sort -V | head -n1) +if ! [ "$min_version" = "10.8.0" ]; then + echo "$0: FRR $frr_version does not support per-nexthop encap source" + exit 125 +fi + +. $(dirname $0)/_init_frr.sh + +create_interface p0 +create_interface p1 + +for n in 0 1; do + p=x-p$n + ns=n$n + netns_add $ns + move_to_netns $p $ns +done +ip -n n0 addr add 192.168.61.2/24 dev x-p0 +ip -n n1 addr add fd00:102::2/32 dev x-p1 + +set_ip_address p0 192.168.61.1/24 +set_ip_address p1 fd00:102::1/32 + +# +# network layout: +# (client) p0(netns) <--> p0 p1 <---> p1(netns) +# ipv4 ---------------| srv6 |-- ipv4 +# +# test case: +# - configure an SRv6 route with an explicit encap source via FRR +# - verify grout uses the per-nexthop source instead of the global one +# + +# only linux's p1 will see srv6 +ip netns exec n1 sysctl -w net.ipv6.conf.x-p1.seg6_enabled=1 +ip netns exec n1 sysctl -w net.ipv6.conf.x-p1.forwarding=1 + +# client default route +ip -n n0 route add default via 192.168.61.1 dev x-p0 + +# linux decap and reply network +ip -n n1 -6 route add fd00:202:200:: \ + encap seg6local action End.DX4 nh4 192.168.60.1 count dev x-p1 +ip -n n1 addr add 192.168.60.1/24 dev x-p1 +ip -n n1 route add 192.168.61.0/24 \ + encap seg6 mode encap segs fd00:202:100:: dev x-p1 +ip -n n1 -6 route add fd00:202::/32 via fd00:102::1 dev x-p1 + +# grout decap localsid +set_srv6_localsid locator_grout fd00:202 fd00:202:100:: + +# underlay route +set_ip_route fd00:202::/32 fd00:102::2 + +# SRv6 route with explicit per-nexthop encap source via FRR +encap_src=fd00:102::42 +set_srv6_route --encap-src $encap_src 192.168.0.0/16 p1 fd00:202:200:: + +# capture and verify the outer source address +ip netns exec n1 timeout 5 tcpdump -c1 -nn -l \ + "ip6 src $encap_src" -i x-p1 > $tmp/tcpdump.out 2>&1 & +tcpdump_pid=$! +sleep 1 + +ip netns exec n0 ping -i0.01 -c3 -n 192.168.60.1 +wait $tcpdump_pid || true + +grep -q "$encap_src" $tmp/tcpdump.out \ + || fail "encapsulated packet did not use per-nexthop encap_src $encap_src"