diff --git a/internal/runtime/gobgp/paths.go b/internal/runtime/gobgp/paths.go index 198a6074..fc149a9b 100644 --- a/internal/runtime/gobgp/paths.go +++ b/internal/runtime/gobgp/paths.go @@ -15,6 +15,7 @@ import ( gobgpserver "github.com/osrg/gobgp/v4/pkg/server" "go.datum.net/galactic/internal/model" + "go.datum.net/galactic/internal/plumbing/ebpf/uformat" ) // deriveRD builds an RFC 4364 Type 1 route distinguisher from the router ID @@ -62,10 +63,25 @@ func gatewayForPrefix(prefix netip.Prefix) netip.Addr { // the sole carrier for the destination SID in this design — see // gatewayForPrefix for why the EVPN route's own Gateway IP field can't be // used for this instead. +// +// The Information Sub-TLV also carries an RFC 9252 §3.2.1 SID Structure +// Sub-Sub-TLV describing the uSID layout (uFMT 48+16, see +// internal/plumbing/ebpf/uformat) already present in sid, with no +// transposition (TL/TO = 0). The field widths are derived from uformat's own +// constants rather than re-hardcoded, so the wire encoding and the eBPF +// datapath's bit layout can't silently drift apart. func prefixSIDAttr(sid netip.Addr) bgp.PathAttributeInterface { + structure := bgp.NewSRv6SIDStructureSubSubTLV( + uformat.BlockBits, // LBL = 48, uSID Block + uformat.NodeIDBits, // LNL = 16, Node-ID + uformat.FunctionBits, // FL = 4, Function + uformat.ArgumentBits, // AL = 12, Instance ID (Argument) + 0, // TL = 0, no transposition + 0, // TO = 0, n/a when TL=0 + ) return bgp.NewPathAttributePrefixSID( bgp.NewSRv6ServiceTLV(bgp.TLVTypeSRv6L3Service, - bgp.NewSRv6InformationSubTLV(sid, bgp.END_DT46), + bgp.NewSRv6InformationSubTLV(sid, bgp.END_DT46, structure), ), ) } diff --git a/internal/runtime/gobgp/paths_test.go b/internal/runtime/gobgp/paths_test.go index a34d89d0..5ebf3e16 100644 --- a/internal/runtime/gobgp/paths_test.go +++ b/internal/runtime/gobgp/paths_test.go @@ -14,6 +14,7 @@ import ( bgp "github.com/osrg/gobgp/v4/pkg/packet/bgp" "go.datum.net/galactic/internal/model" + "go.datum.net/galactic/internal/plumbing/ebpf/uformat" ) const ( @@ -351,6 +352,82 @@ func TestBuildEVPNPathsMixedFamily(t *testing.T) { } } +// TestPrefixSIDAttrCarriesSIDStructure verifies that prefixSIDAttr's +// PathAttributePrefixSID, once serialized to wire bytes and decoded back via +// GoBGP's own DecodeFromBytes, carries an RFC 9252 §3.2.1 SRv6 SID Structure +// Sub-Sub-TLV describing the uFMT 48+16 layout with no transposition. Before +// this fix, NewSRv6InformationSubTLV was called with no sub-sub-TLV values at +// all, so the attribute Galactic put on the wire never contained a Structure +// Sub-Sub-TLV — a spec-compliant third-party EVPN/SRv6 receiver would be +// missing the structure description entirely. +func TestPrefixSIDAttrCarriesSIDStructure(t *testing.T) { + sid := netip.MustParseAddr(testSID1) + + attr := prefixSIDAttr(sid) + + serialized, err := attr.Serialize() + if err != nil { + t.Fatalf("Serialize() error = %v", err) + } + + // PathAttribute.Serialize() includes the attribute header (flags, type + // code, and length); DecodeFromBytes expects that same header back, so + // round-trip through a fresh attribute rather than re-using attr. + got := &bgp.PathAttributePrefixSID{} + if err := got.DecodeFromBytes(serialized); err != nil { + t.Fatalf("DecodeFromBytes() error = %v", err) + } + + structure := extractSIDStructure(t, got) + + if structure.LocatorBlockLength != uformat.BlockBits { + t.Errorf("LocatorBlockLength = %d, want %d", structure.LocatorBlockLength, uformat.BlockBits) + } + if structure.LocatorNodeLength != uformat.NodeIDBits { + t.Errorf("LocatorNodeLength = %d, want %d", structure.LocatorNodeLength, uformat.NodeIDBits) + } + if structure.FunctionLength != uformat.FunctionBits { + t.Errorf("FunctionLength = %d, want %d", structure.FunctionLength, uformat.FunctionBits) + } + if structure.ArgumentLength != uformat.ArgumentBits { + t.Errorf("ArgumentLength = %d, want %d", structure.ArgumentLength, uformat.ArgumentBits) + } + if structure.TranspositionLength != 0 { + t.Errorf("TranspositionLength = %d, want 0", structure.TranspositionLength) + } + if structure.TranspositionOffset != 0 { + t.Errorf("TranspositionOffset = %d, want 0", structure.TranspositionOffset) + } +} + +// extractSIDStructure digs the SRv6 SID Structure Sub-Sub-TLV out of a +// decoded Prefix-SID attribute's L3 Service TLV / Information Sub-TLV, or +// fails the test if any layer is missing. +func extractSIDStructure(t *testing.T, attr *bgp.PathAttributePrefixSID) *bgp.SRv6SIDStructureSubSubTLV { + t.Helper() + + for _, tlv := range attr.TLVs { + svc, ok := tlv.(*bgp.SRv6ServiceTLV) + if !ok || svc.Type != bgp.TLVTypeSRv6L3Service { + continue + } + for _, sub := range svc.SubTLVs { + info, ok := sub.(*bgp.SRv6InformationSubTLV) + if !ok { + continue + } + for _, subSub := range info.SubSubTLVs { + if structure, ok := subSub.(*bgp.SRv6SIDStructureSubSubTLV); ok { + return structure + } + } + } + } + + t.Fatal("no SRv6SIDStructureSubSubTLV found in decoded Prefix-SID attribute") + return nil +} + // TestBuildEVPNPathsMatchesApplyVRFRD verifies that the RD derived by // buildEVPNPaths matches the one used by applyVRF for the same VRFID. // This ensures VRF registration and EVPN path advertisement are consistent.