ipn/ipnlocal,net/dns/resolver: serve MagicDNS names from live indexes
Every netmap change, including an incremental delta of a single peer, rebuilt the full MagicDNS state twice: dnsConfigForNetmap walked all peers to build the dns.Config.Hosts map, and resolver.SetConfig then walked that map again to build its reverse (PTR) index. On a tailnet with 10k peers that is a lot of garbage per delta. Instead, add a resolver.MagicDNSHosts hook, installed once by LocalBackend, that the quad-100 resolver consults on demand at query time. It is backed by nodeBackend's nodeByName, nodeByAddr, and peers indexes, which are already maintained incrementally as netmap deltas arrive. The subdomain-resolve capability check also moves to the hook (checking the node's CapMap at query time), so dns.Config's SubdomainHosts is no longer populated. dns.Config.Hosts remains for control's DNS.ExtraRecords, which are few and which feed the split-DNS decisions in dns.Manager's compileConfig, and on Windows it still carries every node's records because the hosts-file fallback path (compileHostEntries) needs the complete enumerable set. Those compileConfig decisions also consulted the per-node Hosts entries (hasHostsWithoutSplitDNSRoutes), so a new Config.MagicDNSHostsUnrouted bit preserves that signal now that node records are not listed: with MagicDNS names present but MagicDNS domain routing off, quad-100 stays in the OS resolver path. One small behavior change: reverse (PTR) lookups now also answer for node addresses whose forward records are filtered out by the IPv6-suppression rule (issue #1152), since nodeByAddr indexes all node addresses. Previously such addresses were absent from the pushed Hosts map and thus from the reverse index. Updates #12542 Updates tailscale/corp#43949 Change-Id: I63b99199c2b3b124c08cb8bbaea1f63165095294 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
6a635c4e55
commit
7e609b2581
+15
-2
@@ -61,6 +61,14 @@ type Config struct {
|
||||
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
|
||||
// instead of the IPv4 version (100.100.100.100).
|
||||
OnlyIPv6 bool
|
||||
// MagicDNSHostsUnrouted is whether MagicDNS host records are
|
||||
// served on demand via [resolver.MagicDNSHosts] (so are not
|
||||
// listed in Hosts) without being covered by any Routes entry;
|
||||
// that is, MagicDNS domain routing is off. It preserves the
|
||||
// effect the node records had when they were listed in Hosts:
|
||||
// hasHostsWithoutSplitDNSRoutes reports true, keeping quad-100
|
||||
// in the OS resolver path so the names still resolve.
|
||||
MagicDNSHostsUnrouted bool
|
||||
}
|
||||
|
||||
var magicDNSDualStack = envknob.RegisterBool("TS_DEBUG_MAGIC_DNS_DUAL_STACK")
|
||||
@@ -127,8 +135,13 @@ func (c Config) hasDefaultIPResolversOnly() bool {
|
||||
// hasHostsWithoutSplitDNSRoutes reports whether c contains any Host entries
|
||||
// that aren't covered by a SplitDNS route suffix.
|
||||
func (c Config) hasHostsWithoutSplitDNSRoutes() bool {
|
||||
// TODO(bradfitz): this could be more efficient, but we imagine
|
||||
// the number of SplitDNS routes and/or hosts will be small.
|
||||
if c.MagicDNSHostsUnrouted {
|
||||
return true
|
||||
}
|
||||
// Hosts is small here on most platforms: per-node records are
|
||||
// served via [resolver.MagicDNSHosts] (accounted for above), so
|
||||
// only control's DNS.ExtraRecords remain. On Windows it still
|
||||
// carries every node's records for the hosts-file path.
|
||||
for host := range c.Hosts {
|
||||
if !c.hasSplitDNSRouteForHost(host) {
|
||||
return true
|
||||
|
||||
@@ -61,13 +61,14 @@ func (src *Config) Clone() *Config {
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _ConfigCloneNeedsRegeneration = Config(struct {
|
||||
AcceptDNS bool
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
SearchDomains []dnsname.FQDN
|
||||
Hosts map[dnsname.FQDN][]netip.Addr
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
OnlyIPv6 bool
|
||||
AcceptDNS bool
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
SearchDomains []dnsname.FQDN
|
||||
Hosts map[dnsname.FQDN][]netip.Addr
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
OnlyIPv6 bool
|
||||
MagicDNSHostsUnrouted bool
|
||||
}{})
|
||||
|
||||
// Clone duplicates src into dst and reports whether it succeeded.
|
||||
|
||||
+19
-9
@@ -139,16 +139,26 @@ func (v ConfigView) SubdomainHosts() views.Map[dnsname.FQDN, struct{}] {
|
||||
|
||||
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
|
||||
// instead of the IPv4 version (100.100.100.100).
|
||||
func (v ConfigView) OnlyIPv6() bool { return v.ж.OnlyIPv6 }
|
||||
func (v ConfigView) Equal(v2 ConfigView) bool { return v.ж.Equal(v2.ж) }
|
||||
func (v ConfigView) OnlyIPv6() bool { return v.ж.OnlyIPv6 }
|
||||
|
||||
// MagicDNSHostsUnrouted is whether MagicDNS host records are
|
||||
// served on demand via [resolver.MagicDNSHosts] (so are not
|
||||
// listed in Hosts) without being covered by any Routes entry;
|
||||
// that is, MagicDNS domain routing is off. It preserves the
|
||||
// effect the node records had when they were listed in Hosts:
|
||||
// hasHostsWithoutSplitDNSRoutes reports true, keeping quad-100
|
||||
// in the OS resolver path so the names still resolve.
|
||||
func (v ConfigView) MagicDNSHostsUnrouted() bool { return v.ж.MagicDNSHostsUnrouted }
|
||||
func (v ConfigView) Equal(v2 ConfigView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _ConfigViewNeedsRegeneration = Config(struct {
|
||||
AcceptDNS bool
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
SearchDomains []dnsname.FQDN
|
||||
Hosts map[dnsname.FQDN][]netip.Addr
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
OnlyIPv6 bool
|
||||
AcceptDNS bool
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
SearchDomains []dnsname.FQDN
|
||||
Hosts map[dnsname.FQDN][]netip.Addr
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
OnlyIPv6 bool
|
||||
MagicDNSHostsUnrouted bool
|
||||
}{})
|
||||
|
||||
@@ -229,6 +229,53 @@ type Resolver struct {
|
||||
hostToIP map[dnsname.FQDN][]netip.Addr
|
||||
ipToHost map[netip.Addr]dnsname.FQDN
|
||||
subdomainHosts set.Set[dnsname.FQDN]
|
||||
magicHosts MagicDNSHosts // or nil if none installed
|
||||
}
|
||||
|
||||
// MagicDNSHosts is a live source of MagicDNS host records, installed
|
||||
// via [Resolver.SetMagicDNSHosts].
|
||||
//
|
||||
// It replaces the per-node entries of [Config.Hosts]: instead of the
|
||||
// caller pushing a full snapshot of every node's name and addresses
|
||||
// into the resolver on every (possibly incremental) netmap change,
|
||||
// the resolver pulls the answer for one name on demand from the
|
||||
// caller's live indexes. [Config.Hosts] remains for control's
|
||||
// DNS.ExtraRecords entries, which are few, and is consulted first.
|
||||
//
|
||||
// Implementations must be safe for concurrent use and cheap: the
|
||||
// methods are called on the DNS query serving path. Name lookups are
|
||||
// case-insensitive: the resolver passes lowercase names, but
|
||||
// implementations must not rely on that.
|
||||
type MagicDNSHosts interface {
|
||||
// LookupHost returns the IPs to answer for the node with the
|
||||
// given MagicDNS FQDN, and whether the name is known. It returns
|
||||
// all answerable IPs regardless of record type; the resolver
|
||||
// filters them by the query's type, and a known name with no IPs
|
||||
// of the query's family is "name exists, no records", not
|
||||
// NXDOMAIN.
|
||||
LookupHost(dnsname.FQDN) (ips []netip.Addr, ok bool)
|
||||
|
||||
// LookupPTR returns the MagicDNS FQDN of the node that owns
|
||||
// the given Tailscale IP, and whether the IP is known.
|
||||
LookupPTR(netip.Addr) (_ dnsname.FQDN, ok bool)
|
||||
|
||||
// SubdomainHost reports whether fqdn names a node with the
|
||||
// [tailcfg.NodeAttrDNSSubdomainResolve] attribute, whose
|
||||
// subdomains all resolve to the node's own addresses.
|
||||
SubdomainHost(dnsname.FQDN) bool
|
||||
}
|
||||
|
||||
// SetMagicDNSHosts installs the live MagicDNS host source consulted
|
||||
// by forward and reverse MagicDNS lookups that miss [Config.Hosts].
|
||||
// It is expected to be called once, before the resolver serves
|
||||
// queries.
|
||||
func (r *Resolver) SetMagicDNSHosts(h MagicDNSHosts) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.magicHosts = h
|
||||
}
|
||||
|
||||
type ForwardLinkSelector interface {
|
||||
@@ -682,15 +729,23 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
|
||||
hosts := r.hostToIP
|
||||
localDomains := r.localDomains
|
||||
subdomainHosts := r.subdomainHosts
|
||||
magicHosts := r.magicHosts
|
||||
r.mu.Unlock()
|
||||
|
||||
addrs, found := hosts[domain]
|
||||
if !found && magicHosts != nil {
|
||||
addrs, found = magicHosts.LookupHost(domain)
|
||||
}
|
||||
if !found {
|
||||
for parent := domain.Parent(); parent != ""; parent = parent.Parent() {
|
||||
if subdomainHosts.Contains(parent) {
|
||||
addrs, found = hosts[parent]
|
||||
break
|
||||
}
|
||||
if magicHosts != nil && magicHosts.SubdomainHost(parent) {
|
||||
addrs, found = magicHosts.LookupHost(parent)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
@@ -865,6 +920,9 @@ func (r *Resolver) fqdnForIPLocked(ip netip.Addr, name dnsname.FQDN) (dnsname.FQ
|
||||
}
|
||||
|
||||
ret, ok := r.ipToHost[ip]
|
||||
if !ok && r.magicHosts != nil {
|
||||
ret, ok = r.magicHosts.LookupPTR(ip)
|
||||
}
|
||||
if !ok {
|
||||
for _, suffix := range r.localDomains {
|
||||
if suffix.Contains(name) {
|
||||
|
||||
@@ -476,6 +476,111 @@ func TestResolveLocalSubdomain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// fakeMagicDNSHosts is a MagicDNSHosts for tests, serving from fixed maps.
|
||||
type fakeMagicDNSHosts struct {
|
||||
hosts map[dnsname.FQDN][]netip.Addr
|
||||
subdomain set.Set[dnsname.FQDN]
|
||||
ptr map[netip.Addr]dnsname.FQDN
|
||||
}
|
||||
|
||||
func (f fakeMagicDNSHosts) LookupHost(fqdn dnsname.FQDN) (ips []netip.Addr, ok bool) {
|
||||
ips, ok = f.hosts[fqdn]
|
||||
return ips, ok
|
||||
}
|
||||
|
||||
func (f fakeMagicDNSHosts) LookupPTR(ip netip.Addr) (_ dnsname.FQDN, ok bool) {
|
||||
name, ok := f.ptr[ip]
|
||||
return name, ok
|
||||
}
|
||||
|
||||
func (f fakeMagicDNSHosts) SubdomainHost(fqdn dnsname.FQDN) bool {
|
||||
return f.subdomain.Contains(fqdn)
|
||||
}
|
||||
|
||||
// Tests forward, subdomain, and reverse resolution served on demand
|
||||
// via the MagicDNSHosts hook, and that entries pushed via Config.Hosts
|
||||
// take precedence over the hook.
|
||||
func TestResolveLocalMagicDNSHosts(t *testing.T) {
|
||||
r := newResolver(t)
|
||||
defer r.Close()
|
||||
|
||||
r.SetConfig(Config{
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{
|
||||
"extra.ipn.dev.": {netip.MustParseAddr("100.100.1.1")},
|
||||
"both.ipn.dev.": {netip.MustParseAddr("100.100.2.2")},
|
||||
},
|
||||
LocalDomains: []dnsname.FQDN{"ipn.dev.", "64.100.in-addr.arpa."},
|
||||
})
|
||||
node4 := netip.MustParseAddr("100.64.0.7")
|
||||
node6 := netip.MustParseAddr("fd7a:115c:a1e0::7")
|
||||
r.SetMagicDNSHosts(fakeMagicDNSHosts{
|
||||
hosts: map[dnsname.FQDN][]netip.Addr{
|
||||
"node.ipn.dev.": {node4, node6},
|
||||
"v4only.ipn.dev.": {node4},
|
||||
"subber.ipn.dev.": {node4},
|
||||
"both.ipn.dev.": {netip.MustParseAddr("100.100.9.9")}, // masked by Config.Hosts
|
||||
},
|
||||
subdomain: set.Of[dnsname.FQDN]("subber.ipn.dev."),
|
||||
ptr: map[netip.Addr]dnsname.FQDN{node4: "node.ipn.dev."},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
qname dnsname.FQDN
|
||||
qtype dns.Type
|
||||
ip netip.Addr
|
||||
code dns.RCode
|
||||
}{
|
||||
{"hook-ipv4", "node.ipn.dev.", dns.TypeA, node4, dns.RCodeSuccess},
|
||||
{"hook-ipv6", "node.ipn.dev.", dns.TypeAAAA, node6, dns.RCodeSuccess},
|
||||
// A known name with no records of the queried family is
|
||||
// "name exists, no records", not NXDOMAIN.
|
||||
{"hook-no-ipv6", "v4only.ipn.dev.", dns.TypeAAAA, netip.Addr{}, dns.RCodeSuccess},
|
||||
{"hook-nxdomain", "gone.ipn.dev.", dns.TypeA, netip.Addr{}, dns.RCodeNameError},
|
||||
{"hook-foreign", "google.com.", dns.TypeA, netip.Addr{}, dns.RCodeRefused},
|
||||
{"hook-subdomain", "foo.subber.ipn.dev.", dns.TypeA, node4, dns.RCodeSuccess},
|
||||
{"hook-subdomain-deep", "bar.foo.subber.ipn.dev.", dns.TypeA, node4, dns.RCodeSuccess},
|
||||
{"hook-subdomain-no-cap", "foo.node.ipn.dev.", dns.TypeA, netip.Addr{}, dns.RCodeNameError},
|
||||
// Config.Hosts entries (control's ExtraRecords) are
|
||||
// consulted before the hook.
|
||||
{"config-hosts", "extra.ipn.dev.", dns.TypeA, netip.MustParseAddr("100.100.1.1"), dns.RCodeSuccess},
|
||||
{"config-hosts-precedence", "both.ipn.dev.", dns.TypeA, netip.MustParseAddr("100.100.2.2"), dns.RCodeSuccess},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ip, code := r.resolveLocal(tt.qname, tt.qtype)
|
||||
if code != tt.code {
|
||||
t.Errorf("code = %v; want %v", code, tt.code)
|
||||
}
|
||||
if ip != tt.ip {
|
||||
t.Errorf("ip = %v; want %v", ip, tt.ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
revTests := []struct {
|
||||
name string
|
||||
q dnsname.FQDN
|
||||
want dnsname.FQDN
|
||||
code dns.RCode
|
||||
}{
|
||||
{"hook-ptr", "7.0.64.100.in-addr.arpa.", "node.ipn.dev.", dns.RCodeSuccess},
|
||||
{"hook-ptr-nxdomain", "8.0.64.100.in-addr.arpa.", "", dns.RCodeNameError},
|
||||
{"hook-ptr-foreign", "5.4.3.2.in-addr.arpa.", "", dns.RCodeRefused},
|
||||
}
|
||||
for _, tt := range revTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
name, code := r.resolveLocalReverse(tt.q)
|
||||
if code != tt.code {
|
||||
t.Errorf("code = %v; want %v", code, tt.code)
|
||||
}
|
||||
if name != tt.want {
|
||||
t.Errorf("name = %v; want %v", name, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLocalReverse(t *testing.T) {
|
||||
r := newResolver(t)
|
||||
defer r.Close()
|
||||
|
||||
Reference in New Issue
Block a user