mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-26 22:01:09 +01:00 
			
		
		
		
	Previously we were doing this unconditionally. Updates #14428 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
		
			
				
	
	
		
			589 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			589 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| package dns
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"context"
 | |
| 	"encoding/binary"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net"
 | |
| 	"net/netip"
 | |
| 	"runtime"
 | |
| 	"slices"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"sync/atomic"
 | |
| 	"time"
 | |
| 
 | |
| 	"tailscale.com/control/controlknobs"
 | |
| 	"tailscale.com/health"
 | |
| 	"tailscale.com/net/dns/resolver"
 | |
| 	"tailscale.com/net/netmon"
 | |
| 	"tailscale.com/net/tsdial"
 | |
| 	"tailscale.com/syncs"
 | |
| 	"tailscale.com/tstime/rate"
 | |
| 	"tailscale.com/types/dnstype"
 | |
| 	"tailscale.com/types/logger"
 | |
| 	"tailscale.com/util/clientmetric"
 | |
| 	"tailscale.com/util/dnsname"
 | |
| 	"tailscale.com/util/slicesx"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	errFullQueue = errors.New("request queue full")
 | |
| )
 | |
| 
 | |
| // maxActiveQueries returns the maximal number of DNS requests that can
 | |
| // be running.
 | |
| const maxActiveQueries = 256
 | |
| 
 | |
| // We use file-ignore below instead of ignore because on some platforms,
 | |
| // the lint exception is necessary and on others it is not,
 | |
| // and plain ignore complains if the exception is unnecessary.
 | |
| 
 | |
| // Manager manages system DNS settings.
 | |
| type Manager struct {
 | |
| 	logf   logger.Logf
 | |
| 	health *health.Tracker
 | |
| 
 | |
| 	activeQueriesAtomic int32
 | |
| 
 | |
| 	ctx       context.Context    // good until Down
 | |
| 	ctxCancel context.CancelFunc // closes ctx
 | |
| 
 | |
| 	resolver *resolver.Resolver
 | |
| 	os       OSConfigurator
 | |
| 	knobs    *controlknobs.Knobs // or nil
 | |
| 	goos     string              // if empty, gets set to runtime.GOOS
 | |
| 
 | |
| 	mu sync.Mutex // guards following
 | |
| 	// config is the last configuration we successfully compiled or nil if there
 | |
| 	// was any failure applying the last configuration.
 | |
| 	config *Config
 | |
| }
 | |
| 
 | |
| // NewManagers created a new manager from the given config.
 | |
| //
 | |
| // knobs may be nil.
 | |
