diff --git a/Makefile b/Makefile index c4a281a6..1ee0a9ff 100644 --- a/Makefile +++ b/Makefile @@ -222,11 +222,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=. diff --git a/go.mod b/go.mod index 35a910c9..d8c35b57 100644 --- a/go.mod +++ b/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.11.1 - github.com/vishvananda/netlink v1.3.1 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 27dcbc4a..6b3713a9 100644 --- a/go.sum +++ b/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= @@ -143,8 +145,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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -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= @@ -204,8 +204,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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/hostmap.go b/hostmap.go index 45515fc3..373c5cc3 100644 --- a/hostmap.go +++ b/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 @@ -865,59 +863,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 -} diff --git a/lighthouse.go b/lighthouse.go index 9cece233..bbd5ad49 100644 --- a/lighthouse.go +++ b/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" ) @@ -40,6 +41,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 @@ -112,7 +117,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) @@ -913,6 +920,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 diff --git a/lighthouse_test.go b/lighthouse_test.go index 81c883ff..e530d6bc 100644 --- a/lighthouse_test.go +++ b/lighthouse_test.go @@ -1,7 +1,9 @@ package nebula import ( + "bytes" "encoding/binary" + "errors" "fmt" "net/netip" "testing" @@ -738,3 +740,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") +} diff --git a/nicfinder/localaddrs.go b/nicfinder/localaddrs.go new file mode 100644 index 00000000..2f7bd81e --- /dev/null +++ b/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...) +} diff --git a/nicfinder/localaddrs_android.go b/nicfinder/localaddrs_android.go new file mode 100644 index 00000000..b66ecbbf --- /dev/null +++ b/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 +} diff --git a/nicfinder/nicfinder.go b/nicfinder/nicfinder.go new file mode 100644 index 00000000..064e59e6 --- /dev/null +++ b/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 +} diff --git a/nicfinder/nicfinder_test.go b/nicfinder/nicfinder_test.go new file mode 100644 index 00000000..5b56a518 --- /dev/null +++ b/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") +} diff --git a/overlay/tun_linux.go b/overlay/tun_linux.go index 52633842..ba7a4570 100644 --- a/overlay/tun_linux.go +++ b/overlay/tun_linux.go @@ -18,11 +18,11 @@ import ( "time" "unsafe" + "github.com/DefinedNet/netlink" "github.com/gaissmai/bart" "github.com/slackhq/nebula/config" "github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/util" - "github.com/vishvananda/netlink" "golang.org/x/sys/unix" )