// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package ipnlocal

import (
	"cmp"
	"context"
	"iter"
	"maps"
	"net/netip"
	"slices"
	"strings"
	"sync"
	"sync/atomic"

	"go4.org/netipx"
	"tailscale.com/appc"
	"tailscale.com/envknob"
	"tailscale.com/feature/buildfeatures"
	"tailscale.com/ipn"
	"tailscale.com/net/dns"
	"tailscale.com/net/routecheck/peernode"
	"tailscale.com/net/routemanager"
	"tailscale.com/net/tsaddr"
	"tailscale.com/syncs"
	"tailscale.com/tailcfg"
	"tailscale.com/types/dnstype"
	"tailscale.com/types/key"
	"tailscale.com/types/logger"
	"tailscale.com/types/mapx"
	"tailscale.com/types/netmap"
	"tailscale.com/types/views"
	"tailscale.com/util/dnsname"
	"tailscale.com/util/eventbus"
	"tailscale.com/util/mak"
	"tailscale.com/util/set"
	"tailscale.com/util/slicesx"
	"tailscale.com/util/testenv"
	"tailscale.com/wgengine/filter"
)

// nodeBackend is node-specific [LocalBackend] state. It is usually the current node.
//
// Its exported methods are safe for concurrent use, but the struct is not a snapshot of state at a given moment;
// its state can change between calls. For example, asking for the same value (e.g., netmap or prefs) twice
// may return different results. Returned values are immutable and safe for concurrent use.
//
// If both the [LocalBackend]'s internal mutex and the [nodeBackend] mutex must be held at the same time,
// the [LocalBackend] mutex must be acquired first. See the comment on the [LocalBackend] field for more details.
//
// Two pointers to different [nodeBackend] instances represent different local nodes.
// However, there's currently a bug where a new [nodeBackend] might not be created
// during an implicit node switch (see tailscale/corp#28014).
//
// In the future, we might want to include at least the following in this struct (in addition to the current fields).
// However, not everything should be exported or otherwise made available to the outside world (e.g. [ipnext] extensions,
// peer API handlers, etc.).
//   - [ipn.State]: when the LocalBackend switches to a different [nodeBackend], it can update the state of the old one.
//   - [ipn.LoginProfileView] and [ipn.Prefs]: we should update them when the [profileManager] reports changes to them.
//     In the future, [profileManager] (and the corresponding methods of the [LocalBackend]) can be made optional,
//     and something else could be used to set them once or update them as needed.
//   - [tailcfg.HostinfoView]: it includes certain fields that are tied to the current profile/node/prefs. We should also
//     update to build it once instead of mutating it in twelvety different places.
//   - [filter.Filter] (normal and jailed, along with the filterHash): the nodeBackend could have a method to (re-)build
//     the filter for the current netmap/prefs (see [LocalBackend.updateFilterLocked]), and it needs to track the current
//     filters and their hash.
//   - Fields related to a requested or required (re-)auth: authURL, authURLTime, authActor, keyExpired, etc.
//   - [controlclient.Client]/[*controlclient.Auto]: the current control client. It is ties to a node identity.
//   - [tkaState]: it is tied to the current profile / node.
//   - Fields related to scheduled node expiration: nmExpiryTimer, numClientStatusCalls, [expiryManager].
//
// It should not include any fields used by specific features that don't belong in [LocalBackend].
// Even if they're tied to the local node, instead of moving them here, we should extract the entire feature
// into a separate package and have it install proper hooks.
type nodeBackend struct {
	logf logger.Logf

	ctx       context.Context         // canceled by [nodeBackend.shutdown]
	ctxCancel context.CancelCauseFunc // cancels ctx

	// filterAtomic is a stateful packet filter. Immutable once created, but can be
	// replaced with a new one.
	filterAtomic atomic.Pointer[filter.Filter]

	// initialized once and immutable
	eventClient    *eventbus.Client
	derpMapViewPub *eventbus.Publisher[tailcfg.DERPMapView]

	// homeDERP lives here temporarily. as long as mapSession is short lived, we
	// don't have a location delivering netmaps to local backend that knows our
	// homeDERP hence why it is cached here for now.
	// TODO(cmol): move this field into a refactored mapSession that is not
	// short lived.
	homeDERP atomic.Int64

	// TODO(nickkhyl): maybe use sync.RWMutex?
	mu syncs.Mutex // protects the following fields

	shutdownOnce sync.Once     // guards calling [nodeBackend.shutdown]
	readyCh      chan struct{} // closed by [nodeBackend.ready]; nil after shutdown

	// NetMap is the most recently set full netmap from the controlclient.
	// It can't be mutated in place once set. Because it can't be mutated in place,
	// delta updates from the control server don't apply to it. Instead, use
	// the peers map to get up-to-date information on the state of peers.
	// In general, avoid using the netMap.Peers slice. We'd like it to go away
	// as of 2023-09-17.
	// TODO(nickkhyl): make it an atomic pointer to avoid the need for a mutex?
	netMap *netmap.NetworkMap

	// peers is the set of current peers and their current values after applying
	// delta node mutations as they come in (with mu held). The map values can be
	// given out to callers, but the map itself can be mutated in place (with mu held)
	// and must not escape the [nodeBackend].
	peers map[tailcfg.NodeID]tailcfg.NodeView

	// nodeByAddr maps nodes' own addresses (excluding subnet routes) to node IDs.
	// It is mutated in place (with mu held) and must not escape the [nodeBackend].
	nodeByAddr map[netip.Addr]tailcfg.NodeID

	// nodeByKey is an index of node public key to node ID for fast lookups.
	// It is mutated in place (with mu held) and must not escape the [nodeBackend].
	nodeByKey map[key.NodePublic]tailcfg.NodeID

	// nodeByWGString indexes wireguard-go's truncated peer-string form
	// (see [key.NodePublic.WireGuardGoString]) to node ID. It mirrors
	// nodeByKey and lets the wireguard-go log path resolve
	// "peer(XXXX…YYYY)" references in O(1), without scanning every
	// peer, while still tolerating the fact that the wireguard-go form
	// is lossy and can't be inverted to a [key.NodePublic].
	// It is mutated in place (with mu held) and must not escape the [nodeBackend].
	nodeByWGString map[string]tailcfg.NodeID

	// nodeByStableID is an index of peer stable node ID to node ID.
	// Unlike nodeByAddr and nodeByKey, it covers only peers, not the
	// self node.
	// It is mutated in place (with mu held) and must not escape the [nodeBackend].
	nodeByStableID map[tailcfg.StableNodeID]tailcfg.NodeID

	// nodeByName maps MagicDNS hostnames (lowercase, no trailing dot) to
	// node IDs. Both the FQDN and the short name (suffix stripped) are
	// keys. It is used by the tsdial MagicDNS resolution callback.
	// It is mutated in place (with mu held) and must not escape the [nodeBackend].
	nodeByName map[string]tailcfg.NodeID

	// extraDNS stores DNS.ExtraRecords A/AAAA entries from the netmap
	// (typically service VIPs pushed by control), keyed by canonicalized
	// hostname (lowercase, no trailing dot).
	extraDNS map[string]netip.Addr

	// userProfiles is the live set of user profiles, updated incrementally
	// by mergeUserProfiles as deltas arrive. It parallels the peers map:
	// netMap.UserProfiles is the frozen snapshot from the last full install,
	// while this field reflects incremental updates. Readers that need a
	// snapshot (e.g. the legacy Notify.NetMap path) must clone this map.
	userProfiles map[tailcfg.UserID]tailcfg.UserProfileView

	// packetFilterRules and packetFilter are the live packet filter state,
	// updated by setPacketFilter as deltas arrive. Like userProfiles, they
	// exist separately from netMap's frozen fields so that concurrent
	// JSON-encoding of a Notify.NetMap snapshot doesn't race with writes.
	packetFilterRules views.Slice[tailcfg.FilterRule]
	packetFilter      []filter.Match

	// keyWaitersForTest is the test-only registry of channels waiting for
	// a given peer key to first appear in the netmap. See
	// [nodeBackend.AwaitNodeKeyForTest]. It is populated lazily and remains
	// nil in production, where no test installs a waiter.
	keyWaitersForTest map[key.NodePublic]chan struct{}

	// tsmpLearnedDisco records, per node key, a peer disco key that was
	// learned via TSMP (that is, over an existing WireGuard session with
	// that peer). When a netmap update later reports the same disco key
	// change, the peer's WireGuard session does not need to be reset,
	// because the change demonstrably arrived over a working session.
	// See [nodeBackend.discoChangedLocked].
	tsmpLearnedDisco map[key.NodePublic]key.DiscoPublic

	// routeMgr tracks this node's view of which IPs route to which
	// peers and publishes lock-free snapshots for the data plane and
	// the OS router. It is initialized once and immutable, but its
	// Begin/Commit mutations must be serialized, which we do by only
	// mutating it with mu held.
	routeMgr *routemanager.RouteManager
}

