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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user