net/tsdial: start of new package to unify all outbound dialing complexity

For now this just deletes the net/socks5/tssocks implementation (and
the DNSMap stuff from wgengine/netstack) and moves it into net/tsdial.

Then initialize a Dialer early in tailscaled, currently only use for the
outbound and SOCKS5 proxies. It will be plumbed more later. Notably, it
needs to get down into the DNS forwarder for exit node DNS forwading
in netstack mode. But it will also absorb all the peerapi setsockopt
and netns Dial and tlsdial complexity too.

Updates #1713

Change-Id: Ibc6d56ae21a22655b2fa1002d8fc3f2b2ae8b6df
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2021-11-30 15:53:34 -08:00
committed by Brad Fitzpatrick
parent 3ae6f898cf
commit d5405c66b7
12 changed files with 221 additions and 198 deletions

114
net/tsdial/dnsmap.go Normal file
View File

@@ -0,0 +1,114 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tsdial
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"inet.af/netaddr"
"tailscale.com/types/netmap"
"tailscale.com/util/dnsname"
)
// DNSMap maps MagicDNS names (both base + FQDN) to their first IP.
// It must not be mutated once created.
//
// Example keys are "foo.domain.tld.beta.tailscale.net" and "foo",
// both without trailing dots.
type DNSMap map[string]netaddr.IP
func DNSMapFromNetworkMap(nm *netmap.NetworkMap) DNSMap {
ret := make(DNSMap)
suffix := nm.MagicDNSSuffix()
have4 := false
if nm.Name != "" && len(nm.Addresses) > 0 {
ip := nm.Addresses[0].IP()
ret[strings.TrimRight(nm.Name, ".")] = ip
if dnsname.HasSuffix(nm.Name, suffix) {
ret[dnsname.TrimSuffix(nm.Name, suffix)] = ip
}
for _, a := range nm.Addresses {
if a.IP().Is4() {
have4 = true
}
}
}
for _, p := range nm.Peers {
if p.Name == "" {
continue
}
for _, a := range p.Addresses {
ip := a.IP()
if ip.Is4() && !have4 {
continue
}
ret[strings.TrimRight(p.Name, ".")] = ip
if dnsname.HasSuffix(p.Name, suffix) {
ret[dnsname.TrimSuffix(p.Name, suffix)] = ip
}
break
}
}
for _, rec := range nm.DNS.ExtraRecords {
if rec.Type != "" {
continue
}
ip, err := netaddr.ParseIP(rec.Value)
if err != nil {
continue
}
ret[strings.TrimRight(rec.Name, ".")] = ip
}
return ret
}
// Resolve resolves addr into an IP:port using first the MagicDNS contents
// of m, else using the system resolver.
func (m DNSMap) Resolve(ctx context.Context, addr string) (netaddr.IPPort, error) {
ipp, pippErr := netaddr.ParseIPPort(addr)
if pippErr == nil {
return ipp, nil
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
// addr is malformed.
return netaddr.IPPort{}, err
}
if _, err := netaddr.ParseIP(host); err == nil {
// The host part of addr was an IP, so the netaddr.ParseIPPort above should've
// passed. Must've been a bad port number. Return the original error.
return netaddr.IPPort{}, pippErr
}
port16, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return netaddr.IPPort{}, fmt.Errorf("invalid port in address %q", addr)
}
// Host is not an IP, so assume it's a DNS name.
// Try MagicDNS first, otherwise a real DNS lookup.
ip := m[host]
if !ip.IsZero() {
return netaddr.IPPortFrom(ip, uint16(port16)), nil
}
// TODO(bradfitz): wire up net/dnscache too.
// No MagicDNS name so try real DNS.
var r net.Resolver
ips, err := r.LookupIP(ctx, "ip", host)
if err != nil {
return netaddr.IPPort{}, err
}
if len(ips) == 0 {
return netaddr.IPPort{}, fmt.Errorf("DNS lookup returned no results for %q", host)
}
ip, _ = netaddr.FromStdIP(ips[0])
return netaddr.IPPortFrom(ip, uint16(port16)), nil
}

