mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 03:57:03 +02:00
Advertise underlay addresses on Android
On Android 11+ the app sandbox denies bind() on netlink_route_socket, so the stdlib's net.Interfaces fails with EACCES. localAddrs discarded that error and returned an empty slice, so the node advertised no underlay addresses and peers could only ever reach it at the address a lighthouse observed. A device on the same LAN as a peer was unreachable at its LAN address. Split interface enumeration behind a build-tagged seam and use github.com/wlynxg/anet on Android, which reads RTM_GETADDR from an unbound socket. Interface addresses have to come from anet as well, since net.Interface.Addrs goes back through the same denied path. Every other platform keeps the net package implementation. Stop discarding the enumeration errors, which are exceptional now that the sandbox case is handled. anet needs -ldflags=-checklinkname=0 on Go 1.23+. Nebula ships no Android binaries, so build-test-mobile is unaffected, but consumers linking Android artifacts will need the flag.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+28
-2
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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