From caa3d7594fecb3227abcc97b8f7679798acdb587 Mon Sep 17 00:00:00 2001 From: Nick Khyl Date: Thu, 2 May 2024 14:36:26 -0500 Subject: [PATCH] ipn/ipnlocal, net/tsdial: plumb routes into tsdial and use them in UserDial We'd like to use tsdial.Dialer.UserDial instead of SystemDial for DNS over TCP. This is primarily necessary to properly dial internal DNS servers accessible over Tailscale and subnet routes. However, to avoid issues when switching between Wi-Fi and cellular, we need to ensure that we don't retain connections to any external addresses on the old interface. Therefore, we need to determine which dialer to use internally based on the configured routes. This plumbs routes and localRoutes from router.Config to tsdial.Dialer, and updates UserDial to use either the peer dialer or the system dialer, depending on the network address and the configured routes. Updates tailscale/corp#18725 Fixes #4529 Signed-off-by: Nick Khyl --- cmd/tailscaled/depaware.txt | 2 +- ipn/ipnlocal/local.go | 7 +++++++ net/tsdial/tsdial.go | 32 +++++++++++++++++++++++++++++++- tailcfg/tailcfg.go | 7 ++++++- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 56a755ec6..3b7cb2e36 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -89,7 +89,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture 💣 github.com/djherbis/times from tailscale.com/drive/driveimpl github.com/fxamacker/cbor/v2 from tailscale.com/tka - github.com/gaissmai/bart from tailscale.com/net/tstun + github.com/gaissmai/bart from tailscale.com/net/tstun+ github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json/internal/jsonflags+ github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json/internal/jsonopts+ github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 84f18fe01..6cb5855ce 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3590,6 +3590,7 @@ func (b *LocalBackend) authReconfig() { nm := b.netMap hasPAC := b.prevIfState.HasPAC() disableSubnetsIfPAC := nm.HasCap(tailcfg.NodeAttrDisableSubnetsIfPAC) + userDialUseRoutes := nm.HasCap(tailcfg.NodeAttrUserDialUseRoutes) dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID()) dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.logf, version.OS()) // If the current node is an app connector, ensure the app connector machine is started @@ -3647,6 +3648,12 @@ func (b *LocalBackend) authReconfig() { } b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", prefs.RouteAll(), prefs.CorpDNS(), flags, err) + if userDialUseRoutes { + b.dialer.SetRoutes(rcfg.Routes, rcfg.LocalRoutes) + } else { + b.dialer.SetRoutes(nil, nil) + } + b.initPeerAPIListener() } diff --git a/net/tsdial/tsdial.go b/net/tsdial/tsdial.go index 80d208f4f..42433b871 100644 --- a/net/tsdial/tsdial.go +++ b/net/tsdial/tsdial.go @@ -14,9 +14,11 @@ import ( "runtime" "strings" "sync" + "sync/atomic" "syscall" "time" + "github.com/gaissmai/bart" "tailscale.com/net/dnscache" "tailscale.com/net/netknob" "tailscale.com/net/netmon" @@ -66,6 +68,8 @@ type Dialer struct { netnsDialerOnce sync.Once netnsDialer netns.Dialer + routes atomic.Pointer[bart.Table[bool]] // or nil if UserDial should not use routes. `true` indicates routes that point into the Tailscale interface + mu sync.Mutex closed bool dns dnsMap @@ -129,6 +133,23 @@ func (d *Dialer) SetExitDNSDoH(doh string) { } } +// SetRoutes configures the dialer to dial the specified routes via Tailscale, +// and the specified localRoutes using the default interface. +func (d *Dialer) SetRoutes(routes, localRoutes []netip.Prefix) { + var rt *bart.Table[bool] + if len(routes) > 0 || len(localRoutes) > 0 { + rt = &bart.Table[bool]{} + for _, r := range routes { + rt.Insert(r, true) + } + for _, r := range localRoutes { + rt.Insert(r, false) + } + } + + d.routes.Store(rt) +} + func (d *Dialer) Close() error { d.mu.Lock() defer d.mu.Unlock() @@ -387,6 +408,15 @@ func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, } return d.NetstackDialTCP(ctx, ipp) } + + if routes := d.routes.Load(); routes != nil { + if isTailscaleRoute, _ := routes.Get(ipp.Addr()); isTailscaleRoute { + return d.getPeerDialer().DialContext(ctx, network, ipp.String()) + } + + return d.SystemDial(ctx, network, ipp.String()) + } + // Workaround for macOS for now: dial Tailscale IPs with peer dialer. // TODO(bradfitz): fix dialing subnet routers, public IPs via exit nodes, // etc. This is a temporary partial for macOS. We need to plumb ART tables & @@ -424,7 +454,7 @@ func (d *Dialer) dialPeerAPI(ctx context.Context, network, addr string) (net.Con } // getPeerDialer returns the *net.Dialer to use to dial peers (e.g. for peerapi, -// or "tailscale nc") +// "tailscale nc", or querying internal DNS servers over Tailscale) // // This is not used in netstack mode. // diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index dbe9daa8b..11f67386d 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -132,7 +132,8 @@ type CapabilityVersion int // - 89: 2024-03-23: Client no longer respects deleted PeerChange.Capabilities (use CapMap) // - 90: 2024-04-03: Client understands PeerCapabilityTaildrive. // - 91: 2024-04-24: Client understands PeerCapabilityTaildriveSharer. -const CurrentCapabilityVersion CapabilityVersion = 91 +// - 92: 2024-05-06: Client understands NodeAttrUserDialUseRoutes. +const CurrentCapabilityVersion CapabilityVersion = 92 type StableID string @@ -2259,6 +2260,10 @@ const ( // NodeAttrSuggestExitNodeUI allows the currently suggested exit node to appear in the client GUI. NodeAttrSuggestExitNodeUI NodeCapability = "suggest-exit-node-ui" + + // NodeAttrUserDialUseRoutes makes UserDial use either the peer dialer or the system dialer, + // depending on the destination address and the configured routes. + NodeAttrUserDialUseRoutes NodeCapability = "user-dial-routes" ) // SetDNSRequest is a request to add a DNS record.