// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause // Package dnscache contains a minimal DNS cache that makes a bunch of // assumptions that are only valid for us. Not recommended for general use. package dnscache import ( "context" "crypto/tls" "errors" "fmt" "log" "net" "net/netip" "runtime" "sync" "sync/atomic" "time" "tailscale.com/envknob" "tailscale.com/types/logger" "tailscale.com/util/cloudenv" "tailscale.com/util/singleflight" "tailscale.com/util/slicesx" ) var zaddr netip.Addr var single = &Resolver{ Forward: &net.Resolver{PreferGo: preferGoResolver()}, } func preferGoResolver() bool { // There does not appear to be a local resolver running // on iOS, and NetworkExtension is good at isolating DNS. // So do not use the Go resolver on macOS/iOS. if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { return false } // The local resolver is not available on Android. if runtime.GOOS == "android" { return false } // Otherwise, the Go resolver is fine and slightly preferred // since it's lighter, not using cgo calls & threads. return true } // Get returns a caching Resolver singleton. func Get() *Resolver { return single } // Resolver is a minimal DNS caching resolver. // // The TTL is always fixed for now. It's not intended for general use. // Cache entries are never cleaned up so it's intended that this is // only used with a fixed set of hostnames. type Resolver struct { // Forward is the resolver to use to populate the cache. // If nil, net.DefaultResolver is used. Forward *net.Resolver // LookupIPFallback optionally provides a backup DNS mechanism // to use if Forward returns an error or no results. LookupIPFallback func(ctx context.Context, host string) ([]netip.Addr, error) // TTL is how long to keep entries cached // // If zero, a default (currently 10 minutes) is used. TTL time.Duration // UseLastGood controls whether a cached entry older than TTL is used // if a refresh fails. UseLastGood bool // SingleHostStaticResult, if non-nil, is the static result of IPs that is returned // by Resolver.LookupIP for any hostname. When non-nil, SingleHost must also be // set with the expected name. SingleHostStaticResult []netip.Addr // SingleHost is the hostname that SingleHostStaticResult is for. // It is required when SingleHostStaticResult is present. SingleHost string // Logf optionally provides a log function to use for debug logs. If // not present, log.Printf will be used. The prefix "dnscache: " will // be added to all log messages printed with this logger. Logf logger.Logf sf singleflight.Group[string, ipRes] mu sync.Mutex ipCache map[string]ipCacheEntry } // ipRes is the type used by the Resolver.sf singleflight group. type ipRes struct { ip, ip6 netip.Addr allIPs []netip.Addr } type ipCacheEntry struct { ip netip.Addr // either v4 or v6 ip6 netip.Addr // nil if no v4 or no v6 allIPs []netip.Addr // 1+ v4 and/or v6 expires time.Time } func (r *Resolver) fwd() *net.Resolver { if r.Forward != nil { return r.Forward } return net.DefaultResolver } // dlogf logs a debug message if debug logging is enabled either globally via // the TS_DEBUG_DNS_CACHE environment variable or via the per-Resolver // configuration. func (r *Resolver) dlogf(format string, args ...any) { logf := r.Logf if logf == nil { logf = log.Printf } if debug() || debugLogging.Load() { logf("dnscache: "+format, args...) } } // cloudHostResolver returns a Resolver for the current cloud hosting environment. // It currently only supports Google Cloud. func (r *Resolver) cloudHostResolver() (v *net.Resolver, ok bool) { switch runtime.GOOS { case "android", "ios", "darwin": return nil, false } ip := cloudenv.Get().ResolverIP() if ip == "" { return nil, false } return &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, network, net.JoinHostPort(ip, "53")) }, }, true } func (r *Resolver) ttl() time.Duration { if r.TTL > 0 { return r.TTL } return 10 * time.Minute } var debug = envknob.RegisterBool("TS_DEBUG_DNS_CACHE") // debugLogging allows enabling debug logging at runtime, via // SetDebugLoggingEnabled. // // This is a global variable instead of a per-Resolver variable because we // create new Resolvers throughout the lifetime of the program (e.g. on every // new Direct client, etc.). When we enable debug logs, though, we want to do // so for every single created Resolver; we'd need to plumb a bunch of new code // through all of the intermediate packages to accomplish the same behaviour as // just using a global variable. var debugLogging atomic.Bool // SetDebugLoggingEnabled controls whether debug logging is enabled for this // package. // // These logs are also printed when the TS_DEBUG_DNS_CACHE envknob is set, but // we allow configuring this manually as well so that it can be changed at // runtime. func SetDebugLoggingEnabled(v bool) { debugLogging.Store(v) } // LookupIP returns the host's primary IP address (either IPv4 or // IPv6, but preferring IPv4) and optionally its IPv6 address, if // there is both IPv4 and IPv6. // // If err is nil, ip will be non-nil. The v6 address may be nil even // with a nil error. func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr, allIPs []netip.Addr, err error) { if r.SingleHostStaticResult != nil { if r.SingleHost != host { return zaddr, zaddr, nil, fmt.Errorf("dnscache: unexpected hostname %q doesn't match expected %q", host, r.SingleHost) } for _, naIP := range r.SingleHostStaticResult { if !ip.IsValid() && naIP.Is4() { ip = naIP } if !v6.IsValid() && naIP.Is6() { v6 = naIP } allIPs = append(allIPs, naIP) } r.dlogf("returning %d static results", len(allIPs)) return } // Hard-code this to avoid extra work, DNS fallbacks, etc. if host == "localhost" { r.dlogf("host is localhost") // TODO: @raggi mentioned that some distributions don't use // 127.0.0.1 as the localhost IP; should we check the interface // address to determine this? ip = netip.AddrFrom4([4]byte{127, 0, 0, 1}) v6 = netip.IPv6Loopback() allIPs = []netip.Addr{ip, v6} err = nil return } if ip, err := netip.ParseAddr(host); err == nil { ip = ip.Unmap() r.dlogf("%q is an IP", host) return ip, zaddr, []netip.Addr{ip}, nil } if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok { r.dlogf("%q = %v (cached)", host, ip) return ip, ip6, allIPs, nil } ch := r.sf.DoChanContext(ctx, host, func(ctx context.Context) (ret ipRes, _ error) { ip, ip6, allIPs, err := r.lookupIP(ctx, host) if err != nil { return ret, err } return ipRes{ip, ip6, allIPs}, nil }) select { case res := <-ch: if res.Err != nil { if r.UseLastGood { if ip, ip6, allIPs, ok := r.lookupIPCacheExpired(host); ok { r.dlogf("%q using %v after error", host, ip) return ip, ip6, allIPs, nil } } r.dlogf("error resolving %q: %v", host, res.Err) return zaddr, zaddr, nil, res.Err } r := res.Val return r.ip, r.ip6, r.allIPs, nil case <-ctx.Done(): r.dlogf("context done while resolving %q: %v", host, ctx.Err()) return zaddr, zaddr, nil, ctx.Err() } } func (r *Resolver) lookupIPCache(host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, ok bool) { r.mu.Lock() defer r.mu.Unlock() if ent, ok := r.ipCache[host]; ok && ent.expires.After(time.Now()) { return ent.ip, ent.ip6, ent.allIPs, true } return zaddr, zaddr, nil, false } func (r *Resolver) lookupIPCacheExpired(host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, ok bool) { r.mu.Lock() defer r.mu.Unlock() if ent, ok := r.ipCache[host]; ok { return ent.ip, ent.ip6, ent.allIPs, true } return zaddr, zaddr, nil, false } func (r *Resolver) lookupTimeoutForHost(host string) time.Duration { if r.UseLastGood { if _, _, _, ok := r.lookupIPCacheExpired(host); ok { // If we have some previous good value for this host, // don't give this DNS lookup much time. If we're in a // situation where the user's DNS server is unreachable // (e.g. their corp DNS server is behind a subnet router // that can't come up due to Tailscale needing to // connect to itself), then we want to fail fast and let // our caller (who set UseLastGood) fall back to using // the last-known-good IP address. return 3 * time.Second } } return 10 * time.Second } func (r *Resolver) lookupIP(ctx context.Context, host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, err error) { if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok { r.dlogf("%q found in cache as %v", host, ip) return ip, ip6, allIPs, nil } lookupCtx, lookupCancel := context.WithTimeout(ctx, r.lookupTimeoutForHost(host)) defer lookupCancel() ips, err := r.fwd().LookupNetIP(lookupCtx, "ip", host) if err != nil || len(ips) == 0 { if resolver, ok := r.cloudHostResolver(); ok { r.dlogf("resolving %q via cloud resolver", host) ips, err = resolver.LookupNetIP(lookupCtx, "ip", host) } } if (err != nil || len(ips) == 0) && r.LookupIPFallback != nil { lookupCtx, lookupCancel := context.WithTimeout(ctx, 30*time.Second) defer lookupCancel() if err != nil { r.dlogf("resolving %q using fallback resolver due to error", host) } else { r.dlogf("resolving %q using fallback resolver due to no returned IPs", host) } ips, err = r.LookupIPFallback(lookupCtx, host) } if err != nil { return netip.Addr{}, netip.Addr{}, nil, err } if len(ips) == 0 { return netip.Addr{}, netip.Addr{}, nil, fmt.Errorf("no IPs for %q found", host) } // Unmap everything; LookupNetIP can return mapped addresses (see #5698) for i := range ips { ips[i] = ips[i].Unmap() } have4 := false for _, ipa := range ips { if ipa.Is4() { if !have4 { ip6 = ip ip = ipa have4 = true } } else { if have4 { ip6 = ipa } else { ip = ipa } } } r.addIPCache(host, ip, ip6, ips, r.ttl()) return ip, ip6, ips, nil } func (r *Resolver) addIPCache(host string, ip, ip6 netip.Addr, allIPs []netip.Addr, d time.Duration) { if ip.IsPrivate() { // Don't cache obviously wrong entries from captive portals. // TODO: use DoH or DoT for the forwarding resolver? r.dlogf("%q resolved to private IP %v; using but not caching", host, ip) return } r.dlogf("%q resolved to IP %v; caching", host, ip) r.mu.Lock() defer r.mu.Unlock() if r.ipCache == nil { r.ipCache = make(map[string]ipCacheEntry) } r.ipCache[host] = ipCacheEntry{ ip: ip, ip6: ip6, allIPs: allIPs, expires: time.Now().Add(d), } } type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error) // Dialer returns a wrapped DialContext func that uses the provided dnsCache. func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc { d := &dialer{ fwd: fwd, dnsCache: dnsCache, pastConnect: map[netip.Addr]time.Time{}, } return d.DialContext } // dialer is the config and accumulated state for a dial func returned by Dialer. type dialer struct { fwd DialContextFunc dnsCache *Resolver mu sync.Mutex pastConnect map[netip.Addr]time.Time } func (d *dialer) DialContext(ctx context.Context, network, address string) (retConn net.Conn, ret error) { host, port, err := net.SplitHostPort(address) if err != nil { // Bogus. But just let the real dialer return an error rather than // inventing a similar one. return d.fwd(ctx, network, address) } dc := &dialCall{ d: d, network: network, address: address, host: host, port: port, } defer func() { // On failure, consider that our DNS might be wrong and ask the DNS fallback mechanism for // some other IPs to try. if !d.shouldTryBootstrap(ctx, ret, dc) { return } ips, err := d.dnsCache.LookupIPFallback(ctx, host) if err != nil { // Return with original error return } if c, err := dc.raceDial(ctx, ips); err == nil { retConn = c ret = nil return } }() ip, ip6, allIPs, err := d.dnsCache.LookupIP(ctx, host) if err != nil { return nil, fmt.Errorf("failed to resolve %q: %w", host, err) } i4s := v4addrs(allIPs) if len(i4s) < 2 { d.dnsCache.dlogf("dialing %s, %s for %s", network, ip, address) c, err := dc.dialOne(ctx, ip.Unmap()) if err == nil || ctx.Err() != nil || !ip6.IsValid() { return c, err } // Fall back to trying IPv6. return dc.dialOne(ctx, ip6) } // Multiple IPv4 candidates, and 0+ IPv6. ipsToTry := append(i4s, v6addrs(allIPs)...) return dc.raceDial(ctx, ipsToTry) } func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall) bool { // No need to do anything when we succeeded. if err == nil { return false } // Can't try bootstrap DNS if we don't have a fallback function if d.dnsCache.LookupIPFallback == nil { d.dnsCache.dlogf("not using bootstrap DNS: no fallback") return false } // We can't retry if the context is canceled, since any further // operations with this context will fail. if ctxErr := ctx.Err(); ctxErr != nil { d.dnsCache.dlogf("not using bootstrap DNS: context error: %v", ctxErr) return false } wasTrustworthy := dc.dnsWasTrustworthy() if wasTrustworthy { d.dnsCache.dlogf("not using bootstrap DNS: DNS was trustworthy") return false } return true } // dialCall is the state around a single call to dial. type dialCall struct { d *dialer network, address, host, port string mu sync.Mutex // lock ordering: dialer.mu, then dialCall.mu fails map[netip.Addr]error // set of IPs that failed to dial thus far } // dnsWasTrustworthy reports whether we think the IP address(es) we // tried (and failed) to dial were probably the correct IPs. Currently // the heuristic is whether they ever worked previously. func (dc *dialCall) dnsWasTrustworthy() bool { dc.d.mu.Lock() defer dc.d.mu.Unlock() dc.mu.Lock() defer dc.mu.Unlock() if len(dc.fails) == 0 { // No information. return false } // If any of the IPs we failed to dial worked previously in // this dialer, assume the DNS is fine. for ip := range dc.fails { if _, ok := dc.d.pastConnect[ip]; ok { return true } } return false } func (dc *dialCall) dialOne(ctx context.Context, ip netip.Addr) (net.Conn, error) { c, err := dc.d.fwd(ctx, dc.network, net.JoinHostPort(ip.String(), dc.port)) dc.noteDialResult(ip, err) return c, err } // noteDialResult records that a dial to ip either succeeded or // failed. func (dc *dialCall) noteDialResult(ip netip.Addr, err error) { if err == nil { d := dc.d d.mu.Lock() defer d.mu.Unlock() d.pastConnect[ip] = time.Now() return } dc.mu.Lock() defer dc.mu.Unlock() if dc.fails == nil { dc.fails = map[netip.Addr]error{} } dc.fails[ip] = err } // uniqueIPs returns a possibly-mutated subslice of ips, filtering out // dups and ones that have already failed previously. func (dc *dialCall) uniqueIPs(ips []netip.Addr) (ret []netip.Addr) { dc.mu.Lock() defer dc.mu.Unlock() seen := map[netip.Addr]bool{} ret = ips[:0] for _, ip := range ips { if seen[ip] { continue } seen[ip] = true if dc.fails[ip] != nil { continue } ret = append(ret, ip) } return ret } // fallbackDelay is how long to wait between trying subsequent // addresses when multiple options are available. // 300ms is the same as Go's Happy Eyeballs fallbackDelay value. const fallbackDelay = 300 * time.Millisecond // raceDial tries to dial port on each ip in ips, starting a new race // dial every fallbackDelay apart, returning whichever completes first. func (dc *dialCall) raceDial(ctx context.Context, ips []netip.Addr) (net.Conn, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() type res struct { c net.Conn err error } resc := make(chan res) // must be unbuffered failBoost := make(chan struct{}) // best effort send on dial failure // Remove IPs that we tried & failed to dial previously // (such as when we're being called after a dnsfallback lookup and get // the same results) ips = dc.uniqueIPs(ips) if len(ips) == 0 { return nil, errors.New("no IPs") } // Partition candidate list and then merge such that an IPv6 address is // in the first spot if present, and then addresses are interleaved. // This ensures that we're trying an IPv6 address first, then // alternating between v4 and v6 in case one of the two networks is // broken. var iv4, iv6 []netip.Addr for _, ip := range ips { if ip.Is6() { iv6 = append(iv6, ip) } else { iv4 = append(iv4, ip) } } ips = slicesx.Interleave(iv6, iv4) go func() { for i, ip := range ips { if i != 0 { timer := time.NewTimer(fallbackDelay) select { case <-timer.C: case <-failBoost: timer.Stop() case <-ctx.Done(): timer.Stop() return } } go func(ip netip.Addr) { c, err := dc.dialOne(ctx, ip) if err != nil { // Best effort wake-up a pending dial. // e.g. IPv4 dials failing quickly on an IPv6-only system. // In that case we don't want to wait 300ms per IPv4 before // we get to the IPv6 addresses. select { case failBoost <- struct{}{}: default: } } select { case resc <- res{c, err}: case <-ctx.Done(): if c != nil { c.Close() } } }(ip) } }() var firstErr error var fails int for { select { case r := <-resc: if r.c != nil { return r.c, nil } fails++ if firstErr == nil { firstErr = r.err } if fails == len(ips) { return nil, firstErr } case <-ctx.Done(): return nil, ctx.Err() } } } func v4addrs(aa []netip.Addr) (ret []netip.Addr) { for _, a := range aa { a = a.Unmap() if a.Is4() { ret = append(ret, a) } } return ret } func v6addrs(aa []netip.Addr) (ret []netip.Addr) { for _, a := range aa { if a.Is6() && !a.Is4In6() { ret = append(ret, a) } } return ret } // TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext. // It returns a *tls.Conn type on success. // On TLS cert validation failure, it can invoke a backup DNS resolution strategy. func TLSDialer(fwd DialContextFunc, dnsCache *Resolver, tlsConfigBase *tls.Config) DialContextFunc { tcpDialer := Dialer(fwd, dnsCache) return func(ctx context.Context, network, address string) (net.Conn, error) { host, _, err := net.SplitHostPort(address) if err != nil { return nil, err } tcpConn, err := tcpDialer(ctx, network, address) if err != nil { return nil, err } cfg := cloneTLSConfig(tlsConfigBase) if cfg.ServerName == "" { cfg.ServerName = host } tlsConn := tls.Client(tcpConn, cfg) handshakeCtx, handshakeTimeoutCancel := context.WithTimeout(ctx, 5*time.Second) defer handshakeTimeoutCancel() if err := tlsConn.HandshakeContext(handshakeCtx); err != nil { tcpConn.Close() // TODO: if err != errTLSHandshakeTimeout, // assume it might be some captive portal or // otherwise incorrect DNS and try the backup // DNS mechanism. return nil, err } return tlsConn, nil } } func cloneTLSConfig(cfg *tls.Config) *tls.Config { if cfg == nil { return &tls.Config{} } return cfg.Clone() }