net/tsdial: add SystemDial as a wrapper on netns.Dial

The connections returned from SystemDial are automatically closed when
there is a major link change.

Also plumb through the dialer to the noise client so that connections
are auto-reset when moving from cellular to WiFi etc.

Updates #3363

Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali
2022-04-27 11:57:59 -07:00
committed by Maisem Ali
parent e38d3dfc76
commit 5a1ef1bbb9
9 changed files with 138 additions and 20 deletions

View File

@@ -20,8 +20,12 @@ import (
"inet.af/netaddr"
"tailscale.com/net/dnscache"
"tailscale.com/net/interfaces"
"tailscale.com/net/netknob"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/util/mak"
"tailscale.com/wgengine/monitor"
)
@@ -30,6 +34,7 @@ import (
// (TUN, netstack), the OS network sandboxing style (macOS/iOS
// Extension, none), user-selected route acceptance prefs, etc.
type Dialer struct {
Logf logger.Logf
// UseNetstackForIP if non-nil is whether NetstackDialTCP (if
// it's non-nil) should be used to dial the provided IP.
UseNetstackForIP func(netaddr.IP) bool
@@ -46,12 +51,33 @@ type Dialer struct {
peerDialerOnce sync.Once
peerDialer *net.Dialer
mu sync.Mutex
dns dnsMap
tunName string // tun device name
linkMon *monitor.Mon
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
dnsCache *dnscache.MessageCache // nil until first first non-empty SetExitDNSDoH
netnsDialerOnce sync.Once
netnsDialer netns.Dialer
mu sync.Mutex
closed bool
dns dnsMap
tunName string // tun device name
linkMon *monitor.Mon
linkMonUnregister func()
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
dnsCache *dnscache.MessageCache // nil until first first non-empty SetExitDNSDoH
nextSysConnID int
activeSysConns map[int]net.Conn // active connections not yet closed
}
// sysConn wraps a net.Conn that was created using d.SystemDial.
// It exists to track which connections are still open, and should be
// closed on major link changes.
type sysConn struct {
net.Conn
id int
d *Dialer
}
func (c sysConn) Close() error {
c.d.closeSysConn(c.id)
return nil
}
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
@@ -91,10 +117,53 @@ func (d *Dialer) SetExitDNSDoH(doh string) {
}
}
func (d *Dialer) Close() error {
d.mu.Lock()
defer d.mu.Unlock()
d.closed = true
if d.linkMonUnregister != nil {
d.linkMonUnregister()
d.linkMonUnregister = nil
}
for _, c := range d.activeSysConns {
c.Close()
}
d.activeSysConns = nil
return nil
}
func (d *Dialer) SetLinkMonitor(mon *monitor.Mon) {
d.mu.Lock()
defer d.mu.Unlock()
if d.linkMonUnregister != nil {
go d.linkMonUnregister()
d.linkMonUnregister = nil
}
d.linkMon = mon
d.linkMonUnregister = d.linkMon.RegisterChangeCallback(d.linkChanged)
}
func (d *Dialer) linkChanged(major bool, state *interfaces.State) {
if !major {
return
}
d.mu.Lock()
defer d.mu.Unlock()
for id, c := range d.activeSysConns {
go c.Close()
delete(d.activeSysConns, id)
}
}
func (d *Dialer) closeSysConn(id int) {
d.mu.Lock()
defer d.mu.Unlock()
c, ok := d.activeSysConns[id]
if !ok {
return
}
delete(d.activeSysConns, id)
go c.Close() // ignore the error
}
func (d *Dialer) interfaceIndexLocked(ifName string) (index int, ok bool) {
@@ -197,6 +266,42 @@ func ipNetOfNetwork(n string) string {
return "ip"
}
// SystemDial connects to the provided network address without going over
// Tailscale. It prefers going over the default interface and closes existing
// connections if the default interface changes. It is used to connect to
// Control and (in the future, as of 2022-04-27) DERPs..
func (d *Dialer) SystemDial(ctx context.Context, network, addr string) (net.Conn, error) {
d.mu.Lock()
closed := d.closed
d.mu.Unlock()
if closed {
return nil, net.ErrClosed
}
d.netnsDialerOnce.Do(func() {
logf := d.Logf
if logf == nil {
logf = logger.Discard
}
d.netnsDialer = netns.NewDialer(logf)
})
c, err := d.netnsDialer.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
d.mu.Lock()
defer d.mu.Unlock()
id := d.nextSysConnID
d.nextSysConnID++
mak.Set(&d.activeSysConns, id, c)
return sysConn{
id: id,
d: d,
Conn: c,
}, nil
}
// UserDial connects to the provided network address as if a user were initiating the dial.
// (e.g. from a SOCKS or HTTP outbound proxy)
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {