Ver código fonte

Advertise underlay addresses on Android (#1881)

Jack Doan 4 semanas atrás
pai
commit
3a45c01656
11 arquivos alterados com 385 adições e 65 exclusões
  1. 3 0
      Makefile
  2. 1 1
      go.mod
  3. 2 4
      go.sum
  4. 0 58
      hostmap.go
  5. 28 1
      lighthouse.go
  6. 31 0
      lighthouse_test.go
  7. 39 0
      nicfinder/localaddrs.go
  8. 104 0
      nicfinder/localaddrs_android.go
  9. 59 0
      nicfinder/nicfinder.go
  10. 117 0
      nicfinder/nicfinder_test.go
  11. 1 1
      overlay/tun_linux.go

+ 3 - 0
Makefile

@@ -242,11 +242,14 @@ 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
+# 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 -o /dev/null ${NEBULA_CMD_PATH}
 
 bench:
 	go test -bench=.

+ 1 - 1
go.mod

@@ -5,6 +5,7 @@ go 1.26.0
 require (
 	dario.cat/mergo v1.0.2
 	filippo.io/bigmod v0.1.0
+	github.com/DefinedNet/netlink v1.3.2-dn.1
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
 	github.com/armon/go-radix v1.0.0
 	github.com/flynn/noise v1.1.0
@@ -20,7 +21,6 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6
 	github.com/stretchr/testify v1.12.1
-	github.com/vishvananda/netlink v1.3.1
 	go.uber.org/goleak v1.3.0
 	go.yaml.in/yaml/v3 v3.0.5
 	golang.org/x/crypto v0.57.0

+ 2 - 4
go.sum

@@ -3,6 +3,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
 dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
 filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
 filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
+github.com/DefinedNet/netlink v1.3.2-dn.1 h1:Md2PsUE1Xuy1Zxs/Bm6356n1kt5bq2HspXlPJ6pEspg=
+github.com/DefinedNet/netlink v1.3.2-dn.1/go.mod h1:o4YSn8Jw7zoDcYXDmV77TRd4eS5sSLuE+vu2zAN67C8=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -138,8 +140,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
 github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
-github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
-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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -195,8 +195,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
 golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

+ 0 - 58
hostmap.go

@@ -6,7 +6,6 @@ import (
 	"errors"
 	"fmt"
 	"log/slog"
-	"net"
 	"net/netip"
 	"slices"
 	"sync"
@@ -18,7 +17,6 @@ import (
 	"github.com/slackhq/nebula/cert"
 	"github.com/slackhq/nebula/config"
 	"github.com/slackhq/nebula/header"
-	"github.com/slackhq/nebula/logging"
 )
 
 const defaultPromoteEvery = 1000       // Count of packets sent before we try moving a tunnel to a preferred underlay ip address
@@ -930,59 +928,3 @@ func (i *HostInfo) logger(l *slog.Logger) *slog.Logger {
 
 	return li
 }
-
-// Utility functions
-
-func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
-	//FIXME: This function is pretty garbage
-	var finalAddrs []netip.Addr
-	ifaces, _ := net.Interfaces()
-	for _, i := range ifaces {
-		allow := allowList.AllowName(i.Name)
-		if l.Enabled(context.Background(), logging.LevelTrace) {
-			l.Log(context.Background(), logging.LevelTrace, "localAllowList.AllowName",
-				"interfaceName", i.Name,
-				"allow", allow,
-			)
-		}
-
-		if !allow {
-			continue
-		}
-		addrs, _ := i.Addrs()
-		for _, rawAddr := range addrs {
-			var addr netip.Addr
-			switch v := rawAddr.(type) {
-			case *net.IPNet:
-				//continue
-				addr, _ = netip.AddrFromSlice(v.IP)
-			case *net.IPAddr:
-				addr, _ = netip.AddrFromSlice(v.IP)
-			}
-
-			if !addr.IsValid() {
-				if l.Enabled(context.Background(), slog.LevelDebug) {
-					l.Debug("addr was invalid", "localAddr", rawAddr)
-				}
-				continue
-			}
-			addr = addr.Unmap()
-
-			if addr.IsLoopback() == false && addr.IsLinkLocalUnicast() == false {
-				isAllowed := allowList.Allow(addr)
-				if l.Enabled(context.Background(), logging.LevelTrace) {
-					l.Log(context.Background(), logging.LevelTrace, "localAllowList.Allow",
-						"localAddr", addr,
-						"allowed", isAllowed,
-					)
-				}
-				if !isAllowed {
-					continue
-				}
-
-				finalAddrs = append(finalAddrs, addr)
-			}
-		}
-	}
-	return finalAddrs
-}

+ 28 - 1
lighthouse.go

@@ -19,6 +19,7 @@ import (
 	"github.com/slackhq/nebula/config"
 	"github.com/slackhq/nebula/header"
 	"github.com/slackhq/nebula/logging"
+	"github.com/slackhq/nebula/nicfinder"
 	"github.com/slackhq/nebula/udp"
 	"github.com/slackhq/nebula/util"
 )
@@ -42,6 +43,10 @@ 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
@@ -115,7 +120,9 @@ func NewLightHouseFromConfig(ctx context.Context, l *slog.Logger, c *config.C, c
 		l:                  l,
 	}
 	h.localAddrsFn = func(al *LocalAllowList) []netip.Addr {
-		return localAddrs(h.l, al)
+		addrs, err := nicfinder.Find(ctx, h.l, al)
+		h.logLocalAddrsErr(err)
+		return addrs
 	}
 
 	lighthouses := make([]netip.Addr, 0)
@@ -916,6 +923,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() {
 	var v4 []*V4AddrPort
 	var v6 []*V6AddrPort

+ 31 - 0
lighthouse_test.go

@@ -1,7 +1,9 @@
 package nebula
 
 import (
+	"bytes"
 	"encoding/binary"
+	"errors"
 	"fmt"
 	"net/netip"
 	"testing"
@@ -764,3 +766,32 @@ 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")
+}

+ 39 - 0
nicfinder/localaddrs.go

@@ -0,0 +1,39 @@
+//go:build !android
+
+package nicfinder
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log/slog"
+	"net"
+	"net/netip"
+)
+
+func localAddrs(ctx context.Context, l *slog.Logger, filter Filter) ([]netip.Addr, error) {
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		return nil, fmt.Errorf("failed to enumerate local interfaces: %w", err)
+	}
+
+	var out []netip.Addr
+	var errs []error
+
+	for _, i := range ifaces {
+		if !allowName(ctx, l, filter, i.Name) {
+			continue
+		}
+		addrs, err := i.Addrs()
+		if err != nil {
+			errs = append(errs, fmt.Errorf("failed to get addresses for %s: %w", i.Name, err))
+			continue
+		}
+		for _, rawAddr := range addrs {
+			if addr, ok := allowedAddr(ctx, l, filter, rawAddr); ok {
+				out = append(out, addr)
+			}
+		}
+	}
+	return out, errors.Join(errs...)
+}

+ 104 - 0
nicfinder/localaddrs_android.go

@@ -0,0 +1,104 @@
+//go:build android
+
+package nicfinder
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log/slog"
+	"net/netip"
+	"os"
+
+	"github.com/DefinedNet/netlink"
+	"github.com/DefinedNet/netlink/nl"
+	"golang.org/x/sys/unix"
+)
+
+// Android denies bind() on netlink route sockets and RTM_GETLINK, so net.Interfaces fails with EACCES.
+// Enumerate with an RTM_GETADDR dump on an unbound socket instead.
+
+// localInterfaces returns every interface that has an address, with its addresses. It lists
+// addresses over a netlink socket that is never bound and names each index with SIOCGIFNAME, the
+// same operations Bionic's getifaddrs uses for app UIDs, which the sandbox permits.
+func localAddrs(ctx context.Context, l *slog.Logger, filter Filter) ([]netip.Addr, error) {
+	var errs []error
+	addrs, err := netlinkAddrList()
+	if err != nil {
+		if !errors.Is(err, netlink.ErrDumpInterrupted) {
+			return nil, fmt.Errorf("failed to enumerate local interfaces: %w", err)
+		}
+		// The address table kept changing under the dump and netlink gave up retrying. What it
+		// returned is still worth advertising.
+		errs = append(errs, fmt.Errorf("local addresses may be incomplete: %w", err))
+	}
+
+	// Any socket will do for SIOCGIFNAME.
+	sfd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM|unix.SOCK_CLOEXEC, 0)
+	if err != nil {
+		return nil, os.NewSyscallError("socket", err)
+	}
+	defer unix.Close(sfd)
+
+	var out []netip.Addr
+	nameAllowed := make(map[int]bool) // by interface index, once named
+	for _, a := range addrs {
+		allow, named := nameAllowed[a.LinkIndex]
+		if (named && !allow) || a.IPNet == nil {
+			continue
+		}
+		addr, ok := allowedAddr(ctx, l, filter, a.IPNet)
+		if !ok {
+			continue
+		}
+		if !named {
+			name, err := interfaceNameByIndex(sfd, a.LinkIndex)
+			// ENODEV means the interface went away after the dump. Any other failure is reported
+			// and the interface skipped, so the rest are still returned.
+			if err != nil && !errors.Is(err, unix.ENODEV) {
+				errs = append(errs, fmt.Errorf("failed to resolve name of interface %d: %w", a.LinkIndex, err))
+			}
+			allow = err == nil && allowName(ctx, l, filter, name)
+			nameAllowed[a.LinkIndex] = allow
+		}
+		if allow {
+			out = append(out, addr)
+		}
+	}
+	return out, errors.Join(errs...)
+}
+
+// netlinkAddrList is netlink.AddrList over a socket that is never bound. The kernel autobinds it
+// on the first send, which the sandbox permits.
+func netlinkAddrList() ([]netlink.Addr, error) {
+	fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_ROUTE)
+	if err != nil {
+		return nil, os.NewSyscallError("socket", err)
+	}
+	s, err := nl.NewNetlinkSocketFromFd(fd)
+	if err != nil {
+		unix.Close(fd)
+		return nil, err
+	}
+
+	h := netlink.NewHandleFromSockets(
+		map[int]*nl.SocketHandle{unix.NETLINK_ROUTE: {Socket: s}},
+		netlink.HandleOptions{RetryInterrupted: true},
+	)
+	defer h.Close()
+	if err := h.SetSocketTimeout(netlink.GetSocketTimeout()); err != nil {
+		return nil, err
+	}
+
+	return h.AddrList(nil, netlink.FAMILY_ALL)
+}
+
+// interfaceNameByIndex resolves an interface index with SIOCGIFNAME.
+func interfaceNameByIndex(fd int, index int) (string, error) {
+	var ifr unix.Ifreq
+	ifr.SetUint32(uint32(index)) // ifr_ifindex is in the ifr_ifru union
+	if err := unix.IoctlIfreq(fd, unix.SIOCGIFNAME, &ifr); err != nil {
+		return "", err
+	}
+	return ifr.Name(), nil
+}

