Skip to content

Commit b108f01

Browse files
authored
Merge pull request #119 from datum-cloud/fix/gobgp-l2vpn-evpn-capabilities
fix(gobgp): advertise L2VPN/EVPN capability instead of IPv4/IPv6 Unicast
2 parents 1193a7f + 71e9d3f commit b108f01

4 files changed

Lines changed: 127 additions & 7 deletions

File tree

ARCHITECTURE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
> Galactic is the SRv6 data plane for multi-cloud VPC networking, deployed as two
44
> binaries on each Kubernetes node: a CNI plugin that attaches containers to VPC
5-
> networks, and an agent that manages kernel SRv6 routes and distributes L3VPN BGP
6-
> paths via an embedded GoBGP server.
5+
> networks, and an agent that manages kernel SRv6 routes and distributes EVPN
6+
> (L2VPN/EVPN AFI/SAFI) paths via an embedded GoBGP server.
77
88
_Last updated: 2026-06-14_
99

@@ -13,7 +13,7 @@ _Last updated: 2026-06-14_
1313

1414
Galactic implements VPC isolation and cross-cluster reachability using Linux SRv6.
1515
When a pod is attached to a VPC, the CNI plugin creates the required kernel state
16-
(VRF, veth pair, SRv6 ingress route) and injects L3VPN BGP paths into the
16+
(VRF, veth pair, SRv6 ingress route) and injects EVPN paths into the
1717
node-local GoBGP daemon. GoBGP distributes those paths to a BGP route reflector,
1818
enabling pods on different nodes or clusters to reach each other via
1919
SRv6-encapsulated traffic.
@@ -92,7 +92,7 @@ See [docs/agent-startup.md](docs/agent-startup.md) for the agent startup sequenc
9292

9393
- **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.
9494
- **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.
95-
- **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.
95+
- **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.
9696
- **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.
9797

9898
---

internal/gobgp/provider.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
const (
2020
safiUnicast = "unicast"
2121
globalPolicyTable = "global"
22+
afiL2VPN = "L2VPN"
23+
safiEVPN = "EVPN"
2224
)
2325

