net/netmon: in Android, replace system/bin/ip call with cached LinkProperties gateway (#19804)

bind() on NETLINK_ROUTE sockets does not work on Android 11+ (https://developer.android.com/identity/user-data-ids#mac-11-plus) . Since system/bin/ip uses bind(), likelyHomeRouterIPHelper() always fails on Andoroid 11+, so that GatewayAndSelfIP never caches the result, causing repeated ip process spawns on every periodic ReSTUN.

This replaces the system/bin/ip fallback with a cached gateway IP pushed from Android’s ConnectivityManager via LinkProperties.getRoutes(). This is the same patterm used by UpdateLastKnownDefaultRouteInterface for the interface name (see https://github.com/tailscale/tailscale/pull/11784/). We keep the proc/net/route path as a fallback for early startup before NetworkChangeCallback has fired.

Updates tailscale/tailscale#18622
Updates tailscale/tailscale#13352

Signed-off-by: kari-ts <kari@tailscale.com>
This commit is contained in:
kari-ts
2026-05-27 15:42:48 -07:00
committed by GitHub
parent c9fb05b6f5
commit 1a17ec1988
+84 -109
View File
@@ -4,10 +4,8 @@
package netmon package netmon
import ( import (
"bytes"
"log" "log"
"net/netip" "net/netip"
"os/exec"
"sync/atomic" "sync/atomic"
"go4.org/mem" "go4.org/mem"
@@ -19,6 +17,7 @@ import (
var ( var (
lastKnownDefaultRouteIfName syncs.AtomicValue[string] lastKnownDefaultRouteIfName syncs.AtomicValue[string]
lastKnownDefaultGateway syncs.AtomicValue[string]
) )
var procNetRoutePath = "/proc/net/route" 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 ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0
*/ */
func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) { func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) {
if procNetRouteErr.Load() { if gwStr := lastKnownDefaultGateway.Load(); gwStr != "" {
// If we failed to read /proc/net/route previously, don't keep trying. if ip, err := netip.ParseAddr(gwStr); err == nil {
return likelyHomeRouterIPHelper() return ip, netip.Addr{}, true
}
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 ret.IsValid() { // Fall back to /proc/net/route for early startup before
// Try to get the local IP of the interface associated with // NetworkChangeCallback has fired, or if the cached gateway
// this route to short-circuit finding the IP associated with // was empty (e.g., cellular with no private gateway).
// this gateway. This isn't fatal if it fails. if !procNetRouteErr.Load() {
if len(f) > 0 && !disableLikelyHomeRouterIPSelf() { lineNum := 0
ForeachInterface(func(ni Interface, pfxs []netip.Prefix) { var f []mem.RO
// Ensure this is the same interface for lr := range lineiter.File(procNetRoutePath) {
if !f[0].EqualString(ni.Name) { line, err := lr.Value()
return if err != nil {
} procNetRouteErr.Store(true)
return likelyHomeRouterIP()
}
// Find the first IPv4 address and use it. lineNum++
for _, pfx := range pfxs { if lineNum == 1 {
if addr := pfx.Addr(); addr.Is4() { // Skip header line.
myIP = addr continue
break }
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 // Find the first IPv4 address and use it.
} for _, pfx := range pfxs {
if lineNum >= maxProcNetRouteRead { if addr := pfx.Addr(); addr.Is4() {
// If we went over our line limit without finding an answer, assume myIP = addr
// we're a big fancy Linux router (or at least not a home system) break
// 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 return ret, myIP, true
// find one in the future. }
procNetRouteErr.Store(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 return netip.Addr{}, netip.Addr{}, false
} }
// Android apps don't have permission to read /proc/net/route, at // UpdateLastKnownDefaultGateway is called by libtailscale in the Android app when
// least on Google devices and the Android emulator. // the connectivity manager provides an updated default gateway IP from LinkProperties.
func likelyHomeRouterIPHelper() (ret netip.Addr, _ netip.Addr, ok bool) { func UpdateLastKnownDefaultGateway(ipStr string) {
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0") if old := lastKnownDefaultGateway.Swap(ipStr); old != ipStr {
out, err := cmd.StdoutPipe() log.Printf("defaultgateway: update from Android, gateway = %s (was %s)", ipStr, old)
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 "
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 // UpdateLastKnownDefaultRouteInterface is called by libtailscale in the Android app when