diff --git a/net/interfaces/interfaces_android.go b/net/interfaces/interfaces_android.go new file mode 100644 index 000000000..21a205f5a --- /dev/null +++ b/net/interfaces/interfaces_android.go @@ -0,0 +1,183 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package interfaces + +import ( + "bytes" + "errors" + "log" + "net/netip" + "os/exec" + "sync/atomic" + + "go4.org/mem" + "golang.org/x/sys/unix" + "tailscale.com/net/netaddr" + "tailscale.com/syncs" + "tailscale.com/util/lineread" +) + +var ( + lastKnownDefaultRouteIfName syncs.AtomicValue[string] +) + +var procNetRoutePath = "/proc/net/route" + +// maxProcNetRouteRead is the max number of lines to read from +// /proc/net/route looking for a default route. +const maxProcNetRouteRead = 1000 + +func init() { + likelyHomeRouterIP = likelyHomeRouterIPAndroid +} + +var procNetRouteErr atomic.Bool + +// errStopReading is a sentinel error value used internally by +// lineread.File callers to stop reading. It doesn't escape to +// callers/users. +var errStopReading = errors.New("stop reading") + +/* +Parse 10.0.0.1 out of: + +$ cat /proc/net/route +Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT +ens18 00000000 0100000A 0003 0 0 0 00000000 0 0 0 +ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0 +*/ +func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) { + if procNetRouteErr.Load() { + // If we failed to read /proc/net/route previously, don't keep trying. + return likelyHomeRouterIPHelper() + } + lineNum := 0 + var f []mem.RO + err := lineread.File(procNetRoutePath, func(line []byte) error { + lineNum++ + if lineNum == 1 { + // Skip header line. + return nil + } + if lineNum > maxProcNetRouteRead { + return errStopReading + } + f = mem.AppendFields(f[:0], mem.B(line)) + if len(f) < 4 { + return nil + } + gwHex, flagsHex := f[2], f[3] + flags, err := mem.ParseUint(flagsHex, 16, 16) + if err != nil { + return nil // ignore error, skip line and keep going + } + if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY { + return nil + } + ipu32, err := mem.ParseUint(gwHex, 16, 32) + if err != nil { + return nil // ignore error, skip line and keep going + } + ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24)) + if ip.IsPrivate() { + ret = ip + return errStopReading + } + return nil + }) + if errors.Is(err, errStopReading) { + err = nil + } + if err != nil { + procNetRouteErr.Store(true) + return likelyHomeRouterIP() + } + if ret.IsValid() { + // Try to get the local IP of the interface associated with + // this route to short-circuit finding the IP associated with + // this gateway. This isn't fatal if it fails. + if len(f) > 0 && !disableLikelyHomeRouterIPSelf() { + ForeachInterface(func(ni Interface, pfxs []netip.Prefix) { + // Ensure this is the same interface + if !f[0].EqualString(ni.Name) { + return + } + + // Find the first IPv4 address and use it. + for _, pfx := range pfxs { + if addr := pfx.Addr(); addr.Is4() { + myIP = addr + break + } + } + }) + } + + return ret, myIP, true + } + if lineNum >= maxProcNetRouteRead { + // If we went over our line limit without finding an answer, assume + // we're a big fancy Linux router (or at least not a home system) + // and set the error bit so we stop trying this in the future (and wasting CPU). + // See https://github.com/tailscale/tailscale/issues/7621. + // + // Remember that "likelyHomeRouterIP" exists purely to find the port + // mapping service (UPnP, PMP, PCP) often present on a home router. If we hit + // the route (line) limit without finding an answer, we're unlikely to ever + // find one in the future. + procNetRouteErr.Store(true) + } + return netip.Addr{}, netip.Addr{}, false +} + +// Android apps don't have permission to read /proc/net/route, at +// least on Google devices and the Android emulator. +func likelyHomeRouterIPHelper() (ret netip.Addr, _ netip.Addr, ok bool) { + cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0") + out, err := cmd.StdoutPipe() + if err != nil { + return + } + if err := cmd.Start(); err != nil { + log.Printf("interfaces: running /system/bin/ip: %v", err) + return + } + // Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 " + lineread.Reader(out, func(line []byte) error { + const pfx = "default via " + if !mem.HasPrefix(mem.B(line), mem.S(pfx)) { + return nil + } + line = line[len(pfx):] + sp := bytes.IndexByte(line, ' ') + if sp == -1 { + return nil + } + ipb := line[:sp] + if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() { + ret = ip + log.Printf("interfaces: found Android default route %v", ip) + } + return nil + }) + cmd.Process.Kill() + cmd.Wait() + return ret, netip.Addr{}, ret.IsValid() +} + +// UpdateLastKnownDefaultRouteInterface is called by libtailscale in the Android app when +// the connectivity manager detects a network path transition. If ifName is "", network has been lost. +// After updating the interface, Android calls Monitor.InjectEvent(), triggering a link change. +func UpdateLastKnownDefaultRouteInterface(ifName string) { + if old := lastKnownDefaultRouteIfName.Swap(ifName); old != ifName { + log.Printf("defaultroute: update from Android, ifName = %s (was %s)", ifName, old) + } +} + +func defaultRoute() (d DefaultRouteDetails, err error) { + if ifName := lastKnownDefaultRouteIfName.Load(); ifName != "" { + d.InterfaceName = ifName + } + return d, nil +} diff --git a/net/interfaces/interfaces_defaultrouteif_todo.go b/net/interfaces/interfaces_defaultrouteif_todo.go index c48ad246a..f8c636e9d 100644 --- a/net/interfaces/interfaces_defaultrouteif_todo.go +++ b/net/interfaces/interfaces_defaultrouteif_todo.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build !linux && !windows && !darwin && !freebsd +//go:build !linux && !windows && !darwin && !freebsd && !android package interfaces diff --git a/net/interfaces/interfaces_linux.go b/net/interfaces/interfaces_linux.go index 440b74f89..cb164b2f8 100644 --- a/net/interfaces/interfaces_linux.go +++ b/net/interfaces/interfaces_linux.go @@ -1,6 +1,8 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause +//go:build !android + package interfaces import ( @@ -48,7 +50,6 @@ ens18 0000000A 00000000 0001 0 0 0 0000FFFF func likelyHomeRouterIPLinux() (ret netip.Addr, myIP netip.Addr, ok bool) { if procNetRouteErr.Load() { // If we failed to read /proc/net/route previously, don't keep trying. - // But if we're on Android, go into the Android path. if runtime.GOOS == "android" { return likelyHomeRouterIPAndroid() } @@ -177,11 +178,6 @@ func defaultRoute() (d DefaultRouteDetails, err error) { d.InterfaceName = v return d, nil } - if runtime.GOOS == "android" { - v, err = defaultRouteInterfaceAndroidIPRoute() - d.InterfaceName = v - 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. @@ -307,39 +303,3 @@ func defaultRouteInterfaceProcNet() (string, error) { } return rc, err } - -// defaultRouteInterfaceAndroidIPRoute tries to find the machine's default route interface name -// by parsing the "ip route" command output. We use this on Android where /proc/net/route -// can be missing entries or have locked-down permissions. -// See also comments in https://github.com/tailscale/tailscale/pull/666. -func defaultRouteInterfaceAndroidIPRoute() (ifname string, err error) { - cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0") - out, err := cmd.StdoutPipe() - if err != nil { - return "", err - } - if err := cmd.Start(); err != nil { - log.Printf("interfaces: running /system/bin/ip: %v", err) - return "", err - } - // Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 " - lineread.Reader(out, func(line []byte) error { - const pfx = "default via " - if !mem.HasPrefix(mem.B(line), mem.S(pfx)) { - return nil - } - ff := strings.Fields(string(line)) - for i, v := range ff { - if i > 0 && ff[i-1] == "dev" && ifname == "" { - ifname = v - } - } - return nil - }) - cmd.Process.Kill() - cmd.Wait() - if ifname == "" { - return "", errors.New("no default routes found") - } - return ifname, nil -}