diff --git a/go.mod b/go.mod index 9748492d..aa754e1a 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ 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 diff --git a/go.sum b/go.sum index 29d68429..f9ef15a6 100644 --- a/go.sum +++ b/go.sum @@ -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/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= diff --git a/hostmap.go b/hostmap.go index 45515fc3..7feb9f55 100644 --- a/hostmap.go +++ b/hostmap.go @@ -869,9 +869,27 @@ func (i *HostInfo) logger(l *slog.Logger) *slog.Logger { // Utility functions func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr { + 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. +func collectLocalAddrs( + l *slog.Logger, + allowList *LocalAllowList, + interfaces func() ([]net.Interface, error), + interfaceAddrs func(*net.Interface) ([]net.Addr, error), +) []netip.Addr { //FIXME: This function is pretty garbage var finalAddrs []netip.Addr - ifaces, _ := net.Interfaces() + ifaces, err := interfaces() + if err != nil { + l.Warn("Failed to enumerate local interfaces, no underlay addresses will be advertised to lighthouses", + "error", err, + ) + return nil + } + for _, i := range ifaces { allow := allowList.AllowName(i.Name) if l.Enabled(context.Background(), logging.LevelTrace) { @@ -884,7 +902,15 @@ func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr { if !allow { continue } - addrs, _ := i.Addrs() + addrs, err := interfaceAddrs(&i) + if err != nil { + l.Warn("Failed to get addresses for local interface", + "error", err, + "interfaceName", i.Name, + ) + continue + } + for _, rawAddr := range addrs { var addr netip.Addr switch v := rawAddr.(type) { diff --git a/hostmap_test.go b/hostmap_test.go index 9cfebe17..b4991684 100644 --- a/hostmap_test.go +++ b/hostmap_test.go @@ -1,6 +1,9 @@ package nebula import ( + "bytes" + "errors" + "net" "net/netip" "slices" "testing" @@ -401,3 +404,85 @@ 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 := collectLocalAddrs(test.NewLogger(), nil, enumerate, addrsFor) + 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 = collectLocalAddrs(test.NewLogger(), al, enumerate, countingAddrsFor) + 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. + logOut := &bytes.Buffer{} + out = collectLocalAddrs( + test.NewLoggerWithOutput(logOut), + nil, + func() ([]net.Interface, error) { return nil, errors.New("netlinkrib: permission denied") }, + addrsFor, + ) + assert.Nil(t, out) + assert.Contains(t, logOut.String(), "Failed to enumerate local interfaces") + assert.Contains(t, logOut.String(), "netlinkrib: permission denied") + + // One interface failing is reported and skipped, the rest are still collected. + logOut.Reset() + out = collectLocalAddrs( + test.NewLoggerWithOutput(logOut), + 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) + assert.Contains(t, logOut.String(), "Failed to get addresses for local interface") + assert.Contains(t, logOut.String(), "eth0") +} diff --git a/localaddrs.go b/localaddrs.go new file mode 100644 index 00000000..50ce83be --- /dev/null +++ b/localaddrs.go @@ -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() +} diff --git a/localaddrs_android.go b/localaddrs_android.go new file mode 100644 index 00000000..de43f981 --- /dev/null +++ b/localaddrs_android.go @@ -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) +}