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 <nickk@tailscale.com>
This commit is contained in:
Nick Khyl 2024-05-02 14:36:26 -05:00 committed by Nick Khyl
parent ce8969d82b
commit caa3d7594f
4 changed files with 45 additions and 3 deletions

View File

@ -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 LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
💣 github.com/djherbis/times from tailscale.com/drive/driveimpl 💣 github.com/djherbis/times from tailscale.com/drive/driveimpl
github.com/fxamacker/cbor/v2 from tailscale.com/tka 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 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/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 github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext

View File

@ -3590,6 +3590,7 @@ func (b *LocalBackend) authReconfig() {
nm := b.netMap nm := b.netMap
hasPAC := b.prevIfState.HasPAC() hasPAC := b.prevIfState.HasPAC()
disableSubnetsIfPAC := nm.HasCap(tailcfg.NodeAttrDisableSubnetsIfPAC) disableSubnetsIfPAC := nm.HasCap(tailcfg.NodeAttrDisableSubnetsIfPAC)
userDialUseRoutes := nm.HasCap(tailcfg.NodeAttrUserDialUseRoutes)
dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID()) dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID())
dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.logf, version.OS()) 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 // 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) 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() b.initPeerAPIListener()
} }

View File

@ -14,9 +14,11 @@
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
"github.com/gaissmai/bart"
"tailscale.com/net/dnscache" "tailscale.com/net/dnscache"
"tailscale.com/net/netknob" "tailscale.com/net/netknob"
"tailscale.com/net/netmon" "tailscale.com/net/netmon"
@ -66,6 +68,8 @@ type Dialer struct {
netnsDialerOnce sync.Once netnsDialerOnce sync.Once
netnsDialer netns.Dialer 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 mu sync.Mutex
closed bool closed bool
dns dnsMap 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 { func (d *Dialer) Close() error {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() 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) 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. // Workaround for macOS for now: dial Tailscale IPs with peer dialer.
// TODO(bradfitz): fix dialing subnet routers, public IPs via exit nodes, // 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 & // 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, // 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. // This is not used in netstack mode.
// //

View File

@ -132,7 +132,8 @@
// - 89: 2024-03-23: Client no longer respects deleted PeerChange.Capabilities (use CapMap) // - 89: 2024-03-23: Client no longer respects deleted PeerChange.Capabilities (use CapMap)
// - 90: 2024-04-03: Client understands PeerCapabilityTaildrive. // - 90: 2024-04-03: Client understands PeerCapabilityTaildrive.
// - 91: 2024-04-24: Client understands PeerCapabilityTaildriveSharer. // - 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 type StableID string
@ -2259,6 +2260,10 @@ type Oauth2Token struct {
// NodeAttrSuggestExitNodeUI allows the currently suggested exit node to appear in the client GUI. // NodeAttrSuggestExitNodeUI allows the currently suggested exit node to appear in the client GUI.
NodeAttrSuggestExitNodeUI NodeCapability = "suggest-exit-node-ui" 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. // SetDNSRequest is a request to add a DNS record.