mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-16 00:17:03 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8589b76e1f | |||
| 28cff022ee | |||
| 8cbee0e965 | |||
| 72bf111209 |
@@ -222,11 +222,14 @@ test-cov-html:
|
|||||||
go test -coverprofile=coverage.out
|
go test -coverprofile=coverage.out
|
||||||
go tool cover -html=coverage.out
|
go tool cover -html=coverage.out
|
||||||
|
|
||||||
|
# The package builds only compile. The final line links an android binary so a linker-only failure,
|
||||||
|
# such as the //go:linkname reference anet makes, cannot pass CI.
|
||||||
build-test-mobile:
|
build-test-mobile:
|
||||||
GOARCH=amd64 GOOS=ios go build $(shell go list ./... | grep -v '/cmd/\|/examples/')
|
GOARCH=amd64 GOOS=ios go build $(shell go list ./... | grep -v '/cmd/\|/examples/')
|
||||||
GOARCH=arm64 GOOS=ios go build $(shell go list ./... | grep -v '/cmd/\|/examples/')
|
GOARCH=arm64 GOOS=ios go build $(shell go list ./... | grep -v '/cmd/\|/examples/')
|
||||||
GOARCH=amd64 GOOS=android go build $(shell go list ./... | grep -v '/cmd/\|/examples/')
|
GOARCH=amd64 GOOS=android go build $(shell go list ./... | grep -v '/cmd/\|/examples/')
|
||||||
GOARCH=arm64 GOOS=android go build $(shell go list ./... | grep -v '/cmd/\|/examples/')
|
GOARCH=arm64 GOOS=android go build $(shell go list ./... | grep -v '/cmd/\|/examples/')
|
||||||
|
GOARCH=arm64 GOOS=android go build -ldflags=-checklinkname=0 -o /dev/null ${NEBULA_CMD_PATH}
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
go test -bench=.
|
go test -bench=.
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
//go:build e2e_testing
|
||||||
|
// +build e2e_testing
|
||||||
|
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula"
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"github.com/slackhq/nebula/cert_test"
|
||||||
|
"github.com/slackhq/nebula/e2e/router"
|
||||||
|
"github.com/slackhq/nebula/udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRecoveryTiming measures how long a tunnel takes to come back after the peer stops accepting our traffic,
|
||||||
|
// which is what a laptop waking on a new network looks like from the peer's side: its NAT has no state for where
|
||||||
|
// we are now, so everything we send disappears.
|
||||||
|
//
|
||||||
|
// It is a measurement, not a pass/fail assertion. Recovery is timed to the moment the peer punches back at us,
|
||||||
|
// since that is when its NAT opens and the tunnel is usable again.
|
||||||
|
//
|
||||||
|
// go test -tags e2e_testing -v -run TestRecoveryTiming ./e2e/
|
||||||
|
func TestRecoveryTiming(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
rebind bool
|
||||||
|
}{
|
||||||
|
{"no trigger", false},
|
||||||
|
{"rebind counter", true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
d, lost := measureRecovery(t, tc.rebind)
|
||||||
|
t.Logf("RESULT %-16s recovered in %-9v (%d packets lost)", tc.name, d.Round(time.Millisecond), lost)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// measureRecovery returns how long until the peer punched back, and how many of our packets died meanwhile. When
|
||||||
|
// rebind is true we call RebindUDPServer once the tunnel goes dark, which is what the darwin network change
|
||||||
|
// monitor does and what iOS has always done. When false, nothing tells nebula anything is wrong.
|
||||||
|
func measureRecovery(t *testing.T, rebind bool) (time.Duration, int) {
|
||||||
|
t.Helper()
|
||||||
|
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||||
|
|
||||||
|
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{
|
||||||
|
"lighthouse": m{"am_lighthouse": true},
|
||||||
|
})
|
||||||
|
|
||||||
|
peerCfg := m{
|
||||||
|
"lighthouse": m{
|
||||||
|
"hosts": []any{lhVpnIpNet[0].Addr().String()},
|
||||||
|
"interval": 600,
|
||||||
|
"local_allow_list": m{
|
||||||
|
"10.0.0.0/24": true,
|
||||||
|
"::/0": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"static_host_map": m{
|
||||||
|
lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", peerCfg)
|
||||||
|
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.3/24", peerCfg)
|
||||||
|
|
||||||
|
r := router.NewR(t, lhControl, myControl, theirControl)
|
||||||
|
defer r.RenderFlow()
|
||||||
|
defer func() {
|
||||||
|
lhControl.Stop()
|
||||||
|
myControl.Stop()
|
||||||
|
theirControl.Stop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
lhControl.Start()
|
||||||
|
myControl.Start()
|
||||||
|
theirControl.Start()
|
||||||
|
r.RouteFor(time.Millisecond * 500)
|
||||||
|
|
||||||
|
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||||
|
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||||
|
|
||||||
|
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("establish")))
|
||||||
|
r.RouteFor(time.Second)
|
||||||
|
if myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false) == nil {
|
||||||
|
t.Fatal("failed to establish the tunnel we are measuring")
|
||||||
|
}
|
||||||
|
r.RouteFor(time.Millisecond * 500)
|
||||||
|
|
||||||
|
// From here the peer's NAT has no state for us, everything we send it disappears
|
||||||
|
start := time.Now()
|
||||||
|
blackholed := 0
|
||||||
|
var recovered time.Duration
|
||||||
|
|
||||||
|
if rebind {
|
||||||
|
myControl.RebindUDPServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the tun busy the way someone retrying a stalled connection would
|
||||||
|
stop := make(chan struct{})
|
||||||
|
defer close(stop)
|
||||||
|
go func() {
|
||||||
|
tick := time.NewTicker(time.Millisecond * 200)
|
||||||
|
defer tick.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case <-tick.C:
|
||||||
|
myControl.InjectTunPacket(BuildTunUDPPacket(
|
||||||
|
theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("retry")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
r.RouteForAllExitFuncOrTimeout(time.Second*30, func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
||||||
|
if c == theirControl && p.From == myControl.GetUDPAddr() {
|
||||||
|
blackholed++
|
||||||
|
return router.Drop
|
||||||
|
}
|
||||||
|
|
||||||
|
// The peer reaching us directly is the moment its NAT opened, whether that is a punch or a handshake
|
||||||
|
if c == myControl && p.From == theirUdpAddr {
|
||||||
|
recovered = time.Since(start)
|
||||||
|
return router.RouteAndExit
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.KeepRouting
|
||||||
|
})
|
||||||
|
|
||||||
|
if recovered == 0 {
|
||||||
|
t.Fatalf("no recovery within 30s (%d packets blackholed)", blackholed)
|
||||||
|
}
|
||||||
|
return recovered, blackholed
|
||||||
|
}
|
||||||
+19
-2
@@ -153,6 +153,9 @@ const (
|
|||||||
ExitNow ExitType = 1
|
ExitNow ExitType = 1
|
||||||
// RouteAndExit routes this packet and exits immediately afterwards
|
// RouteAndExit routes this packet and exits immediately afterwards
|
||||||
RouteAndExit ExitType = 2
|
RouteAndExit ExitType = 2
|
||||||
|
// Drop discards this packet without delivering it and keeps routing. Use it to simulate a blackhole, such as
|
||||||
|
// a restrictive NAT refusing traffic from an address it has not seen.
|
||||||
|
Drop ExitType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
|
type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
|
||||||
@@ -163,7 +166,9 @@ type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
|
|||||||
func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
if err := os.MkdirAll("mermaid", 0755); err != nil {
|
// t.Name() contains a slash for subtests, so the flow log can land in a nested directory
|
||||||
|
fn := filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name()))
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fn), 0755); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +179,7 @@ func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
|||||||
outNat: make(map[outNatKey]netip.AddrPort),
|
outNat: make(map[outNatKey]netip.AddrPort),
|
||||||
flow: []flowEntry{},
|
flow: []flowEntry{},
|
||||||
ignoreFlows: []ignoreFlow{},
|
ignoreFlows: []ignoreFlow{},
|
||||||
fn: filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name())),
|
fn: fn,
|
||||||
t: t,
|
t: t,
|
||||||
cancelRender: cancel,
|
cancelRender: cancel,
|
||||||
}
|
}
|
||||||
@@ -687,6 +692,10 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
|
|||||||
p.Release()
|
p.Release()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
case Drop:
|
||||||
|
// Record it so the flow log shows the attempt, but never hand it to the receiver
|
||||||
|
r.unlockedInjectFlow(sender, receiver, p, false)
|
||||||
|
|
||||||
case KeepRouting:
|
case KeepRouting:
|
||||||
fp := r.unlockedInjectFlow(sender, receiver, p, false)
|
fp := r.unlockedInjectFlow(sender, receiver, p, false)
|
||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
@@ -779,6 +788,10 @@ func (r *R) RouteForAllExitFuncOrTimeout(timeout time.Duration, whatDo ExitFunc)
|
|||||||
p.Release()
|
p.Release()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
case Drop:
|
||||||
|
// Record it so the flow log shows the attempt, but never hand it to the receiver
|
||||||
|
r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||||
|
|
||||||
case KeepRouting:
|
case KeepRouting:
|
||||||
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
@@ -884,6 +897,10 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
|
|||||||
p.Release()
|
p.Release()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
case Drop:
|
||||||
|
// Record it so the flow log shows the attempt, but never hand it to the receiver
|
||||||
|
r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||||
|
|
||||||
case KeepRouting:
|
case KeepRouting:
|
||||||
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ require (
|
|||||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6
|
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
|
github.com/wlynxg/anet v0.0.5
|
||||||
go.uber.org/goleak v1.3.0
|
go.uber.org/goleak v1.3.0
|
||||||
go.yaml.in/yaml/v3 v3.0.4
|
go.yaml.in/yaml/v3 v3.0.4
|
||||||
golang.org/x/crypto v0.54.0
|
golang.org/x/crypto v0.54.0
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW
|
|||||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
|||||||
+27
-4
@@ -868,10 +868,28 @@ func (i *HostInfo) logger(l *slog.Logger) *slog.Logger {
|
|||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
|
|
||||||
func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
|
func localAddrs(l *slog.Logger, allowList *LocalAllowList) ([]netip.Addr, error) {
|
||||||
|
return collectLocalAddrs(l, allowList, localInterfaces, localInterfaceAddrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectLocalAddrs takes its enumerators as arguments so tests can drive the filtering and the
|
||||||
|
// failure branches without depending on the addresses of whatever host they run on. It reports
|
||||||
|
// failures to the caller rather than logging them, because it runs on every lighthouse update and
|
||||||
|
// only the caller can tell a new failure from a repeat of the same one.
|
||||||
|
func collectLocalAddrs(
|
||||||
|
l *slog.Logger,
|
||||||
|
allowList *LocalAllowList,
|
||||||
|
interfaces func() ([]net.Interface, error),
|
||||||
|
interfaceAddrs func(*net.Interface) ([]net.Addr, error),
|
||||||
|
) ([]netip.Addr, error) {
|
||||||
//FIXME: This function is pretty garbage
|
//FIXME: This function is pretty garbage
|
||||||
var finalAddrs []netip.Addr
|
var finalAddrs []netip.Addr
|
||||||
ifaces, _ := net.Interfaces()
|
var errs []error
|
||||||
|
ifaces, err := interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to enumerate local interfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, i := range ifaces {
|
for _, i := range ifaces {
|
||||||
allow := allowList.AllowName(i.Name)
|
allow := allowList.AllowName(i.Name)
|
||||||
if l.Enabled(context.Background(), logging.LevelTrace) {
|
if l.Enabled(context.Background(), logging.LevelTrace) {
|
||||||
@@ -884,7 +902,12 @@ func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
|
|||||||
if !allow {
|
if !allow {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
addrs, _ := i.Addrs()
|
addrs, err := interfaceAddrs(&i)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to get addresses for %s: %w", i.Name, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for _, rawAddr := range addrs {
|
for _, rawAddr := range addrs {
|
||||||
var addr netip.Addr
|
var addr netip.Addr
|
||||||
switch v := rawAddr.(type) {
|
switch v := rawAddr.(type) {
|
||||||
@@ -919,5 +942,5 @@ func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return finalAddrs
|
return finalAddrs, errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -401,3 +403,83 @@ func TestHostMap_RelayState(t *testing.T) {
|
|||||||
assert.Equal(t, []netip.Addr{}, h1.relayState.relays)
|
assert.Equal(t, []netip.Addr{}, h1.relayState.relays)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCollectLocalAddrs(t *testing.T) {
|
||||||
|
ifaces := []net.Interface{
|
||||||
|
{Index: 1, Name: "lo"},
|
||||||
|
{Index: 2, Name: "eth0"},
|
||||||
|
{Index: 3, Name: "docker0"},
|
||||||
|
}
|
||||||
|
addrs := map[string][]net.Addr{
|
||||||
|
"lo": {
|
||||||
|
&net.IPNet{IP: net.ParseIP("127.0.0.1"), Mask: net.CIDRMask(8, 32)},
|
||||||
|
&net.IPNet{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
|
||||||
|
},
|
||||||
|
"eth0": {
|
||||||
|
&net.IPNet{IP: net.ParseIP("10.0.0.5"), Mask: net.CIDRMask(24, 32)},
|
||||||
|
&net.IPNet{IP: net.ParseIP("fe80::1"), Mask: net.CIDRMask(64, 128)},
|
||||||
|
&net.IPAddr{IP: net.ParseIP("fd00::5")},
|
||||||
|
},
|
||||||
|
"docker0": {
|
||||||
|
&net.IPNet{IP: net.ParseIP("172.17.0.1"), Mask: net.CIDRMask(16, 32)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
enumerate := func() ([]net.Interface, error) { return ifaces, nil }
|
||||||
|
addrsFor := func(i *net.Interface) ([]net.Addr, error) { return addrs[i.Name], nil }
|
||||||
|
|
||||||
|
// Loopback and link local are dropped, everything else on every interface is kept.
|
||||||
|
out, err := collectLocalAddrs(test.NewLogger(), nil, enumerate, addrsFor)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []netip.Addr{
|
||||||
|
netip.MustParseAddr("10.0.0.5"),
|
||||||
|
netip.MustParseAddr("fd00::5"),
|
||||||
|
netip.MustParseAddr("172.17.0.1"),
|
||||||
|
}, out)
|
||||||
|
|
||||||
|
// An interface the allow list rejects by name is never asked for its addresses.
|
||||||
|
c := config.NewC(test.NewLogger())
|
||||||
|
c.Settings["allowlist"] = map[string]any{
|
||||||
|
"interfaces": map[string]any{`docker.*`: false},
|
||||||
|
}
|
||||||
|
al, err := NewLocalAllowListFromConfig(c, "allowlist")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
asked := make(map[string]struct{})
|
||||||
|
countingAddrsFor := func(i *net.Interface) ([]net.Addr, error) {
|
||||||
|
asked[i.Name] = struct{}{}
|
||||||
|
return addrs[i.Name], nil
|
||||||
|
}
|
||||||
|
out, err = collectLocalAddrs(test.NewLogger(), al, enumerate, countingAddrsFor)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []netip.Addr{
|
||||||
|
netip.MustParseAddr("10.0.0.5"),
|
||||||
|
netip.MustParseAddr("fd00::5"),
|
||||||
|
}, out)
|
||||||
|
assert.NotContains(t, asked, "docker0")
|
||||||
|
|
||||||
|
// A failure to enumerate interfaces at all is reported rather than silently advertising nothing.
|
||||||
|
out, err = collectLocalAddrs(
|
||||||
|
test.NewLogger(),
|
||||||
|
nil,
|
||||||
|
func() ([]net.Interface, error) { return nil, errors.New("netlinkrib: permission denied") },
|
||||||
|
addrsFor,
|
||||||
|
)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
require.EqualError(t, err, "failed to enumerate local interfaces: netlinkrib: permission denied")
|
||||||
|
|
||||||
|
// One interface failing is reported and skipped, the rest are still collected.
|
||||||
|
out, err = collectLocalAddrs(
|
||||||
|
test.NewLogger(),
|
||||||
|
nil,
|
||||||
|
enumerate,
|
||||||
|
func(i *net.Interface) ([]net.Addr, error) {
|
||||||
|
if i.Name == "eth0" {
|
||||||
|
return nil, errors.New("nope")
|
||||||
|
}
|
||||||
|
return addrs[i.Name], nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert.Equal(t, []netip.Addr{netip.MustParseAddr("172.17.0.1")}, out)
|
||||||
|
require.EqualError(t, err, "failed to get addresses for eth0: nope")
|
||||||
|
}
|
||||||
|
|||||||
+27
-1
@@ -40,6 +40,10 @@ type LightHouse struct {
|
|||||||
// addresses rather than whatever this machine's NICs happen to be. Set it before Start.
|
// addresses rather than whatever this machine's NICs happen to be. Set it before Start.
|
||||||
localAddrsFn func(*LocalAllowList) []netip.Addr
|
localAddrsFn func(*LocalAllowList) []netip.Addr
|
||||||
|
|
||||||
|
// lastLocalAddrsErr is the previous localAddrsFn failure. Enumeration runs on every update, so an
|
||||||
|
// unchanged failure is demoted to Debug rather than warning every lighthouse.interval forever.
|
||||||
|
lastLocalAddrsErr atomic.Pointer[string]
|
||||||
|
|
||||||
// Local cache of answers from light houses
|
// Local cache of answers from light houses
|
||||||
// map of vpn addr to answers
|
// map of vpn addr to answers
|
||||||
addrMap map[netip.Addr]*RemoteList
|
addrMap map[netip.Addr]*RemoteList
|
||||||
@@ -112,7 +116,9 @@ func NewLightHouseFromConfig(ctx context.Context, l *slog.Logger, c *config.C, c
|
|||||||
l: l,
|
l: l,
|
||||||
}
|
}
|
||||||
h.localAddrsFn = func(al *LocalAllowList) []netip.Addr {
|
h.localAddrsFn = func(al *LocalAllowList) []netip.Addr {
|
||||||
return localAddrs(h.l, al)
|
addrs, err := localAddrs(h.l, al)
|
||||||
|
h.logLocalAddrsErr(err)
|
||||||
|
return addrs
|
||||||
}
|
}
|
||||||
|
|
||||||
lighthouses := make([]netip.Addr, 0)
|
lighthouses := make([]netip.Addr, 0)
|
||||||
@@ -913,6 +919,26 @@ func (lh *LightHouse) TriggerUpdate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logLocalAddrsErr reports a localAddrs failure at Warn the first time it is seen and at Debug while
|
||||||
|
// it persists unchanged, so a permanent failure does not warn on every update forever.
|
||||||
|
func (lh *LightHouse) logLocalAddrsErr(err error) {
|
||||||
|
if err == nil {
|
||||||
|
lh.lastLocalAddrsErr.Store(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := err.Error()
|
||||||
|
prev := lh.lastLocalAddrsErr.Swap(&msg)
|
||||||
|
if prev != nil && *prev == msg {
|
||||||
|
if lh.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
|
lh.l.Debug("Failed to collect local addresses to advertise", "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lh.l.Warn("Failed to collect local addresses to advertise", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
func (lh *LightHouse) SendUpdate() {
|
func (lh *LightHouse) SendUpdate() {
|
||||||
var v4 []*V4AddrPort
|
var v4 []*V4AddrPort
|
||||||
var v6 []*V6AddrPort
|
var v6 []*V6AddrPort
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -738,3 +740,32 @@ func TestLighthouse_DeletesWork(t *testing.T) {
|
|||||||
out = lh.Query(testHost)
|
out = lh.Query(testHost)
|
||||||
assert.Nil(t, out)
|
assert.Nil(t, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLightHouse_logLocalAddrsErr(t *testing.T) {
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
lh := &LightHouse{l: test.NewLoggerWithOutput(out)}
|
||||||
|
|
||||||
|
// The first sighting of a failure warns.
|
||||||
|
lh.logLocalAddrsErr(errors.New("permission denied"))
|
||||||
|
assert.Contains(t, out.String(), "level=WARN")
|
||||||
|
assert.Contains(t, out.String(), "permission denied")
|
||||||
|
|
||||||
|
// Repeating unchanged does not warn again, which is what keeps a permanent failure from warning
|
||||||
|
// on every lighthouse.interval for the life of the process.
|
||||||
|
out.Reset()
|
||||||
|
lh.logLocalAddrsErr(errors.New("permission denied"))
|
||||||
|
assert.NotContains(t, out.String(), "level=WARN")
|
||||||
|
|
||||||
|
// A different failure is a new event and warns.
|
||||||
|
out.Reset()
|
||||||
|
lh.logLocalAddrsErr(errors.New("something else"))
|
||||||
|
assert.Contains(t, out.String(), "level=WARN")
|
||||||
|
assert.Contains(t, out.String(), "something else")
|
||||||
|
|
||||||
|
// Recovering resets, so the same failure returning later warns again.
|
||||||
|
out.Reset()
|
||||||
|
lh.logLocalAddrsErr(nil)
|
||||||
|
assert.Empty(t, out.String())
|
||||||
|
lh.logLocalAddrsErr(errors.New("something else"))
|
||||||
|
assert.Contains(t, out.String(), "level=WARN")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package nebula
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
func localInterfaces() ([]net.Interface, error) {
|
||||||
|
return net.Interfaces()
|
||||||
|
}
|
||||||
|
|
||||||
|
func localInterfaceAddrs(i *net.Interface) ([]net.Addr, error) {
|
||||||
|
return i.Addrs()
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/wlynxg/anet"
|
||||||
|
)
|
||||||
|
|
||||||
|
// anet relies on //go:linkname and so needs -ldflags=-checklinkname=0 on Go 1.23+. Nebula ships no
|
||||||
|
// Android binaries of its own, so that burden falls on consumers linking Android artifacts.
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// anet only takes its bind-free path when it believes it is on API 30+, and detecting the running
|
||||||
|
// device's level requires cgo. Pin it so a CGO_ENABLED=0 build cannot quietly fall back to the
|
||||||
|
// denied path. The bind-free path is correct on older releases too, just unnecessary there.
|
||||||
|
anet.SetAndroidVersion(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The app sandbox denies bind() on netlink_route_socket, so the stdlib's RTM_GETLINK enumeration
|
||||||
|
// fails with EACCES and we advertise no underlay addresses at all. anet reads RTM_GETADDR from an
|
||||||
|
// unbound socket instead, so this must not be collapsed back into net.Interfaces.
|
||||||
|
func localInterfaces() ([]net.Interface, error) {
|
||||||
|
return anet.Interfaces()
|
||||||
|
}
|
||||||
|
|
||||||
|
// net.Interface.Addrs goes back through the denied netlink path, so addresses have to come from anet
|
||||||
|
// as well. anet cannot report HardwareAddr, which localAddrs does not read.
|
||||||
|
func localInterfaceAddrs(i *net.Interface) ([]net.Addr, error) {
|
||||||
|
return anet.InterfaceAddrsByInterface(i)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user