From c2869171afeaa903f007166fbbabd5e963c09572 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 6 Jul 2026 09:04:32 -0400 Subject: [PATCH 1/2] feat: add SEG6_LOCAL_FLAVORS support for seg6local NEXT-CSID Add the SEG6_LOCAL_FLAVORS attribute and its nested sub-attributes (SEG6_LOCAL_FLV_OPERATION, SEG6_LOCAL_FLV_LCBLOCK_BITS, SEG6_LOCAL_FLV_LCNODE_FN_BITS), introduced in Linux 6.1 (commit 848f3c0d47, "seg6: add support for the SRv6 End* flavors"). This exposes the NEXT-CSID flavor operation on SEG6LocalEncap, letting callers configure the locator-block and node-function bit lengths used for compressed SID (uSID/C-SID) behaviors, matching what iproute2 exposes as `flavors next-csid lblen nflen `. --- nl/seg6local_linux.go | 41 ++++++++++++++++++ route_linux.go | 41 +++++++++++++++++- route_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) diff --git a/nl/seg6local_linux.go b/nl/seg6local_linux.go index 361e7642e..d9aa58340 100644 --- a/nl/seg6local_linux.go +++ b/nl/seg6local_linux.go @@ -14,12 +14,39 @@ const ( SEG6_LOCAL_OIF SEG6_LOCAL_BPF SEG6_LOCAL_VRFTABLE + SEG6_LOCAL_COUNTERS + SEG6_LOCAL_FLAVORS __SEG6_LOCAL_MAX ) const ( SEG6_LOCAL_MAX = __SEG6_LOCAL_MAX ) +// seg6local flavors (SEG6_LOCAL_FLAVORS nested attributes) +const ( + SEG6_LOCAL_FLV_UNSPEC = iota + SEG6_LOCAL_FLV_OPERATION + SEG6_LOCAL_FLV_LCBLOCK_BITS + SEG6_LOCAL_FLV_LCNODE_FN_BITS + __SEG6_LOCAL_FLV_MAX +) +const ( + SEG6_LOCAL_FLV_MAX = __SEG6_LOCAL_FLV_MAX - 1 +) + +// seg6local flavor operations +const ( + SEG6_LOCAL_FLV_OP_UNSPEC = iota + SEG6_LOCAL_FLV_OP_PSP + SEG6_LOCAL_FLV_OP_USP + SEG6_LOCAL_FLV_OP_USD + SEG6_LOCAL_FLV_OP_NEXT_CSID + __SEG6_LOCAL_FLV_OP_MAX +) +const ( + SEG6_LOCAL_FLV_OP_MAX = __SEG6_LOCAL_FLV_OP_MAX - 1 +) + // seg6local actions const ( SEG6_LOCAL_ACTION_END = iota + 1 // 1 @@ -80,3 +107,17 @@ func SEG6LocalActionString(action int) string { } return "unknown" } + +func SEG6LocalFlavorOperationString(op int) string { + switch op { + case SEG6_LOCAL_FLV_OP_PSP: + return "PSP" + case SEG6_LOCAL_FLV_OP_USP: + return "USP" + case SEG6_LOCAL_FLV_OP_USD: + return "USD" + case SEG6_LOCAL_FLV_OP_NEXT_CSID: + return "NEXT-CSID" + } + return "unknown" +} diff --git a/route_linux.go b/route_linux.go index c8c600d37..e3deac96c 100644 --- a/route_linux.go +++ b/route_linux.go @@ -282,6 +282,16 @@ type SEG6LocalEncap struct { Iif int Oif int bpf bpfObj + Flavors SEG6LocalFlavors // flavors (e.g. NEXT-CSID) applied to the action +} + +// SEG6LocalFlavors carries the SEG6_LOCAL_FLAVORS sub-attributes: the +// flavor operation and, for NEXT-CSID, the locator block/node function +// bit lengths. +type SEG6LocalFlavors struct { + Operation uint32 + LcBlockBits uint8 + LcNodeFnBits uint8 } func (e *SEG6LocalEncap) SetProg(progFd int, progName string) error { @@ -343,6 +353,24 @@ func (e *SEG6LocalEncap) Decode(buf []byte) error { } e.bpf = bpfobj e.Flags[nl.SEG6_LOCAL_BPF] = true + case nl.SEG6_LOCAL_FLAVORS: + var flvAttrs []syscall.NetlinkRouteAttr + flvAttrs, err = nl.ParseRouteAttr(attr.Value) + flavors := SEG6LocalFlavors{} + for _, flvAttr := range flvAttrs { + switch flvAttr.Attr.Type { + case nl.SEG6_LOCAL_FLV_OPERATION: + flavors.Operation = native.Uint32(flvAttr.Value) + case nl.SEG6_LOCAL_FLV_LCBLOCK_BITS: + flavors.LcBlockBits = flvAttr.Value[0] + case nl.SEG6_LOCAL_FLV_LCNODE_FN_BITS: + flavors.LcNodeFnBits = flvAttr.Value[0] + default: + err = fmt.Errorf("seg6local flavors decode: unknown attribute: Type %d", flvAttr.Attr) + } + } + e.Flavors = flavors + e.Flags[nl.SEG6_LOCAL_FLAVORS] = true } } return err @@ -423,6 +451,13 @@ func (e *SEG6LocalEncap) Encode() ([]byte, error) { } res = append(res, attr.Serialize()...) } + if e.Flags[nl.SEG6_LOCAL_FLAVORS] { + attr := nl.NewRtAttr(nl.SEG6_LOCAL_FLAVORS, []byte{}) + attr.AddRtAttr(nl.SEG6_LOCAL_FLV_OPERATION, nl.Uint32Attr(e.Flavors.Operation)) + attr.AddRtAttr(nl.SEG6_LOCAL_FLV_LCBLOCK_BITS, nl.Uint8Attr(e.Flavors.LcBlockBits)) + attr.AddRtAttr(nl.SEG6_LOCAL_FLV_LCNODE_FN_BITS, nl.Uint8Attr(e.Flavors.LcNodeFnBits)) + res = append(res, attr.Serialize()...) + } return res, err } func (e *SEG6LocalEncap) String() string { @@ -470,6 +505,10 @@ func (e *SEG6LocalEncap) String() string { if e.Flags[nl.SEG6_LOCAL_BPF] { strs = append(strs, fmt.Sprintf("bpf %s[%d]", e.bpf.progName, e.bpf.progFd)) } + if e.Flags[nl.SEG6_LOCAL_FLAVORS] { + strs = append(strs, fmt.Sprintf("flavors %s lblen %d nflen %d", + nl.SEG6LocalFlavorOperationString(int(e.Flavors.Operation)), e.Flavors.LcBlockBits, e.Flavors.LcNodeFnBits)) + } return strings.Join(strs, " ") } func (e *SEG6LocalEncap) Equal(x Encap) bool { @@ -501,7 +540,7 @@ func (e *SEG6LocalEncap) Equal(x Encap) bool { if !e.InAddr.Equal(o.InAddr) || !e.In6Addr.Equal(o.In6Addr) { return false } - if e.Action != o.Action || e.Table != o.Table || e.Iif != o.Iif || e.Oif != o.Oif || e.bpf != o.bpf || e.VrfTable != o.VrfTable { + if e.Action != o.Action || e.Table != o.Table || e.Iif != o.Iif || e.Oif != o.Oif || e.bpf != o.bpf || e.VrfTable != o.VrfTable || e.Flavors != o.Flavors { return false } return true diff --git a/route_test.go b/route_test.go index 2f4d61524..d537efed5 100644 --- a/route_test.go +++ b/route_test.go @@ -2071,6 +2071,104 @@ func TestSEG6LocalEqual(t *testing.T) { } } } + +func TestSEG6LocalEncapFlavors(t *testing.T) { + var flagsEnd [nl.SEG6_LOCAL_MAX]bool + flagsEnd[nl.SEG6_LOCAL_ACTION] = true + + var flagsEndDt46 [nl.SEG6_LOCAL_MAX]bool + flagsEndDt46[nl.SEG6_LOCAL_ACTION] = true + flagsEndDt46[nl.SEG6_LOCAL_VRFTABLE] = true + + var flagsEndFlavors [nl.SEG6_LOCAL_MAX]bool + flagsEndFlavors[nl.SEG6_LOCAL_ACTION] = true + flagsEndFlavors[nl.SEG6_LOCAL_FLAVORS] = true + + testCases := []struct { + name string + encap *SEG6LocalEncap + wantString string + }{ + { + name: "bare End action without flavors", + encap: &SEG6LocalEncap{ + Flags: flagsEnd, + Action: nl.SEG6_LOCAL_ACTION_END, + }, + wantString: "action End", + }, + { + name: "End.DT46 with vrftable only, no flavors", + encap: &SEG6LocalEncap{ + Flags: flagsEndDt46, + Action: nl.SEG6_LOCAL_ACTION_END_DT46, + VrfTable: 50, + }, + // SEG6LocalActionString has no case for END_DT46; this documents + // existing behavior, unrelated to flavors. + wantString: "action unknown vrftable 50", + }, + { + name: "End with NEXT-CSID flavor, lblen 32 nflen 16", + encap: &SEG6LocalEncap{ + Flags: flagsEndFlavors, + Action: nl.SEG6_LOCAL_ACTION_END, + Flavors: SEG6LocalFlavors{ + Operation: nl.SEG6_LOCAL_FLV_OP_NEXT_CSID, + LcBlockBits: 32, + LcNodeFnBits: 16, + }, + }, + wantString: "action End flavors NEXT-CSID lblen 32 nflen 16", + }, + { + name: "End with NEXT-CSID flavor, lblen 48 nflen 0", + encap: &SEG6LocalEncap{ + Flags: flagsEndFlavors, + Action: nl.SEG6_LOCAL_ACTION_END, + Flavors: SEG6LocalFlavors{ + Operation: nl.SEG6_LOCAL_FLV_OP_NEXT_CSID, + LcBlockBits: 48, + LcNodeFnBits: 0, + }, + }, + wantString: "action End flavors NEXT-CSID lblen 48 nflen 0", + }, + { + // The kernel is the source of truth for which operation values are + // valid; Decode/Encode round-trip the raw value without validating it. + name: "End with an unsupported flavor operation", + encap: &SEG6LocalEncap{ + Flags: flagsEndFlavors, + Action: nl.SEG6_LOCAL_ACTION_END, + Flavors: SEG6LocalFlavors{ + Operation: 99, + }, + }, + wantString: "action End flavors unknown lblen 0 nflen 0", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf, err := tc.encap.Encode() + if err != nil { + t.Fatal(err) + } + decoded := &SEG6LocalEncap{} + if err := decoded.Decode(buf); err != nil { + t.Fatal(err) + } + if !decoded.Equal(tc.encap) { + t.Fatalf("Decode(Encode(encap)) = %v, want %v", decoded, tc.encap) + } + if got := tc.encap.String(); got != tc.wantString { + t.Fatalf("String() = %q, want %q", got, tc.wantString) + } + }) + } +} + func TestSEG6RouteAddDel(t *testing.T) { // add/del routes with LWTUNNEL_SEG6 to/from interface. // Test both seg6 modes: encap (IPv4) & inline (IPv6). From 290b871aaee5e8fc4db4831ea9da72013b481415 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 6 Jul 2026 11:30:08 -0400 Subject: [PATCH 2/2] fix: format attribute type instead of struct in flavors decode error - Report flvAttr.Attr.Type rather than the raw syscall.RtAttr struct in the SEG6_LOCAL_FLAVORS unknown-attribute error - Prevents %d from printing the struct's Len/Type fields together, which produced a confusing error message --- route_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/route_linux.go b/route_linux.go index e3deac96c..81f9a6a24 100644 --- a/route_linux.go +++ b/route_linux.go @@ -366,7 +366,7 @@ func (e *SEG6LocalEncap) Decode(buf []byte) error { case nl.SEG6_LOCAL_FLV_LCNODE_FN_BITS: flavors.LcNodeFnBits = flvAttr.Value[0] default: - err = fmt.Errorf("seg6local flavors decode: unknown attribute: Type %d", flvAttr.Attr) + err = fmt.Errorf("seg6local flavors decode: unknown attribute: Type %d", flvAttr.Attr.Type) } } e.Flavors = flavors