Pārlūkot izejas kodu

interfaces: create android impl (#11784)

-Move Android impl into interfaces_android.go
-Instead of using ip route to get the interface name, use the one passed in by Android (ip route is restricted in Android 13+ per termux/termux-app#2993)

Follow-up will be to do the same for router

Fixes tailscale/corp#19215
Fixes tailscale/corp#19124

Signed-off-by: kari-ts <[email protected]>
kari-ts 1 gadu atpakaļ
vecāks
revīzija
048cb61dd0

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

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

+ 2 - 42
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
-}