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 <kari@tailscale.com>main
parent
7132b782d4
commit
048cb61dd0
@ -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 |
||||
} |
||||
Loading…
Reference in new issue