2426
// ProviderServer implements providerv1alpha1.BGPProviderServiceServer, translating
@@ -55,8 +57,7 @@ func (p *ProviderServer) Capabilities(_ context.Context, _ *providerv1alpha1.Cap
5557
return &providerv1alpha1.CapabilitiesResponse{
5658
Capabilities: &providerv1alpha1.CapabilitySet{
5759
AddressFamilies: []*providerv1alpha1.AddressFamily{
58-
{Afi: "IPv4", Safi: "Unicast"},
59-
{Afi: "IPv6", Safi: "Unicast"},
60+
{Afi: afiL2VPN, Safi: safiEVPN},
6061
},
6162
RouteReflection: false,
6263
Bfd: false,

internal/gobgp/provider_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package gobgp
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
api "github.com/osrg/gobgp/v4/api"
8+
providerv1alpha1 "go.miloapis.com/cosmos/api/proto/bgp/provider/v1alpha1"
9+
)
10+
11+
const safiUnicastCaps = "Unicast"
12+
13+
func newTestProvider() *ProviderServer {
14+
return NewProviderServer(New(Config{}))
15+
}
16+
17+
func TestCapabilities_AddressFamily(t *testing.T) {
18+
p := newTestProvider()
19+
resp, err := p.Capabilities(context.Background(), &providerv1alpha1.CapabilitiesRequest{})
20+
if err != nil {
21+
t.Fatalf("Capabilities() error = %v", err)
22+
}
23+
caps := resp.GetCapabilities()
24+
if caps == nil {
25+
t.Fatal("Capabilities() returned nil CapabilitySet")
26+
}
27+
afs := caps.GetAddressFamilies()
28+
if len(afs) != 1 {
29+
t.Fatalf("len(AddressFamilies) = %d, want 1", len(afs))
30+
}
31+
af := afs[0]
32+
if got := af.GetAfi(); got != afiL2VPN {
33+
t.Errorf("AFI = %q, want %q", got, afiL2VPN)
34+
}
35+
if got := af.GetSafi(); got != safiEVPN {
36+
t.Errorf("SAFI = %q, want %q", got, safiEVPN)
37+
}
38+
}
39+
40+
func TestCapabilities_NoUnicast(t *testing.T) {
41+
p := newTestProvider()
42+
resp, err := p.Capabilities(context.Background(), &providerv1alpha1.CapabilitiesRequest{})
43+
if err != nil {
44+
t.Fatalf("Capabilities() error = %v", err)
45+
}
46+
for _, af := range resp.GetCapabilities().GetAddressFamilies() {
47+
if af.GetSafi() == safiUnicastCaps {
48+
t.Errorf("unexpected Unicast AF advertised: AFI=%s SAFI=%s", af.GetAfi(), af.GetSafi())
49+
}
50+
}
51+
}
52+
53+
func TestCapabilities_Features(t *testing.T) {
54+
p := newTestProvider()
55+
resp, err := p.Capabilities(context.Background(), &providerv1alpha1.CapabilitiesRequest{})
56+
if err != nil {
57+
t.Fatalf("Capabilities() error = %v", err)
58+
}
59+
caps := resp.GetCapabilities()
60+
if caps.GetRouteReflection() {
61+
t.Error("RouteReflection should be false")
62+
}
63+
if caps.GetBfd() {
64+
t.Error("BFD should be false")
65+
}
66+
}
67+
68+
func TestCapabilities_DoesNotRequireLiveBGP(t *testing.T) {
69+
// Capabilities must return a valid response without a running GoBGP instance.
70+
p := NewProviderServer(New(Config{})) // server never started
71+
_, err := p.Capabilities(context.Background(), &providerv1alpha1.CapabilitiesRequest{})
72+
if err != nil {
73+
t.Errorf("Capabilities() should not require a live server, got error: %v", err)
74+
}
75+
}
76+
77+
func TestFamilyFromSpec_L2VPN_EVPN(t *testing.T) {
78+
af := &providerv1alpha1.AddressFamily{Afi: afiL2VPN, Safi: safiEVPN}
79+
f := familyFromSpec(af)
80+
if f.Afi != api.Family_AFI_L2VPN {
81+
t.Errorf("Afi = %v, want AFI_L2VPN", f.Afi)
82+
}
83+
if f.Safi != api.Family_SAFI_EVPN {
84+
t.Errorf("Safi = %v, want SAFI_EVPN", f.Safi)
85+
}
86+
}
87+
88+
func TestFamilyFromSpec_IPv4_Unicast(t *testing.T) {
89+
af := &providerv1alpha1.AddressFamily{Afi: "IPv4", Safi: safiUnicastCaps}
90+
f := familyFromSpec(af)
91+
if f.Afi != api.Family_AFI_IP {
92+
t.Errorf("Afi = %v, want AFI_IP", f.Afi)
93+
}
94+
if f.Safi != api.Family_SAFI_UNICAST {
95+
t.Errorf("Safi = %v, want SAFI_UNICAST", f.Safi)
96+
}
97+
}
98+
99+
func TestFamilyFromSpec_IPv6_Unicast(t *testing.T) {
100+
af := &providerv1alpha1.AddressFamily{Afi: "IPv6", Safi: safiUnicastCaps}
101+
f := familyFromSpec(af)
102+
if f.Afi != api.Family_AFI_IP6 {
103+
t.Errorf("Afi = %v, want AFI_IP6", f.Afi)
104+
}
105+
if f.Safi != api.Family_SAFI_UNICAST {
106+
t.Errorf("Safi = %v, want SAFI_UNICAST", f.Safi)
107+
}
108+
}
109+
110+
func TestFamilyFromSpec_Unknown(t *testing.T) {
111+
af := &providerv1alpha1.AddressFamily{Afi: "bogus", Safi: "bogus"}
112+
f := familyFromSpec(af)
113+
if f.Afi != api.Family_AFI_UNSPECIFIED {
114+
t.Errorf("Afi = %v, want AFI_UNSPECIFIED for unrecognised input", f.Afi)
115+
}
116+
if f.Safi != api.Family_SAFI_UNSPECIFIED {
117+
t.Errorf("Safi = %v, want SAFI_UNSPECIFIED for unrecognised input", f.Safi)
118+
}
119+
}

scripts/ci.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ case "$COMMAND" in
1919
echo "--- Loading kernel modules required by galactic"
2020
sudo apt-get update -qq
2121
sudo apt-get install -y --no-install-recommends linux-modules-extra-azure
22-
sudo modprobe vrf
2322
fi
23+
sudo modprobe vrf
2424

2525
echo "--- Installing kind"
2626
go install sigs.k8s.io/kind@latest

0 commit comments

Comments
 (0)