From 00dacf47625c77d87b4407aba9efa510f9265f34 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 18 Feb 2026 22:26:45 +0100 Subject: [PATCH 1/4] srv6: add compressed SID support RFC 9800 defines two flavors for compressed SRv6 segment lists: NEXT-CSID and REPLACE-CSID. This adds support for NEXT-CSID, which allows packing multiple compressed SIDs into a single 128-bit IPv6 address (a "CSID container"). Each transit node consumes its CSID by shifting the remaining ones left within the container and performing a new FIB lookup on the result. When the container is exhausted (all remaining bytes are zero), standard End processing takes over. This is needed because FRR staticd programs SRv6 local SIDs using CSID behaviors by default (uN, uDT4, uDT6, uDT46), and grout must implement the shift-and-lookup algorithm to act as a transit node. The CSID container layout is governed by two parameters: block_bits (locator-block length, default 32) and csid_bits (compressed SID length, default 16). These match FRR defaults and can be overridden via the CLI for non-standard deployments. Both must be multiples of 8 and their sum must not exceed 128. For decapsulation behaviors (End.DT4/DT6/DT46), the NEXT-CSID flag is accepted but requires no datapath change since the outer header is removed entirely. Similarly, no change is required at the encap side since CSID containers are regular IPv6 addresses from the encapsulator's perspective. Link: https://www.rfc-editor.org/rfc/rfc8986#section-3.1 Link: https://www.rfc-editor.org/rfc/rfc9800 Link: https://www.rfc-editor.org/rfc/rfc9800#section-4.1.1 Signed-off-by: Robin Jarry Reviewed-by: Christophe Fontaine --- modules/srv6/api/gr_srv6.h | 3 ++ modules/srv6/cli/localsid.c | 80 +++++++++++++++++++++++++----- modules/srv6/control/localsid.c | 18 ++++++- modules/srv6/datapath/srv6_local.c | 60 ++++++++++++++++++++++ 4 files changed, 148 insertions(+), 13 deletions(-) diff --git a/modules/srv6/api/gr_srv6.h b/modules/srv6/api/gr_srv6.h index 86e86b1d5..224bfccfb 100644 --- a/modules/srv6/api/gr_srv6.h +++ b/modules/srv6/api/gr_srv6.h @@ -85,6 +85,7 @@ static inline const char *gr_srv6_behavior_name(gr_srv6_behavior_t b) { typedef enum : uint8_t { GR_SR_FL_FLAVOR_PSP = GR_BIT8(0), // Penultimate Segment Popping. GR_SR_FL_FLAVOR_USD = GR_BIT8(1), // Ultimate Segment Decapsulation. + GR_SR_FL_FLAVOR_NEXT_CSID = GR_BIT8(2), // Compressed SID (RFC 9800). } gr_srv6_flags_t; // SRv6 local nexthop information for Local SID processing. @@ -93,4 +94,6 @@ struct gr_nexthop_info_srv6_local { uint16_t out_vrf_id; gr_srv6_behavior_t behavior; gr_srv6_flags_t flags; + uint8_t block_bits; // Locator-block length in bits (default 32). + uint8_t csid_bits; // Compressed SID length in bits (default 16). }; diff --git a/modules/srv6/cli/localsid.c b/modules/srv6/cli/localsid.c index 20867d67e..cec6ab626 100644 --- a/modules/srv6/cli/localsid.c +++ b/modules/srv6/cli/localsid.c @@ -74,9 +74,18 @@ static cmd_status_t srv6_localsid_add(struct gr_api_client *c, const struct ec_p sr6->flags |= GR_SR_FL_FLAVOR_PSP; if (!strcmp(str, "usd")) sr6->flags |= GR_SR_FL_FLAVOR_USD; + if (!strcmp(str, "next-csid")) + sr6->flags |= GR_SR_FL_FLAVOR_NEXT_CSID; } } + if (sr6->flags & GR_SR_FL_FLAVOR_NEXT_CSID) { + if (arg_u8(p, "BLOCK_BITS", &sr6->block_bits) < 0 && errno != ENOENT) + goto out; + if (arg_u8(p, "CSID_BITS", &sr6->csid_bits) < 0 && errno != ENOENT) + goto out; + } + n = ec_pnode_find(p, "BEHAVIOR"); if (n == NULL || ec_pnode_len(n) < 1) goto out; @@ -97,25 +106,54 @@ static cmd_status_t srv6_localsid_add(struct gr_api_client *c, const struct ec_p static ssize_t format_nexthop_info_srv6_local(char *buf, size_t len, const void *info) { const struct gr_nexthop_info_srv6_local *sr6 = info; - ssize_t n = 0; - char vrf[64]; + char flavors[64], vrf[64]; + ssize_t n = 0, f = 0; SAFE_BUF(snprintf, len, "behavior=%s", gr_srv6_behavior_name(sr6->behavior)); vrf[0] = 0; if (sr6->out_vrf_id != GR_VRF_ID_UNDEF) snprintf(vrf, sizeof(vrf), "out_vrf=%d", sr6->out_vrf_id); + flavors[0] = 0; + if (sr6->flags & GR_SR_FL_FLAVOR_PSP) + f += snprintf(flavors + f, sizeof(flavors) - f, "psp,"); + if (sr6->flags & GR_SR_FL_FLAVOR_USD) + f += snprintf(flavors + f, sizeof(flavors) - f, "usd,"); + if (sr6->flags & GR_SR_FL_FLAVOR_NEXT_CSID) + f += snprintf(flavors + f, sizeof(flavors) - f, "next-csid,"); + if (f > 0) + flavors[f - 1] = 0; // trim trailing comma + switch (sr6->behavior) { case SR_BEHAVIOR_END: - SAFE_BUF(snprintf, len, " flavor=%#02x", sr6->flags); - break; case SR_BEHAVIOR_END_T: - SAFE_BUF(snprintf, len, " flavor=%#02x %s", sr6->flags, vrf); + SAFE_BUF(snprintf, len, " flavor=%s", flavors[0] ? flavors : "none"); + if (sr6->flags & GR_SR_FL_FLAVOR_NEXT_CSID) + SAFE_BUF( + snprintf, + len, + " block-bits=%u csid-bits=%u", + sr6->block_bits, + sr6->csid_bits + ); + if (vrf[0]) + SAFE_BUF(snprintf, len, " %s", vrf); break; case SR_BEHAVIOR_END_DT6: case SR_BEHAVIOR_END_DT4: case SR_BEHAVIOR_END_DT46: - SAFE_BUF(snprintf, len, " %s", vrf); + if (flavors[0]) + SAFE_BUF(snprintf, len, " flavor=%s", flavors); + if (sr6->flags & GR_SR_FL_FLAVOR_NEXT_CSID) + SAFE_BUF( + snprintf, + len, + " block-bits=%u csid-bits=%u", + sr6->block_bits, + sr6->csid_bits + ); + if (vrf[0]) + SAFE_BUF(snprintf, len, " %s", vrf); break; } return n; @@ -135,9 +173,10 @@ static int ctx_init(struct ec_node *root) { flavor_node = EC_NODE_CMD( "FLAVORS", - "(psp,usd)", + "(psp,usd,next-csid)", with_help("Penultimate Segment Pop of the SRH", ec_node_str("psp", "psp")), - with_help("Ultimate Segment Decapsulation of the SRH", ec_node_str("usd", "usd")) + with_help("Ultimate Segment Decapsulation of the SRH", ec_node_str("usd", "usd")), + with_help("NEXT-CSID uSID flavor (RFC 9800)", ec_node_str("next-csid", "next-csid")) ); if (flavor_node == NULL) return -1; @@ -145,13 +184,22 @@ static int ctx_init(struct ec_node *root) { "BEHAVIOR", EC_NODE_CMD( EC_NO_ID, - "end [flavor FLAVORS]", + "end [(flavor FLAVORS),(block-bits BLOCK_BITS),(csid-bits CSID_BITS)]", with_help("Transit endpoint.", ec_node_str("end", "end")), - with_help("Endpoint flavor(s).", ec_node_clone(flavor_node)) + with_help("Endpoint flavor(s).", ec_node_clone(flavor_node)), + with_help( + "Locator-block length in bits.", + ec_node_uint("BLOCK_BITS", 8, 120, 10) + ), + with_help( + "Compressed SID length in bits.", + ec_node_uint("CSID_BITS", 8, 64, 10) + ) ), EC_NODE_CMD( EC_NO_ID, - "end.t [flavor FLAVORS] table TABLE", + "end.t [(flavor FLAVORS),(block-bits BLOCK_BITS),(csid-bits CSID_BITS)]" + " table TABLE", with_help( "L3 routing domain name.", ec_node_dyn("TABLE", complete_vrf_names, NULL) @@ -160,7 +208,15 @@ static int ctx_init(struct ec_node *root) { "Transit endpoint with specific IPv6 table lookup.", ec_node_str("end.t", "end.t") ), - with_help("Endpoint flavor(s).", flavor_node) + with_help("Endpoint flavor(s).", flavor_node), + with_help( + "Locator-block length in bits.", + ec_node_uint("BLOCK_BITS", 8, 120, 10) + ), + with_help( + "Compressed SID length in bits.", + ec_node_uint("CSID_BITS", 8, 64, 10) + ) ), EC_NODE_CMD( EC_NO_ID, diff --git a/modules/srv6/control/localsid.c b/modules/srv6/control/localsid.c index a7d36729d..0e26abcdb 100644 --- a/modules/srv6/control/localsid.c +++ b/modules/srv6/control/localsid.c @@ -14,14 +14,30 @@ static bool srv6_local_nh_equal(const struct nexthop *a, const struct nexthop *b struct nexthop_info_srv6_local *bd = nexthop_info_srv6_local(b); return ad->behavior == bd->behavior && ad->out_vrf_id == bd->out_vrf_id - && ad->flags == bd->flags; + && ad->flags == bd->flags && ad->block_bits == bd->block_bits + && ad->csid_bits == bd->csid_bits; } static int srv6_local_nh_import_info(struct nexthop *nh, const void *info) { struct nexthop_info_srv6_local *priv = nexthop_info_srv6_local(nh); const struct gr_nexthop_info_srv6_local *pub = info; + uint8_t block_bits = pub->block_bits; + uint8_t csid_bits = pub->csid_bits; + + if (pub->flags & GR_SR_FL_FLAVOR_NEXT_CSID) { + if (block_bits == 0) + block_bits = 32; + if (priv->csid_bits == 0) + csid_bits = 16; + if (block_bits % CHAR_BIT != 0 || csid_bits % CHAR_BIT != 0) + return errno_set(EDOM); + if (block_bits + csid_bits > RTE_IPV6_MAX_DEPTH) + return errno_set(ERANGE); + } priv->base = *pub; + priv->block_bits = block_bits; + priv->csid_bits = csid_bits; return 0; } diff --git a/modules/srv6/datapath/srv6_local.c b/modules/srv6/datapath/srv6_local.c index 86060af6e..94476cfe3 100644 --- a/modules/srv6/datapath/srv6_local.c +++ b/modules/srv6/datapath/srv6_local.c @@ -279,6 +279,49 @@ static int process_behav_decap( return edge; } +// +// Compressed SID shift-and-lookup (RFC 9800, Section 4.1.1). +// +// A CSID container packs multiple compressed SIDs after the locator block: +// +// |<-- block -->|<-- csid -->|<--- argument (remaining csids) --->| +// 0 ^ ^ 15 +// block_end arg_off +// +// If the argument portion is non-zero, shift it left into the active CSID +// position and zero the vacated tail. This exposes the next CSID in the +// container as the new destination for FIB lookup. E.g.: +// +// Before: fd00:0202 : 0300 : 0100 : 0000 : 0000 : 0000 : 0000 +// block csid ~~~~ argument (next csid) ~~~~~~~ +// +// After: fd00:0202 : 0100 : 0000 : 0000 : 0000 : 0000 : 0000 +// block csid ~~~~~~~~ zeroed tail ~~~~~~~~~~~~ +// +// Returns true if the shift was performed, false if the container is exhausted +// (argument is all zeros, fall through to standard End). +// +static inline bool csid_shift(struct rte_ipv6_addr *da, uint8_t block_bits, uint8_t csid_bits) { + uint8_t block_end = block_bits / CHAR_BIT; + uint8_t csid_len = csid_bits / CHAR_BIT; + uint8_t arg_off = block_end + csid_len; + uint8_t arg = 0; + + assert(arg_off <= ARRAY_DIM(da->a)); + + for (uint8_t i = arg_off; i < ARRAY_DIM(da->a); i++) + arg |= da->a[i]; + if (arg == 0) + return false; // argument portion is all zeros + + // shift argument left into the active CSID position + memmove(&da->a[block_end], &da->a[arg_off], ARRAY_DIM(da->a) - arg_off); + // zero the vacated tail + memset(&da->a[ARRAY_DIM(da->a) - csid_len], 0, csid_len); + + return true; +} + // // End behavior // @@ -290,6 +333,23 @@ static int process_behav_end( struct rte_ipv6_routing_ext *sr = ip6_info->sr; const struct iface *iface; + // NEXT-CSID processing (RFC 9800) + if (sr_d->flags & GR_SR_FL_FLAVOR_NEXT_CSID) { + struct rte_ipv6_hdr *ip6 = ip6_info->ip6_hdr; + + if (csid_shift(&ip6->dst_addr, sr_d->block_bits, sr_d->csid_bits)) { + if (sr_d->out_vrf_id != GR_VRF_ID_UNDEF) { + iface = get_vrf_iface(sr_d->out_vrf_id); + if (iface == NULL) + return DEST_UNREACH; + mbuf_data(m)->iface = iface; + } + eth_input_mbuf_data(m)->domain = ETH_DOMAIN_LOCAL; + return IP6_INPUT; + } + // container exhausted, fall through to standard End + } + // at the end of the tunnel if (sr == NULL || sr->segments_left == 0) { // 4.16.3 USD From 8bedd0fed2d8adff541f072e3405e642b86d57a2 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 18 Feb 2026 22:27:50 +0100 Subject: [PATCH 2/4] smoke: add compressed CSID transit check to srv6_test Add a NEXT-CSID transit test case to the non-FRR SRv6 smoke test. A local SID with "end flavor next-csid" is installed at fd00:202:300::/48 (CSID 0x0300). The linux peer sends packets to the CSID container fd00:202:0300:0100:: which packs two 16-bit CSIDs after the 32-bit locator-block. Grout matches the first CSID, shifts the destination address to fd00:202:0100:: (= fd00:202:100::), and the second FIB lookup hits the existing End.DT4 endpoint which decapsulates the packet. This exercises the csid_shift() datapath function end-to-end without requiring FRR. Signed-off-by: Robin Jarry Reviewed-by: Christophe Fontaine --- smoke/srv6_test.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/smoke/srv6_test.sh b/smoke/srv6_test.sh index c3226cf60..24b1b3696 100755 --- a/smoke/srv6_test.sh +++ b/smoke/srv6_test.sh @@ -64,3 +64,23 @@ grcli route add fd00:202:100::/48 via id 666 ip netns exec n0 ping -i0.01 -c3 -n 192.168.60.1 # check that sid is reachable ip netns exec n1 ping6 -i0.01 -c3 -n fd00:202:100:: + +# +# NEXT-CSID transit test +# +# A CSID container fd00:202:0300:0100:: packs two CSIDs (0300 and 0100) +# with block-bits=32, csid-bits=16. Grout matches fd00:202:0300::/48 as +# a NEXT-CSID End transit node, shifts the DA to fd00:202:0100:: +# (= fd00:202:100::), and the second lookup hits the existing End.DT4 +# endpoint. +# + +# NEXT-CSID End transit node +grcli nexthop add srv6-local behavior end flavor next-csid id 700 +grcli route add fd00:202:300::/48 via id 700 + +# linux sends to the CSID container instead of the single SID +ip -n n1 route replace 192.168.61.0/24 encap seg6 mode encap segs fd00:202:0300:0100:: dev x-p1 + +# test: ping goes through the NEXT-CSID transit node +ip netns exec n0 ping -i0.01 -c3 -n 192.168.60.1 From 138fc0c46eb60e28b9687e4613d7658173af782f Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 18 Feb 2026 22:28:00 +0100 Subject: [PATCH 3/4] frr: accept NEXT_CSID flavor for SRv6 local SIDs FRR staticd programs SRv6 local SIDs using compressed SID by default (uN, uDT4, uDT6, uDT46). These map to standard seg6local actions (End, End.DT4, etc.) with the NEXT_CSID flavor flag set in seg6local_ctx.flv, along with lcblock_len and lcnode_func_len parameters that define the CSID container layout. Map ZEBRA_SEG6_LOCAL_FLV_OP_NEXT_CSID to GR_SR_FL_FLAVOR_NEXT_CSID and copy the block/CSID length parameters from the FRR flavor context into the grout nexthop info. Do this in both directions: FRR-to-grout (when FRR programs a local SID into grout) and grout-to-FRR (when grout notifies FRR of existing local SIDs). Link: https://docs.frrouting.org/en/latest/zebra.html#clicmd-behavior-usid Signed-off-by: Robin Jarry Reviewed-by: Christophe Fontaine --- frr/rt_grout.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frr/rt_grout.c b/frr/rt_grout.c index 6ae2b637e..1aa628275 100644 --- a/frr/rt_grout.c +++ b/frr/rt_grout.c @@ -249,6 +249,11 @@ static int grout_gr_nexthop_to_frr_nexthop( SET_SRV6_FLV_OP(ctx.flv.flv_ops, ZEBRA_SEG6_LOCAL_FLV_OP_PSP); if (sr6->flags & GR_SR_FL_FLAVOR_USD) SET_SRV6_FLV_OP(ctx.flv.flv_ops, ZEBRA_SEG6_LOCAL_FLV_OP_USD); + if (sr6->flags & GR_SR_FL_FLAVOR_NEXT_CSID) { + SET_SRV6_FLV_OP(ctx.flv.flv_ops, ZEBRA_SEG6_LOCAL_FLV_OP_NEXT_CSID); + ctx.flv.lcblock_len = sr6->block_bits; + ctx.flv.lcnode_func_len = sr6->csid_bits; + } switch (sr6->behavior) { case SR_BEHAVIOR_END: @@ -723,8 +728,11 @@ grout_add_nexthop(uint32_t nh_id, gr_nh_origin_t origin, const struct nexthop *n gr_log_debug("USP always enabled, ignoring flag"); if (CHECK_SRV6_FLV_OP(flv, ZEBRA_SEG6_LOCAL_FLV_OP_USD)) sr6_local->flags |= GR_SR_FL_FLAVOR_USD; - if (CHECK_SRV6_FLV_OP(flv, ZEBRA_SEG6_LOCAL_FLV_OP_NEXT_CSID)) - gr_log_debug("next-c-sid not supported, ignoring flag"); + if (CHECK_SRV6_FLV_OP(flv, ZEBRA_SEG6_LOCAL_FLV_OP_NEXT_CSID)) { + sr6_local->flags |= GR_SR_FL_FLAVOR_NEXT_CSID; + sr6_local->block_bits = nh->nh_srv6->seg6local_ctx.flv.lcblock_len; + sr6_local->csid_bits = nh->nh_srv6->seg6local_ctx.flv.lcnode_func_len; + } break; case GR_NH_T_SR6_OUTPUT: From 066c62f7916c6c6e1dc677c699ffa0458bf30c3d Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 18 Feb 2026 22:28:08 +0100 Subject: [PATCH 4/4] smoke: support uN behavior in FRR smoke test helpers FRR staticd uses "uN" as the behavior name for NEXT-CSID End. Add the end -> uN mapping to set_srv6_localsid() so that smoke tests can create transit CSID local SIDs through FRR. The uN behavior does not take a VRF argument (it is a pure transit function), so omit the "vrf default" clause when generating the FRR static-sids configuration for that case. Signed-off-by: Robin Jarry Reviewed-by: Christophe Fontaine --- smoke/_init_frr.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/smoke/_init_frr.sh b/smoke/_init_frr.sh index a399bb928..573dec7ea 100644 --- a/smoke/_init_frr.sh +++ b/smoke/_init_frr.sh @@ -127,16 +127,21 @@ set_srv6_localsid() { local count=0 # ---- translate behaviour aliases -------------------------------------- - # map: end.dt4 -> uDT4, end.dt6 -> uDT6, end.dt46 -> uDT46 local frr_behavior case "${grout_behavior,,}" in # ,, = lower-case + end) frr_behavior="uN" ;; end.dt4) frr_behavior="uDT4" ;; end.dt6) frr_behavior="uDT6" ;; end.dt46) frr_behavior="uDT46" ;; - *) echo "Unsupported behavior '${grout_behavior}'. Use end.dt4, end.dt6, end.dt46."; exit 1 ;; + *) echo "Unsupported behavior '${grout_behavior}'."; exit 1 ;; esac # --- push the config into FRR ------------------------------------------ + local vrf_clause="vrf default" + case "${frr_behavior}" in + uN) vrf_clause="" ;; + esac + vtysh <<-EOF configure terminal segment-routing @@ -147,7 +152,7 @@ set_srv6_localsid() { exit exit static-sids - sid ${sid_local}/48 locator ${locator} behavior ${frr_behavior} vrf default + sid ${sid_local}/48 locator ${locator} behavior ${frr_behavior} ${vrf_clause} exit exit EOF