diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8ec1e88..137624d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,8 +2,8 @@ > Galactic is the SRv6 data plane for multi-cloud VPC networking, deployed as two > binaries on each Kubernetes node: a CNI plugin that attaches containers to VPC -> networks, and an agent that manages kernel SRv6 routes and distributes L3VPN BGP -> paths via an embedded GoBGP server. +> networks, and an agent that manages kernel SRv6 routes and distributes EVPN +> (L2VPN/EVPN AFI/SAFI) paths via an embedded GoBGP server. _Last updated: 2026-06-14_ @@ -13,7 +13,7 @@ _Last updated: 2026-06-14_ Galactic implements VPC isolation and cross-cluster reachability using Linux SRv6. When a pod is attached to a VPC, the CNI plugin creates the required kernel state -(VRF, veth pair, SRv6 ingress route) and injects L3VPN BGP paths into the +(VRF, veth pair, SRv6 ingress route) and injects EVPN paths into the node-local GoBGP daemon. GoBGP distributes those paths to a BGP route reflector, enabling pods on different nodes or clusters to reach each other via SRv6-encapsulated traffic. @@ -92,7 +92,7 @@ See [docs/agent-startup.md](docs/agent-startup.md) for the agent startup sequenc - **Identifiers in the SID.** VPC (48-bit) and VPCAttachment (16-bit) identifiers are packed into the low 64 bits of the SRv6 SID, making forwarding state fully self-describing without a lookup table. - **Base62 interface names.** Kernel interface names are Base62-encoded to stay within the 15-character limit (`vrfX-Y`, `galX-Y`). The hex form is used for BGP and SRv6; base62 for kernel interfaces. -- **GoBGP embedded, not sidecar.** GoBGP runs in-process so the agent owns its lifecycle and can gate readiness on BGP availability. Peer and policy config is applied by the cosmos operator via `BGPProvider` / `BGPInstance` / `BGPPeer` CRDs. +- **GoBGP embedded, not sidecar.** GoBGP runs in-process so the agent owns its lifecycle and can gate readiness on BGP availability. Peer and policy config is applied by the cosmos operator via `BGPProvider` / `BGPInstance` / `BGPPeer` CRDs. The provider advertises `L2VPN/EVPN` (AFI=25, SAFI=70) as its sole address family capability. - **CNI binary auto-detects mode.** The `galactic-cni` binary runs as both the CNI plugin (when `CNI_COMMAND` is set) and a CLI tool. This avoids shipping two separate binaries on the node. --- diff --git a/internal/gobgp/provider.go b/internal/gobgp/provider.go index 9009ae9..50b35b0 100644 --- a/internal/gobgp/provider.go +++ b/internal/gobgp/provider.go @@ -19,6 +19,8 @@ import ( const ( safiUnicast = "unicast" globalPolicyTable = "global" + afiL2VPN = "L2VPN" + safiEVPN = "EVPN" ) // ProviderServer implements providerv1alpha1.BGPProviderServiceServer, translating @@ -55,8 +57,7 @@ func (p *ProviderServer) Capabilities(_ context.Context, _ *providerv1alpha1.Cap return &providerv1alpha1.CapabilitiesResponse{ Capabilities: &providerv1alpha1.CapabilitySet{ AddressFamilies: []*providerv1alpha1.AddressFamily{ - {Afi: "IPv4", Safi: "Unicast"}, - {Afi: "IPv6", Safi: "Unicast"}, + {Afi: afiL2VPN, Safi: safiEVPN}, }, RouteReflection: false, Bfd: false, diff --git a/internal/gobgp/provider_test.go b/internal/gobgp/provider_test.go new file mode 100644 index 0000000..99b9118 --- /dev/null +++ b/internal/gobgp/provider_test.go @@ -0,0 +1,119 @@ +package gobgp + +import ( + "context" + "testing" + + api "github.com/osrg/gobgp/v4/api" + providerv1alpha1 "go.miloapis.com/cosmos/api/proto/bgp/provider/v1alpha1" +) + +const safiUnicastCaps = "Unicast" + +func newTestProvider() *ProviderServer { + return NewProviderServer(New(Config{})) +} + +func TestCapabilities_AddressFamily(t *testing.T) { + p := newTestProvider() + resp, err := p.Capabilities(context.Background(), &providerv1alpha1.CapabilitiesRequest{}) + if err != nil { + t.Fatalf("Capabilities() error = %v", err) + } + caps := resp.GetCapabilities() + if caps == nil { + t.Fatal("Capabilities() returned nil CapabilitySet") + } + afs := caps.GetAddressFamilies() + if len(afs) != 1 { + t.Fatalf("len(AddressFamilies) = %d, want 1", len(afs)) + } + af := afs[0] + if got := af.GetAfi(); got != afiL2VPN { + t.Errorf("AFI = %q, want %q", got, afiL2VPN) + } + if got := af.GetSafi(); got != safiEVPN { + t.Errorf("SAFI = %q, want %q", got, safiEVPN) + } +} + +func TestCapabilities_NoUnicast(t *testing.T) { + p := newTestProvider() + resp, err := p.Capabilities(context.Background(), &providerv1alpha1.CapabilitiesRequest{}) + if err != nil { + t.Fatalf("Capabilities() error = %v", err) + } + for _, af := range resp.GetCapabilities().GetAddressFamilies() { + if af.GetSafi() == safiUnicastCaps { + t.Errorf("unexpected Unicast AF advertised: AFI=%s SAFI=%s", af.GetAfi(), af.GetSafi()) + } + } +} + +func TestCapabilities_Features(t *testing.T) { + p := newTestProvider() + resp, err := p.Capabilities(context.Background(), &providerv1alpha1.CapabilitiesRequest{}) + if err != nil { + t.Fatalf("Capabilities() error = %v", err) + } + caps := resp.GetCapabilities() + if caps.GetRouteReflection() { + t.Error("RouteReflection should be false") + } + if caps.GetBfd() { + t.Error("BFD should be false") + } +} + +func TestCapabilities_DoesNotRequireLiveBGP(t *testing.T) { + // Capabilities must return a valid response without a running GoBGP instance. + p := NewProviderServer(New(Config{})) // server never started + _, err := p.Capabilities(context.Background(), &providerv1alpha1.CapabilitiesRequest{}) + if err != nil { + t.Errorf("Capabilities() should not require a live server, got error: %v", err) + } +} + +func TestFamilyFromSpec_L2VPN_EVPN(t *testing.T) { + af := &providerv1alpha1.AddressFamily{Afi: afiL2VPN, Safi: safiEVPN} + f := familyFromSpec(af) + if f.Afi != api.Family_AFI_L2VPN { + t.Errorf("Afi = %v, want AFI_L2VPN", f.Afi) + } + if f.Safi != api.Family_SAFI_EVPN { + t.Errorf("Safi = %v, want SAFI_EVPN", f.Safi) + } +} + +func TestFamilyFromSpec_IPv4_Unicast(t *testing.T) { + af := &providerv1alpha1.AddressFamily{Afi: "IPv4", Safi: safiUnicastCaps} + f := familyFromSpec(af) + if f.Afi != api.Family_AFI_IP { + t.Errorf("Afi = %v, want AFI_IP", f.Afi) + } + if f.Safi != api.Family_SAFI_UNICAST { + t.Errorf("Safi = %v, want SAFI_UNICAST", f.Safi) + } +} + +func TestFamilyFromSpec_IPv6_Unicast(t *testing.T) { + af := &providerv1alpha1.AddressFamily{Afi: "IPv6", Safi: safiUnicastCaps} + f := familyFromSpec(af) + if f.Afi != api.Family_AFI_IP6 { + t.Errorf("Afi = %v, want AFI_IP6", f.Afi) + } + if f.Safi != api.Family_SAFI_UNICAST { + t.Errorf("Safi = %v, want SAFI_UNICAST", f.Safi) + } +} + +func TestFamilyFromSpec_Unknown(t *testing.T) { + af := &providerv1alpha1.AddressFamily{Afi: "bogus", Safi: "bogus"} + f := familyFromSpec(af) + if f.Afi != api.Family_AFI_UNSPECIFIED { + t.Errorf("Afi = %v, want AFI_UNSPECIFIED for unrecognised input", f.Afi) + } + if f.Safi != api.Family_SAFI_UNSPECIFIED { + t.Errorf("Safi = %v, want SAFI_UNSPECIFIED for unrecognised input", f.Safi) + } +} diff --git a/scripts/ci.sh b/scripts/ci.sh index 5eb14c3..e432535 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -19,8 +19,8 @@ case "$COMMAND" in echo "--- Loading kernel modules required by galactic" sudo apt-get update -qq sudo apt-get install -y --no-install-recommends linux-modules-extra-azure - sudo modprobe vrf fi + sudo modprobe vrf echo "--- Installing kind" go install sigs.k8s.io/kind@latest