This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+253 -71
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package resolver implements a stub DNS resolver that can also serve
@@ -16,7 +16,7 @@ import (
"net/netip"
"os"
"runtime"
"sort"
"slices"
"strconv"
"strings"
"sync"
@@ -39,6 +39,7 @@ import (
"tailscale.com/util/clientmetric"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/set"
)
const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon."
@@ -48,8 +49,26 @@ const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon."
// truncation in a platform-agnostic way.
const maxResponseBytes = 4095
// defaultTTL is the TTL of all responses from Resolver.
const defaultTTL = 600 * time.Second
// defaultTTL is the TTL of positive responses from Resolver.
//
// It's short because the source of truth (the netmap-fed host maps)
// is local and in-memory, so re-queries are nearly free, while
// anything cached downstream (e.g. mDNSResponder on macOS) delays
// clients noticing node renames for the full TTL (tailscale/corp#45631).
const defaultTTL = 5 * time.Second
// negativeTTL is how long resolvers may cache the nonexistence of a
// name (or of records of the queried type) for domains we're
// authoritative for. It's advertised via the SOA record attached to
// the authority section of NXDOMAIN and no-data responses, per RFC
// 2308. Without it, some resolvers (notably mDNSResponder) seem to
// cache negative entries for a really long time, so a name queried
// shortly before a node rename doesn't start resolving for a while
// (tailscale/corp#45631).
const negativeTTL = 10 * time.Second
// timeNow is time.Now, except in tests.
var timeNow = time.Now
var (
errNotQuery = errors.New("not a DNS query")
@@ -69,6 +88,9 @@ type packet struct {
// Else forward the query to the most specific matching entry in Routes.
// Else return SERVFAIL.
type Config struct {
// True if [Prefs.CorpDNS] is true or --accept-dns=true was specified.
// This should only be used for error handling and health reporting.
AcceptDNS bool
// Routes is a map of DNS name suffix to the resolvers to use for
// queries within that suffix.
// Queries only match the most specific suffix.
@@ -79,6 +101,12 @@ type Config struct {
// LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers.
LocalDomains []dnsname.FQDN
// SubdomainHosts is a set of FQDNs from Hosts that should also
// resolve subdomain queries to the same IPs. If a query like
// "sub.node.tailnet.ts.net" doesn't match Hosts directly, and
// "node.tailnet.ts.net" is in SubdomainHosts, the query resolves
// to the IPs for "node.tailnet.ts.net".
SubdomainHosts set.Set[dnsname.FQDN]
}
// WriteToBufioWriter write a debug version of c for logs to w, omitting
@@ -162,7 +190,7 @@ func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]*dnstype.Resolver) {
}
kk = append(kk, k)
}
sort.Slice(kk, func(i, j int) bool { return kk[i] < kk[j] })
slices.Sort(kk)
w.WriteByte('{')
for i, k := range kk {
if i > 0 {
@@ -214,10 +242,61 @@ type Resolver struct {
closed chan struct{}
// mu guards the following fields from being updated while used.
mu syncs.Mutex
localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netip.Addr
ipToHost map[netip.Addr]dnsname.FQDN
mu syncs.Mutex
localDomains []dnsname.FQDN
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.
//
// Short names ("foo") are only resolved if MagicDNS is enabled
// for the current tailnet.
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 {
@@ -255,6 +334,16 @@ func New(logf logger.Logf, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, h
func (r *Resolver) TestOnlySetHook(hook func(Config)) { r.saveConfigForTests = hook }
// ProbeLocks acquires and releases the resolver's internal mutexes.
func (r *Resolver) ProbeLocks() {
r.mu.Lock()
r.mu.Unlock()
if r.forwarder != nil {
r.forwarder.probeLocks()
}
}
func (r *Resolver) SetConfig(cfg Config) error {
if !buildfeatures.HasDNS {
return nil
@@ -271,16 +360,29 @@ func (r *Resolver) SetConfig(cfg Config) error {
}
}
r.forwarder.setRoutes(cfg.Routes)
r.forwarder.setRoutes(cfg.Routes, cfg.AcceptDNS)
r.mu.Lock()
defer r.mu.Unlock()
r.localDomains = cfg.LocalDomains
r.hostToIP = cfg.Hosts
r.ipToHost = reverse
r.subdomainHosts = cfg.SubdomainHosts
return nil
}
// CustomSchemeHandler takes a URI (retrieved from [dnstype.Resolver.Addr]) and
// returns an updated URI to use for the current query. The result is only valid
// for right now and may change over time.
type CustomSchemeHandler func(addr string) (newAddr string, err error)
// RegisterCustomScheme adds a [CustomSchemaHandler] that is called to provide
// an updated address to the forwarder when a [dnstype.Resolver.Addr] uses that
// scheme.
func (r *Resolver) RegisterCustomScheme(scheme string, h CustomSchemeHandler) error {
return r.forwarder.RegisterCustomScheme(scheme, h)
}
// Close shuts down the resolver and ensures poll goroutines have exited.
// The Resolver cannot be used again after Close is called.
func (r *Resolver) Close() {
@@ -328,7 +430,12 @@ func (r *Resolver) Query(ctx context.Context, bs []byte, family string, from net
return (<-responses).bs, nil
}
return out, err
if err != nil {
return out, err
}
out = checkResponseSizeAndSetTC(out, bs, family, r.logf)
return out, nil
}
// GetUpstreamResolvers returns the resolvers that would be used to resolve
@@ -642,9 +749,26 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
r.mu.Lock()
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 {
for _, suffix := range localDomains {
if suffix.Contains(domain) {
@@ -715,23 +839,16 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
}
// resolveViaDomain synthesizes an IP address for quad-A DNS requests of the form
// `<IPv4-address-with-hypens-instead-of-dots>-via-<siteid>[.*]`. Two prior formats that
// didn't pan out (due to a Chrome issue and DNS search ndots issues) were
// `<IPv4-address>.via-<X>` and the older `via-<X>.<IPv4-address>`,
// where X is a decimal, or hex-encoded number with a '0x' prefix.
// `<IPv4-address-with-hypens-instead-of-dots>-via-<siteid>[.*]`.
// For example: "192-168-1-2-via-7" or "192-168-1-2-via-7.foo.ts.net."
//
// This exists as a convenient mapping into Tailscales 'Via Range'.
//
// It returns a zero netip.Addr and true to indicate a successful response with
// an empty answers section if the specified domain is a valid Tailscale 4via6
// domain, but the request type is neither quad-A nor ALL.
//
// TODO(maisem/bradfitz/tom): `<IPv4-address>.via-<X>` was introduced
// (2022-06-02) to work around an issue in Chrome where it would treat
// "http://via-1.1.2.3.4" as a search string instead of a URL. We should rip out
// the old format in early 2023.
func (r *Resolver) resolveViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Addr, bool) {
fqdn := string(domain.WithoutTrailingDot())
func (r *Resolver) resolveViaDomain(dnsName dnsname.FQDN, typ dns.Type) (netip.Addr, bool) {
fqdn := string(dnsName.WithoutTrailingDot())
switch typ {
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
// For Type A requests, we should return a successful response
@@ -745,45 +862,23 @@ func (r *Resolver) resolveViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Ad
default:
return netip.Addr{}, false
}
if len(fqdn) < len("via-X.0.0.0.0") {
if len(fqdn) < len("0-0-0-0-via-0") {
return netip.Addr{}, false // too short to be valid
}
var siteID string
var ip4Str string
switch {
case strings.Contains(fqdn, "-via-"):
// Format number 3: "192-168-1-2-via-7" or "192-168-1-2-via-7.foo.ts.net."
// Third time's a charm. The earlier two formats follow after this block.
firstLabel, domain, _ := strings.Cut(fqdn, ".") // "192-168-1-2-via-7"
if !(domain == "" || dnsname.HasSuffix(domain, "ts.net") || dnsname.HasSuffix(domain, "tailscale.net")) {
return netip.Addr{}, false
}
v4hyphens, suffix, ok := strings.Cut(firstLabel, "-via-")
if !ok {
return netip.Addr{}, false
}
siteID = suffix
ip4Str = strings.ReplaceAll(v4hyphens, "-", ".")
case strings.HasPrefix(fqdn, "via-"):
firstDot := strings.Index(fqdn, ".")
if firstDot < 0 {
return netip.Addr{}, false // missing dot delimiters
}
siteID = fqdn[len("via-"):firstDot]
ip4Str = fqdn[firstDot+1:]
default:
lastDot := strings.LastIndex(fqdn, ".")
if lastDot < 0 {
return netip.Addr{}, false // missing dot delimiters
}
suffix := fqdn[lastDot+1:]
if !strings.HasPrefix(suffix, "via-") {
return netip.Addr{}, false
}
siteID = suffix[len("via-"):]
ip4Str = fqdn[:lastDot]
if !strings.Contains(fqdn, "-via-") {
return netip.Addr{}, false // not a 4via6 domain
}
firstLabel, domain, _ := strings.Cut(fqdn, ".") // "192-168-1-2-via-7"
if !(domain == "" || dnsname.HasSuffix(domain, "ts.net") || dnsname.HasSuffix(domain, "tailscale.net")) {
return netip.Addr{}, false
}
v4hyphens, suffix, ok := strings.Cut(firstLabel, "-via-")
if !ok {
return netip.Addr{}, false
}
siteID := suffix
ip4Str := strings.ReplaceAll(v4hyphens, "-", ".")
ip4, err := netip.ParseAddr(ip4Str)
if err != nil {
@@ -846,6 +941,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) {
@@ -883,6 +981,14 @@ type response struct {
// NSs are the responses to an NS query.
NSs []*net.NS
// SOAZone, if non-empty, is the zone we're authoritative for
// that contains Question's name. marshalResponse attaches its
// SOA record to the authority section so that resolvers bound
// their negative caching to negativeTTL, per RFC 2308. It must
// only be set on negative responses: NXDOMAIN, or success with
// no records of the queried type.
SOAZone dnsname.FQDN
}
var dnsParserPool = &sync.Pool{
@@ -1064,6 +1170,35 @@ func marshalSRV(queryName dns.Name, srvs []*net.SRV, builder *dns.Builder) error
return nil
}
// marshalSOA serializes zone's SOA record into the authority section
// of an active builder, which must have had StartAuthorities called.
// Its only purpose is communicating negativeTTL to caching resolvers
// (RFC 2308), so all fields other than the TTLs are placeholders.
func marshalSOA(zone dnsname.FQDN, builder *dns.Builder) error {
name, err := dns.NewName(zone.WithTrailingDot())
if err != nil {
return err
}
return builder.SOAResource(dns.ResourceHeader{
Name: name,
Type: dns.TypeSOA,
Class: dns.ClassINET,
TTL: uint32(negativeTTL / time.Second),
}, dns.SOAResource{
NS: name,
MBox: name,
// A serial should only change when the zone data does,
// but nothing consumes ours (no secondaries, no zone
// transfers), and the current time is at least monotonic
// and cheap. Unix seconds fit in the uint32 until 2106.
Serial: uint32(timeNow().Unix()),
Refresh: uint32(negativeTTL / time.Second),
Retry: uint32(negativeTTL / time.Second),
Expire: uint32(negativeTTL / time.Second),
MinTTL: uint32(negativeTTL / time.Second),
})
}
// marshalResponse serializes the DNS response into a new buffer.
func marshalResponse(resp *response) ([]byte, error) {
resp.Header.Response = true
@@ -1098,6 +1233,14 @@ func marshalResponse(resp *response) ([]byte, error) {
// Only successful responses contain answers.
if !isSuccess {
if resp.SOAZone != "" {
if err := builder.StartAuthorities(); err != nil {
return nil, err
}
if err := marshalSOA(resp.SOAZone, &builder); err != nil {
return nil, err
}
}
return builder.Finish()
}
@@ -1131,6 +1274,15 @@ func marshalResponse(resp *response) ([]byte, error) {
return nil, err
}
if resp.SOAZone != "" {
if err := builder.StartAuthorities(); err != nil {
return nil, err
}
if err := marshalSOA(resp.SOAZone, &builder); err != nil {
return nil, err
}
}
return builder.Finish()
}
@@ -1248,6 +1400,19 @@ func rdnsNameToIPv6(name dnsname.FQDN) (ip netip.Addr, ok bool) {
// respondReverse returns a DNS response to a PTR query.
// It is assumed that resp.Question is populated by respond before this is called.
// authoritativeZoneFor returns the zone from the configured local
// domains that contains name, or "" if none does.
func (r *Resolver) authoritativeZoneFor(name dnsname.FQDN) dnsname.FQDN {
r.mu.Lock()
defer r.mu.Unlock()
for _, suffix := range r.localDomains {
if suffix.Contains(name) {
return suffix
}
}
return ""
}
func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *response) ([]byte, error) {
if hasRDNSBonjourPrefix(name) {
metricDNSReverseMissBonjour.Add(1)
@@ -1259,6 +1424,9 @@ func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *respons
metricDNSReverseMissOther.Add(1)
return nil, errNotOurName
}
if resp.Header.RCode == dns.RCodeNameError {
resp.SOAZone = r.authoritativeZoneFor(name)
}
metricDNSMagicDNSSuccessReverse.Add(1)
return marshalResponse(resp)
@@ -1315,6 +1483,17 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
resp := parser.response()
resp.Header.RCode = rcode
resp.IP = ip
switch {
case rcode == dns.RCodeNameError:
resp.SOAZone = r.authoritativeZoneFor(name)
case rcode == dns.RCodeSuccess && !ip.IsValid():
switch parser.Question.Type {
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
// The name exists but has no records of the queried
// type (e.g. an AAAA query for an IPv4-only node).
resp.SOAZone = r.authoritativeZoneFor(name)
}
}
metricDNSMagicDNSSuccessName.Add(1)
return marshalResponse(resp)
}
@@ -1376,21 +1555,24 @@ var (
metricDNSFwdErrorType = clientmetric.NewCounter("dns_query_fwd_error_type")
metricDNSFwdTruncated = clientmetric.NewCounter("dns_query_fwd_truncated")
metricDNSFwdUDP = clientmetric.NewCounter("dns_query_fwd_udp") // on entry
metricDNSFwdUDPWrote = clientmetric.NewCounter("dns_query_fwd_udp_wrote") // sent UDP packet
metricDNSFwdUDPErrorWrite = clientmetric.NewCounter("dns_query_fwd_udp_error_write")
metricDNSFwdUDPErrorServer = clientmetric.NewCounter("dns_query_fwd_udp_error_server")
metricDNSFwdUDPErrorTxID = clientmetric.NewCounter("dns_query_fwd_udp_error_txid")
metricDNSFwdUDPErrorRead = clientmetric.NewCounter("dns_query_fwd_udp_error_read")
metricDNSFwdUDPSuccess = clientmetric.NewCounter("dns_query_fwd_udp_success")
metricDNSFwdUDP = clientmetric.NewCounter("dns_query_fwd_udp") // on entry
metricDNSFwdUDPWrote = clientmetric.NewCounter("dns_query_fwd_udp_wrote") // sent UDP packet
metricDNSFwdUDPDropSrc = clientmetric.NewCounter("dns_query_fwd_udp_drop_src")
metricDNSFwdUDPErrorWrite = clientmetric.NewCounter("dns_query_fwd_udp_error_write")
metricDNSFwdUDPErrorServer = clientmetric.NewCounter("dns_query_fwd_udp_error_server")
metricDNSFwdUDPErrorRefused = clientmetric.NewCounter("dns_query_fwd_udp_error_refused")
metricDNSFwdUDPErrorTxID = clientmetric.NewCounter("dns_query_fwd_udp_error_txid")
metricDNSFwdUDPErrorRead = clientmetric.NewCounter("dns_query_fwd_udp_error_read")
metricDNSFwdUDPSuccess = clientmetric.NewCounter("dns_query_fwd_udp_success")
metricDNSFwdTCP = clientmetric.NewCounter("dns_query_fwd_tcp") // on entry
metricDNSFwdTCPWrote = clientmetric.NewCounter("dns_query_fwd_tcp_wrote") // sent TCP packet
metricDNSFwdTCPErrorWrite = clientmetric.NewCounter("dns_query_fwd_tcp_error_write")
metricDNSFwdTCPErrorServer = clientmetric.NewCounter("dns_query_fwd_tcp_error_server")
metricDNSFwdTCPErrorTxID = clientmetric.NewCounter("dns_query_fwd_tcp_error_txid")
metricDNSFwdTCPErrorRead = clientmetric.NewCounter("dns_query_fwd_tcp_error_read")
metricDNSFwdTCPSuccess = clientmetric.NewCounter("dns_query_fwd_tcp_success")
metricDNSFwdTCP = clientmetric.NewCounter("dns_query_fwd_tcp") // on entry
metricDNSFwdTCPWrote = clientmetric.NewCounter("dns_query_fwd_tcp_wrote") // sent TCP packet
metricDNSFwdTCPErrorWrite = clientmetric.NewCounter("dns_query_fwd_tcp_error_write")
metricDNSFwdTCPErrorServer = clientmetric.NewCounter("dns_query_fwd_tcp_error_server")
metricDNSFwdTCPErrorRefused = clientmetric.NewCounter("dns_query_fwd_tcp_error_refused")
metricDNSFwdTCPErrorTxID = clientmetric.NewCounter("dns_query_fwd_tcp_error_txid")
metricDNSFwdTCPErrorRead = clientmetric.NewCounter("dns_query_fwd_tcp_error_read")
metricDNSFwdTCPSuccess = clientmetric.NewCounter("dns_query_fwd_tcp_success")
metricDNSFwdDoH = clientmetric.NewCounter("dns_query_fwd_doh")
metricDNSFwdDoHErrorStatus = clientmetric.NewCounter("dns_query_fwd_doh_error_status")