diff --git a/pickregion.go b/pickregion.go index 1a954d9b7..c246ece54 100644 --- a/pickregion.go +++ b/pickregion.go @@ -13,7 +13,6 @@ import ( "tailscale.com/net/netcheck" "tailscale.com/net/netmon" - "tailscale.com/net/netns" "tailscale.com/tailcfg" "tailscale.com/types/logger" ) @@ -23,14 +22,10 @@ import ( // nil error) if the netcheck report contained no usable region // latencies. func PickBestRegion(ctx context.Context, dm *tailcfg.DERPMap) (regionID tailcfg.DERPRegionID, err error) { - // Disable netns before netcheck binds its STUN sockets, not just - // at engine creation as createEngine does, which runs after this. - // tailcat has no TUN device whose routes netns would need to - // avoid, and netns's unprivileged fallback (SO_BINDTODEVICE to - // the default interface) blackholes loopback UDP, making every - // netcheck against a loopback DERP map burn the full 3-second - // STUN timeout before falling back to HTTPS probes. - netns.SetEnabled(false) + // Set the netns policy before netcheck binds its STUN sockets, not + // just at engine creation as createEngine does, which runs after + // this. See configureNetNS for why it depends on the OS. + configureNetNS() nc := &netcheck.Client{ NetMon: netmon.NewStatic(), Verbose: Verbose, diff --git a/tailcat.go b/tailcat.go index 7585d3ace..6a1a4ad74 100644 --- a/tailcat.go +++ b/tailcat.go @@ -1905,7 +1905,7 @@ func createEngine(logf logger.Logf, lb *locoBackend) (err error) { // call-me-maybe messages that advertise our UDP endpoints (see // locoBackend.advertiseEndpoints). conf.ForceDiscoKey = discoPrivateForNode(lb.priv) - netns.SetEnabled(false) + configureNetNS() e, err := wgengine.NewUserspaceEngine(logf, conf) if err != nil { logf("wgengine.NewUserspaceEngine(tun %q) error: %v", "userspace-networking", err) @@ -1916,6 +1916,26 @@ func createEngine(logf logger.Logf, lb *locoBackend) (err error) { return nil } +// configureNetNS sets the process-wide netns policy before tailcat opens +// any sockets. +// +// On macOS netns stays on. It binds tailcat's UDP sockets and DERP +// connections to the default physical interface, as tailscaled does, so +// a co-located VPN that takes the default route, such as the Tailscale +// app with an exit node selected, does not carry tailcat's WireGuard +// packets inside its own tunnel. Loopback traffic still works from +// sockets bound this way. +// +// Other platforms turn netns off. On Linux, netns's unprivileged fallback +// (SO_BINDTODEVICE to the default interface) blackholes loopback UDP, +// making every netcheck against a loopback DERP map burn the full +// 3-second STUN timeout before falling back to HTTPS probes. +func configureNetNS() { + if runtime.GOOS != "darwin" { + netns.SetEnabled(false) + } +} + // Client connects to a [Server] over a WireGuard tunnel relayed through DERP. // Populate Server (the only required field, or use the [NewClient] // shorthand), then just dial: [Client.Dial], the DialTCP methods, and the diff --git a/tailcat_test.go b/tailcat_test.go index 6b7e7b285..bae47fbae 100644 --- a/tailcat_test.go +++ b/tailcat_test.go @@ -16,6 +16,7 @@ import ( "net/http/httptest" "net/netip" "os" + "runtime" "strings" "sync" "sync/atomic" @@ -35,6 +36,8 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" "tailscale.com/envknob" "tailscale.com/net/netcheck" + "tailscale.com/net/netmon" + "tailscale.com/net/netns" "tailscale.com/syncs" "tailscale.com/tailcfg" "tailscale.com/tstest/integration" @@ -1682,3 +1685,21 @@ func TestPeerKey(t *testing.T) { } } } + +// TestNetNSPolicy checks the netns policy that configureNetNS sets when a +// Server starts. On macOS netns stays on, so tailcat's sockets bind to the +// default physical interface instead of following a co-located VPN's +// default route. Elsewhere it is off. +func TestNetNSPolicy(t *testing.T) { + dm := integration.RunDERPAndSTUN(t, mkLogger(t, "derpstun"), "127.0.0.1") + s := &Server{Key: key.NewNode(), Logf: mkLogger(t, "server"), Region: dm.Regions[1]} + t.Cleanup(func() { s.Close() }) + if err := s.Start(); err != nil { + t.Fatalf("server Start: %v", err) + } + + bound := netns.Listener(t.Logf, netmon.NewStatic()).Control != nil + if want := runtime.GOOS == "darwin"; bound != want { + t.Errorf("on %s after Server.Start, netns binds sockets = %v; want %v", runtime.GOOS, bound, want) + } +}