func newNodeBackend(ctx context.Context, logf logger.Logf, bus *eventbus.Bus) *nodeBackend {
	ctx, ctxCancel := context.WithCancelCause(ctx)
	nb := &nodeBackend{
		logf:        logf,
		ctx:         ctx,
		ctxCancel:   ctxCancel,
		eventClient: bus.Client("ipnlocal.nodeBackend"),
		readyCh:     make(chan struct{}),
		routeMgr:    routemanager.New(logf),
	}
	// Default filter blocks everything and logs nothing.
	noneFilter := filter.NewAllowNone(logger.Discard, &netipx.IPSet{})
	nb.filterAtomic.Store(noneFilter)
	nb.derpMapViewPub = eventbus.Publish[tailcfg.DERPMapView](nb.eventClient)
	return nb
}

// Context returns a context that is canceled when the [nodeBackend] shuts down,
// either because [LocalBackend] is switching to a different [nodeBackend]
// or is shutting down itself.
func (nb *nodeBackend) Context() context.Context {
	return nb.ctx
}

// Self returns the current node.
func (nb *nodeBackend) Self() tailcfg.NodeView {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.netMap == nil {
		return tailcfg.NodeView{}
	}
	return nb.netMap.SelfNode
}

func (nb *nodeBackend) SelfUserID() tailcfg.UserID {
	self := nb.Self()
	if !self.Valid() {
		return 0
	}
	return self.User()
}

// SelfHasCap reports whether the specified capability was granted to the self node in the most recent netmap.
func (nb *nodeBackend) SelfHasCap(wantCap tailcfg.NodeCapability) bool {
	return nb.SelfHasCapOr(wantCap, false)
}

// SelfHasCapOr is like [nodeBackend.SelfHasCap], but returns the specified default value
// if the netmap is not available yet.
func (nb *nodeBackend) SelfHasCapOr(wantCap tailcfg.NodeCapability, def bool) bool {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.netMap == nil {
		return def
	}
	return nb.netMap.AllCaps.Contains(wantCap)
}

func (nb *nodeBackend) NetworkProfile() ipn.NetworkProfile {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return ipn.NetworkProfile{
		// These are ok to call with nil netMap.
		MagicDNSName: nb.netMap.MagicDNSSuffix(),
		DomainName:   nb.netMap.DomainName(),
		DisplayName:  nb.netMap.TailnetDisplayName(),
	}
}

// TODO(nickkhyl): update it to return a [tailcfg.DERPMapView]?
func (nb *nodeBackend) DERPMap() *tailcfg.DERPMap {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.netMap == nil {
		return nil
	}
	return nb.netMap.DERPMap
}

func (nb *nodeBackend) NodeByAddr(ip netip.Addr) (_ tailcfg.NodeID, ok bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	nid, ok := nb.nodeByAddr[ip]
	return nid, ok
}

func (nb *nodeBackend) NodeByKey(k key.NodePublic) (_ tailcfg.NodeID, ok bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	nid, ok := nb.nodeByKey[k]
	return nid, ok
}

// PeerAllowedIPs returns the prefixes from which the peer with the
// given public key is currently allowed to originate traffic, or
// ok=false if the key is unknown or the peer currently contributes no
// prefixes.
func (nb *nodeBackend) PeerAllowedIPs(k key.NodePublic) ([]netip.Prefix, bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	id, ok := nb.nodeByKey[k]
	if !ok {
		return nil, false
	}
	// Holding nb.mu satisfies routeMgr's serialization requirement:
	// all routeMgr mutations also run under nb.mu.
	return nb.routeMgr.PeerAllowedIPs(id)
}

// NodeByWireGuardString returns the node ID of the peer whose
// [key.NodePublic.WireGuardGoString] form is s (e.g. "peer(IMTB…r7lM)").
// ok is false if no current peer matches.
func (nb *nodeBackend) NodeByWireGuardString(s string) (_ tailcfg.NodeID, ok bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	nid, ok := nb.nodeByWGString[s]
	return nid, ok
}

func (nb *nodeBackend) NodeByID(id tailcfg.NodeID) (_ tailcfg.NodeView, ok bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return nb.nodeByIDLocked(id)
}

// nodeByIDLocked returns the node (peer or self) with the given node
// ID. nb.mu must be held.
func (nb *nodeBackend) nodeByIDLocked(id tailcfg.NodeID) (_ tailcfg.NodeView, ok bool) {
	if nb.netMap != nil {
		if self := nb.netMap.SelfNode; self.Valid() && self.ID() == id {
			return self, true
		}
	}
	n, ok := nb.peers[id]
	return n, ok
}

func (nb *nodeBackend) PeerByStableID(id tailcfg.StableNodeID) (_ tailcfg.NodeView, ok bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nid, ok := nb.nodeByStableID[id]; ok {
		n, ok := nb.peers[nid]
		return n, ok
	}
	return tailcfg.NodeView{}, false
}

func (nb *nodeBackend) UserByID(id tailcfg.UserID) (_ tailcfg.UserProfileView, ok bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	u, ok := nb.userProfiles[id]
	return u, ok
}

// Peers returns all the current peers in an undefined order.
func (nb *nodeBackend) Peers() []tailcfg.NodeView {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return slicesx.MapValues(nb.peers)
}

// PeerByID returns the current state of the peer (not self) node with
// the given ID, or ok=false if it is not a current peer.
func (nb *nodeBackend) PeerByID(id tailcfg.NodeID) (_ tailcfg.NodeView, ok bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	n, ok := nb.peers[id]
	return n, ok
}

// peerDiscoKeys returns the disco public keys of all current peers,
// keyed by their node public keys. Peers without a disco key are
// omitted.
func (nb *nodeBackend) peerDiscoKeys() map[key.NodePublic]key.DiscoPublic {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	m := make(map[key.NodePublic]key.DiscoPublic, len(nb.peers))
	for _, p := range nb.peers {
		if dk := p.DiscoKey(); !dk.IsZero() {
			m[p.Key()] = dk
		}
	}
	return m
}

