Compare commits

..

1 Commits

Author SHA1 Message Date
John Maguire 6d124d0441 Tolerate ErrDumpInterrupted when listing tun addresses (#1835)
smoke-extra / freebsd-amd64 (push) Failing after 37s
smoke-extra / linux-amd64-ipv6disable (push) Failing after 11s
smoke-extra / netbsd-amd64 (push) Failing after 12s
smoke-extra / openbsd-amd64 (push) Failing after 13s
smoke-extra / linux-386 (push) Failing after 14s
smoke / Run multi node smoke test (push) Failing after 1m41s
Build and test / Static checks (push) Successful in 2m10s
Build and test / Test linux (push) Failing after 1m32s
Build and test / Test linux-boringcrypto (push) Failing after 2m45s
Build and test / Test linux-pkcs11 (push) Failing after 1m57s
Build and test / Cross-build linux-arm (push) Successful in 3m8s
Build and test / Cross-build linux-mips (push) Successful in 3m49s
Build and test / Cross-build linux-other (push) Successful in 3m10s
Build and test / Cross-build windows (push) Successful in 1m3s
Build and test / Cross-build freebsd (push) Successful in 1m35s
Build and test / Cross-build netbsd (push) Successful in 1m33s
Build and test / Cross-build openbsd (push) Successful in 1m36s
Build and test / Cross-build mobile (push) Successful in 3m22s
smoke-extra / Run windows smoke test (push) Has been cancelled
Build and test / Test macos (push) Has been cancelled
Build and test / Test windows (push) Has been cancelled
Build and test / CI status (push) Has been cancelled
2026-07-31 19:09:28 +00:00
10 changed files with 16 additions and 219 deletions
-3
View File
@@ -222,14 +222,11 @@ test-cov-html:
go test -coverprofile=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:
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=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 -ldflags=-checklinkname=0 -o /dev/null ${NEBULA_CMD_PATH}
bench:
go test -bench=.
-1
View File
@@ -22,7 +22,6 @@ require (
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6
github.com/stretchr/testify v1.11.1
github.com/vishvananda/netlink v1.3.1
github.com/wlynxg/anet v0.0.5
go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.54.0
-2
View File
@@ -149,8 +149,6 @@ 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/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+4 -27
View File
@@ -868,28 +868,10 @@ func (i *HostInfo) logger(l *slog.Logger) *slog.Logger {
// Utility functions
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) {
func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
//FIXME: This function is pretty garbage
var finalAddrs []netip.Addr
var errs []error
ifaces, err := interfaces()
if err != nil {
return nil, fmt.Errorf("failed to enumerate local interfaces: %w", err)
}
ifaces, _ := net.Interfaces()
for _, i := range ifaces {
allow := allowList.AllowName(i.Name)
if l.Enabled(context.Background(), logging.LevelTrace) {
@@ -902,12 +884,7 @@ func collectLocalAddrs(
if !allow {
continue
}
addrs, err := interfaceAddrs(&i)
if err != nil {
errs = append(errs, fmt.Errorf("failed to get addresses for %s: %w", i.Name, err))
continue
}
addrs, _ := i.Addrs()
for _, rawAddr := range addrs {
var addr netip.Addr
switch v := rawAddr.(type) {
@@ -942,5 +919,5 @@ func collectLocalAddrs(
}
}
}
return finalAddrs, errors.Join(errs...)
return finalAddrs
}
-82
View File
@@ -1,8 +1,6 @@
package nebula
import (
"errors"
"net"
"net/netip"
"slices"
"testing"
@@ -403,83 +401,3 @@ func TestHostMap_RelayState(t *testing.T) {
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")
}
+1 -27
View File
@@ -40,10 +40,6 @@ type LightHouse struct {
// addresses rather than whatever this machine's NICs happen to be. Set it before Start.
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
// map of vpn addr to answers
addrMap map[netip.Addr]*RemoteList
@@ -116,9 +112,7 @@ func NewLightHouseFromConfig(ctx context.Context, l *slog.Logger, c *config.C, c
l: l,
}
h.localAddrsFn = func(al *LocalAllowList) []netip.Addr {
addrs, err := localAddrs(h.l, al)
h.logLocalAddrsErr(err)
return addrs
return localAddrs(h.l, al)
}
lighthouses := make([]netip.Addr, 0)
@@ -919,26 +913,6 @@ 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() {
var v4 []*V4AddrPort
var v6 []*V6AddrPort
-31
View File
@@ -1,9 +1,7 @@
package nebula
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"net/netip"
"testing"
@@ -740,32 +738,3 @@ func TestLighthouse_DeletesWork(t *testing.T) {
out = lh.Query(testHost)
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")
}
-13
View File
@@ -1,13 +0,0 @@
//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()
}
-32
View File
@@ -1,32 +0,0 @@
//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)
}
+11 -1
View File
@@ -5,6 +5,7 @@ package overlay
import (
"encoding/binary"
"errors"
"fmt"
"io"
"log/slog"
@@ -483,7 +484,16 @@ func (t *tun) addIPs(link netlink.Link) error {
//iterate over remainder, remove whoever shouldn't be there
al, err := netlink.AddrList(link, netlink.FAMILY_ALL)
if err != nil {
return fmt.Errorf("failed to get tun address list: %s", err)
//RTM_GETADDR dumps the whole system, so any concurrent address change
//interrupts it - including the kernel's async tentative->preferred
//flip of an IPv6 address the AddrReplace calls above just added,
//which makes this a race against our own setup. Partial results are
//still returned; the worst case is a stale address surviving until
//the next config reload, which beats failing startup over it.
if !errors.Is(err, netlink.ErrDumpInterrupted) {
return fmt.Errorf("failed to get tun address list: %s", err)
}
t.l.Warn("tun address list dump was interrupted, stale addresses may remain")
}
for i := range al {