// Copyright (c) Tailscale Inc & contributors
// 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"
	"tailscale.com/version"
)

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

// ResponseMapper is a function that accepts the bytes representing
// a DNS response and returns bytes representing a DNS response.
// Used to observe and/or mutate DNS responses managed by this manager.
type ResponseMapper func([]byte) []byte

// 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.
	queryResponseMapper ResponseMapper
}

// 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
}

// ProbeLocks acquires and releases the manager's internal mutexes.
func (m *Manager) ProbeLocks() {
	m.mu.Lock()
	m.mu.Unlock()

	if r := m.Resolver(); r != nil {
		r.ProbeLocks()
	}
}

// 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
	rcfg.SubdomainHosts = cfg.SubdomainHosts
	rcfg.AcceptDNS = cfg.AcceptDNS
	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"
	isIOS := m.goos == "ios"
	supportsSplitDNS := m.os.SupportsSplitDNS()
	// Sandboxed macOS builds use NetworkExtension DNS settings, not
	// tailscaled's /etc/resolver configurator, so keep the Apple workaround.
	appleSplitDNSWorkaround := isIOS || (m.goos == "darwin" && isSandboxedMacOS())
	if supportsSplitDNS && !isWindows && !appleSplitDNSWorkaround {
		if srs := toIPsOnly(cfg.singleResolverSet()); len(srs) > 0 {
			// Split DNS configuration requested, where all split domains
			// go to the same resolvers. We can let the OS do it.
			ocfg.Nameservers = srs
			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)

	if supportsSplitDNS && !appleSplitDNSWorkaround {
		ocfg.MatchDomains = cfg.matchDomains()
		return rcfg, ocfg, nil
	}

	// Even though iOS 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 the OS can't do native split-DNS, read out the underlying resolver
	// config and blend it into our config. On iOS, [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].
	base, err := m.os.GetBaseConfig()
	if err != nil {
		if (isIOS || isNoopManager(m.os)) && err == ErrGetBaseConfigNotSupported {
			// Expected when using noopManager (userspace networking) or on
			// certain iOS builds. Continue without base config.
			m.health.SetHealthy(osConfigurationReadWarnable)
			ocfg.MatchDomains = cfg.matchDomains()
			return rcfg, ocfg, nil
		}
		m.health.SetUnhealthy(osConfigurationReadWarnable, health.Args{health.ArgError: err.Error()})
		return resolver.Config{}, OSConfig{}, err
	}
	m.health.SetHealthy(osConfigurationReadWarnable)

	// 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 isIOS && 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 base.Nameservers {
		defaultRoutes = append(defaultRoutes, &dnstype.Resolver{Addr: ip.String()})
	}
	rcfg.Routes["."] = defaultRoutes
	// Append base config search domains, but only if not already present.
	// This prevents duplicates when GetBaseConfig() reads back domains that
	// Tailscale itself previously wrote to resolv.conf.
	for _, domain := range base.SearchDomains {
		if !slices.Contains(ocfg.SearchDomains, domain) {
			ocfg.SearchDomains = append(ocfg.SearchDomains, domain)
		}
	}

	return rcfg, ocfg, nil
}

func (m *Manager) disableSplitDNSOptimization() bool {
	return m.knobs != nil && m.knobs.DisableSplitDNSWhenNoCustomResolvers.Load()
}

var isSandboxedMacOS = version.IsSandboxedMacOS

// 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)
	outbs, err := m.resolver.Query(ctx, bs, family, from)
	if err != nil {
		return outbs, err
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.queryResponseMapper != nil {
		outbs = m.queryResponseMapper(outbs)
	}
	return outbs, err
}

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 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")

func (m *Manager) SetQueryResponseMapper(fx ResponseMapper) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.queryResponseMapper = fx
}