func (nb *nodeBackend) PeersForTest() []tailcfg.NodeView {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	ret := slicesx.MapValues(nb.peers)
	slices.SortFunc(ret, func(a, b tailcfg.NodeView) int {
		return cmp.Compare(a.ID(), b.ID())
	})
	return ret
}

func (nb *nodeBackend) CollectServices() bool {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return nb.netMap != nil && nb.netMap.CollectServices
}

// AppendMatchingPeers returns base with all peers that match pred appended.
//
// It acquires nb.mu to snapshot the peers but releases it before
// calling pred.
func (nb *nodeBackend) AppendMatchingPeers(base []tailcfg.NodeView, pred func(tailcfg.NodeView) bool) []tailcfg.NodeView {
	nb.mu.Lock()
	peers := slicesx.MapValues(nb.peers)
	nb.mu.Unlock()

	// Sort by node ID for deterministic results; the map iteration
	// above is randomly ordered.
	slices.SortFunc(peers, func(a, b tailcfg.NodeView) int {
		return cmp.Compare(a.ID(), b.ID())
	})

	ret := base
	for _, peer := range peers {
		if pred(peer) {
			ret = append(ret, peer)
		}
	}
	return ret
}

// PeerCaps returns the capabilities that remote src IP has to
// ths current node.
func (nb *nodeBackend) PeerCaps(src netip.Addr) tailcfg.PeerCapMap {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return nb.peerCapsLocked(src)
}

// srcIsUnsignedPeerLocked reports whether src is an address of a peer with
// UnsignedPeerAPIOnly set. Such peers are not covered by tailnet lock and must
// never be granted peer capabilities.
//
// nb.mu must be held before calling.
func (nb *nodeBackend) srcIsUnsignedPeerLocked(src netip.Addr) bool {
	id, ok := nb.nodeByAddr[src]
	if !ok {
		return false
	}
	n, ok := nb.nodeByIDLocked(id)
	return ok && n.UnsignedPeerAPIOnly()
}

func (nb *nodeBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap {
	if nb.srcIsUnsignedPeerLocked(src) {
		return nil
	}
	if nb.netMap == nil {
		return nil
	}
	filt := nb.filterAtomic.Load()
	if filt == nil {
		return nil
	}
	addrs := nb.netMap.GetAddresses()
	for i := range addrs.Len() {
		a := addrs.At(i)
		if !a.IsSingleIP() {
			continue
		}
		dst := a.Addr()
		if dst.BitLen() == src.BitLen() { // match on family
			return filt.CapsWithValues(src, dst)
		}
	}
	return nil
}

// PeerCapsForIP returns the capabilities that remote src IP has when
// talking to the given destination IP on this node. The destination may
// be any IP the node handles: its own tailnet address, a VIP service
// address, or any future routable IP.
func (nb *nodeBackend) PeerCapsForIP(src, dst netip.Addr) tailcfg.PeerCapMap {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.srcIsUnsignedPeerLocked(src) {
		return nil
	}
	if nb.netMap == nil {
		return nil
	}
	filt := nb.filterAtomic.Load()
	if filt == nil {
		return nil
	}
	return filt.CapsWithValues(src, dst)
}

// PeerCapsForService returns the capabilities that remote src IP has when
// talking to the named VIP service on this node. The service name is
// resolved to its VIP addresses via the node's service IP mappings, and
// the first address matching the src IP family is used for cap lookup.
func (nb *nodeBackend) PeerCapsForService(src netip.Addr, svcName tailcfg.ServiceName) tailcfg.PeerCapMap {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.srcIsUnsignedPeerLocked(src) {
		return nil
	}
	if nb.netMap == nil {
		return nil
	}
	filt := nb.filterAtomic.Load()
	if filt == nil {
		return nil
	}
	addrs := nb.netMap.GetVIPServiceIPMap()[svcName]
	for _, ip := range addrs {
		if ip.BitLen() == src.BitLen() {
			return filt.CapsWithValues(src, ip)
		}
	}
	return nil
}

// PeerHasCap reports whether the peer contains the given capability string,
// with any value(s).
func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap tailcfg.PeerCapability) bool {
	if !peer.Valid() {
		return false
	}

	nb.mu.Lock()
	defer nb.mu.Unlock()
	for _, ap := range peer.Addresses().All() {
		if nb.peerHasCapLocked(ap.Addr(), wantCap) {
			return true
		}
	}
	return false
}

func (nb *nodeBackend) peerHasCapLocked(addr netip.Addr, wantCap tailcfg.PeerCapability) bool {
	return nb.peerCapsLocked(addr).HasCapability(wantCap)
}

func (nb *nodeBackend) PeerHasPeerAPI(p tailcfg.NodeView) bool {
	return nb.PeerAPIBase(p) != ""
}

// PeerAPIBase returns the "http://ip:port" URL base to reach peer's PeerAPI,
// or the empty string if the peer is invalid or doesn't support PeerAPI.
func (nb *nodeBackend) PeerAPIBase(p tailcfg.NodeView) string {
	nb.mu.Lock()
	nm := nb.netMap
	nb.mu.Unlock()
	return peerAPIBase(nm, p)
}

// RouteCheckReport is an interface that reports whether a peer is reachable by the current node.
type RouteCheckReport interface {
	// IsReachable reports whether a peer is reachable by the current node.
	IsReachable(tailcfg.NodeID) peernode.Reachability
}

// PeerIsReachable reports whether the current node can reach p.
// This function may return a result based on stale reachability data,
// either from the control plane or because the latest routecheck report is old.
// If rp is nil, then this will report whether p is connected to the control plane
// according to [tailcfg.NodeView.Online].
//
// The latest routecheck report will be considered if the current node has both
// [tailcfg.NodeAttrClientSideReachability] and [tailcfg.NodeAttrClientSideReachabilityRouteCheck]
// in its CapMap.
func (nb *nodeBackend) PeerIsReachable(rp RouteCheckReport, p tailcfg.NodeView) bool {
	nb.mu.Lock()
	nm := nb.netMap
	nb.mu.Unlock()

	if nm == nil || !p.Valid() {
		// If there is no netmap, then how did we get a NodeView?
		// Assuming that p came from the control plane,
		// report whether it was connected to tailcontrol.
		return p.Valid() && p.Online().Get()
	}

	self := nm.SelfNode
	useRouteCheck := isRouteCheckEnabled(self)

	if !useRouteCheck && !self.HasCap(tailcfg.NodeAttrClientSideReachability) {
		// Legacy behavior is to always trust the control plane, which
		// isn’t always correct because the peer could be slow to check
		// in so that control marks it as offline.
		// See tailscale/corp#32686.
		return p.Online().Get()
	}

	if self.Valid() && self.ID() == p.ID() {
		// This node can always reach itself.
		return true
	}

	if !useRouteCheck && !self.HasCap(tailcfg.NodeAttrClientSideReachabilityRouteCheck) {
		// TODO(sfllaw): The following does not actually test for client-side
		// reachability. This would require a mechanism that tracks whether the
		// current node can actually reach this peer, either because they are
		// already communicating or because they can ping each other.
		//
		// Instead, it makes the client ignore p.Online completely.
		//
		// See tailscale/corp#32686.
		return true
	}

	if rp == nil {
		// The routecheck report hasn’t been collected yet,
		// so fall back and report whether it was connected to tailcontrol.
		return p.Online().Get()
	}
	r := rp.IsReachable(p.ID())
	if r == peernode.Unknown {
		// Reachability is unknown, because the node is a new router, so fall back.
		return p.Online().Get()
	}
	return r.IsReachable()
}

