netns: add Android callback to bind socket to network (#18915)

After switching from cellular to wifi without ipv6, ForeachInterface still sees rmnet prefixes, so HaveV6 stays true, and magicsock keeps attempting ipv6 connections that either route through cellular or time out for users on wifi without ipv6

This:
-Adds SetAndroidBindToNetworkFunc, a callback to bind the socket to the selected Android Network object

Updates tailscale/tailscale#6152

Signed-off-by: kari-ts <kari@tailscale.com>
main
kari-ts 1 month ago committed by GitHub
parent dd1da0b389
commit 4c7c1091ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      ipn/ipnlocal/local.go
  2. 12
      net/netns/netns.go
  3. 39
      net/netns/netns_android.go
  4. 9
      tailcfg/tailcfg.go

@ -6299,6 +6299,9 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
// See the netns package for documentation on what these capability do.
netns.SetBindToInterfaceByRoute(b.logf, nm.HasCap(tailcfg.CapabilityBindToInterfaceByRoute))
if runtime.GOOS == "android" {
netns.SetDisableAndroidBindToActiveNetwork(b.logf, nm.HasCap(tailcfg.NodeAttrDisableAndroidBindToActiveNetwork))
}
netns.SetDisableBindConnToInterface(b.logf, nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterface))
netns.SetDisableBindConnToInterfaceAppleExt(b.logf, nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterfaceAppleExt))

@ -46,6 +46,18 @@ func SetBindToInterfaceByRoute(logf logger.Logf, v bool) {
}
}
// When true, disableAndroidBindToActiveNetwork skips binding sockets to the currently
// active network on Android.
var disableAndroidBindToActiveNetwork atomic.Bool
// SetDisableAndroidBindToActiveNetwork disables the default behavior of binding
// sockets to the currently active network on Android.
func SetDisableAndroidBindToActiveNetwork(logf logger.Logf, v bool) {
if runtime.GOOS == "android" && disableAndroidBindToActiveNetwork.Swap(v) != v {
logf("netns: disableAndroidBindToActiveNetwork changed to %v", v)
}
}
var disableBindConnToInterface atomic.Bool
// SetDisableBindConnToInterface disables the (normal) behavior of binding

@ -17,6 +17,9 @@ import (
var (
androidProtectFuncMu sync.Mutex
androidProtectFunc func(fd int) error
androidBindToNetworkFuncMu sync.Mutex
androidBindToNetworkFunc func(fd int) error
)
// UseSocketMark reports whether SO_MARK is in use. Android does not use SO_MARK.
@ -50,6 +53,14 @@ func SetAndroidProtectFunc(f func(fd int) error) {
androidProtectFunc = f
}
// SetAndroidBindToNetworkFunc registers a func provided by Android that binds
// the socket FD to the currently selected underlying network.
func SetAndroidBindToNetworkFunc(f func(fd int) error) {
androidBindToNetworkFuncMu.Lock()
defer androidBindToNetworkFuncMu.Unlock()
androidBindToNetworkFunc = f
}
func control(logger.Logf, *netmon.Monitor) func(network, address string, c syscall.RawConn) error {
return controlC
}
@ -60,14 +71,36 @@ func control(logger.Logf, *netmon.Monitor) func(network, address string, c sysca
// and net.ListenConfig.Control.
func controlC(network, address string, c syscall.RawConn) error {
var sockErr error
err := c.Control(func(fd uintptr) {
fdInt := int(fd)
// Protect from VPN loops
androidProtectFuncMu.Lock()
f := androidProtectFunc
pf := androidProtectFunc
androidProtectFuncMu.Unlock()
if f != nil {
sockErr = f(int(fd))
if pf != nil {
if err := pf(fdInt); err != nil {
sockErr = err
return
}
}
if disableAndroidBindToActiveNetwork.Load() {
return
}
androidBindToNetworkFuncMu.Lock()
bf := androidBindToNetworkFunc
androidBindToNetworkFuncMu.Unlock()
if bf != nil {
if err := bf(fdInt); err != nil {
sockErr = err
return
}
}
})
if err != nil {
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
}

@ -180,7 +180,8 @@ type CapabilityVersion int
// - 131: 2025-11-25: client respects [NodeAttrDefaultAutoUpdate]
// - 132: 2026-02-13: client respects [NodeAttrDisableHostsFileUpdates]
// - 133: 2026-02-17: client understands [NodeAttrForceRegisterMagicDNSIPv4Only]; MagicDNS IPv6 registered w/ OS by default
const CurrentCapabilityVersion CapabilityVersion = 133
// - 134: 2026-03-09: Client understands [NodeAttrDisableAndroidBindToActiveNetwork]
const CurrentCapabilityVersion CapabilityVersion = 134
// ID is an integer ID for a user, node, or login allocated by the
// control plane.
@ -2463,6 +2464,12 @@ const (
// details on the behaviour of this capability.
CapabilityBindToInterfaceByRoute NodeCapability = "https://tailscale.com/cap/bind-to-interface-by-route"
// NodeAttrDisableAndroidBindToActiveNetwork disables binding sockets to the
// currently active network on Android, which is enabled by default.
// This allows the control plane to turn off the behavior if it causes
// problems.
NodeAttrDisableAndroidBindToActiveNetwork NodeCapability = "disable-android-bind-to-active-network"
// CapabilityDebugDisableAlternateDefaultRouteInterface changes how Darwin
// nodes get the default interface. There is an optional hook (used by the
// macOS and iOS clients) to override the default interface, this capability

Loading…
Cancel
Save