From e8d2f964499989d1cd99db556b0a3e3f293dd86b Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Wed, 12 Nov 2025 10:25:27 -0500 Subject: [PATCH] ipn/ipnlocal, net/netns: add node cap to disable netns interface binding on netext Apple clients (#17691) updates tailscale/corp#31571 It appears that on the latest macOS, iOS and tVOS versions, the work that netns is doing to bind outgoing connections to the default interface (and all of the trimmings and workarounds in netmon et al that make that work) are not needed. The kernel is extension-aware and doing nothing, is the right thing. This is, however, not the case for tailscaled (which is not a special process). To allow us to test this assertion (and where it might break things), we add a new node cap that turns this behaviour off only for network-extension equipped clients, making it possible to turn this off tailnet-wide, without breaking any tailscaled macos nodes. Signed-off-by: Jonathan Nobels --- ipn/ipnlocal/local.go | 7 ++++--- net/netns/netns.go | 23 +++++++++++++++++++---- net/netns/netns_darwin.go | 7 +++---- tailcfg/tailcfg.go | 4 ++++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 8bdc1a14a..62d8ea490 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -6169,9 +6169,10 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { b.setDebugLogsByCapabilityLocked(nm) } - // See the netns package for documentation on what this capability does. - netns.SetBindToInterfaceByRoute(nm.HasCap(tailcfg.CapabilityBindToInterfaceByRoute)) - netns.SetDisableBindConnToInterface(nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterface)) + // See the netns package for documentation on what these capability do. + netns.SetBindToInterfaceByRoute(b.logf, nm.HasCap(tailcfg.CapabilityBindToInterfaceByRoute)) + netns.SetDisableBindConnToInterface(b.logf, nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterface)) + netns.SetDisableBindConnToInterfaceAppleExt(b.logf, nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterfaceAppleExt)) b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) if buildfeatures.HasServe { diff --git a/net/netns/netns.go b/net/netns/netns.go index a473506fa..ccb20d27e 100644 --- a/net/netns/netns.go +++ b/net/netns/netns.go @@ -39,20 +39,35 @@ var bindToInterfaceByRoute atomic.Bool // setting the TS_BIND_TO_INTERFACE_BY_ROUTE. // // Currently, this only changes the behaviour on macOS and Windows. -func SetBindToInterfaceByRoute(v bool) { +func SetBindToInterfaceByRoute(logf logger.Logf, v bool) { + logf("netns: bindToInterfaceByRoute to %v", v) bindToInterfaceByRoute.Store(v) } var disableBindConnToInterface atomic.Bool // SetDisableBindConnToInterface disables the (normal) behavior of binding -// connections to the default network interface. +// connections to the default network interface on Darwin nodes. // -// Currently, this only has an effect on Darwin. -func SetDisableBindConnToInterface(v bool) { +// Unless you intended to disable this for tailscaled on macos (which is likely +// to break things), you probably wanted to set +// SetDisableBindConnToInterfaceAppleExt which will disable explicit interface +// binding only when tailscaled is running inside a network extension process. +func SetDisableBindConnToInterface(logf logger.Logf, v bool) { + logf("netns: disableBindConnToInterface set to %v", v) disableBindConnToInterface.Store(v) } +var disableBindConnToInterfaceAppleExt atomic.Bool + +// SetDisableBindConnToInterfaceAppleExt disables the (normal) behavior of binding +// connections to the default network interface but only on Apple clients where +// tailscaled is running inside a network extension. +func SetDisableBindConnToInterfaceAppleExt(logf logger.Logf, v bool) { + logf("netns: disableBindConnToInterfaceAppleExt set to %v", v) + disableBindConnToInterfaceAppleExt.Store(v) +} + // Listener returns a new net.Listener with its Control hook func // initialized as necessary to run in logical network namespace that // doesn't route back into Tailscale. diff --git a/net/netns/netns_darwin.go b/net/netns/netns_darwin.go index 1f30f00d2..ff05a3f31 100644 --- a/net/netns/netns_darwin.go +++ b/net/netns/netns_darwin.go @@ -21,6 +21,7 @@ import ( "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" "tailscale.com/types/logger" + "tailscale.com/version" ) func control(logf logger.Logf, netMon *netmon.Monitor) func(network, address string, c syscall.RawConn) error { @@ -36,13 +37,11 @@ var errInterfaceStateInvalid = errors.New("interface state invalid") // controlLogf binds c to a particular interface as necessary to dial the // provided (network, address). func controlLogf(logf logger.Logf, netMon *netmon.Monitor, network, address string, c syscall.RawConn) error { - if isLocalhost(address) { - // Don't bind to an interface for localhost connections. + if disableBindConnToInterface.Load() || (version.IsMacGUIVariant() && disableBindConnToInterfaceAppleExt.Load()) { return nil } - if disableBindConnToInterface.Load() { - logf("netns_darwin: binding connection to interfaces disabled") + if isLocalhost(address) { return nil } diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 43ed3188f..346957803 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -2460,6 +2460,10 @@ const ( // of connections to the default network interface on Darwin nodes. CapabilityDebugDisableBindConnToInterface NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface" + // CapabilityDebugDisableBindConnToInterface disables the automatic binding + // of connections to the default network interface on Darwin nodes using network extensions + CapabilityDebugDisableBindConnToInterfaceAppleExt NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface-apple-ext" + // CapabilityTailnetLock indicates the node may initialize tailnet lock. CapabilityTailnetLock NodeCapability = "https://tailscale.com/cap/tailnet-lock"