func nodeIP(n tailcfg.NodeView, pred func(netip.Addr) bool) netip.Addr {
	for _, pfx := range n.Addresses().All() {
		if pfx.IsSingleIP() && pred(pfx.Addr()) {
			return pfx.Addr()
		}
	}
	return netip.Addr{}
}

func (nb *nodeBackend) NetMap() *netmap.NetworkMap {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return nb.netMap
}

func (nb *nodeBackend) netMapWithPeers() *netmap.NetworkMap {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.netMap == nil {
		return nil
	}
	nm := new(*nb.netMap) // shallow clone
	nm.Peers = slicesx.MapValues(nb.peers)
	slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int {
		return cmp.Compare(a.ID(), b.ID())
	})
	nm.UserProfiles = maps.Clone(nb.userProfiles)
	nm.PacketFilterRules = nb.packetFilterRules
	nm.PacketFilter = nb.packetFilter
	return nm
}

func (nb *nodeBackend) SetNetMap(nm *netmap.NetworkMap) (discoChanged []key.NodePublic, routeChanged routemanager.PeersWithRouteChanges) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	nb.netMap = nm
	nb.updateNodeByAddrLocked()
	nb.updateNodeByKeyLocked()
	nb.updateNodeByStableIDLocked()
	nb.updateNodeByNameLocked()
	discoChanged, routeChanged = nb.updatePeersLocked()
	nb.signalKeyWaitersForTestLocked()
	if nm != nil {
		nb.userProfiles = maps.Clone(nm.UserProfiles)
		nb.packetFilterRules = nm.PacketFilterRules
		nb.packetFilter = nm.PacketFilter
		nb.derpMapViewPub.Publish(nm.DERPMap.View())
	} else {
		nb.userProfiles = nil
		nb.packetFilterRules = views.Slice[tailcfg.FilterRule]{}
		nb.packetFilter = nil
		nb.derpMapViewPub.Publish(tailcfg.DERPMapView{})
	}
	return discoChanged, routeChanged
}

// AwaitNodeKeyForTest returns a channel that is closed once a peer with the
// given node key first appears in this nodeBackend's peer index, or
// immediately (a closed channel) if it's already present. It is intended for
// in-process benchmarks that drive synthetic netmap deltas and need a
// zero-overhead signal that the client has applied a delta, replacing
// poll-based [local.Client.WhoIsNodeKey] loops in tests. It panics outside
// of tests.
func (nb *nodeBackend) AwaitNodeKeyForTest(k key.NodePublic) <-chan struct{} {
	testenv.AssertInTest()
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if _, ok := nb.nodeByKey[k]; ok {
		return syncs.ClosedChan()
	}
	if ch, ok := nb.keyWaitersForTest[k]; ok {
		return ch
	}
	ch := make(chan struct{})
	mak.Set(&nb.keyWaitersForTest, k, ch)
	return ch
}

// signalKeyWaitersForTestLocked closes any waiter channels whose keys now
// appear in nb.nodeByKey. It is cheap when there are no waiters, which is
// the common case in production. It is called from [nodeBackend.SetNetMap]
// after the per-key index has been rebuilt.
//
// Caller must hold nb.mu.
func (nb *nodeBackend) signalKeyWaitersForTestLocked() {
	for k, ch := range nb.keyWaitersForTest {
		if _, ok := nb.nodeByKey[k]; ok {
			close(ch)
			delete(nb.keyWaitersForTest, k)
		}
	}
}

func (nb *nodeBackend) updateNodeByAddrLocked() {
	nm := nb.netMap
	if nm == nil {
		nb.nodeByAddr = nil
		return
	}

	addNodeAddr := func(n tailcfg.NodeView) {
		for _, ipp := range n.Addresses().All() {
			if ipp.IsSingleIP() {
				nb.nodeByAddr[ipp.Addr()] = n.ID()
			}
		}
	}
	mapx.RepopulateNonzero(&nb.nodeByAddr, func() {
		if nm.SelfNode.Valid() {
			addNodeAddr(nm.SelfNode)
		}
		for _, p := range nm.Peers {
			addNodeAddr(p)
		}
	})
}

func (nb *nodeBackend) updateNodeByKeyLocked() {
	nm := nb.netMap
	if nm == nil {
		nb.nodeByKey = nil
		nb.nodeByWGString = nil
		return
	}

	mapx.RepopulateNonzero(&nb.nodeByKey, func() {
		if nm.SelfNode.Valid() {
			nb.nodeByKey[nm.SelfNode.Key()] = nm.SelfNode.ID()
		}
		for _, p := range nm.Peers {
			nb.nodeByKey[p.Key()] = p.ID()
		}
	})
	mapx.RepopulateNonzero(&nb.nodeByWGString, func() {
		if nm.SelfNode.Valid() {
			nb.nodeByWGString[nm.SelfNode.Key().WireGuardGoString()] = nm.SelfNode.ID()
		}
		for _, p := range nm.Peers {
			nb.nodeByWGString[p.Key().WireGuardGoString()] = p.ID()
		}
	})
}

func (nb *nodeBackend) updateNodeByStableIDLocked() {
	nm := nb.netMap
	if nm == nil {
		nb.nodeByStableID = nil
		return
	}

	mapx.RepopulateNonzero(&nb.nodeByStableID, func() {
		for _, p := range nm.Peers {
			nb.nodeByStableID[p.StableID()] = p.ID()
		}
	})
}

// addNodeNameLocked adds both the FQDN and short-name keys for the given
// node to nb.nodeByName. nb.mu must be held.
func (nb *nodeBackend) addNodeNameLocked(name string, nid tailcfg.NodeID) {
	if name == "" {
		// We might support name-less nodes in the future; tailscale/corp#43949
		return
	}
	canon := strings.ToLower(strings.TrimSuffix(name, "."))
	mak.Set(&nb.nodeByName, canon, nid)
	if suffix := nb.netMap.MagicDNSSuffix(); dnsname.HasSuffix(canon, suffix) {
		mak.Set(&nb.nodeByName, dnsname.TrimSuffix(canon, suffix), nid)
	}
}

// removeNodeNameLocked removes both the FQDN and short-name keys for the
// given node from nb.nodeByName. nb.mu must be held.
func (nb *nodeBackend) removeNodeNameLocked(name string) {
	if name == "" {
		// We might support name-less nodes in the future; tailscale/corp#43949
		return
	}
	canon := strings.ToLower(strings.TrimSuffix(name, "."))
	delete(nb.nodeByName, canon)
	if suffix := nb.netMap.MagicDNSSuffix(); dnsname.HasSuffix(canon, suffix) {
		delete(nb.nodeByName, dnsname.TrimSuffix(canon, suffix))
	}
}

func (nb *nodeBackend) updateNodeByNameLocked() {
	nm := nb.netMap
	if nm == nil {
		nb.nodeByName = nil
		nb.extraDNS = nil
		return
	}

	mapx.RepopulateNonzero(&nb.nodeByName, func() {
		if nm.SelfNode.Valid() {
			nb.addNodeNameLocked(nm.SelfNode.Name(), nm.SelfNode.ID())
		}
		for _, p := range nm.Peers {
			nb.addNodeNameLocked(p.Name(), p.ID())
		}
	})

	// Rebuild extraDNS from DNS.ExtraRecords (service VIPs, etc).
	nb.extraDNS = nil
	for _, rec := range nm.DNS.ExtraRecords {
		if rec.Type != "" {
			continue
		}
		ip, err := netip.ParseAddr(rec.Value)
		if err != nil {
			continue
		}
		mak.Set(&nb.extraDNS, strings.ToLower(strings.TrimSuffix(rec.Name, ".")), ip)
	}
}

