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
11 changes: 10 additions & 1 deletion internal/ingresssidecar/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"regexp"
"strings"

"go.datum.net/galactic/internal/plumbing/ebpf/egressroutemap"
"go.datum.net/galactic/internal/plumbing/srv6"
"go.datum.net/galactic/internal/plumbing/vrf"
)
Expand Down Expand Up @@ -80,7 +81,15 @@ type kernelBackend struct{}

// NewKernelBackend returns the production Backend, wired to real kernel state.
// Requires CAP_NET_ADMIN.
func NewKernelBackend() Backend { return kernelBackend{} }
//
// It also makes the process accept a default route to a backend's SID. The
// sidecar resolves SIDs in Envoy's pod namespace, where the default toward the
// host is the only route, so the shard check that rejects a default-route
// match would refuse every backend.
func NewKernelBackend() Backend {
egressroutemap.AcceptDefaultRoute()
return kernelBackend{}
}

func (kernelBackend) EnsureVRF(vpc string) (uint32, error) {
if err := vrf.AddInRange(vpc, sidecarTableIDMin, sidecarTableIDMax); err != nil {
Expand Down
20 changes: 20 additions & 0 deletions internal/ingresssidecar/backend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2026 Datum Cloud, Inc.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package ingresssidecar

import (
"testing"

"go.datum.net/galactic/internal/plumbing/ebpf/egressroutemap"
)

func TestNewKernelBackend_AcceptsADefaultRouteToBackendSIDs(t *testing.T) {
NewKernelBackend()

if !egressroutemap.DefaultRouteAccepted() {
t.Error("the kernel backend left default routes rejected, so every backend SID resolved from " +
"Envoy's pod namespace, whose only route is the default, would be refused")
}
}
27 changes: 26 additions & 1 deletion internal/plumbing/ebpf/egressroutemap/egressroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net"
"net/netip"
"strconv"
"sync/atomic"
"time"

"github.com/cilium/ebpf"
Expand Down Expand Up @@ -213,8 +214,12 @@ func resolveLinkAndL2(sid net.IP) (linkIndex int, dmac, smac net.HardwareAddr, e
// is down.
//
// A kernel that cannot answer a FIB-match lookup cannot tell the two apart, so
// the check is skipped there rather than failing every SID.
// the check is skipped there rather than failing every SID. So is a process
// that called AcceptDefaultRoute.
func rejectDefaultRoute(sid net.IP) error {
if defaultRouteAccepted.Load() {
return nil
}
matched, err := netlink.RouteGetWithOptions(sid, &netlink.RouteGetOptions{FIBMatch: true})
if err != nil || len(matched) == 0 {
return nil
Expand All @@ -227,6 +232,26 @@ func rejectDefaultRoute(sid net.IP) error {
return nil
}

// defaultRouteAccepted is set by AcceptDefaultRoute.
var defaultRouteAccepted atomic.Bool

// AcceptDefaultRoute makes a default route an acceptable route to any SID for
// the rest of the process, turning off rejectDefaultRoute.
//
// It is for a process that resolves SIDs from a network namespace that never
// holds fabric routes. The ingress sidecar runs in Envoy's pod namespace, whose
// only route is the default toward the host, and the host carries the fabric's
// specific routes. There a default-route match is the only answer a reachable
// SID can ever give, so rejecting it would refuse every backend.
func AcceptDefaultRoute() {
defaultRouteAccepted.Store(true)
}

// DefaultRouteAccepted reports whether AcceptDefaultRoute has been called.
func DefaultRouteAccepted() bool {
return defaultRouteAccepted.Load()
}

func isDefault(dst *net.IPNet) bool {
ones, _ := dst.Mask.Size()
return ones == 0
Expand Down
49 changes: 49 additions & 0 deletions internal/plumbing/ebpf/egressroutemap/resolve_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,52 @@ func TestResolveLinkAndL2_RejectsADefaultRouteOverAnUplink(t *testing.T) {
t.Errorf("resolve through the advertised locator: %v", specificErr)
}
}

func TestResolveLinkAndL2_AcceptsADefaultRouteWhenTheProcessAllowsIt(t *testing.T) {
requireRoot(t)

nearNS, linkIndex, farIP := setUpVethPair(t,
"vrfdr1", "fd00:beef:4::1/64",
"vrfdr1p", "fd00:beef:4::2/64")

restore := uplinkIndexesFn
uplinkIndexesFn = func() (map[int]struct{}, error) { return map[int]struct{}{linkIndex: {}}, nil }
t.Cleanup(func() { uplinkIndexesFn = restore })

acceptDefaultRouteForTest(t)

sid := net.ParseIP("2001:db8:ff02:2002:e001::")

var resolveErr error
err := nearNS.Do(func(_ ns.NetNS) error {
if err := netlink.RouteAdd(&netlink.Route{LinkIndex: linkIndex, Gw: farIP}); err != nil {
return fmt.Errorf("add default route: %w", err)
}
_, _, _, resolveErr = resolveLinkAndL2(sid)
return nil
})
if err != nil {
t.Fatal(err)
}
if resolveErr != nil {
t.Errorf("resolve through only a default route with default routes accepted: %v", resolveErr)
}
}

func TestRejectDefaultRoute_SkippedOnceDefaultRoutesAreAccepted(t *testing.T) {
acceptDefaultRouteForTest(t)

if !DefaultRouteAccepted() {
t.Fatal("DefaultRouteAccepted() = false after AcceptDefaultRoute")
}
if err := rejectDefaultRoute(net.ParseIP("2001:db8:ff02:2002:e001::")); err != nil {
t.Errorf("rejectDefaultRoute with default routes accepted: %v", err)
}
}

func acceptDefaultRouteForTest(t *testing.T) {
t.Helper()
prev := defaultRouteAccepted.Load()
AcceptDefaultRoute()
t.Cleanup(func() { defaultRouteAccepted.Store(prev) })
}
Loading