diff --git a/net/netmon/interfaces_android.go b/net/netmon/interfaces_android.go index 2cd7f23f6..1bd22db8e 100644 --- a/net/netmon/interfaces_android.go +++ b/net/netmon/interfaces_android.go @@ -4,10 +4,8 @@ package netmon import ( - "bytes" "log" "net/netip" - "os/exec" "sync/atomic" "go4.org/mem" @@ -19,6 +17,7 @@ import ( var ( lastKnownDefaultRouteIfName syncs.AtomicValue[string] + lastKnownDefaultGateway syncs.AtomicValue[string] ) var procNetRoutePath = "/proc/net/route" @@ -42,123 +41,99 @@ ens18 00000000 0100000A 0003 0 0 0 00000000 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 - for lr := range lineiter.File(procNetRoutePath) { - line, err := lr.Value() - if err != nil { - procNetRouteErr.Store(true) - return likelyHomeRouterIP() - } - - lineNum++ - if lineNum == 1 { - // Skip header line. - continue - } - if lineNum > maxProcNetRouteRead { - break - } - f = mem.AppendFields(f[:0], mem.B(line)) - if len(f) < 4 { - continue - } - gwHex, flagsHex := f[2], f[3] - flags, err := mem.ParseUint(flagsHex, 16, 16) - if err != nil { - continue // ignore error, skip line and keep going - } - if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY { - continue - } - ipu32, err := mem.ParseUint(gwHex, 16, 32) - if err != nil { - continue // 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 - break + if gwStr := lastKnownDefaultGateway.Load(); gwStr != "" { + if ip, err := netip.ParseAddr(gwStr); err == nil { + return ip, netip.Addr{}, true } } - 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 - } + // Fall back to /proc/net/route for early startup before + // NetworkChangeCallback has fired, or if the cached gateway + // was empty (e.g., cellular with no private gateway). + if !procNetRouteErr.Load() { + lineNum := 0 + var f []mem.RO + for lr := range lineiter.File(procNetRoutePath) { + line, err := lr.Value() + if err != nil { + procNetRouteErr.Store(true) + return likelyHomeRouterIP() + } - // Find the first IPv4 address and use it. - for _, pfx := range pfxs { - if addr := pfx.Addr(); addr.Is4() { - myIP = addr - break + lineNum++ + if lineNum == 1 { + // Skip header line. + continue + } + if lineNum > maxProcNetRouteRead { + break + } + f = mem.AppendFields(f[:0], mem.B(line)) + if len(f) < 4 { + continue + } + gwHex, flagsHex := f[2], f[3] + flags, err := mem.ParseUint(flagsHex, 16, 16) + if err != nil { + continue // ignore error, skip line and keep going + } + if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY { + continue + } + ipu32, err := mem.ParseUint(gwHex, 16, 32) + if err != nil { + continue // 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 + break + } + } + 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 } - } - }) - } - 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) + // 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 +// UpdateLastKnownDefaultGateway is called by libtailscale in the Android app when +// the connectivity manager provides an updated default gateway IP from LinkProperties. +func UpdateLastKnownDefaultGateway(ipStr string) { + if old := lastKnownDefaultGateway.Swap(ipStr); old != ipStr { + log.Printf("defaultgateway: update from Android, gateway = %s (was %s)", ipStr, old) } - 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 " - for lr := range lineiter.Reader(out) { - line, err := lr.Value() - if err != nil { - break - } - const pfx = "default via " - if !mem.HasPrefix(mem.B(line), mem.S(pfx)) { - continue - } - line = line[len(pfx):] - sp := bytes.IndexByte(line, ' ') - if sp == -1 { - continue - } - 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) - } - } - cmd.Process.Kill() - cmd.Wait() - return ret, netip.Addr{}, ret.IsValid() } // UpdateLastKnownDefaultRouteInterface is called by libtailscale in the Android app when