// NodeByName returns the node ID for a MagicDNS hostname. The input
// must be lowercase with no trailing dot; both short names ("foo") and
// FQDNs ("foo.tail-scale.ts.net") are accepted.
func (nb *nodeBackend) NodeByName(hostname string) (_ tailcfg.NodeID, ok bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	nid, ok := nb.nodeByName[hostname]
	return nid, ok
}

// ExtraDNSByName returns the IP for a DNS.ExtraRecords entry (e.g. service VIPs).
func (nb *nodeBackend) ExtraDNSByName(hostname string) (_ netip.Addr, ok bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	ip, ok := nb.extraDNS[hostname]
	return ip, ok
}

func (nb *nodeBackend) updatePeersLocked() (discoChanged []key.NodePublic, routeChanged routemanager.PeersWithRouteChanges) {
	nm := nb.netMap
	oldIDs := slices.Collect(maps.Keys(nb.peers))

	// Snapshot the previous disco keys (by node key) before the peers
	// map is repopulated, to detect restarted peers below.
	var prevDisco map[key.NodePublic]key.DiscoPublic
	for _, p := range nb.peers {
		if !p.DiscoKey().IsZero() {
			mak.Set(&prevDisco, p.Key(), p.DiscoKey())
		}
	}

	if nm == nil {
		nb.peers = nil
	} else {
		mapx.RepopulateNonzero(&nb.peers, func() {
			for _, p := range nm.Peers {
				nb.peers[p.ID()] = p
			}
		})
	}

	for _, p := range nb.peers {
		if prev, ok := prevDisco[p.Key()]; ok && nb.discoChangedLocked(p.Key(), prev, p.DiscoKey()) {
			discoChanged = append(discoChanged, p.Key())
		}
	}
	// Drop TSMP-learned disco keys for peers no longer in the netmap.
	for k := range nb.tsmpLearnedDisco {
		if _, ok := nb.nodeByKey[k]; !ok {
			delete(nb.tsmpLearnedDisco, k)
		}
	}

	// Resync the route manager to the new peer set. Upserts of
	// unchanged peers are cheap no-ops; this is a full-netmap event,
	// so O(n) work is expected here.
	rt := nb.routeMgr.Begin()
	for _, id := range oldIDs {
		if _, ok := nb.peers[id]; !ok {
			rt.RemovePeer(id)
		}
	}
	for _, p := range nb.peers {
		rt.UpsertPeer(p)
	}
	res := rt.Commit()
	return discoChanged, res.AllowedIPs
}

// recordTSMPLearnedDisco notes that a peer's new disco key was learned via
// TSMP, so the netmap update carrying the same change need not reset the
// peer's WireGuard session. See the [nodeBackend.tsmpLearnedDisco] field doc.
func (nb *nodeBackend) recordTSMPLearnedDisco(pub key.NodePublic, disco key.DiscoPublic) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	mak.Set(&nb.tsmpLearnedDisco, pub, disco)
}

// discoChangedLocked reports whether a peer's disco key change from prev to
// cur should reset the peer's WireGuard session. A changed disco key means
// the peer restarted, so any existing session key material is dead weight;
// resetting lets the handshake start over immediately. The exception is a
// key change already learned via TSMP: that arrived over a working WireGuard
// session with the peer, so the session is demonstrably fine and is kept.
//
// It consumes any [nodeBackend.tsmpLearnedDisco] entry for pub.
// nb.mu must be held.
func (nb *nodeBackend) discoChangedLocked(pub key.NodePublic, prev, cur key.DiscoPublic) bool {
	if prev.IsZero() || cur.IsZero() || prev == cur {
		return false
	}
	if discoTSMP, ok := nb.tsmpLearnedDisco[pub]; ok {
		delete(nb.tsmpLearnedDisco, pub)
		if discoTSMP == cur {
			nb.logf("nodeBackend: skipping WireGuard session reset (TSMP key): %s changed from %q to %q",
				pub.ShortString(), prev, cur)
			return false
		}
		// The new disco key does not match what we received via
		// TSMP for this peer. This is unexpected, though possible
		// if processing a change in a large netmap ends up taking
		// longer than the 2 second timeout in
		// [controlclient.mapRoutineState.UpdateNetmapDelta], or if
		// the context is cancelled mid update. Log the event, and reset
		// the session as it is possibly a stale entry in the map
		// instead of a TSMP disco key update that led us here.
		nb.logf("nodeBackend: [unexpected] using TSMP key for %s (control stale): tsmp=%q control=%q old=%q",
			pub.ShortString(), discoTSMP, cur, prev)
		metricTSMPLearnedKeyMismatch.Add(1)
	}
	nb.logf("nodeBackend: peer %s disco key changed from %q to %q", pub.ShortString(), prev, cur)
	return true
}

// routePrefs is the subset of routing-relevant prefs (and derived
// state) that [nodeBackend.updateRouteManagerPrefs] pushes into the
// RouteManager.
type routePrefs struct {
	// ExitNodeID is the stable node ID of the selected exit node, or
	// zero if none is selected.
	ExitNodeID tailcfg.StableNodeID

	// ExitNodeSelected is whether the prefs select any exit node at
	// all, even one that doesn't resolve (e.g. an ExitNodeIP that
	// never resolved to an ExitNodeID); it mirrors
	// routerConfigLocked's blackhole condition.
	ExitNodeSelected bool

	// RouteAll is whether advertised subnet routes from peers are
	// accepted.
	RouteAll bool

	// OneCGNAT is whether the OS route set should collapse peers'
	// CGNAT addresses into the single /10 route.
	OneCGNAT bool
}

// updateRouteManagerPrefs pushes p into the route manager.
//
// It returns the peers whose allowed source prefixes changed as a
// result, for example the old and new exit node when the selection
// changes.
func (nb *nodeBackend) updateRouteManagerPrefs(p routePrefs) routemanager.PeersWithRouteChanges {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	var exitID tailcfg.NodeID
	if !p.ExitNodeID.IsZero() {
		// The lookup can miss: the selected exit node may not exist,
		// or may be a placeholder like MDM's "auto:any" awaiting
		// resolution. ExitNodeSelected then makes the RouteManager
		// blackhole internet traffic rather than treat it as "no
		// exit node"; see [routemanager.Prefs.ExitNodeSelected].
		exitID = nb.nodeByStableID[p.ExitNodeID]
	}
	rt := nb.routeMgr.Begin()
	rt.SetPrefs(routemanager.Prefs{
		ExitNodeID:       exitID,
		ExitNodeSelected: p.ExitNodeSelected,
		RouteAll:         p.RouteAll,
	})
	rt.SetTailnetConfig(routemanager.TailnetConfig{OneCGNAT: p.OneCGNAT})
	res := rt.Commit()
	return res.AllowedIPs
}

// updateRouteManagerExtras pushes extra WireGuard-only allowed IPs
// into the route manager, obtained by calling fn (the
// [ipnext.Hooks.ExtraWireGuardAllowedIPs] hook) with a sequence of
// the current peers.
//
// It returns the peers whose allowed source prefixes changed as a
// result.
func (nb *nodeBackend) updateRouteManagerExtras(fn func(peers iter.Seq2[tailcfg.NodeID, key.NodePublic]) map[tailcfg.NodeID][]netip.Prefix) routemanager.PeersWithRouteChanges {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	extras := fn(func(yield func(tailcfg.NodeID, key.NodePublic) bool) {
		for id, p := range nb.peers {
			if !yield(id, p.Key()) {
				return
			}
		}
	})
	rt := nb.routeMgr.Begin()
	rt.SetExtraAllowedIPs(extras)
	res := rt.Commit()
	return res.AllowedIPs
}

