Files
tailscale/feature/androidbin/androidbin.go
Brad Fitzpatrick 60d9c54b6b feature/androidbin: make netmon and TLS work in raw binaries on Android
Android denies app UIDs both NETLINK_ROUTE (golang/go#40569, #2293)
and /proc/net, so net.Interfaces always fails and netmon.New errors
out before a standalone binary can do anything. The Android app solves
this from Java via netmon.RegisterInterfaceGetter, but raw binaries
run under Termux or a rooted shell have no Java to lean on. This is
the second half of #21129, following the androiddns feature.

I added a netmon fallback hook, consulted only when no interface
getter was registered and net.Interfaces failed, and a new androidbin
feature (ts_omit_androidbin) that implements it: report a single
synthetic interface whose v4 and v6 addresses come from asking the
kernel to route an outbound UDP socket, which sends no packets and is
permitted in the app sandbox. That's enough for magicsock to discover
local endpoints. The fallback also requires runtime evidence of
Android (GOOS=android, or /dev/__properties__ existing for GOOS=linux
binaries running under an Android kernel), so it's inert on regular
Linux.

The androidbin feature also blank imports androiddns, and fixes a
third gap I found while testing: GOOS=linux binaries on Android have
an empty CA root pool, because Go's unix root loader doesn't know
Android's /system/etc/security/cacerts (the GOOS=android loader
does), so all TLS verification fails. On Android it points
SSL_CERT_DIR there unless the user already set it, as Termux's
ca-certificates package does.

The feature is on by default in tailscaled builds on Linux and
Android via condregister, and deliberately not linked into tsnet by
default; tsnet apps and other programs opt in with a blank import of
tailscale.com/feature/androidbin.

I verified on an Android 13 emulator with SELinux enforcing, running
GOOS=linux static binaries under the app UID (run-as), where
net.Interfaces fails with the exact netlinkrib permission denial from
the issue: netmon.New succeeds with the synthetic interface, and a
tailcat binary importing this feature does DNS via dnsproxyd, fetches
its DERP map over TLS using the Android cert store, completes STUN,
selects a DERP region, and prints its address.

Updates #21129

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: If7dbcbb825ecd24bcf6d9d64b1e334dd00700d51
2026-09-08 11:41:54 -07:00

128 lines
4.5 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux
// Package androidbin makes Tailscale binaries work when run as raw
// (non-GUI app) executables on Android, such as under Termux, adb, or
// a rooted shell. Android denies app UIDs the NETLINK_ROUTE socket
// and /proc/net (Issue 2293, golang/go#40569), so net.Interfaces
// always fails and netmon cannot start. The Android app solves this
// from Java via netmon.RegisterInterfaceGetter, but a standalone
// binary has no Java to lean on.
//
// This package registers a netmon fallback that reports a single
// synthetic interface whose addresses are discovered by asking the
// kernel to route an outbound UDP socket (which sends no packets and
// is permitted). That's enough for magicsock to discover local
// endpoints, though it can't see the full multi-interface picture.
// The fallback runs only when nothing was registered with
// RegisterInterfaceGetter, net.Interfaces failed, and runtime
// detection says the process is on Android, so it's inert on regular
// Linux.
//
// It also pulls in the androiddns feature, which fixes name
// resolution with no /etc/resolv.conf, and points Go's CA root loader
// at Android's system certificate store in GOOS=linux builds (see
// cacerts.go), which otherwise find no roots and fail all TLS
// verification.
//
// Like androiddns, the package builds on GOOS=linux as well as
// GOOS=android, because static linux binaries are commonly run under
// Android kernels. This is an optional feature, included by default
// in tailscaled builds on Linux and Android; build with the
// ts_omit_androidbin tag to omit it. It is not linked into tsnet by
// default; tsnet apps and other programs opt in with a blank import
// of this package.
package androidbin
import (
"errors"
"net"
"net/netip"
"os"
"runtime"
"time"
"tailscale.com/net/netmon"
_ "tailscale.com/feature/androiddns"
)
// fallbackInterfaces implements netmon.HookInterfacesFallback. It
// runs only after net.Interfaces has already failed.
func fallbackInterfaces() ([]netmon.Interface, error) {
if !onAndroid() {
return nil, errors.New("androidbin: not running on Android")
}
return buildSynthetic()
}
// buildSynthetic returns a single made-up interface carrying the
// process's outbound IPv4 and IPv6 source addresses.
func buildSynthetic() ([]netmon.Interface, error) {
var addrs []net.Addr
// The dial targets are never sent any packets; dialing a UDP
// socket only makes the kernel pick a route and a source address.
// Use Google Public DNS addresses rather than documentation range
// (TEST-NET) addresses in case Android ever grows special routing
// treatment of ranges that are never supposed to appear on a real
// network.
if ip, ok := outboundIP("udp4", "8.8.8.8:53"); ok {
addrs = append(addrs, &net.IPNet{IP: ip.AsSlice(), Mask: net.CIDRMask(32, 32)})
}
if ip, ok := outboundIP("udp6", "[2001:4860:4860::8888]:53"); ok {
addrs = append(addrs, &net.IPNet{IP: ip.AsSlice(), Mask: net.CIDRMask(128, 128)})
}
if len(addrs) == 0 {
return nil, errors.New("androidbin: no outbound routes found")
}
return []netmon.Interface{{
Interface: &net.Interface{
MTU: 1500, // TODO: getsockopt IP_MTU_* to discover it if it ever matters
Name: "android",
Flags: net.FlagUp | net.FlagRunning,
},
AltAddrs: addrs,
}}, nil
}
// outboundIP reports the local source address the kernel picks for an
// outbound socket to addr. Dialing UDP performs no I/O, so this works
// under Android's app sandbox where interface enumeration does not.
func outboundIP(network, addr string) (netip.Addr, bool) {
d := net.Dialer{Timeout: 2 * time.Second}
c, err := d.Dial(network, addr)
if err != nil {
return netip.Addr{}, false
}
defer c.Close()
ua, ok := c.LocalAddr().(*net.UDPAddr)
if !ok {
return netip.Addr{}, false
}
ip, ok := netip.AddrFromSlice(ua.IP)
if !ok {
return netip.Addr{}, false
}
ip = ip.Unmap()
if ip.IsLoopback() || ip.IsUnspecified() {
return netip.Addr{}, false
}
return ip, true
}
// onAndroid reports whether the process is running on Android, even
// if it's a GOOS=linux binary running under an Android kernel. For
// that case it checks for /dev/__properties__, the bionic property
// service's backing store, which has existed on every Android version
// since 5.0, is stat-able by all processes because every app's libc
// reads properties, and is never present on regular Linux systems.
func onAndroid() bool {
if runtime.GOOS == "android" {
return true
}
_, err := os.Stat("/dev/__properties__")
return err == nil
}