From 55095df6445f15be35d64dc36c23b719be62be5e Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 28 Feb 2022 14:49:37 -0800 Subject: [PATCH] net/interfaces: get Linux default route from netlink as fallback If it's in a non-standard table, as it is on Unifi UDM Pro, apparently. Updates #4038 (probably fixes, but don't have hardware to verify) Change-Id: I2cb9a098d8bb07d1a97a6045b686aca31763a937 Signed-off-by: Brad Fitzpatrick --- cmd/tailscale/depaware.txt | 7 +++ cmd/tailscaled/depaware.txt | 2 +- net/interfaces/interfaces.go | 3 +- net/interfaces/interfaces_linux.go | 69 +++++++++++++++++++++++-- net/interfaces/interfaces_linux_test.go | 13 +++++ 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 829b3c9fa..6bd4bad83 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -4,8 +4,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy github.com/golang/groupcache/lru from tailscale.com/net/dnscache + L github.com/josharian/native from github.com/mdlayher/netlink+ + L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces + L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli L github.com/klauspost/compress/flate from nhooyr.io/websocket + L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+ + L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+ + L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink 💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+ github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli @@ -96,6 +102,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305 golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+ + L golang.org/x/net/bpf from github.com/mdlayher/netlink+ golang.org/x/net/dns/dnsmessage from net+ golang.org/x/net/http/httpguts from net/http+ golang.org/x/net/http/httpproxy from net/http diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 1ce47615c..f112cdec7 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -74,7 +74,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4 L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm L github.com/josharian/native from github.com/mdlayher/netlink+ - L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor + L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor+ L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink github.com/klauspost/compress from github.com/klauspost/compress/zstd L github.com/klauspost/compress/flate from nhooyr.io/websocket diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go index b8f0de826..4f6c6e680 100644 --- a/net/interfaces/interfaces.go +++ b/net/interfaces/interfaces.go @@ -687,7 +687,8 @@ func netInterfaces() ([]Interface, error) { return ret, nil } -// DefaultRouteDetails are the +// DefaultRouteDetails are the details about a default route returned +// by DefaultRoute. type DefaultRouteDetails struct { // InterfaceName is the interface name. It must always be populated. // It's like "eth0" (Linux), "Ethernet 2" (Windows), "en0" (macOS). diff --git a/net/interfaces/interfaces_linux.go b/net/interfaces/interfaces_linux.go index 83131a47f..2465fcfa2 100644 --- a/net/interfaces/interfaces_linux.go +++ b/net/interfaces/interfaces_linux.go @@ -11,12 +11,16 @@ import ( "fmt" "io" "log" + "net" "os" "os/exec" "runtime" "strings" + "github.com/jsimonetti/rtnetlink" + "github.com/mdlayher/netlink" "go4.org/mem" + "golang.org/x/sys/unix" "inet.af/netaddr" "tailscale.com/syncs" "tailscale.com/util/lineread" @@ -70,9 +74,7 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) { if err != nil { return nil // ignore error, skip line and keep going } - const RTF_UP = 0x0001 - const RTF_GATEWAY = 0x0002 - if flags&(RTF_UP|RTF_GATEWAY) != RTF_UP|RTF_GATEWAY { + if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY { return nil } ipu32, err := mem.ParseUint(gwHex, 16, 32) @@ -145,7 +147,62 @@ func defaultRoute() (d DefaultRouteDetails, err error) { d.InterfaceName = v return d, err } - return d, err + // Issue 4038: the default route (such as on Unifi UDM Pro) + // might be in a non-default table, so it won't show up in + // /proc/net/route. Use netlink to find the default route. + // + // TODO(bradfitz): this allocates a fair bit. We should track + // this in wgengine/monitor instead and have + // interfaces.GetState take a link monitor or similar so the + // routing table can be cached and the monitor's existing + // subscription to route changes can update the cached state, + // rather than querying the whole thing every time like + // defaultRouteFromNetlink does. + // + // Then we should just always try to use the cached route + // table from netlink every time, and only use /proc/net/route + // as a fallback for weird environments where netlink might be + // banned but /proc/net/route is emulated (e.g. stuff like + // Cloud Run?). + return defaultRouteFromNetlink() +} + +func defaultRouteFromNetlink() (d DefaultRouteDetails, err error) { + c, err := rtnetlink.Dial(&netlink.Config{Strict: true}) + if err != nil { + return d, fmt.Errorf("defaultRouteFromNetlink: Dial: %w", err) + } + defer c.Close() + rms, err := c.Route.List() + if err != nil { + return d, fmt.Errorf("defaultRouteFromNetlink: List: %w", err) + } + for _, rm := range rms { + if rm.Attributes.Gateway == nil { + // A default route has a gateway. If it doesn't, skip it. + continue + } + if rm.Attributes.Dst != nil { + // A default route has a nil destination to mean anything + // so ignore any route for a specific destination. + // TODO(bradfitz): better heuristic? + // empirically this seems like enough. + continue + } + // TODO(bradfitz): care about address family, if + // callers ever start caring about v4-vs-v6 default + // route differences. + idx := int(rm.Attributes.OutIface) + if idx == 0 { + continue + } + if iface, err := net.InterfaceByIndex(idx); err == nil { + d.InterfaceName = iface.Name + d.InterfaceIndex = idx + return d, nil + } + } + return d, errNoDefaultRoute } var zeroRouteBytes = []byte("00000000") @@ -155,6 +212,8 @@ var procNetRoutePath = "/proc/net/route" // /proc/net/route looking for a default route. const maxProcNetRouteRead = 1000 +var errNoDefaultRoute = errors.New("no default route found") + func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) { f, err := os.Open(procNetRoutePath) if err != nil { @@ -168,7 +227,7 @@ func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) { lineNum++ line, err := br.ReadSlice('\n') if err == io.EOF || lineNum > maxProcNetRouteRead { - return "", fmt.Errorf("no default routes found: %w", err) + return "", errNoDefaultRoute } if err != nil { return "", err diff --git a/net/interfaces/interfaces_linux_test.go b/net/interfaces/interfaces_linux_test.go index 2e362c084..43bf40044 100644 --- a/net/interfaces/interfaces_linux_test.go +++ b/net/interfaces/interfaces_linux_test.go @@ -5,7 +5,9 @@ package interfaces import ( + "errors" "fmt" + "io/fs" "io/ioutil" "os" "path/filepath" @@ -107,3 +109,14 @@ func BenchmarkDefaultRouteInterface(b *testing.B) { } } } + +func TestRouteLinuxNetlink(t *testing.T) { + d, err := defaultRouteFromNetlink() + if errors.Is(err, fs.ErrPermission) { + t.Skip(err) + } + if err != nil { + t.Fatal(err) + } + t.Logf("Got: %+v", d) +}