| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- // 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/feature/buildfeatures"
- "tailscale.com/health"
- "tailscale.com/net/dns/resolver"
- "tailscale.com/net/netmon"
- "tailscale.com/net/tsdial"
- "tailscale.com/syncs"
- "tailscale.com/types/dnstype"
- "tailscale.com/types/logger"
- "tailscale.com/util/clientmetric"
- "tailscale.com/util/dnsname"
- "tailscale.com/util/eventbus"
- "tailscale.com/util/slicesx"
- "tailscale.com/util/syspolicy/policyclient"
- )
- var (
- errFullQueue = errors.New("request queue full")
- // ErrNoDNSConfig is returned by RecompileDNSConfig when the Manager
- // has no existing DNS configuration.
- ErrNoDNSConfig = errors.New("no DNS configuration")
- )
- // 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
- eventClient *eventbus.Client
- 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 *Config // Tracks the last viable DNS configuration set by Set. nil on failures other than compilation failures or if set has never been called.
- }
- // NewManager 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, bus *eventbus.Bus) *Manager {
- if !buildfeatures.HasDNS {
- return nil
- }
- 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,
- }
- m.eventClient = bus.Client("dns.Manager")
- eventbus.SubscribeFunc(m.eventClient, func(trample TrampleDNS) {
- m.mu.Lock()
- defer m.mu.Unlock()
- if m.config == nil {
- m.logf("resolve.conf was trampled, but there is no DNS config")
- return
- }
- m.logf("resolve.conf was trampled, setting existing config again")
- if err := m.setLocked(*m.config); err != nil {
- m.logf("error setting DNS config: %s", err)
- }
- })
- 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 {
- if !buildfeatures.HasDNS {
- return nil
- }
- return m.resolver
- }
- // RecompileDNSConfig recompiles the last attempted DNS configuration, which has
- // the side effect of re-querying the OS's interface nameservers. This should be used
- // on platforms where the interface nameservers can change. Darwin, for example,
- // where the nameservers aren't always available when we process a major interface
- // change event, or platforms where the nameservers may change while tunnel is up.
- //
- // This should be called if it is determined that [OSConfigurator.GetBaseConfig] may
- // give a better or different result than when [Manager.Set] was last called. The
- // logic for making that determination is up to the caller.
- //
- // It returns [ErrNoDNSConfig] if [Manager.Set] has never been called.
- func (m *Manager) RecompileDNSConfig() error {
- if !buildfeatures.HasDNS {
- return nil
- }
- m.mu.Lock()
- defer m.mu.Unlock()
- if m.config != nil {
- return m.setLocked(*m.config)
- }
- return ErrNoDNSConfig
- }
- func (m *Manager) Set(cfg Config) error {
- if !buildfeatures.HasDNS {
- return nil
- }
- 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) {
- if !buildfeatures.HasDNS {
- panic("unreachable")
- }
- 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)
- m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
- cfg.WriteToBufioWriter(w)
- }))
- rcfg, ocfg, err := m.compileConfig(cfg)
- if err != nil {
- // On a compilation failure, set m.config set for later reuse by
- // [Manager.RecompileDNSConfig] and return the error.
- m.config = &cfg
- 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 {
- m.config = nil
- return err
- }
- if err := m.setDNSLocked(ocfg); err != nil {
- return err
- }
- m.health.SetHealthy(osConfigurationSetWarnable)
- m.config = &cfg
- return nil
- }
- func (m *Manager) setDNSLocked(ocfg OSConfig) error {
- if err := m.os.SetDNS(ocfg); err != nil {
- m.config = nil
- m.health.SetUnhealthy(osConfigurationSetWarnable, health.Args{health.ArgError: err.Error()})
- return err
- }
- 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() || runtime.GOOS == "plan9":
- // 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 = cfg.serviceIPs(m.knobs)
- 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 = cfg.serviceIPs(m.knobs)
- 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. On apple platforms, [OSConfigurator.GetBaseConfig]
- // has a tendency to temporarily fail if called immediately following
- // an interface change. These failures should be retried if/when the OS
- // indicates that the DNS configuration has changed via [RecompileDNSConfig].
- 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
- )
- // TrampleDNS is an an event indicating we detected that DNS config was
- // overwritten by another process.
- type TrampleDNS struct {
- LastTrample time.Time
- TramplesInTimeout int64
- }
- // 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 {
- if !buildfeatures.HasDNS {
- return nil
- }
- m.ctxCancel()
- if err := m.os.Close(); err != nil {
- return err
- }
- m.eventClient.Close()
- m.resolver.Close()
- return nil
- }
- func (m *Manager) FlushCaches() error {
- if !buildfeatures.HasDNS {
- return nil
- }
- 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, bus *eventbus.Bus, health *health.Tracker, interfaceName string) {
- if !buildfeatures.HasDNS {
- return
- }
- oscfg, err := NewOSConfigurator(logf, health, bus, policyclient.Get(), nil, interfaceName)
- if err != nil {
- logf("creating dns cleanup: %v", err)
- return
- }
- d := &tsdial.Dialer{Logf: logf}
- d.SetNetMon(netMon)
- d.SetBus(bus)
- dns := NewManager(logf, oscfg, health, d, nil, nil, runtime.GOOS, bus)
- if err := dns.Down(); err != nil {
- logf("dns down: %v", err)
- }
- }
- var metricDNSQueryErrorQueue = clientmetric.NewCounter("dns_query_local_error_queue")
|