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>
165 lines
5.7 KiB
Go
165 lines
5.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
|
|
|
|
package dns
|
|
|
|
import (
|
|
jsonv1 "encoding/json"
|
|
"errors"
|
|
"net/netip"
|
|
|
|
jsonv2 "github.com/go-json-experiment/json"
|
|
"github.com/go-json-experiment/json/jsontext"
|
|
"tailscale.com/types/dnstype"
|
|
"tailscale.com/types/views"
|
|
"tailscale.com/util/dnsname"
|
|
"tailscale.com/util/set"
|
|
)
|
|
|
|
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=Config
|
|
|
|
// View returns a read-only view of Config.
|
|
func (p *Config) View() ConfigView {
|
|
return ConfigView{ж: p}
|
|
}
|
|
|
|
// ConfigView provides a read-only view over Config.
|
|
//
|
|
// Its methods should only be called if `Valid()` returns true.
|
|
type ConfigView struct {
|
|
// ж is the underlying mutable value, named with a hard-to-type
|
|
// character that looks pointy like a pointer.
|
|
// It is named distinctively to make you think of how dangerous it is to escape
|
|
// to callers. You must not let callers be able to mutate it.
|
|
ж *Config
|
|
}
|
|
|
|
// Valid reports whether v's underlying value is non-nil.
|
|
func (v ConfigView) Valid() bool { return v.ж != nil }
|
|
|
|
// AsStruct returns a clone of the underlying value which aliases no memory with
|
|
// the original.
|
|
func (v ConfigView) AsStruct() *Config {
|
|
if v.ж == nil {
|
|
return nil
|
|
}
|
|
return v.ж.Clone()
|
|
}
|
|
|
|
// MarshalJSON implements [jsonv1.Marshaler].
|
|
func (v ConfigView) MarshalJSON() ([]byte, error) {
|
|
return jsonv1.Marshal(v.ж)
|
|
}
|
|
|
|
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
|
func (v ConfigView) MarshalJSONTo(enc *jsontext.Encoder) error {
|
|
return jsonv2.MarshalEncode(enc, v.ж)
|
|
}
|
|
|
|
// UnmarshalJSON implements [jsonv1.Unmarshaler].
|
|
func (v *ConfigView) UnmarshalJSON(b []byte) error {
|
|
if v.ж != nil {
|
|
return errors.New("already initialized")
|
|
}
|
|
if len(b) == 0 {
|
|
return nil
|
|
}
|
|
var x Config
|
|
if err := jsonv1.Unmarshal(b, &x); err != nil {
|
|
return err
|
|
}
|
|
v.ж = &x
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
|
func (v *ConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
|
if v.ж != nil {
|
|
return errors.New("already initialized")
|
|
}
|
|
var x Config
|
|
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
|
|
return err
|
|
}
|
|
v.ж = &x
|
|
return nil
|
|
}
|
|
|
|
// AcceptDNS true if [Prefs.CorpDNS] is enabled (or --accept-dns=true).
|
|
// This should be used for error handling and health reporting
|
|
// purposes only.
|
|
func (v ConfigView) AcceptDNS() bool { return v.ж.AcceptDNS }
|
|
|
|
// DefaultResolvers are the DNS resolvers to use for DNS names
|
|
// which aren't covered by more specific per-domain routes below.
|
|
// If empty, the OS's default resolvers (the ones that predate
|
|
// Tailscale altering the configuration) are used.
|
|
func (v ConfigView) DefaultResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
|
|
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.DefaultResolvers)
|
|
}
|
|
|
|
// Routes maps a DNS suffix to the resolvers that should be used
|
|
// for queries that fall within that suffix.
|
|
// If a query doesn't match any entry in Routes, the
|
|
// DefaultResolvers are used.
|
|
// A Routes entry with no resolvers means the route should be
|
|
// authoritatively answered using the contents of Hosts.
|
|
func (v ConfigView) Routes() views.MapFn[dnsname.FQDN, []*dnstype.Resolver, views.SliceView[*dnstype.Resolver, dnstype.ResolverView]] {
|
|
return views.MapFnOf(v.ж.Routes, func(t []*dnstype.Resolver) views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
|
|
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](t)
|
|
})
|
|
}
|
|
|
|
// SearchDomains are DNS suffixes to try when expanding
|
|
// single-label queries.
|
|
func (v ConfigView) SearchDomains() views.Slice[dnsname.FQDN] {
|
|
return views.SliceOf(v.ж.SearchDomains)
|
|
}
|
|
|
|
// Hosts maps DNS FQDNs to their IPs, which can be a mix of IPv4
|
|
// and IPv6.
|
|
// Queries matching entries in Hosts are resolved locally by
|
|
// 100.100.100.100 without leaving the machine.
|
|
// Adding an entry to Hosts merely creates the record. If you want
|
|
// it to resolve, you also need to add appropriate routes to
|
|
// Routes.
|
|
func (v ConfigView) Hosts() views.MapSlice[dnsname.FQDN, netip.Addr] {
|
|
return views.MapSliceOf(v.ж.Hosts)
|
|
}
|
|
|
|
// SubdomainHosts is a set of FQDNs from Hosts that should also
|
|
// resolve subdomain queries to the same IPs. For example, if
|
|
// "node.tailnet.ts.net" is in SubdomainHosts, then queries for
|
|
// "anything.node.tailnet.ts.net" will resolve to node's IPs.
|
|
func (v ConfigView) SubdomainHosts() views.Map[dnsname.FQDN, struct{}] {
|
|
return views.MapOf(v.ж.SubdomainHosts)
|
|
}
|
|
|
|
// 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 }
|
|
|
|
// 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
|
|
MagicDNSHostsUnrouted bool
|
|
}{})
|