| func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector, knobs *controlknobs.Knobs, goos string) *Manager {
 | |
| 	if dialer == nil {
 | |
| 		panic("nil Dialer")
 | |
| 	}
 | |
| 	if dialer.NetMon() == nil {
 | |
| 		panic("Dialer has nil NetMon")
 | |
| 	}
 | |
| 	logf = logger.WithPrefix(logf, "dns: ")
 | |
| 	if goos == "" {
 | |
| 		goos = runtime.GOOS
 | |
| 	}
 | |
| 
 | |
| 	m := &Manager{
 | |
| 		logf:     logf,
 | |
| 		resolver: resolver.New(logf, linkSel, dialer, health, knobs),
 | |
| 		os:       oscfg,
 | |
| 		health:   health,
 | |
| 		knobs:    knobs,
 | |
| 		goos:     goos,
 | |
| 	}
 | |
| 
 | |
| 	// Rate limit our attempts to correct our DNS configuration.
 | |
| 	limiter := rate.NewLimiter(1.0/5.0, 1)
 | |
| 
 | |
| 	// This will recompile the DNS config, which in turn will requery the system
 | |
| 	// DNS settings. The recovery func should triggered only when we are missing
 | |
| 	// upstream nameservers and require them to forward a query.
 | |
| 	m.resolver.SetMissingUpstreamRecovery(func() {
 | |
| 		m.mu.Lock()
 | |
| 		defer m.mu.Unlock()
 | |
| 		if m.config == nil {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if limiter.Allow() {
 | |
| 			m.logf("DNS resolution failed due to missing upstream nameservers.  Recompiling DNS configuration.")
 | |
| 			m.setLocked(*m.config)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	m.ctx, m.ctxCancel = context.WithCancel(context.Background())
 | |
| 	m.logf("using %T", m.os)
 | |
| 	return m
 | |
| }
 | |
| 
 | |
| // Resolver returns the Manager's DNS Resolver.
 | |
| func (m *Manager) Resolver() *resolver.Resolver { return m.resolver }
 | |
| 
 | |
| func (m *Manager) Set(cfg Config) error {
 | |
| 	m.mu.Lock()
 | |
| 	defer m.mu.Unlock()
 | |
| 	return m.setLocked(cfg)
 | |
| }
 | |
| 
 | |
| // GetBaseConfig returns the current base OS DNS configuration as provided by the OSConfigurator.
 | |
| func (m *Manager) GetBaseConfig() (OSConfig, error) {
 | |
| 	return m.os.GetBaseConfig()
 | |
| }
 | |
| 
 | |
| // setLocked sets the DNS configuration.
 | |
| //
 | |
| // m.mu must be held.
 | |
| func (m *Manager) setLocked(cfg Config) error {
 | |
| 	syncs.AssertLocked(&m.mu)
 | |
| 
 | |
| 	// On errors, the 'set' config is cleared.
 | |
| 	m.config = nil
 | |
| 
 | |
| 	m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
 | |
| 		cfg.WriteToBufioWriter(w)
 | |
| 	}))
 | |
| 
 | |
| 	rcfg, ocfg, err := m.compileConfig(cfg)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
 | |
| 		rcfg.WriteToBufioWriter(w)
 | |
| 	}))
 | |
| 	m.logf("OScfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
 | |
| 		ocfg.WriteToBufioWriter(w)
 | |
| 	}))
 | |
| 
 | |