// osRoutes returns the sorted set of prefixes that the route manager
// wants programmed into the OS routing table.
func (nb *nodeBackend) osRoutes() []netip.Prefix {
	var routes []netip.Prefix
	for pfx := range nb.routeMgr.OSRoutes().All() {
		routes = append(routes, pfx)
	}
	tsaddr.SortPrefixes(routes)
	return routes
}

// setPacketFilter stores the live packet filter rules and parsed
// matches. It does not touch the frozen netMap. nb.mu is acquired by
// this method.
func (nb *nodeBackend) setPacketFilter(rules views.Slice[tailcfg.FilterRule], parsed []filter.Match) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	nb.packetFilterRules = rules
	nb.packetFilter = parsed
}

// PacketFilter returns the current live packet filter matches.
func (nb *nodeBackend) PacketFilter() []filter.Match {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return nb.packetFilter
}

// mergeUserProfiles merges new/updated [tailcfg.UserProfileView]
// entries into the live userProfiles map. It does not touch
// netMap.UserProfiles (which is frozen once set). Callers must hold
// [LocalBackend.mu]. nb.mu is acquired by this method.
func (nb *nodeBackend) mergeUserProfiles(profiles map[tailcfg.UserID]tailcfg.UserProfileView) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	for id, up := range profiles {
		mak.Set(&nb.userProfiles, id, up)
	}
}

// netmapDeltaResult describes the side effects of applying netmap
// delta mutations that the caller must propagate.
type netmapDeltaResult struct {
	// ChangedAllowedIPs are the peers whose allowed source prefixes
	// changed; the caller syncs those peers to the WireGuard device.
	ChangedAllowedIPs routemanager.PeersWithRouteChanges

	// DiscoChanged is the set of peers whose disco key changed in a
	// way that requires a WireGuard session reset (see
	// [nodeBackend.discoChangedLocked]).
	DiscoChanged set.Set[key.NodePublic]
}

// UpdateNetmapDelta applies the given netmap mutations to the live
// peer state. It returns the side effects for the caller to
// propagate, and reports whether it handled all of the mutations.
func (nb *nodeBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (res netmapDeltaResult, handled bool) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.netMap == nil {
		return res, false
	}

	// Locally cloned mutable nodes, to avoid calling AsStruct (clone)
	// multiple times on a node if it's mutated multiple times in this
	// call (e.g. its endpoints + online status both change)
	var mutableNodes map[tailcfg.NodeID]*tailcfg.Node

	// Mirror peer add/remove mutations into the route manager.
	// The Commit is unconditional (even if we panic or return false)
	// to stay in sync with the peers map, which is likewise mutated
	// in place as the loop runs. And if we return false, we'll just
	// get a full netmap soon and reset all our state anyway.
	rt := nb.routeMgr.Begin()
	defer func() {
		res.ChangedAllowedIPs = rt.Commit().AllowedIPs
	}()

	for _, m := range muts {
		switch m := m.(type) {
		case netmap.NodeMutationUpsert:
			nid := m.Node.ID()
			if old, ok := nb.peers[nid]; ok {
				if old.Key() == m.Node.Key() {
					if nb.discoChangedLocked(m.Node.Key(), old.DiscoKey(), m.Node.DiscoKey()) {
						res.DiscoChanged.Make()
						res.DiscoChanged.Add(m.Node.Key())
					}
				} else {
					delete(nb.tsmpLearnedDisco, old.Key())
				}
			}
			mak.Set(&nb.peers, nid, m.Node)
			for _, ipp := range m.Node.Addresses().All() {
				if ipp.IsSingleIP() {
					mak.Set(&nb.nodeByAddr, ipp.Addr(), nid)
				}
			}
			mak.Set(&nb.nodeByKey, m.Node.Key(), nid)
			mak.Set(&nb.nodeByWGString, m.Node.Key().WireGuardGoString(), nid)
			mak.Set(&nb.nodeByStableID, m.Node.StableID(), nid)
			nb.addNodeNameLocked(m.Node.Name(), nid)
			rt.UpsertPeer(m.Node)
			continue
		case netmap.NodeMutationRemove:
			nid := m.NodeIDBeingMutated()
			if old, ok := nb.peers[nid]; ok {
				for _, ipp := range old.Addresses().All() {
					if ipp.IsSingleIP() {
						delete(nb.nodeByAddr, ipp.Addr())
					}
				}
				delete(nb.nodeByKey, old.Key())
				delete(nb.nodeByWGString, old.Key().WireGuardGoString())
				delete(nb.nodeByStableID, old.StableID())
				delete(nb.tsmpLearnedDisco, old.Key())
				nb.removeNodeNameLocked(old.Name())
				delete(nb.peers, nid)
				rt.RemovePeer(nid)
			}
			continue
		}
		// Per-field mutation.
		nid := m.NodeIDBeingMutated()
		n, ok := mutableNodes[nid]
		if !ok {
			nv, ok := nb.peers[nid]
			if !ok {
				// TODO(bradfitz): unexpected metric?
				return res, false
			}
			n = nv.AsStruct()
			mak.Set(&mutableNodes, nv.ID(), n)
		}
		m.Apply(n)
	}
	for nid, n := range mutableNodes {
		nb.peers[nid] = n.View()
	}
	nb.signalKeyWaitersForTestLocked()
	return res, true
}

// unlockedNodesPermitted reports whether any peer with theUnsignedPeerAPIOnly bool set true has any of its allowed IPs
// in the specified packet filter.
//
// TODO(nickkhyl): It is here temporarily until we can move the whole [LocalBackend.updateFilterLocked] here,
// but change it so it builds and returns a filter for the current netmap/prefs instead of re-configuring the engine filter.
// Something like (*nodeBackend).RebuildFilters() (filter, jailedFilter *filter.Filter, changed bool) perhaps?
func (nb *nodeBackend) unlockedNodesPermitted(packetFilter []filter.Match) bool {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return packetFilterPermitsUnlockedNodes(nb.peers, packetFilter)
}

func (nb *nodeBackend) filter() *filter.Filter {
	return nb.filterAtomic.Load()
}

func (nb *nodeBackend) setFilter(f *filter.Filter) {
	nb.filterAtomic.Store(f)
}

func (nb *nodeBackend) dnsConfigForNetmap(prefs ipn.PrefsView, selfExpired bool, goos string) *dns.Config {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return dnsConfigForNetmap(nb.netMap, nb.peers, prefs, selfExpired, nb.logf, goos)
}

// magicDNSHostAddrs returns the MagicDNS A/AAAA answer for fqdn from
// the live node indexes, and whether the name is known. It backs
// [resolver.MagicDNSHosts.LookupHost], replacing the full
// dns.Config.Hosts map so that netmap deltas need no per-peer DNS
// rebuild.
func (nb *nodeBackend) magicDNSHostAddrs(fqdn dnsname.FQDN) (ips []netip.Addr, ok bool) {
	if !buildfeatures.HasDNS {
		return nil, false
	}
	nb.mu.Lock()
	defer nb.mu.Unlock()
	n, ok := nb.nodeByFQDNLocked(fqdn)
	if !ok || n.Addresses().Len() == 0 {
		return nil, false
	}
	nm := nb.netMap
	var flags magicDNSAddrsFlags
	if nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs6) &&
		!nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs4) {
		flags |= selfV6Only
	}
	if nm.AllCaps.Contains(tailcfg.NodeAttrMagicDNSPeerAAAA) {
		flags |= wantAAAA
	}
	return magicDNSAddrs(n.Addresses(), flags), true
}

