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 <bradfitz@tailscale.com>
pull/4050/head
Brad Fitzpatrick 2 years ago committed by Brad Fitzpatrick
parent 518f6cee63
commit 55095df644

@ -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

@ -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

@ -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).

@ -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

@ -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)
}

Loading…
Cancel
Save