Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_

Expand All @@ -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.
Expand Down Expand Up @@ -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.

---
Expand Down
5 changes: 3 additions & 2 deletions internal/gobgp/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
const (
safiUnicast = "unicast"
globalPolicyTable = "global"
afiL2VPN = "L2VPN"
safiEVPN = "EVPN"
)

// ProviderServer implements providerv1alpha1.BGPProviderServiceServer, translating
Expand Down Expand Up @@ -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,
Expand Down
119 changes: 119 additions & 0 deletions internal/gobgp/provider_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion scripts/ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading