net/tsdial, ipn/ipnlocal: stop using netmap.NetworkMap in Dialer
tsdial.Dialer.SetNetMap rebuilt an O(n peers) map of MagicDNS names on every netmap change. As we move toward per-peer incremental deltas, this becomes quadratic. This removes it and replaces it with SetResolveMagicDNS, a callback into LocalBackend that looks up hostnames from nodeBackend's new nodeByName index (populated alongside nodeByAddr/nodeByKey on both full and delta paths). The index stores both FQDNs and short names as keys. This is the same treatment applied to netlog (8f210454d), wglog (988b0905b), and drive (1d6989408): stop pushing *netmap.NetworkMap into subsystems and instead have them pull from LocalBackend's live data via callbacks. Updates #12542 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I24557ab0c8a27636e08e4779bcfd3ec633db0a78
This commit is contained in:
committed by
Brad Fitzpatrick
parent
8dde9b725b
commit
aefb1531d1
+4
-90
@@ -4,80 +4,21 @@
|
||||
package tsdial
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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, and both always lowercase.
|
||||
type dnsMap map[string]netip.Addr
|
||||
|
||||
// canonMapKey canonicalizes its input s to be a dnsMap map key.
|
||||
// canonMapKey canonicalizes its input s to be a MagicDNS lookup key:
|
||||
// lowercase with no trailing dot.
|
||||
func canonMapKey(s string) string {
|
||||
return strings.ToLower(strings.TrimSuffix(s, "."))
|
||||
}
|
||||
|
||||
func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
|
||||
if nm == nil {
|
||||
return nil
|
||||
}
|
||||
ret := make(dnsMap)
|
||||
suffix := nm.MagicDNSSuffix()
|
||||
have4 := false
|
||||
addrs := nm.GetAddresses()
|
||||
if name := nm.SelfName(); name != "" && addrs.Len() > 0 {
|
||||
ip := addrs.At(0).Addr()
|
||||
ret[canonMapKey(name)] = ip
|
||||
if dnsname.HasSuffix(name, suffix) {
|
||||
ret[canonMapKey(dnsname.TrimSuffix(name, suffix))] = ip
|
||||
}
|
||||
for _, p := range addrs.All() {
|
||||
if p.Addr().Is4() {
|
||||
have4 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.Name() == "" {
|
||||
continue
|
||||
}
|
||||
for _, pfx := range p.Addresses().All() {
|
||||
ip := pfx.Addr()
|
||||
if ip.Is4() && !have4 {
|
||||
continue
|
||||
}
|
||||
ret[canonMapKey(p.Name())] = ip
|
||||
if dnsname.HasSuffix(p.Name(), suffix) {
|
||||
ret[canonMapKey(dnsname.TrimSuffix(p.Name(), suffix))] = ip
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, rec := range nm.DNS.ExtraRecords {
|
||||
if rec.Type != "" {
|
||||
continue
|
||||
}
|
||||
ip, err := netip.ParseAddr(rec.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret[canonMapKey(rec.Name)] = ip
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// errUnresolved is a sentinel error returned by dnsMap.resolveMemory.
|
||||
// errUnresolved is a sentinel error returned when a hostname is not
|
||||
// resolvable via MagicDNS.
|
||||
var errUnresolved = errors.New("address well formed but not resolved")
|
||||
|
||||
func splitHostPort(addr string) (host string, port uint16, err error) {
|
||||
@@ -91,30 +32,3 @@ func splitHostPort(addr string) (host string, port uint16, err error) {
|
||||
}
|
||||
return host, uint16(port16), nil
|
||||
}
|
||||
|
||||
// Resolve resolves addr into an IP:port using first the MagicDNS contents
|
||||
// of m, else using the system resolver.
|
||||
//
|
||||
// The error is [exactly] errUnresolved if the addr is a name that isn't known
|
||||
// in the map.
|
||||
func (m dnsMap) resolveMemory(ctx context.Context, network, addr string) (_ netip.AddrPort, err error) {
|
||||
host, port, err := splitHostPort(addr)
|
||||
if err != nil {
|
||||
// addr malformed or invalid port.
|
||||
return netip.AddrPort{}, err
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host); err == nil {
|
||||
// addr was literal ip:port.
|
||||
return netip.AddrPortFrom(ip, port), nil
|
||||
}
|
||||
|
||||
// Host is not an IP, so assume it's a DNS name.
|
||||
|
||||
// Try MagicDNS first, otherwise a real DNS lookup.
|
||||
ip := m[canonMapKey(host)]
|
||||
if ip.IsValid() {
|
||||
return netip.AddrPortFrom(ip, port), nil
|
||||
}
|
||||
|
||||
return netip.AddrPort{}, errUnresolved
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tsdial
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
|
||||
nv := make([]tailcfg.NodeView, len(v))
|
||||
for i, n := range v {
|
||||
nv[i] = n.View()
|
||||
}
|
||||
return nv
|
||||
}
|
||||
|
||||
func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
pfx := netip.MustParsePrefix
|
||||
ip := netip.MustParseAddr
|
||||
tests := []struct {
|
||||
name string
|
||||
nm *netmap.NetworkMap
|
||||
want dnsMap
|
||||
}{
|
||||
{
|
||||
name: "self",
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "foo.tailnet.",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.102.103.104/32"),
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
want: dnsMap{
|
||||
"foo": ip("100.102.103.104"),
|
||||
"foo.tailnet": ip("100.102.103.104"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "self_and_peers",
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "foo.tailnet.",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.102.103.104/32"),
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
Name: "a.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.0.0.201/32"),
|
||||
pfx("100::201/128"),
|
||||
},
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
Name: "b.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::202/128"),
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
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{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "foo.tailnet.",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Name: "a.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.0.0.201/32"),
|
||||
pfx("100::201/128"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "b.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+37
-13
@@ -32,7 +32,6 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -90,9 +89,16 @@ type Dialer struct {
|
||||
|
||||
routes atomic.Pointer[bart.Table[bool]] // or nil if UserDial should not use routes. `true` indicates routes that point into the Tailscale interface
|
||||
|
||||
// resolveMagicDNS, if non-nil, resolves a MagicDNS hostname (short
|
||||
// name or FQDN, without trailing dot, lowercased) to an IP address.
|
||||
// The network parameter ("tcp", "tcp4", "tcp6", "udp", "udp4",
|
||||
// "udp6") constrains the address family of the result. The normal
|
||||
// implementation is [ipnlocal.LocalBackend.resolveMagicDNS],
|
||||
// installed at construction time. It is read without holding mu.
|
||||
resolveMagicDNS atomic.Pointer[func(hostname, network string) (_ netip.Addr, ok bool)]
|
||||
|
||||
mu syncs.Mutex
|
||||
closed bool
|
||||
dns dnsMap
|
||||
tunName string // tun device name
|
||||
netMon *netmon.Monitor
|
||||
netMonUnregister func()
|
||||
@@ -357,14 +363,34 @@ func (d *Dialer) PeerDialControlFunc() func(network, address string, c syscall.R
|
||||
return peerDialControlFunc(d)
|
||||
}
|
||||
|
||||
// SetNetMap sets the current network map and notably, the DNS names
|
||||
// in its DNS configuration.
|
||||
func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
|
||||
m := dnsMapFromNetworkMap(nm)
|
||||
// SetResolveMagicDNS installs a callback that resolves MagicDNS hostnames
|
||||
// to IP addresses for UserDial.
|
||||
func (d *Dialer) SetResolveMagicDNS(fn func(hostname, network string) (_ netip.Addr, ok bool)) {
|
||||
if fn == nil {
|
||||
d.resolveMagicDNS.Store(nil)
|
||||
return
|
||||
}
|
||||
d.resolveMagicDNS.Store(&fn)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.dns = m
|
||||
// resolveAddr tries to resolve addr (a "host:port" string) via MagicDNS.
|
||||
// The network parameter ("tcp", "tcp4", "tcp6", etc.) constrains the
|
||||
// address family. It returns errUnresolved if the hostname is not a
|
||||
// known MagicDNS name.
|
||||
func (d *Dialer) resolveAddr(_ context.Context, network, addr string) (netip.AddrPort, error) {
|
||||
host, port, err := splitHostPort(addr)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, err
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host); err == nil {
|
||||
return netip.AddrPortFrom(ip, port), nil
|
||||
}
|
||||
if fn := d.resolveMagicDNS.Load(); fn != nil {
|
||||
if ip, ok := (*fn)(canonMapKey(host), network); ok {
|
||||
return netip.AddrPortFrom(ip, port), nil
|
||||
}
|
||||
}
|
||||
return netip.AddrPort{}, errUnresolved
|
||||
}
|
||||
|
||||
// userDialResolveAll resolves addr as if a user initiating the dial.
|
||||
@@ -375,14 +401,12 @@ func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
|
||||
// non-empty on a nil-error return.
|
||||
func (d *Dialer) userDialResolveAll(ctx context.Context, network, addr string) ([]netip.AddrPort, error) {
|
||||
d.mu.Lock()
|
||||
dns := d.dns
|
||||
exitDNSDoH := d.exitDNSDoHBase
|
||||
d.mu.Unlock()
|
||||
|
||||
// MagicDNS or otherwise baked into the NetworkMap? Try that first.
|
||||
// dns.resolveMemory returns a single address; tailnet names have
|
||||
// one IP each, so there's nothing to race.
|
||||
ipp, err := dns.resolveMemory(ctx, network, addr)
|
||||
// Tailnet names have one IP each, so there's nothing to race.
|
||||
ipp, err := d.resolveAddr(ctx, network, addr)
|
||||
if err != errUnresolved {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user