// magicDNSPTR returns the MagicDNS name of the node (peer or self)
// that owns ip, backing [resolver.MagicDNSHosts.LookupPTR].
func (nb *nodeBackend) magicDNSPTR(ip netip.Addr) (_ dnsname.FQDN, ok bool) {
	if !buildfeatures.HasDNS {
		return "", false
	}
	nb.mu.Lock()
	defer nb.mu.Unlock()
	nid, ok := nb.nodeByAddr[ip]
	if !ok {
		return "", false
	}
	n, ok := nb.nodeByIDLocked(nid)
	if !ok {
		return "", false
	}
	fqdn, err := dnsname.ToFQDN(n.Name())
	if err != nil {
		return "", false
	}
	return fqdn, true
}

// magicDNSSubdomainHost reports whether fqdn names a node with the
// [tailcfg.NodeAttrDNSSubdomainResolve] attribute, backing
// [resolver.MagicDNSHosts.SubdomainHost].
func (nb *nodeBackend) magicDNSSubdomainHost(fqdn dnsname.FQDN) bool {
	if !buildfeatures.HasDNS {
		return false
	}
	nb.mu.Lock()
	defer nb.mu.Unlock()
	n, ok := nb.nodeByFQDNLocked(fqdn)
	if !ok {
		return false
	}
	if nm := nb.netMap; nm != nil && nm.SelfNode.Valid() && nm.SelfNode.ID() == n.ID() {
		return nm.AllCaps.Contains(tailcfg.NodeAttrDNSSubdomainResolve)
	}
	return n.CapMap().Contains(tailcfg.NodeAttrDNSSubdomainResolve)
}

// nodeByFQDNLocked returns the node (peer or self) with the given
// MagicDNS FQDN. nb.mu must be held.
func (nb *nodeBackend) nodeByFQDNLocked(fqdn dnsname.FQDN) (_ tailcfg.NodeView, ok bool) {
	// The resolver already lowercases query names, but lowercase
	// again (nearly free when already lowercase) so that no other
	// caller of the [resolver.MagicDNSHosts] hook can miss on case.
	nid, ok := nb.nodeByName[strings.ToLower(strings.TrimSuffix(string(fqdn), "."))]
	if !ok {
		return tailcfg.NodeView{}, false
	}
	return nb.nodeByIDLocked(nid)
}

// magicDNSAddrsFlags is a bitmask of flags for magicDNSAddrs.
type magicDNSAddrsFlags uint8

const (
	selfV6Only magicDNSAddrsFlags = 1 << iota // the querying node has only IPv6 addresses
	wantAAAA                                  // include IPv6 addresses even for nodes that also have IPv4
)

// magicDNSAddrs returns the MagicDNS answer addresses for a node
// owning the given addresses: all of its IPv6 addresses if this node
// (the querier) is IPv6-only, otherwise everything except the node's
// IPv6 addresses when it also has IPv4, unless wantAAAA. The result
// may be empty (name exists, no records of the desired family).
func magicDNSAddrs(addrs views.Slice[netip.Prefix], flags magicDNSAddrsFlags) (ips []netip.Addr) {
	wantAAAA := flags&wantAAAA != 0
	selfV6Only := flags&selfV6Only != 0
	var have4 bool
	for _, addr := range addrs.All() {
		if addr.Addr().Is4() {
			have4 = true
			break
		}
	}
	for _, addr := range addrs.All() {
		if selfV6Only {
			if addr.Addr().Is6() {
				ips = append(ips, addr.Addr())
			}
			continue
		}
		// If this node has an IPv4 address, then remove peers'
		// IPv6 addresses for now, as we don't guarantee that
		// the peer node actually can speak IPv6 correctly.
		//
		// https://github.com/tailscale/tailscale/issues/1152
		// tracks adding the right capability reporting to
		// enable AAAA in MagicDNS.
		if addr.Addr().Is6() && have4 && !wantAAAA {
			continue
		}
		ips = append(ips, addr.Addr())
	}
	return ips
}

func (nb *nodeBackend) exitNodeCanProxyDNS(exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
	if !buildfeatures.HasUseExitNode {
		return "", false
	}
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return exitNodeCanProxyDNS(nb.netMap, nb.peers, exitNodeID)
}

// ready signals that [LocalBackend] has completed the switch to this [nodeBackend]
// and any pending calls to [nodeBackend.Wait] must be unblocked.
func (nb *nodeBackend) ready() {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.readyCh != nil {
		close(nb.readyCh)
	}
}

// Wait blocks until [LocalBackend] completes the switch to this [nodeBackend]
// and calls [nodeBackend.ready]. It returns an error if the provided context
// is canceled or if the [nodeBackend] shuts down or is already shut down.
//
// It must not be called with the [LocalBackend]'s internal mutex held as [LocalBackend]
// may need to acquire it to complete the switch.
//
// TODO(nickkhyl): Relax this restriction once [LocalBackend]'s state machine
// runs in its own goroutine, or if we decide that waiting for the state machine
// restart to finish isn't necessary for [LocalBackend] to consider the switch complete.
// We mostly need this because of [LocalBackend.Start] acquiring b.mu and the fact that
// methods like [LocalBackend.SwitchProfile] must report any errors returned by it.
// Perhaps we could report those errors asynchronously as [health.Warnable]s?
func (nb *nodeBackend) Wait(ctx context.Context) error {
	nb.mu.Lock()
	readyCh := nb.readyCh
	nb.mu.Unlock()

	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-nb.ctx.Done():
		return context.Cause(nb.ctx)
	case <-readyCh:
		return nil
	}
}

// shutdown shuts down the [nodeBackend] and cancels its context
// with the provided cause.
func (nb *nodeBackend) shutdown(cause error) {
	nb.shutdownOnce.Do(func() {
		nb.doShutdown(cause)
	})
}

func (nb *nodeBackend) doShutdown(cause error) {
	nb.mu.Lock()
	defer nb.mu.Unlock()
	nb.ctxCancel(cause)
	nb.readyCh = nil
	nb.eventClient.Close()
}

// useWithExitNodeResolvers filters out resolvers so the ones that remain
// are all the ones marked for use with exit nodes.
func useWithExitNodeResolvers(resolvers []*dnstype.Resolver) []*dnstype.Resolver {
	var filtered []*dnstype.Resolver
	for _, res := range resolvers {
		if res.UseWithExitNode {
			filtered = append(filtered, res)
		}
	}
	return filtered
}

// useWithExitNodeRoutes filters out routes so the ones that remain
// are either zero-length resolver lists, or lists containing only
// resolvers marked for use with exit nodes.
func useWithExitNodeRoutes(routes map[string][]*dnstype.Resolver) map[string][]*dnstype.Resolver {
	var filtered map[string][]*dnstype.Resolver
	for suffix, resolvers := range routes {
		// Suffixes with no resolvers represent a valid configuration,
		// and should persist regardless of exit node considerations.
		if len(resolvers) == 0 {
			mak.Set(&filtered, suffix, make([]*dnstype.Resolver, 0))
			continue
		}

		// In exit node contexts, we filter out resolvers not configured for use with
		// exit nodes. If there are no such configured resolvers, there should not be an entry for that suffix.
		filteredResolvers := useWithExitNodeResolvers(resolvers)
		if len(filteredResolvers) > 0 {
			mak.Set(&filtered, suffix, filteredResolvers)
		}
	}

	return filtered
}

