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