| 	if err := m.resolver.SetConfig(rcfg); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if err := m.os.SetDNS(ocfg); err != nil {
 | |
| 		m.health.SetUnhealthy(osConfigurationSetWarnable, health.Args{health.ArgError: err.Error()})
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	m.health.SetHealthy(osConfigurationSetWarnable)
 | |
| 	m.config = &cfg
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // compileHostEntries creates a list of single-label resolutions possible
 | |
| // from the configured hosts and search domains.
 | |
| // The entries are compiled in the order of the search domains, then the hosts.
 | |
| // The returned list is sorted by the first hostname in each entry.
 | |
| func compileHostEntries(cfg Config) (hosts []*HostEntry) {
 | |
| 	didLabel := make(map[string]bool, len(cfg.Hosts))
 | |
| 	hostsMap := make(map[netip.Addr]*HostEntry, len(cfg.Hosts))
 | |
| 	for _, sd := range cfg.SearchDomains {
 | |
| 		for h, ips := range cfg.Hosts {
 | |
| 			if !sd.Contains(h) || h.NumLabels() != (sd.NumLabels()+1) {
 | |
| 				continue
 | |
| 			}
 | |
| 			ipHosts := []string{string(h.WithTrailingDot())}
 | |
| 			if label := dnsname.FirstLabel(string(h)); !didLabel[label] {
 | |
| 				didLabel[label] = true
 | |
| 				ipHosts = append(ipHosts, label)
 | |
| 			}
 | |
| 			for _, ip := range ips {
 | |
| 				if cfg.OnlyIPv6 && ip.Is4() {
 | |
| 					continue
 | |
| 				}
 | |
| 				if e := hostsMap[ip]; e != nil {
 | |
| 					e.Hosts = append(e.Hosts, ipHosts...)
 | |
| 				} else {
 | |
| 					hostsMap[ip] = &HostEntry{
 | |
| 						Addr:  ip,
 | |
| 						Hosts: ipHosts,
 | |
| 					}
 | |
| 				}
 | |
| 				// Only add IPv4 or IPv6 per host, like we do in the resolver.
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	if len(hostsMap) == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	hosts = slicesx.MapValues(hostsMap)
 | |
| 	slices.SortFunc(hosts, func(a, b *HostEntry) int {
 | |
| 		if len(a.Hosts) == 0 && len(b.Hosts) == 0 {
 | |
| 			return 0
 | |
| 		} else if len(a.Hosts) == 0 {
 | |
| 			return -1
 | |
| 		} else if len(b.Hosts) == 0 {
 | |
| 			return 1
 | |
| 		}
 | |
| 		return strings.Compare(a.Hosts[0], b.Hosts[0])
 | |
| 	})
 | |
| 	return hosts
 | |
| }
 | |
| 
 | |
| var osConfigurationReadWarnable = health.Register(&health.Warnable{
 | |
| 	Code:  "dns-read-os-config-failed",
 | |
| 	Title: "Failed to read system DNS configuration",
 | |
| 	Text: func(args health.Args) string {
 | |
| 		return fmt.Sprintf("Tailscale failed to fetch the DNS configuration of your device: %v", args[health.ArgError])
 | |
| 	},
 | |
| 	Severity:  health.SeverityLow,
 | |
| 	DependsOn: []*health.Warnable{health.NetworkStatusWarnable},
 | |
| })
 | |
| 
 | |
| var osConfigurationSetWarnable = health.Register(&health.Warnable{
 | |
| 	Code:  "dns-set-os-config-failed",
 | |
| 	Title: "Failed to set system DNS configuration",
 | |
| 	Text: func(args health.Args) string {
 | |
| 		return fmt.Sprintf("Tailscale failed to set the DNS configuration of your device: %v", args[health.ArgError])
 | |
| 	},
 | |
| 	Severity:  health.SeverityMedium,
 | |
| 	DependsOn: []*health.Warnable{health.NetworkStatusWarnable},
 | |
| })
 | |
| 
 | |
| // compileConfig converts cfg into a quad-100 resolver configuration
 | |
| // and an OS-level configuration.
 | |
| func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig, err error) {
 | |
| 	// The internal resolver always gets MagicDNS hosts and
 | |
| 	// authoritative suffixes, even if we don't propagate MagicDNS to
 | |
| 	// the OS.
 | |
| 	rcfg.Hosts = cfg.Hosts
 | |
| 	routes := map[dnsname.FQDN][]*dnstype.Resolver{} // assigned conditionally to rcfg.Routes below.
 | |
| 	var propagateHostsToOS bool
 | |
| 	for suffix, resolvers := range cfg.Routes {
 | |
| 		if len(resolvers) == 0 {
 | |
| 			propagateHostsToOS = true
 | |
| 			rcfg.LocalDomains = append(rcfg.LocalDomains, suffix)
 | |
| 		} else {
 | |
| 			routes[suffix] = resolvers
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Similarly, the OS always gets search paths.
 | |
| 	ocfg.SearchDomains = cfg.SearchDomains
 | |
| 	if propagateHostsToOS && m.goos == "windows" {
 | |
| 		ocfg.Hosts = compileHostEntries(cfg)
 | |
| 	}
 | |
| 
 | |
| 	// Deal with trivial configs first.
 | |
| 	switch {
 | |
| 	case !cfg.needsOSResolver():
 | |
| 		// Set search domains, but nothing else. This also covers the
 | |
| 		// case where cfg is entirely zero, in which case these
 | |
| 		// configs clear all Tailscale DNS settings.
 | |
| 		return rcfg, ocfg, nil
 | |
| 	case cfg.hasDefaultIPResolversOnly() && !cfg.hasHostsWithoutSplitDNSRoutes():
 | |
| 		// Trivial CorpDNS configuration, just override the OS resolver.
 | |
| 		//
 | |
| 		// If there are hosts (ExtraRecords) that are not covered by an existing
 | |
| 		// SplitDNS route, then we don't go into this path so that we fall into
 | |
| 		// the next case and send the extra record hosts queries through
 | |
| 		// 100.100.100.100 instead where we can answer them.
 | |
| 		//
 | |
| 		// TODO: for OSes that support it, pass IP:port and DoH
 | |
| 		// addresses directly to OS.
 | |
| 		// https://github.com/tailscale/tailscale/issues/1666
 | |
| 		ocfg.Nameservers = toIPsOnly(cfg.DefaultResolvers)
 | |
| 		return rcfg, ocfg, nil
 | |
| 	case cfg.hasDefaultResolvers():
 | |
| 		// Default resolvers plus other stuff always ends up proxying
 | |
| 		// through quad-100.
 | |
| 		rcfg.Routes = routes
 | |
| 		rcfg.Routes["."] = cfg.DefaultResolvers
 | |
| 		ocfg.Nameservers = []netip.Addr{cfg.serviceIP()}
 | |
| 		return rcfg, ocfg, nil
 | |
| 	}
 | |
| 
 | |
| 	// From this point on, we're figuring out split DNS
 | |
| 	// configurations. The possible cases don't return directly any
 | |
| 	// more, because as a final step we have to handle the case where
 | |
| 	// the OS can't do split DNS.
 | |
| 
 | |
| 	// Workaround for
 | |
| 	// https://github.com/tailscale/corp/issues/1662. Even though
 | |
| 	// Windows natively supports split DNS, it only configures linux
 | |
| 	// containers using whatever the primary is, and doesn't apply
 | |
| 	// NRPT rules to DNS traffic coming from WSL.
 | |
| 	//
 | |
| 	// In order to make WSL work okay when the host Windows is using
 | |
| 	// Tailscale, we need to set up quad-100 as a "full proxy"
 | |
| 	// resolver, regardless of whether Windows itself can do split
 | |
| 	// DNS. We still make Windows do split DNS itself when it can, but
 | |
| 	// quad-100 will still have the full split configuration as well,
 | |
| 	// and so can service WSL requests correctly.
 | |
| 	//
 | |
| 	// This bool is used in a couple of places below to implement this
 | |
| 	// workaround.
 | |
| 	isWindows := m.goos == "windows"
 | |
| 	isApple := (m.goos == "darwin" || m.goos == "ios")
 | |
| 	if len(cfg.singleResolverSet()) > 0 && m.os.SupportsSplitDNS() && !isWindows && !isApple {
 | |
| 		// Split DNS configuration requested, where all split domains
 | |
| 		// go to the same resolvers. We can let the OS do it.
 | |
| 		ocfg.Nameservers = toIPsOnly(cfg.singleResolverSet())
 | |
| 		ocfg.MatchDomains = cfg.matchDomains()
 | |
| 		return rcfg, ocfg, nil
 | |
| 	}
 | |
| 
 | |
| 	// Split DNS configuration with either multiple upstream routes,
 | |
| 	// or routes + MagicDNS, or just MagicDNS, or on an OS that cannot
 | |
| 	// split-DNS. Install a split config pointing at quad-100.
 | |
| 	rcfg.Routes = routes
 | |
| 	ocfg.Nameservers = []netip.Addr{cfg.serviceIP()}
 | |
| 
 | |
| 	var baseCfg *OSConfig // base config; non-nil if/when known
 | |
| 
 | |
| 	// Even though Apple devices can do split DNS, they don't provide a way to
 | |
| 	// selectively answer ExtraRecords, and ignore other DNS traffic. As a
 | |
| 	// workaround, we read the existing default resolver configuration and use
 | |
| 	// that as the forwarder for all DNS traffic that quad-100 doesn't handle.
 | |
| 	if isApple || !m.os.SupportsSplitDNS() {
 | |
| 		// If the OS can't do native split-dns, read out the underlying
 | |
| 		// resolver config and blend it into our config.
 | |
| 		cfg, err := m.os.GetBaseConfig()
 | |
| 		if err == nil {
 | |
| 			baseCfg = &cfg
 | |
| 		} else if isApple && err == ErrGetBaseConfigNotSupported {
 | |
| 			// This is currently (2022-10-13) expected on certain iOS and macOS
 | |
| 			// builds.
 | |
| 		} else {
 | |
| 			m.health.SetUnhealthy(osConfigurationReadWarnable, health.Args{health.ArgError: err.Error()})
 | |
| 			return resolver.Config{}, OSConfig{}, err
 | |
| 		}
 | |
| 		m.health.SetHealthy(osConfigurationReadWarnable)
 | |
| 	}
 | |
| 
 | |
| 	if baseCfg == nil {
 | |
| 		// If there was no base config, then we need to fallback to SplitDNS mode.
 | |
| 		ocfg.MatchDomains = cfg.matchDomains()
 | |
| 	} else {
 | |
| 		// On iOS only (for now), check if all route names point to resources inside the tailnet.
 | |
| 		// If so, we can set those names as MatchDomains to enable a split DNS configuration
 | |
| 		// which will help preserve battery life.
 | |
| 		// Because on iOS MatchDomains must equal SearchDomains, we cannot do this when
 | |
| 		// we have any Routes outside the tailnet. Otherwise when app connectors are enabled,
 | |
| 		// a query for 'work-laptop' might lead to search domain expansion, resolving
 | |
| 		// as 'work-laptop.aws.com' for example.
 | |
| 		if m.goos == "ios" && rcfg.RoutesRequireNoCustomResolvers() {
 | |
| 			if !m.disableSplitDNSOptimization() {
 | |
| 				for r := range rcfg.Routes {
 | |
| 					ocfg.MatchDomains = append(ocfg.MatchDomains, r)
 | |
| 				}
 | |
| 			} else {
 | |
| 				m.logf("iOS split DNS is disabled by nodeattr")
 | |
| 			}
 | |
| 		}
 | |
| 		var defaultRoutes []*dnstype.Resolver
 | |
| 		for _, ip := range baseCfg.Nameservers {
 | |
| 			defaultRoutes = append(defaultRoutes, &dnstype.Resolver{Addr: ip.String()})
 | |
| 		}
 | |
| 		rcfg.Routes["."] = defaultRoutes
 | |
| 		ocfg.SearchDomains = append(ocfg.SearchDomains, baseCfg.SearchDomains...)
 | |
| 	}
 | |
| 
 | |
| 	return rcfg, ocfg, nil
 | |
| }
 | |
| 
 | |
| func (m *Manager) disableSplitDNSOptimization() bool {
 | |
| 	return m.knobs != nil && m.knobs.DisableSplitDNSWhenNoCustomResolvers.Load()
 | |
| }
 | |
| 
 | |
| // toIPsOnly returns only the IP portion of dnstype.Resolver.
 | |
| // Only safe to use if the resolvers slice has been cleared of
 | |
| // DoH or custom-port entries with something like hasDefaultIPResolversOnly.
 | |
| func toIPsOnly(resolvers []*dnstype.Resolver) (ret []netip.Addr) {
 | |
| 	for _, r := range resolvers {
 | |
| 		if ipp, ok := r.IPPort(); ok && ipp.Port() == 53 {
 | |
| 			ret = append(ret, ipp.Addr())
 | |
| 		}
 | |
| 	}
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| // Query executes a DNS query received from the given address. The query is
 | |
| // provided in bs as a wire-encoded DNS query without any transport header.
 | |
| // This method is called for requests arriving over UDP and TCP.
 | |
| //
 | |
| // The "family" parameter should indicate what type of DNS query this is:
 | |
| // either "tcp" or "udp".
 | |
| func (m *Manager) Query(ctx context.Context, bs []byte, family string, from netip.AddrPort) ([]byte, error) {
 | |
| 	select {
 | |
| 	case <-m.ctx.Done():
 | |
| 		return nil, net.ErrClosed
 | |
| 	default:
 | |
| 		// continue
 | |
| 	}
 | |
| 
 | |
| 	if n := atomic.AddInt32(&m.activeQueriesAtomic, 1); n > maxActiveQueries {
 | |
| 		atomic.AddInt32(&m.activeQueriesAtomic, -1)
 | |
| 		metricDNSQueryErrorQueue.Add(1)
 | |
| 		return nil, errFullQueue
 | |
| 	}
 | |
| 	defer atomic.AddInt32(&m.activeQueriesAtomic, -1)
 | |
| 	return m.resolver.Query(ctx, bs, family, from)
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	// RFC 7766 6.2 recommends connection reuse & request pipelining
 | |
| 	// be undertaken, and the connection be closed by the server
 | |
| 	// using an idle timeout on the order of seconds.
 | |
| 	idleTimeoutTCP = 45 * time.Second
 | |
| 	// The RFCs don't specify the max size of a TCP-based DNS query,
 | |
| 	// but we want to keep this reasonable. Given payloads are typically
 | |
| 	// much larger and all known client send a single query, I've arbitrarily
 | |
| 	// chosen 4k.
 | |
| 	maxReqSizeTCP = 4096
 | |
| )
 | |
| 
 | |
| // dnsTCPSession services DNS requests sent over TCP.
 | |
| type dnsTCPSession struct {
 | |
| 	m *Manager
 | |
| 
 | |
| 	conn    net.Conn
 | |
| 	srcAddr netip.AddrPort
 | |
| 
 | |
| 	readClosing chan struct{}
 | |
| 	responses   chan []byte // DNS replies pending writing
 | |
| 
 | |
| 	ctx      context.Context
 | |
| 	closeCtx context.CancelFunc
 | |
| }
 | |
| 
 | |
| func (s *dnsTCPSession) handleWrites() {
 | |
| 	defer s.conn.Close()
 | |
| 	defer s.closeCtx()
 | |
| 
 | |
| 	// NOTE(andrew): we explicitly do not close the 'responses' channel
 | |
| 	// when this function exits. If we hit an error and return, we could
 | |
| 	// still have outstanding 'handleQuery' goroutines running, and if we
 | |
| 	// closed this channel they'd end up trying to send on a closed channel
 | |
| 	// when they finish.
 | |
| 	//
 | |
| 	// Because we call closeCtx, those goroutines will not hang since they
 | |
| 	// select on <-s.ctx.Done() as well as s.responses.
 | |
| 
 | |
| 	for {
 | |
| 		select {
 | |
| 		case <-s.readClosing:
 | |
| 			return // connection closed or timeout, teardown time
 | |
| 
 | |
| 		case resp := <-s.responses:
 | |
| 			s.conn.SetWriteDeadline(time.Now().Add(idleTimeoutTCP))
 | |
| 			if err := binary.Write(s.conn, binary.BigEndian, uint16(len(resp))); err != nil {
 | |
| 				s.m.logf("tcp write (len): %v", err)
 | |
| 				return
 | |
| 			}
 | |
| 			if _, err := s.conn.Write(resp); err != nil {
 | |
| 				s.m.logf("tcp write (response): %v", err)
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *dnsTCPSession) handleQuery(q []byte) {
 | |
| 	resp, err := s.m.Query(s.ctx, q, "tcp", s.srcAddr)
 | |
| 	if err != nil {
 | |
| 		s.m.logf("tcp query: %v", err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// See note in handleWrites (above) regarding this select{}
 | |
| 	select {
 | |
| 	case <-s.ctx.Done():
 | |
| 	case s.responses <- resp:
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *dnsTCPSession) handleReads() {
 | |
| 	defer s.conn.Close()
 | |
| 	defer close(s.readClosing)
 | |
| 
 | |
| 	for {
 | |
| 		select {
 | |
| 		case <-s.ctx.Done():
 | |
| 			return
 | |
| 
 | |
| 		default:
 | |
| 			s.conn.SetReadDeadline(time.Now().Add(idleTimeoutTCP))
 | |
| 			var reqLen uint16
 | |
| 			if err := binary.Read(s.conn, binary.BigEndian, &reqLen); err != nil {
 | |
| 				if err == io.EOF || err == io.ErrClosedPipe {
 | |
| 					return // connection closed nominally, we gucci
 | |
| 				}
 | |
| 				s.m.logf("tcp read (len): %v", err)
 | |
| 				return
 | |
| 			}
 | |
| 			if int(reqLen) > maxReqSizeTCP {
 | |
| 				s.m.logf("tcp request too large (%d > %d)", reqLen, maxReqSizeTCP)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			buf := make([]byte, int(reqLen))
 | |
| 			if _, err := io.ReadFull(s.conn, buf); err != nil {
 | |
| 				s.m.logf("tcp read (payload): %v", err)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			select {
 | |
| 			case <-s.ctx.Done():
 | |
| 				return
 | |
| 			default:
 | |
| 				// NOTE: by kicking off the query handling in a
 | |
| 				// new goroutine, it is possible that we'll
 | |
| 				// deliver responses out-of-order. This is
 | |
| 				// explicitly allowed by RFC7766, Section
 | |
| 				// 6.2.1.1 ("Query Pipelining").
 | |
| 				go s.handleQuery(buf)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // HandleTCPConn implements magicDNS over TCP, taking a connection and
 | |
| // servicing DNS requests sent down it.
 | |
| func (m *Manager) HandleTCPConn(conn net.Conn, srcAddr netip.AddrPort) {
 | |
| 	s := dnsTCPSession{
 | |
| 		m:           m,
 | |
| 		conn:        conn,
 | |
| 		srcAddr:     srcAddr,
 | |
| 		responses:   make(chan []byte),
 | |
| 		readClosing: make(chan struct{}),
 | |
| 	}
 | |
| 	s.ctx, s.closeCtx = context.WithCancel(m.ctx)
 | |
| 	go s.handleReads()
 | |
| 	s.handleWrites()
 | |
| }
 | |
| 
 | |
| func (m *Manager) Down() error {
 | |
| 	m.ctxCancel()
 | |
| 	if err := m.os.Close(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	m.resolver.Close()
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (m *Manager) FlushCaches() error {
 | |
| 	return flushCaches()
 | |
| }
 | |
| 
 | |
| // CleanUp restores the system DNS configuration to its original state
 | |
| // in case the Tailscale daemon terminated without closing the router.
 | |
| // No other state needs to be instantiated before this runs.
 | |
| //
 | |
| // health must not be nil
 | |
| func CleanUp(logf logger.Logf, netMon *netmon.Monitor, health *health.Tracker, interfaceName string) {
 | |
| 	oscfg, err := NewOSConfigurator(logf, nil, nil, interfaceName)
 | |
| 	if err != nil {
 | |
| 		logf("creating dns cleanup: %v", err)
 | |
| 		return
 | |
| 	}
 | |
| 	d := &tsdial.Dialer{Logf: logf}
 | |
| 	d.SetNetMon(netMon)
 | |
| 	dns := NewManager(logf, oscfg, health, d, nil, nil, runtime.GOOS)
 | |
| 	if err := dns.Down(); err != nil {
 | |
| 		logf("dns down: %v", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	metricDNSQueryErrorQueue = clientmetric.NewCounter("dns_query_local_error_queue")
 | |
| )
 |