Browse Source

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 <[email protected]>
Brad Fitzpatrick 4 years ago
parent
commit
55095df644

+ 7 - 0
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

+ 1 - 1
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

+ 2 - 1
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).

+ 64 - 5
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

+ 13 - 0
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)
+}