// dnsConfigForNetmap returns a *dns.Config for the given netmap,
// prefs, client OS version, and cloud hosting environment.
//
// The goos is runtime.GOOS usually, except in tests, where it may
// vary to test any OS's behavior from any host.
func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView, selfExpired bool, logf logger.Logf, goos string) *dns.Config {
	if nm == nil {
		return nil
	}
	if !buildfeatures.HasDNS {
		return &dns.Config{}
	}

	// If the current node's key is expired, then we don't program any DNS
	// configuration into the operating system. This ensures that if the
	// DNS configuration specifies a DNS server that is only reachable over
	// Tailscale, we don't break connectivity for the user.
	//
	// TODO(andrew-d): this also stops returning anything from quad-100; we
	// could do the same thing as having "CorpDNS: false" and keep that but
	// not program the OS?
	if selfExpired {
		return &dns.Config{}
	}

	dcfg := &dns.Config{
		AcceptDNS: prefs.CorpDNS(),
		Routes:    map[dnsname.FQDN][]*dnstype.Resolver{},
		Hosts:     map[dnsname.FQDN][]netip.Addr{},
	}

	var addrFlags magicDNSAddrsFlags
	if nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs6) &&
		!nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs4) {
		// We only have IPv6 addresses ourselves.
		addrFlags |= selfV6Only
		dcfg.OnlyIPv6 = true
	}
	if nm.AllCaps.Contains(tailcfg.NodeAttrMagicDNSPeerAAAA) {
		addrFlags |= wantAAAA
	}

	// Populate MagicDNS records. The internal quad-100 resolver pulls
	// per-node records on demand from the live node indexes (see
	// [nodeBackend.magicDNSHostAddrs]), so quad-100 can always respond
	// to MagicDNS queries without dcfg.Hosts; details in
	// https://github.com/tailscale/tailscale/issues/1886. dcfg.Hosts
	// carries only what must be enumerable in full: control's
	// DNS.ExtraRecords below, plus every node's records on Windows,
	// whose hosts-file fallback path (see compileHostEntries in
	// net/dns) needs the complete set.
	if goos == "windows" {
		set := func(name string, addrs views.Slice[netip.Prefix]) {
			if addrs.Len() == 0 || name == "" {
				return
			}
			fqdn, err := dnsname.ToFQDN(name)
			if err != nil {
				return // TODO: propagate error?
			}
			dcfg.Hosts[fqdn] = magicDNSAddrs(addrs, addrFlags)
		}
		set(nm.SelfName(), nm.GetAddresses())
		for _, peer := range peers {
			set(peer.Name(), peer.Addresses())
		}
	}
	for _, rec := range nm.DNS.ExtraRecords {
		switch rec.Type {
		case "", "A", "AAAA":
			// Treat these all the same for now: infer from the value
		default:
			// TODO: more
			continue
		}
		ip, err := netip.ParseAddr(rec.Value)
		if err != nil {
			// Ignore.
			continue
		}
		fqdn, err := dnsname.ToFQDN(rec.Name)
		if err != nil {
			continue
		}
		dcfg.Hosts[fqdn] = append(dcfg.Hosts[fqdn], ip)
	}

	if !prefs.CorpDNS() {
		return dcfg
	}

	for _, dom := range nm.DNS.Domains {
		fqdn, err := dnsname.ToFQDN(dom)
		if err != nil {
			logf("[unexpected] non-FQDN search domain %q", dom)
		}
		dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
	}
	if nm.DNS.Proxied { // actually means "enable MagicDNS"
		for _, dom := range magicDNSRootDomains(nm) {
			dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
		}
	} else {
		// Without MagicDNS domain routing, no Routes entry covers the
		// node records served on demand via [magicDNSHosts]. Tell
		// dns.Manager they exist, so it keeps quad-100 in the OS
		// resolver path and the names still resolve, as they did when
		// they were listed in dcfg.Hosts. Any netmap with a self node
		// has such records.
		dcfg.MagicDNSHostsUnrouted = nm.SelfNode.Valid()
	}

	addDefault := func(resolvers []*dnstype.Resolver) {
		dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, resolvers...)
	}

	addSplitDNSRoutes := func(routes map[string][]*dnstype.Resolver) {
		for suffix, resolvers := range routes {
			fqdn, err := dnsname.ToFQDN(suffix)
			if err != nil {
				logf("[unexpected] non-FQDN route suffix %q", suffix)
			}

			// Create map entry even if len(resolvers) == 0; Issue 2706.
			// This lets the control plane send ExtraRecords for which we
			// can authoritatively answer "name not exists" for when the
			// control plane also sends this explicit but empty route
			// making it as something we handle.
			dcfg.Routes[fqdn] = slices.Clone(resolvers)
		}
	}

	// If we're using an exit node and that exit node is new enough (1.19.x+)
	// to run a DoH DNS proxy, then send all our DNS traffic through it,
	// unless we find resolvers with UseWithExitNode set, in which case we use that.
	if buildfeatures.HasUseExitNode {
		if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok {
			filtered := useWithExitNodeResolvers(nm.DNS.Resolvers)
			if len(filtered) > 0 {
				addDefault(filtered)
			} else {
				// If no default global resolvers with the override
				// are configured, configure the exit node's resolver.
				addDefault([]*dnstype.Resolver{{Addr: dohURL}})
			}

			addSplitDNSRoutes(useWithExitNodeRoutes(nm.DNS.Routes))
			return dcfg
		}
	}

	// If the user has set default resolvers ("override local DNS"), prefer to
	// use those resolvers as the default, otherwise if there are WireGuard exit
	// node resolvers, use those as the default.
	if len(nm.DNS.Resolvers) > 0 {
		addDefault(nm.DNS.Resolvers)
	} else if buildfeatures.HasUseExitNode {
		if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok {
			addDefault(resolvers)
		}
	}

	// Add split DNS routes, with no regard to exit node configuration.
	addSplitDNSRoutes(nm.DNS.Routes)

	if (envknob.UseWIPCode() || testenv.InTest()) && buildfeatures.HasConn25 && !prefs.AppConnector().Advertise {
		// Add split DNS routes for conn25
		if appRoutes := appc.AppDNSRoutes(nm.HasCap, nm.SelfNode); appRoutes != nil {
			addSplitDNSRoutes(appRoutes)
		}
	}

	// Set FallbackResolvers as the default resolvers in the
	// scenarios that can't handle a purely split-DNS config. See
	// https://github.com/tailscale/tailscale/issues/1743 for
	// details.
	switch {
	case len(dcfg.DefaultResolvers) != 0:
		// Default resolvers already set.
	case !prefs.ExitNodeID().IsZero():
		// When using an exit node, we send all DNS traffic to the exit node, so
		// we don't need a fallback resolver.
		//
		// However, if the exit node is too old to run a DoH DNS proxy, then we
		// need to use a fallback resolver as it's very likely the LAN resolvers
		// will become unreachable.
		//
		// This is especially important on Apple OSes, where
		// adding the default route to the tunnel interface makes
		// it "primary", and we MUST provide VPN-sourced DNS
		// settings or we break all DNS resolution.
		//
		// https://github.com/tailscale/tailscale/issues/1713
		addDefault(nm.DNS.FallbackResolvers)
	case len(dcfg.Routes) == 0:
		// No settings requiring split DNS, no problem.
	}

	return dcfg
}