112
net/tsdial/dnsmap_test.go Normal file
View File

@@ -0,0 +1,112 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tsdial
import (
"reflect"
"testing"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/netmap"
)
func TestDNSMapFromNetworkMap(t *testing.T) {
pfx := netaddr.MustParseIPPrefix
ip := netaddr.MustParseIP
tests := []struct {
name string
nm *netmap.NetworkMap
want DNSMap
}{
{
name: "self",
nm: &netmap.NetworkMap{
Name: "foo.tailnet",
Addresses: []netaddr.IPPrefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
},
want: DNSMap{
"foo": ip("100.102.103.104"),
"foo.tailnet": ip("100.102.103.104"),
},
},
{
name: "self_and_peers",
nm: &netmap.NetworkMap{
Name: "foo.tailnet",
Addresses: []netaddr.IPPrefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
Peers: []*tailcfg.Node{
{
Name: "a.tailnet",
Addresses: []netaddr.IPPrefix{
pfx("100.0.0.201/32"),
pfx("100::201/128"),
},
},
{
Name: "b.tailnet",
Addresses: []netaddr.IPPrefix{
pfx("100::202/128"),
},
},
},
},
want: DNSMap{
"foo": ip("100.102.103.104"),
"foo.tailnet": ip("100.102.103.104"),
"a": ip("100.0.0.201"),
"a.tailnet": ip("100.0.0.201"),
"b": ip("100::202"),
"b.tailnet": ip("100::202"),
},
},
{
name: "self_has_v6_only",
nm: &netmap.NetworkMap{
Name: "foo.tailnet",
Addresses: []netaddr.IPPrefix{
pfx("100::123/128"),
},
Peers: []*tailcfg.Node{
{
Name: "a.tailnet",
Addresses: []netaddr.IPPrefix{
pfx("100.0.0.201/32"),
pfx("100::201/128"),
},
},
{
Name: "b.tailnet",
Addresses: []netaddr.IPPrefix{
pfx("100::202/128"),
},
},
},
},
want: DNSMap{
"foo": ip("100::123"),
"foo.tailnet": ip("100::123"),
"a": ip("100::201"),
"a.tailnet": ip("100::201"),
"b": ip("100::202"),
"b.tailnet": ip("100::202"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DNSMapFromNetworkMap(tt.nm)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mismatch:\n got %v\nwant %v\n", got, tt.want)
}
})
}
}

61
net/tsdial/tsdial.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package tsdial provides a Dialer type that can dial out of tailscaled.
package tsdial
import (
"context"
"errors"
"net"
"sync"
"inet.af/netaddr"
)
// Dialer dials out of tailscaled, while taking care of details while
// handling the dozens of edge cases depending on the server mode
// (TUN, netstack), the OS network sandboxing style (macOS/iOS
// Extension, none), user-selected route acceptance prefs, etc.
type Dialer struct {
// 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
// NetstackDialTCP dials the provided IPPort using netstack.
// If nil, it's not used.
NetstackDialTCP func(context.Context, netaddr.IPPort) (net.Conn, error)
mu sync.Mutex
dns DNSMap
}
func (d *Dialer) SetDNSMap(m DNSMap) {
d.mu.Lock()
defer d.mu.Unlock()
d.dns = m
}
func (d *Dialer) resolve(ctx context.Context, addr string) (netaddr.IPPort, error) {
d.mu.Lock()
dns := d.dns
d.mu.Unlock()
return dns.Resolve(ctx, addr)
}
func (d *Dialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := d.resolve(ctx, addr)
if err != nil {
return nil, err
}
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.IP()) {
if d.NetstackDialTCP == nil {
return nil, errors.New("Dialer not initialized correctly")
}
return d.NetstackDialTCP(ctx, ipp)
}
// TODO(bradfitz): netns, etc
var stdDialer net.Dialer
return stdDialer.DialContext(ctx, network, ipp.String())
}