+ 59 - 0
nicfinder/nicfinder.go

@@ -0,0 +1,59 @@
+package nicfinder
+
+import (
+	"context"
+	"log/slog"
+	"net"
+	"net/netip"
+
+	"github.com/slackhq/nebula/logging"
+)
+
+// Filter decides which interfaces, by name, and which of their addresses Find returns.
+type Filter interface {
+	AllowName(string) bool
+	Allow(netip.Addr) bool
+}
+
+// Find returns the addresses of this host's network interfaces that allowList accepts, with
+// loopback and link-local addresses left out. When only some interfaces could be read it returns
+// their addresses alongside an error for the rest. allowList must not be nil; it is called as
+// given, so a nil pointer of a type whose methods accept a nil receiver is fine.
+func Find(ctx context.Context, l *slog.Logger, allowList Filter) ([]netip.Addr, error) {
+	return localAddrs(ctx, l, allowList)
+}
+
+// allowName asks filter about an interface name and traces the answer.
+func allowName(ctx context.Context, l *slog.Logger, filter Filter, name string) bool {
+	allow := filter.AllowName(name)
+	if l.Enabled(ctx, logging.LevelTrace) {
+		l.Log(ctx, logging.LevelTrace, "localAllowList.AllowName", "interfaceName", name, "allow", allow)
+	}
+	return allow
+}
+
+// allowedAddr converts one address a platform reports and says whether to advertise it: it must
+// be a *net.IPNet or *net.IPAddr carrying an IP, not be loopback or link-local, and be accepted by
+// filter, whose answer is traced. IPv4 comes back unmapped.
+func allowedAddr(ctx context.Context, l *slog.Logger, filter Filter, rawAddr net.Addr) (netip.Addr, bool) {
+	var ip net.IP
+	switch v := rawAddr.(type) {
+	case *net.IPNet:
+		ip = v.IP
+	case *net.IPAddr:
+		ip = v.IP
+	}
+	addr, ok := netip.AddrFromSlice(ip)
+	if !ok {
+		return netip.Addr{}, false
+	}
+	addr = addr.Unmap()
+	if addr.IsLoopback() || addr.IsLinkLocalUnicast() {
+		return netip.Addr{}, false
+	}
+	isAllowed := filter.Allow(addr)
+	if l.Enabled(ctx, logging.LevelTrace) {
+		l.Log(ctx, logging.LevelTrace, "localAllowList.Allow", "localAddr", addr, "allowed", isAllowed)
+	}
+	return addr, isAllowed
+}

