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/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: 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: 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" 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"