+ 117 - 0
nicfinder/nicfinder_test.go

@@ -0,0 +1,117 @@
+package nicfinder
+
+import (
+	"context"
+	"net"
+	"net/netip"
+	"regexp"
+	"testing"
+
+	"github.com/slackhq/nebula/test"
+	"github.com/stretchr/testify/assert"
+)
+
+type allowAll struct{}
+
+func (allowAll) Allow(netip.Addr) bool { return true }
+func (allowAll) AllowName(string) bool { return true }
+
+type testAllowList struct {
+	bannedName *regexp.Regexp
+	bannedAddr netip.Addr
+}
+
+func (l *testAllowList) Allow(addr netip.Addr) bool {
+	return addr != l.bannedAddr
+}
+
+func (l *testAllowList) AllowName(n string) bool {
+	return !l.bannedName.MatchString(n)
+}
+
+func newTestAllowList(bannedName string) *testAllowList {
+	return &testAllowList{
+		bannedName: regexp.MustCompile(bannedName),
+	}
+}
+
+func TestAllowedAddr(t *testing.T) {
+	ctx, l := context.Background(), test.NewLogger()
+	allowed := func(filter Filter, addrs []net.Addr) []netip.Addr {
+		var out []netip.Addr
+		for _, rawAddr := range addrs {
+			if addr, ok := allowedAddr(ctx, l, filter, rawAddr); ok {
+				out = append(out, addr)
+			}
+		}
+		return out
+	}
+	addrs := []net.Addr{
+		&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)},
+		&net.IPNet{IP: net.ParseIP("10.0.0.5"), Mask: net.CIDRMask(24, 32)},
+		&net.IPNet{IP: net.ParseIP("169.254.10.10"), Mask: net.CIDRMask(16, 32)},
+		&net.IPNet{IP: net.ParseIP("fe80::1"), Mask: net.CIDRMask(64, 128)},
+		&net.IPAddr{IP: net.ParseIP("fd00::5")},
+		&net.IPNet{},
+		otherAddr{},
+		nil,
+	}
+
+	// Loopback, link local and anything without an IP are dropped, and IPv4 comes back unmapped
+	// even though net.ParseIP hands over a 16 byte slice.
+	assert.Equal(t, []netip.Addr{
+		netip.MustParseAddr("10.0.0.5"),
+		netip.MustParseAddr("fd00::5"),
+	}, allowed(allowAll{}, addrs))
+
+	// The filter sees the unmapped form and can drop a single address.
+	al := newTestAllowList("none")
+	al.bannedAddr = netip.MustParseAddr("10.0.0.5")
+	assert.Equal(t, []netip.Addr{netip.MustParseAddr("fd00::5")}, allowed(al, addrs))
+}
+
+// otherAddr is a net.Addr that is neither *net.IPNet nor *net.IPAddr. netlink.Addr is one such
+// type: it satisfies net.Addr through an embedded *net.IPNet, and passing it directly once
+// silently produced no addresses.
+type otherAddr struct{}
+
+func (otherAddr) Network() string { return "other" }
+func (otherAddr) String() string  { return "other" }
+
+// hostFilter counts AllowName calls and can reject every name or every address.
+type hostFilter struct {
+	rejectNames bool
+	rejectAddrs bool
+	asked       map[string]int
+}
+
+func (f *hostFilter) Allow(netip.Addr) bool { return !f.rejectAddrs }
+func (f *hostFilter) AllowName(name string) bool {
+	f.asked[name]++
+	return !f.rejectNames
+}
+
+// Runs this platform's localAddrs against the host and checks it honors the whole filter.
+func TestLocalAddrsAppliesFilter(t *testing.T) {
+	ctx, l := context.Background(), test.NewLogger()
+
+	f := &hostFilter{asked: map[string]int{}}
+	all, err := localAddrs(ctx, l, f)
+	if len(f.asked) == 0 {
+		t.Skipf("no interfaces to enumerate here: %v", err)
+	}
+	for name, n := range f.asked {
+		assert.Equal(t, 1, n, "AllowName asked %d times for %s", n, name)
+	}
+	// Loopback and link-local never come back, whatever the filter says.
+	for _, addr := range all {
+		assert.False(t, addr.IsLoopback() || addr.IsLinkLocalUnicast(), "%s", addr)
+	}
+
+	none, _ := localAddrs(ctx, l, &hostFilter{rejectNames: true, asked: map[string]int{}})
+	assert.Empty(t, none, "addresses got past a filter that rejects every interface name")
+
+	none, _ = localAddrs(ctx, l, &hostFilter{rejectAddrs: true, asked: map[string]int{}})
+	assert.Empty(t, none, "addresses got past a filter that rejects every address")
+}

+ 1 - 1
overlay/tun_linux.go

@@ -15,12 +15,12 @@ import (
 	"time"
 	"unsafe"
 
+	"github.com/DefinedNet/netlink"
 	"github.com/gaissmai/bart"
 	"github.com/slackhq/nebula/config"
 	"github.com/slackhq/nebula/overlay/tio"
 	"github.com/slackhq/nebula/routing"
 	"github.com/slackhq/nebula/util"
-	"github.com/vishvananda/netlink"
 	"golang.org/x/sys/unix"
 )