Skip to main content

ts_runtime/
magic_dns.rs

1//! MagicDNS responder with a split-DNS / recursive forwarder.
2//!
3//! An in-netstack DNS server bound to `100.100.100.100:53`. It is authoritative for in-tailnet
4//! peer names and control-pushed [`ExtraRecord`][ts_control::ExtraRecord]s, answering `A`/`AAAA`/
5//! `PTR` for those directly — plus, for a peer control has marked with the `dns-subdomain-resolve`
6//! node attribute ([`Node::resolves_subdomains`]), every name *under* that peer's name
7//! ([`DnsView::subdomain_host_for`]). For names it is *not* authoritative for, it brings tsnet-style
8//! split-DNS and recursive resolution:
9//!
10//! - **Split DNS** ([`DnsConfig::routes`]): the longest matching suffix route forwards the query
11//!   to one of that route's upstream resolvers. A route with an **empty** upstream list is a
12//!   negative route — names under it are `NXDOMAIN` (Go keeps them on the built-in resolver; for
13//!   us that means fail-closed unless an overlay/extra record matched first).
14//! - **Recursive** ([`DnsConfig::fallback_resolvers`] / [`DnsConfig::resolvers`]): names matching
15//!   no route are forwarded to the fallback resolvers, else the global resolvers.
16//! - **Fail closed**: if no route and no resolver is configured, an unknown name is `NXDOMAIN`.
17//! - **A refusing upstream does not end a forward**: a `REFUSED` or `SERVFAIL` from one upstream is
18//!   a *soft* error — the next upstream on the route (or in the fallback list) is tried. A refusal
19//!   is relayed to the client only when every upstream refused; a mix of failures is a SERVFAIL
20//!   (see [`forward_query`]).
21//!
22//! Anti-leak / IPv6-off posture: upstream forwarding binds `0.0.0.0:0` on the overlay netstack —
23//! UDP for the query, TCP for the hop raced against it, IPv4 only either way — and never opens an
24//! IPv6 socket, nor a host socket. AAAA handling is gated on [`DnsView::enable_ipv6`] (default
25//! off): with the gate OFF an AAAA query for a tailnet/overlay/self name returns NoError with an
26//! empty answer (NODATA) rather than the overlay v6 address — answering a v6 the IPv4-only client
27//! can't route would only create dead connections and a fingerprint. With the gate ON, AAAA is
28//! answered from overlay data (the v6 overlay addr), as historically. AAAA for tailnet names is
29//! never forwarded to a recursive upstream regardless of the gate.
30//!
31//! - MagicDNS disabled (`dns_config == None` or `magic_dns == false`), OR the node does not accept
32//!   the tailnet DNS config ([`DnsView::accept_dns`] is `false`, i.e. `--accept-dns` / `CorpDNS`
33//!   off) => `REFUSED` for every query (the responder serves nothing, mirroring Go applying an empty
34//!   `dns.Config` when `CorpDNS` is off).
35//! - A qtype/class we don't serve authoritatively (anything but IN-class A/AAAA/PTR — TXT, SRV, MX,
36//!   HTTPS/SVCB, a CHAOS-class query, …) => NODATA (empty NOERROR) for a tailnet-authoritative name,
37//!   forwarded verbatim to upstream for an off-tailnet name — exactly like Go's resolver, NOT
38//!   `REFUSED` (a stub reads REFUSED as "won't serve me" and abandons the resolver). Tailnet reverse
39//!   zones (CGNAT `in-addr.arpa` / any `ip6.arpa`) still fail closed to NXDOMAIN for every qtype
40//!   (never forwarded — anti-leak).
41//! - A **negative** answer this node is authoritative for — an NXDOMAIN for a name inside a zone we
42//!   serve (a tailnet search domain, a negative split-DNS route, or the CGNAT reverse zone), or a
43//!   NODATA for such a name — carries that zone's `SOA` in the authority section, advertising a
44//!   10-second negative-caching bound (RFC 2308). Without one, macOS `mDNSResponder` keeps an
45//!   SOA-less negative answer on its own schedule, so a name queried shortly *before* a node was
46//!   renamed to it stays unresolvable until something flushes the cache. Positive answers carry a
47//!   5-second TTL for the same reason in the other direction. `SERVFAIL`, `REFUSED` and the blanket
48//!   `ip6.arpa` refusal claim no zone and carry no SOA.
49//! - Malformed query => dropped (no response).
50//! - A reply larger than the UDP payload size the query advertised — its EDNS(0) OPT record, or 512
51//!   bytes when it carried none or carried one this node will not act on (RFC 1035) — comes back
52//!   with the `TC` (truncated) bit set and its body intact, so the stub resolver knows to retry over
53//!   TCP ([`check_response_size_and_set_tc`]). This applies to forwarded replies, where the query is
54//!   relayed verbatim and so this is what catches an upstream that ignores the size its requestor
55//!   asked for, and equally to answers this node composes itself. The retry that bit asks for is served
56//!   by the `dns_over_tcp` server (TUN mode), which reaches the same [`decide`] through
57//!   the same view — and which is never handed a `TC` bit for size, since a TCP client has no
58//!   datagram to overflow (see [`ClientTransport`]).
59//! - The hop to the **upstream** resolver makes the same retry on the client's behalf: a forwarded
60//!   answer that comes back with `TC` set is re-asked of the same resolver over TCP, over the same
61//!   overlay netstack channel the UDP hop uses and never a host socket ([`ask_upstream`]). Without
62//!   it the client's own TCP retry lands back on a UDP hop and is answered with the same truncated
63//!   message, and a name whose answer does not fit a datagram simply does not resolve through this
64//!   node. The TCP hop is also **raced** against a slow or failed UDP hop, as Go's forwarder races
65//!   them: it joins after [`UDP_RACE_TIMEOUT`] (at once for a TCP client, or when the UDP hop has
66//!   already failed), so a resolver whose UDP path drops but whose TCP path answers still resolves.
67//!   Control's `dns-forwarder-disable-tcp-retries` node attribute is the *off* switch for all of it
68//!   ([`DnsView::upstream_tcp_retry`]); the TCP hop is on by default.
69
70use std::{
71    net::{IpAddr, Ipv4Addr, SocketAddr},
72    sync::Arc,
73    time::Duration,
74};
75
76use kameo::{
77    actor::ActorRef,
78    message::{Context, Message},
79};
80use netstack::{CreateSocket, netcore::Channel};
81use tokio::{
82    io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
83    sync::{Semaphore, watch},
84    task::JoinSet,
85    time::timeout,
86};
87use ts_control::{DnsConfig, DnsResolver, Node};
88use ts_dns_wire::{Name, QType, RData, Rcode, SoaZone, decode_query, encode_response};
89
90use crate::{
91    Error,
92    env::Env,
93    peer_tracker::{PeerDb, PeerState},
94};
95
96/// How long to wait for an upstream resolver to answer a forwarded query before giving up.
97const UPSTREAM_TIMEOUT: Duration = Duration::from_secs(5);
98/// Cap on concurrent in-flight forwarded queries on the local `100.100.100.100:53` responder.
99///
100/// Each forward is spawned onto a task that holds an overlay UDP socket until the upstream answers
101/// or [`UPSTREAM_TIMEOUT`] elapses. Without a cap, a local/tailnet client spraying distinct
102/// forwardable names opens unbounded concurrent overlay sockets + tasks (a resource-exhaustion DoS
103/// on a slow/black-holed upstream, since each lingers for the full timeout). Bound it the same way
104/// the peerAPI DoH server bounds its request handlers ([`crate::peerapi`]'s `MAX_INFLIGHT`): acquire
105/// a permit before spawning and drop the query fail-closed when saturated. A dropped DNS query is a
106/// benign outcome — the stub resolver simply retries or times out — and Go's resolver likewise
107/// bounds outstanding forwards rather than spawning without limit.
108const MAX_INFLIGHT_FORWARDS: usize = 512;
109/// Cap on how much of a forwarded upstream response we relay back to the stub resolver (a single
110/// UDP datagram).
111///
112/// This is Go's `maxResponseBytes` — `const maxResponseBytes = 4095`, defined in
113/// net/dns/resolver/tsdns.go @ 9ea7cba44591e0cd840c6c94d23274dd222059bf and used by the forwarder.
114/// The odd-looking 4095 is deliberate upstream: `sendUDP` reads into a `maxResponseBytes+1` buffer
115/// precisely so that a 4096-byte read is *detectable* as "the answer did not fit", and then cuts the
116/// reply back to 4095 and sets `TC`. A 4096-byte answer is a truncated answer everywhere else in a
117/// tailnet, so it has to be one here too: relaying it whole with `TC` clear is a message a Go peer
118/// would have handed its stub resolver as truncated.
119///
120/// *Where the bound applies differs from Go*: here it is not a read bound and it does not bound
121/// memory. [`forward_query`] reads with `recv_from_bytes`, which issues `Recv { max_len: None }`, so
122/// the netstack has already copied the whole queued datagram out before [`cap_response`] sees it.
123/// What bounds the read is the netstack UDP socket's receive ring —
124/// `netcore::Config::udp_buffer_size`, 4 KiB by default and not overridden by `ts_runtime` — and
125/// smoltcp drops a datagram larger than that ring at enqueue rather than delivering a chopped one.
126/// That ring is 4096 and this cap is 4095, so the ring plays exactly the part Go's `+1` byte plays:
127/// the one datagram size it can deliver and this cap cannot pass is the full-ring 4096-byte answer,
128/// which [`cap_response`] then chops to 4095 and marks `TC` (pinned by
129/// `full_ring_datagram_is_chopped_and_marked_truncated`).
130///
131/// The client's query is forwarded verbatim, so a client advertising a large EDNS UDP size can
132/// elicit a legitimately large (1300–4095 byte) UDP answer (big TXT sets, DNSSEC, many-record
133/// round-robins). Capping at the old 1232 truncated those and set TC, forcing a TCP retry — which
134/// nothing served at the time, so the large answer became unreachable. 4095 relays them intact.
135///
136/// It is a **datagram** bound, and since [`ask_upstream`] gained its TCP retry it is applied as one:
137/// an answer that came over the TCP hop and is going back to a TCP client never crossed a datagram
138/// and is not cut here (see [`cap_response`]). Every other combination is, because a UDP hop could
139/// not have delivered more than the receive ring anyway and a UDP client cannot be answered with
140/// more than one datagram. So an answer over 4095 bytes still reaches a UDP stub resolver as a
141/// `TC`-marked 4095 bytes — which is the retry RFC 1035 §4.2.1 asks it to make, and `dns_over_tcp`
142/// answers that retry whole.
143const MAX_UPSTREAM_RESPONSE: usize = 4095;
144
145/// The MagicDNS service IP. The netstack interface owns this address, so a `udp_bind` here
146/// receives the tailnet's DNS traffic.
147const MAGIC_DNS_IP: Ipv4Addr = Ipv4Addr::new(100, 100, 100, 100);
148/// The DNS service port.
149const MAGIC_DNS_PORT: u16 = 53;
150
151/// The latest view the answer loop resolves queries against.
152///
153/// Updated by the actor's message handlers (from control `StateUpdate` and peer `PeerState`
154/// updates) and read fresh by the answer loop for every packet.
155#[derive(Clone, Default)]
156pub(crate) struct DnsView {
157    /// The DNS configuration. `magic_dns == false` (the default) means serve nothing.
158    pub(crate) cfg: DnsConfig,
159    /// The current peer database, if we've seen a peer update.
160    pub(crate) peers: Option<Arc<PeerDb>>,
161    /// This node, if we've seen a self-node update.
162    pub(crate) self_node: Option<Node>,
163    /// The peerAPI DoH socket address of the currently-selected exit node, if one is active and can
164    /// proxy DNS ([`Node::peerapi_doh_addr`]). When set, the MagicDNS *client* serve loop delegates
165    /// recursive resolution to this address over the overlay instead of forwarding to the locally
166    /// configured upstream resolvers — so recursive DNS egresses from the exit node, not this host.
167    ///
168    /// Only consumed by the local MagicDNS responder's serve loop (the client side). The peerAPI
169    /// DoH *server* shares this same view but ignores this field: an exit-node DNS proxy resolves
170    /// recursively itself (gated by `forward_exit_egress`), it never re-delegates to its own exit
171    /// node. `None` means no active exit node / no DoH delegation — recursion stays local.
172    pub(crate) exit_doh: Option<SocketAddr>,
173    /// Whether IPv6 is enabled on the tailnet overlay (from [`Env::enable_ipv6`], default `false`).
174    ///
175    /// Governs the AAAA answer path only: with the gate OFF (default) an AAAA query for a
176    /// tailnet/overlay/self name is answered NoError-with-empty-answer (NODATA) instead of the
177    /// overlay v6 address; with it ON, AAAA is answered from overlay data as historically. Set once
178    /// from the runtime `Env` when the actor starts; never changes for the life of the runtime.
179    pub(crate) enable_ipv6: bool,
180    /// Whether the tailnet's DNS configuration is accepted (`--accept-dns` / `CorpDNS`, from
181    /// [`Env::accept_dns`]). When `false`, [`decide`] refuses every query (the responder serves
182    /// nothing), mirroring Go applying an empty `dns.Config` when `CorpDNS` is off — so a node can
183    /// join for connectivity without taking over DNS.
184    ///
185    /// Unlike [`enable_ipv6`](DnsView::enable_ipv6) (snapshotted once at actor spawn), this is
186    /// runtime-settable via `Device::set_accept_dns`, so it is re-read from the live
187    /// [`Env::accept_dns`] cell on **every** view rebuild (the `StateUpdate` and `PeerState`
188    /// handlers), not just at spawn — otherwise a runtime toggle would never reach the served view.
189    pub(crate) accept_dns: bool,
190}
191
192impl DnsView {
193    /// Find the node (peer or self) that answers to `name`, case/dot-insensitively.
194    fn node_by_name(&self, name: &str) -> Option<Node> {
195        if let Some(node) = self
196            .peers
197            .as_ref()
198            .and_then(|p| p.get(&name).map(|(_, n)| n.clone()))
199        {
200            return Some(node);
201        }
202
203        self.self_node
204            .as_ref()
205            .filter(|n| n.matches_name(name))
206            .cloned()
207    }
208
209    /// Find the node a **parent** of `canon` names, when that node is a *subdomain host* — a node
210    /// control has set the `dns-subdomain-resolve` attribute on
211    /// ([`Node::resolves_subdomains`]), meaning every name under it resolves to its addresses.
212    ///
213    /// Mirrors the miss path of Go's resolver (`net/dns/resolver/tsdns.go`): a name that matches no
214    /// host walks its parents (`util/dnsname`'s `Parent`) and answers from the first parent that is
215    /// a subdomain host. The walk is over *every* parent, not one level: for a node `machine`, both
216    /// `my.machine` and `be.my.machine` resolve to it.
217    ///
218    /// Two bounds keep the walk from becoming a wildcard:
219    ///
220    /// - It stops at a **tailnet search domain**. `user.ts.net` is the zone apex, not a host under
221    ///   it, so the walk never climbs past it into names this node is not authoritative for.
222    /// - A candidate parent must be **fully qualified** (at least two labels) and is matched
223    ///   *exactly*, with no search-domain qualification. Unlike [`DnsView::resolve_addr`]'s exact
224    ///   lookup, the walk must not expand a short name against the search list: the peer-name index
225    ///   also holds bare hostnames, so a peer named after a public suffix (`com`, `dev`) carrying
226    ///   the attribute would otherwise swallow every name under that suffix — a hijack Go cannot
227    ///   perform, because its resolver does no search-list expansion at all (the client stub does).
228    ///   A stub resolver qualifies a short name against the search list before asking, so the
229    ///   fully-qualified form is what arrives here anyway.
230    fn subdomain_host_for(&self, canon: &str) -> Option<Node> {
231        let mut parent = canon;
232        while let Some((_, rest)) = parent.split_once('.') {
233            parent = rest;
234            // A bare label is never a candidate (see the doc comment): nothing is left to walk.
235            if !parent.contains('.') {
236                return None;
237            }
238            // The tailnet zone apex itself: stop rather than climb out of the zone we serve.
239            if self.cfg.search_domains.iter().any(|zone| zone == parent) {
240                return None;
241            }
242            if let Some(node) = self.node_by_name(parent)
243                && node.resolves_subdomains()
244            {
245                return Some(node);
246            }
247        }
248        None
249    }
250
251    /// Resolve `canon` to an answer address of the requested family. A tailnet peer/self match
252    /// wins first — tried as written and then qualified by each tailnet search domain (so a
253    /// short/partially-qualified name like `host` or `host.user` still resolves to
254    /// `host.user.ts.net`). Failing that, a control-pushed [`ExtraRecord`] of the matching family
255    /// answers, matched as a fully-qualified name only (no search-domain expansion — like Go tsnet,
256    /// ExtraRecords are authoritative FQDN entries, not subject to client search-list qualification).
257    /// Only when nothing matched the name *exactly* does the subdomain-host parent walk run
258    /// ([`DnsView::subdomain_host_for`]) — so an exact name always beats a parent match, as it does
259    /// upstream, where the parent walk is the lookup-miss path.
260    /// Still fail-closed: only ever resolves to a known tailnet peer/self or an explicitly
261    /// control-pushed static record — never anything else.
262    fn resolve_addr(&self, canon: &str, want_v4: bool) -> Option<IpAddr> {
263        let addr_of = |node: Node| -> IpAddr {
264            if want_v4 {
265                IpAddr::from(node.tailnet_address.ipv4.addr())
266            } else {
267                IpAddr::from(node.tailnet_address.ipv6.addr())
268            }
269        };
270
271        if let Some(node) = self.node_by_name(canon) {
272            return Some(addr_of(node));
273        }
274        for suffix in &self.cfg.search_domains {
275            if let Some(node) = self.node_by_name(&format!("{canon}.{suffix}")) {
276                return Some(addr_of(node));
277            }
278        }
279
280        // Control-pushed static records match the fully-qualified query name only.
281        let mut named_by_extra_record = false;
282        for rec in &self.cfg.extra_records {
283            if rec.name != canon {
284                continue;
285            }
286            named_by_extra_record = true;
287            if matches!(
288                (rec.addr, want_v4),
289                (IpAddr::V4(_), true) | (IpAddr::V6(_), false)
290            ) {
291                return Some(rec.addr);
292            }
293        }
294        // An extra record for this exact name but of the other family means the name *exists* and
295        // simply holds no address of the queried type — Go's lookup found it, so the parent walk
296        // (its miss path) does not run and the answer stays NODATA.
297        if named_by_extra_record {
298            return None;
299        }
300
301        // Nothing answers this name exactly: fall back to a parent that resolves its subdomains.
302        self.subdomain_host_for(canon).map(addr_of)
303    }
304
305    /// Find the node (peer or self) that owns the tailnet IP `ip`.
306    fn node_by_ip(&self, ip: IpAddr) -> Option<Node> {
307        if let Some(node) = self
308            .peers
309            .as_ref()
310            .and_then(|p| p.get(&ip).map(|(_, n)| n.clone()))
311        {
312            return Some(node);
313        }
314
315        self.self_node
316            .as_ref()
317            .filter(|n| {
318                IpAddr::from(n.tailnet_address.ipv4.addr()) == ip
319                    || IpAddr::from(n.tailnet_address.ipv6.addr()) == ip
320            })
321            .cloned()
322    }
323
324    /// Decide how to resolve a non-overlay `name` against the split-DNS routes and recursive
325    /// resolvers, returning the upstreams to forward to.
326    ///
327    /// Longest-suffix wins among [`DnsConfig::routes`]: a route's suffix matches `name` if `name`
328    /// equals it or ends with `.suffix`. A matched route with a non-empty upstream list forwards
329    /// there; a matched route with an **empty** list is a negative route ([`Upstreams::Block`] =>
330    /// NXDOMAIN). With no route match, [`DnsConfig::fallback_resolvers`] (preferred) or
331    /// [`DnsConfig::resolvers`] resolve recursively; if neither is configured we stay fail-closed
332    /// ([`Upstreams::None`] => NXDOMAIN).
333    fn route_for(&self, name: &str) -> Upstreams<'_> {
334        let mut best: Option<(&str, &Vec<DnsResolver>)> = None;
335        for (suffix, upstreams) in &self.cfg.routes {
336            if suffix_matches(name, suffix) && best.is_none_or(|(b, _)| suffix.len() > b.len()) {
337                best = Some((suffix.as_str(), upstreams));
338            }
339        }
340
341        if let Some((_, upstreams)) = best {
342            return if upstreams.is_empty() {
343                Upstreams::Block
344            } else {
345                // A deliberately-configured split-DNS route: not eligible for exit-node DoH
346                // delegation — these upstreams (e.g. an internal resolver reachable over a subnet
347                // route) must keep receiving the query directly.
348                Upstreams::Route(upstreams)
349            };
350        }
351
352        if !self.cfg.fallback_resolvers.is_empty() {
353            return Upstreams::Recursive(&self.cfg.fallback_resolvers);
354        }
355        if !self.cfg.resolvers.is_empty() {
356            return Upstreams::Recursive(&self.cfg.resolvers);
357        }
358        Upstreams::None
359    }
360
361    /// Whether the forwarder may use TCP to an upstream resolver — to re-ask a truncated answer, or
362    /// to race a slow or failed UDP hop ([`ask_upstream`]).
363    ///
364    /// On unless control's `dns-forwarder-disable-tcp-retries` node attribute is set on the **self**
365    /// node ([`Node::disable_dns_forwarder_tcp_retries`]) — the attribute is the retry's *off*
366    /// switch, not its on switch, so a node with no self-node update yet (and every tailnet that
367    /// never set it) retries. Go reads the same knob at the same point:
368    /// `skipTCP := skipTCPRetry() || (f.controlKnobs != nil &&
369    /// f.controlKnobs.DisableDNSForwarderTCPRetries.Load())`, net/dns/resolver/forwarder.go:677 @
370    /// `023255e8a27ec9f6a21d24e3eda21c052ff72af3`.
371    ///
372    /// Go's other disjunct, `skipTCPRetry()`, is a process-level environment knob and has no
373    /// counterpart here: this tree carries no `envknob` equivalent, and the deployment switch a
374    /// tailnet operator actually reaches for is the node attribute.
375    pub(crate) fn upstream_tcp_retry(&self) -> TcpRetry {
376        match &self.self_node {
377            Some(node) if node.disable_dns_forwarder_tcp_retries() => TcpRetry::Disabled,
378            _ => TcpRetry::Enabled,
379        }
380    }
381}
382
383/// Whether the forwarder may ask an upstream over **TCP** at all — to re-ask a truncated answer, or
384/// to race a slow or failed UDP hop. Decided per query from the current view
385/// ([`DnsView::upstream_tcp_retry`]) and carried to [`ask_upstream`], which is the only thing that
386/// reads it.
387#[derive(Clone, Copy, Debug, PartialEq, Eq)]
388pub(crate) enum TcpRetry {
389    /// Race the TCP hop against the UDP hop, and retry a `TC`-marked UDP answer over TCP, to the
390    /// same resolver. The default.
391    Enabled,
392    /// UDP only, on every arm: relay whatever the UDP hop brings back, truncated or not, and never
393    /// open a TCP connection. Control's `dns-forwarder-disable-tcp-retries` attribute.
394    Disabled,
395}
396
397/// The upstreams a non-overlay query should be forwarded to (or why it should not be forwarded).
398enum Upstreams<'a> {
399    /// A split-DNS route matched: forward to these route-specific upstreams (never DoH-delegated).
400    Route(&'a [DnsResolver]),
401    /// No route matched: forward to these recursive (fallback/global) resolvers. Eligible for
402    /// exit-node DoH delegation in the client serve loop.
403    Recursive(&'a [DnsResolver]),
404    /// A negative split-DNS route matched: do not resolve (NXDOMAIN). The route's suffix is a zone
405    /// this node is authoritative for — Go's `localDomains` is exactly the set of routes configured
406    /// with no resolvers — so [`authoritative_zone_for`] finds it again when naming the negative
407    /// answer's SOA zone.
408    Block,
409    /// No route and no resolver configured: fail closed (NXDOMAIN).
410    None,
411}
412
413/// What the (sync) decision step concluded for a query: either a complete response to send back,
414/// or a request to forward the original query to an upstream resolver.
415pub(crate) enum Decision {
416    /// A fully-formed response is ready to send.
417    Reply(Vec<u8>),
418    /// Forward the original query datagram to one of these upstream UDP resolvers; on success
419    /// relay the upstream answer, on failure/timeout answer with the prebuilt `servfail` buffer
420    /// (an off-tailnet name we failed to forward is a soft failure, not a cacheable non-existence —
421    /// Go forwarder.go:1297-1307).
422    Forward {
423        /// UDP upstreams to try, in order.
424        upstreams: Vec<SocketAddr>,
425        /// The original query bytes to forward verbatim.
426        query: Vec<u8>,
427        /// Fallback SERVFAIL response if every upstream fails or times out.
428        servfail: Vec<u8>,
429        /// Whether this is a *recursive* (catch-all fallback/global resolver) forward, as opposed
430        /// to a deliberately-configured split-DNS route. Only recursive forwards are eligible for
431        /// exit-node DoH delegation in the client serve loop (see [`DnsView::exit_doh`]); split-DNS
432        /// routes always stay on their configured upstreams (typically subnet-reachable internal
433        /// resolvers). The peerAPI DoH *server* ignores this flag entirely.
434        recursive: bool,
435    },
436}
437
438/// Whether `name` is `suffix` or sits under it at a label boundary: `"a.corp"` matches `"corp"`,
439/// `"acorp"` does not. An **empty** suffix never matches (defense-in-depth: an empty suffix would
440/// otherwise make `ends_with("")` match every name and either over-route or treat everything as a
441/// tailnet name — both leak-prone).
442fn suffix_matches(name: &str, suffix: &str) -> bool {
443    if suffix.is_empty() {
444        return false;
445    }
446    name == suffix
447        || (name.len() > suffix.len()
448            && name.ends_with(suffix)
449            && name.as_bytes()[name.len() - suffix.len() - 1] == b'.')
450}
451
452/// Returns `true` if `name` falls under one of the tailnet search domains. Such names are
453/// authoritative MagicDNS names and are NEVER forwarded to an upstream resolver — anti-leak: a
454/// tailnet name (and the fact that it was queried) must not escape to a third-party resolver.
455fn is_tailnet_name(view: &DnsView, name: &str) -> bool {
456    view.cfg
457        .search_domains
458        .iter()
459        .any(|suffix| suffix_matches(name, suffix))
460}
461
462/// Whether `name` is an IPv6 reverse-DNS (`PTR`) name (ends in `ip6.arpa`). This fork is IPv4-only
463/// on the tailnet; an IPv6 reverse lookup must NEVER be forwarded to a third-party resolver
464/// (anti-leak: it would reveal that a tailnet v6 address — e.g. a ULA `fd7a:…` — was probed). All
465/// such queries fail closed to NXDOMAIN.
466fn is_ip6_arpa(name: &str) -> bool {
467    suffix_matches(name, "ip6.arpa")
468}
469
470/// Whether `ip` is in the Tailscale CGNAT range `100.64.0.0/10` (RFC 6598, the tailnet IPv4 space).
471/// Reverse (`PTR`) queries for these addresses are authoritative to MagicDNS: if no peer owns the
472/// IP we fail closed to NXDOMAIN rather than forwarding the probe to a third-party resolver.
473fn is_tailnet_cgnat(ip: Ipv4Addr) -> bool {
474    let o = ip.octets();
475    o[0] == 100 && (64..=127).contains(&o[1])
476}
477
478/// The zone this node is authoritative for that contains `canon`, or `None` when it is not
479/// authoritative for the name.
480///
481/// Mirrors Go `net/dns/resolver/tsdns.go` `authoritativeZoneFor`, which scans `Resolver.localDomains`.
482/// Go's `localDomains` is exactly the set of control-pushed routes with **no** resolvers
483/// (`net/dns/manager.go` `compileConfig`), so the equivalent set here is the union of:
484///
485/// - the tailnet search domains — what [`is_tailnet_name`] tests, and the zone a tailnet-suffix
486///   NXDOMAIN belongs to;
487/// - the negative split-DNS routes (a route with an empty upstream list), the literal shape of
488///   Go's `localDomains`;
489/// - the CGNAT reverse zone `<b>.100.in-addr.arpa` covering a `100.64.0.0/10` reverse name.
490///   Synthesized rather than read from the routes: this fork's reverse guard is structural
491///   ([`is_tailnet_cgnat`]) and holds whether or not control pushed the matching route, and the
492///   zone it names is the same per-/16 chunk real tailscaled advertises.
493///
494/// `ip6.arpa` is deliberately absent. This fork NXDOMAINs *every* `ip6.arpa` name as an anti-leak
495/// measure ([`is_ip6_arpa`]) rather than because it serves that zone, and an SOA naming `ip6.arpa`
496/// would claim authority over the whole IPv6 reverse tree — a claim we do not have and one that
497/// would have a client negative-cache far more than this node answers for.
498///
499/// The longest match wins. Go returns the first match from an unordered slice; longest gives the
500/// same answer whenever the zones nest (the usual case) and is a defensible tie-break when they
501/// do not.
502fn authoritative_zone_for(view: &DnsView, name: &Name, canon: &str) -> Option<String> {
503    if let Some(octets) = name.ptr_to_ipv4() {
504        let v4: Ipv4Addr = octets.into();
505        if is_tailnet_cgnat(v4) {
506            return Some(format!("{}.100.in-addr.arpa", v4.octets()[1]));
507        }
508    }
509
510    view.cfg
511        .search_domains
512        .iter()
513        .map(String::as_str)
514        .chain(
515            view.cfg
516                .routes
517                .iter()
518                .filter(|(_, upstreams)| upstreams.is_empty())
519                .map(|(suffix, _)| suffix.as_str()),
520        )
521        .filter(|zone| suffix_matches(canon, zone))
522        .max_by_key(|zone| zone.len())
523        .map(str::to_owned)
524}
525
526/// The SOA record to attach to an authoritative **negative** answer (NXDOMAIN, or NODATA for a
527/// name we serve), or `None` when this node is not authoritative for a zone containing the name.
528///
529/// Without it, a downstream cache decides for itself how long to remember the nonexistence: macOS
530/// `mDNSResponder` holds an SOA-less negative answer for a long time, so a name queried shortly
531/// *before* a node was renamed to it keeps failing until something flushes the cache. The SOA
532/// bounds that at 10 seconds (RFC 2308), which is what Go's resolver advertises.
533fn soa_for(view: &DnsView, name: &Name, canon: &str) -> Option<SoaZone> {
534    let zone = authoritative_zone_for(view, name, canon)?;
535    Some(SoaZone {
536        zone: Name(zone.split('.').map(str::to_owned).collect()),
537        serial: soa_serial(),
538    })
539}
540
541/// The SOA SERIAL to publish: the response time in unix seconds.
542///
543/// A serial is meant to change only when the zone data does, but nothing consumes ours — this node
544/// has no secondaries and serves no zone transfers — so Go uses the current time and so do we. It
545/// is monotonic, cheap, and fits in a `u32` until 2106. A clock before the epoch yields 0 rather
546/// than panicking; the value carries no meaning either way.
547fn soa_serial() -> u32 {
548    std::time::SystemTime::now()
549        .duration_since(std::time::UNIX_EPOCH)
550        .map_or(0, |since| since.as_secs() as u32)
551}
552
553/// Decide what to do with a single DNS query against `view`: either a complete response is ready
554/// ([`Decision::Reply`]), the query should be forwarded to upstream resolvers
555/// ([`Decision::Forward`]), or the packet should be dropped without answering (`None`).
556///
557/// Factored out of the socket loop so it can be unit-tested without a netstack: it does no I/O and
558/// reads no state but `view` and the wall clock (the SOA SERIAL of a negative answer, which nothing
559/// consumes — see [`soa_serial`]). It never panics and fails closed: an unknown, unroutable, or
560/// tailnet-suffix name resolves to NXDOMAIN rather than leaking to an upstream resolver.
561pub(crate) fn decide(view: &DnsView, buf: &[u8]) -> Option<Decision> {
562    // Malformed / non-query input is dropped: we never answer something we can't parse.
563    let query = decode_query(buf).ok()?;
564    let q = &query.question;
565    let id = query.id;
566    // Echo the query's RD bit (and set RA when set) on the response — Go derives the response header
567    // from the query header.
568    let rd = query.recursion_desired;
569
570    let reply = |rcode, answers: &[RData]| {
571        Decision::Reply(encode_response(id, q, rd, rcode, answers, None))
572    };
573    // A negative answer (NXDOMAIN, or NODATA) for a name inside a zone we serve carries that zone's
574    // SOA in the authority section, which bounds how long a downstream resolver may cache the
575    // nonexistence (RFC 2308). `soa_for` returns `None` when we are not authoritative for the name,
576    // in which case this is exactly `reply`.
577    let reply_negative = |rcode, canon: &str| {
578        Decision::Reply(encode_response(
579            id,
580            q,
581            rd,
582            rcode,
583            &[],
584            soa_for(view, &q.name, canon).as_ref(),
585        ))
586    };
587
588    // Fail closed: MagicDNS off, or the node doesn't accept the tailnet's DNS config
589    // (`--accept-dns` / `CorpDNS` is false) => serve nothing. The `accept_dns` gate mirrors Go
590    // applying an empty `dns.Config` when `CorpDNS` is off: the node ignores the control-pushed DNS
591    // config and refuses every query. This one read site covers the netstack responder, the peerAPI
592    // DoH server that shares the view, and (via `tun_actor::plan_intercept`) the TUN query path.
593    if !view.cfg.magic_dns || !view.accept_dns {
594        return Some(reply(Rcode::Refused, &[]));
595    }
596
597    let canon = q.name.to_canon();
598
599    // We only serve the internet (IN) class authoritatively. A non-IN class (CHAOS, HESIOD, the
600    // ANY/255 class, ...) is NOT refused outright: Go's local resolver does no class check and
601    // forwards such a query like any other name. Treat it as an unsupported authoritative type —
602    // NODATA for a tailnet name, forward for an off-tailnet name — so a `CH TXT version.bind`
603    // diagnostic or a `qclass=ANY` probe reaches upstream instead of getting REFUSED.
604    const CLASS_IN: u16 = 1;
605    if q.qclass != CLASS_IN {
606        return Some(forward_or_nodata(view, &canon, buf, id, q, rd));
607    }
608
609    Some(match &q.qtype {
610        QType::A => match view.resolve_addr(&canon, true) {
611            Some(IpAddr::V4(v4)) => reply(Rcode::NoError, &[RData::A(v4.octets())]),
612            // No overlay/extra-record answer: try split-DNS / recursive upstreams.
613            _ => forward_or_nxdomain(view, &canon, buf, id, q, rd),
614        },
615        QType::Aaaa => match view.resolve_addr(&canon, false) {
616            // A tailnet/overlay/self (or extra-record) AAAA match. Gate on IPv6: with IPv6 OFF
617            // (default) the client is IPv4-only, so answering with the overlay v6 address would
618            // only hand out an unroutable address — dead connections plus a fingerprint. Return
619            // NoError with an empty answer (NODATA) instead. With the gate ON, answer from overlay
620            // data as historically. We never forward this name to a recursive upstream either way:
621            // a positive overlay match is authoritative.
622            Some(IpAddr::V6(v6)) if view.enable_ipv6 => {
623                reply(Rcode::NoError, &[RData::Aaaa(v6.octets())])
624            }
625            // NODATA: the name exists but we hold no address of the queried family for it, so it
626            // takes the SOA — Go sets `SOAZone` on exactly this case (`rcode == RCodeSuccess &&
627            // !ip.IsValid()` for an A/AAAA/ALL question).
628            Some(IpAddr::V6(_)) => reply_negative(Rcode::NoError, &canon),
629            // No overlay/extra-record answer: split-DNS / recursive upstreams (off-tailnet names);
630            // tailnet names fail closed to NXDOMAIN inside `forward_or_nxdomain`.
631            _ => forward_or_nxdomain(view, &canon, buf, id, q, rd),
632        },
633        QType::Ptr => match q.name.ptr_to_ipv4() {
634            Some(octets) => {
635                let v4: Ipv4Addr = octets.into();
636                let ip = IpAddr::V4(v4);
637                match view.node_by_ip(ip) {
638                    Some(node) => {
639                        let fqdn = node.fqdn(false);
640                        let labels: Vec<String> = fqdn.split('.').map(str::to_owned).collect();
641                        reply(Rcode::NoError, &[RData::Ptr(Name(labels))])
642                    }
643                    // Anti-leak: a reverse query for an IP in the tailnet CGNAT range
644                    // (100.64.0.0/10) that misses the peer set is authoritative-but-unknown; fail
645                    // closed to NXDOMAIN rather than leaking the probed tailnet IP upstream. Only
646                    // genuinely off-tailnet reverse queries are forwarded.
647                    None if is_tailnet_cgnat(v4) => reply_negative(Rcode::NxDomain, &canon),
648                    None => forward_or_nxdomain(view, &canon, buf, id, q, rd),
649                }
650            }
651            // Anti-leak / IPv4-only-tailnet: an IPv6 reverse (`ip6.arpa`) PTR must never be
652            // forwarded — relaying it would reveal that a tailnet v6 address (e.g. a ULA `fd7a:…`)
653            // was probed. Fail closed to NXDOMAIN, exactly like the IPv4 CGNAT guard above. No SOA:
654            // this blanket refusal is anti-leak, not a claim to serve `ip6.arpa` (see
655            // [`authoritative_zone_for`]).
656            None if is_ip6_arpa(&canon) => reply(Rcode::NxDomain, &[]),
657            None => forward_or_nxdomain(view, &canon, buf, id, q, rd),
658        },
659        // Anything else (TXT, SRV, MX, HTTPS/SVCB, CNAME, ...): we hold no authoritative record of
660        // that type, so — like Go's resolver — forward it to upstream for an off-tailnet name and
661        // return NODATA (empty NOERROR) for a tailnet-authoritative name. NOT REFUSED: a stub reads
662        // REFUSED as "this server won't serve me" and abandons the resolver, which would break
663        // ordinary client lookups (notably HTTPS/SVCB type 65, issued routinely by browsers for
664        // HTTP/3 + ECH) for the same off-tailnet names whose A/AAAA already forward.
665        QType::Other(_) => forward_or_nodata(view, &canon, buf, id, q, rd),
666    })
667}
668
669/// For a name with no overlay answer, consult the split-DNS routes + recursive resolvers and
670/// either forward (to UDP upstreams), answer authoritatively absent (NXDOMAIN), or fail soft
671/// (SERVFAIL) when an off-tailnet name simply can't be forwarded.
672///
673/// Rcode parity with Go's resolver (`net/dns/resolver/tsdns.go` resolution order + `forwarder.go`):
674/// - A **tailnet-authoritative** name (search-domain suffix) or a **negative split-DNS route**
675///   (`Upstreams::Block` — a route configured with no resolvers, which Go answers authoritatively
676///   from Hosts, so an unmatched name under it is authoritatively absent) → **NXDOMAIN**.
677/// - An **off-tailnet** name we cannot forward — no route and no resolver configured
678///   (`Upstreams::None`), or a route whose resolvers are all filtered out (IPv6-only under the
679///   IPv4-only egress) → **SERVFAIL**, matching Go forwarder.go:1207 ("no upstream resolvers set,
680///   returning SERVFAIL"). A cacheable NXDOMAIN on a transient/structural inability to forward would
681///   make a downstream stub cache the *non-existence* of a real name; SERVFAIL is a soft failure the
682///   stub retries.
683///
684/// Anti-leak: a tailnet-suffix name is authoritative and is never forwarded — neither the name nor
685/// the query leaks to a third-party resolver. (The CGNAT `in-addr.arpa` / `ip6.arpa` reverse-zone
686/// NXDOMAIN guards live in the PTR arm of [`decide`] and are likewise unaffected.)
687fn forward_or_nxdomain(
688    view: &DnsView,
689    canon: &str,
690    buf: &[u8],
691    id: u16,
692    q: &ts_dns_wire::Question,
693    rd: bool,
694) -> Decision {
695    // NXDOMAIN for authoritative-absent names; SERVFAIL for an off-tailnet name we can't forward.
696    // An authoritative NXDOMAIN carries the zone's SOA so a downstream cache bounds how long it
697    // remembers the nonexistence (RFC 2308); a SERVFAIL never does — it asserts nothing to cache,
698    // and we are not authoritative for the name we failed to forward.
699    let nxdomain = |canon: &str| {
700        encode_response(
701            id,
702            q,
703            rd,
704            Rcode::NxDomain,
705            &[],
706            soa_for(view, &q.name, canon).as_ref(),
707        )
708    };
709    let servfail = encode_response(id, q, rd, Rcode::ServFail, &[], None);
710
711    if is_tailnet_name(view, canon) {
712        return Decision::Reply(nxdomain(canon));
713    }
714
715    let (resolvers, recursive) = match view.route_for(canon) {
716        Upstreams::Route(resolvers) => (resolvers, false),
717        Upstreams::Recursive(resolvers) => (resolvers, true),
718        // A negative split-DNS route is authoritative-absent (Go answers it from Hosts): NXDOMAIN.
719        // Go's `localDomains` *is* this route set, so the route's own suffix names the zone.
720        Upstreams::Block => return Decision::Reply(nxdomain(canon)),
721        // No route and no resolver: an off-tailnet name we have nowhere to forward — SERVFAIL, not
722        // a cacheable non-existence (Go forwarder.go:1207).
723        Upstreams::None => return Decision::Reply(servfail),
724    };
725
726    let upstreams: Vec<SocketAddr> = resolvers
727        .iter()
728        .map(DnsResolver::udp_addr)
729        // Anti-leak / IPv6-off: only forward over IPv4 upstreams; never open a v6 socket.
730        .filter(SocketAddr::is_ipv4)
731        .collect();
732    if upstreams.is_empty() {
733        // We had a route but every resolver was filtered out (IPv6-only): we cannot forward this
734        // off-tailnet name, so soft-fail rather than assert non-existence.
735        Decision::Reply(servfail)
736    } else {
737        Decision::Forward {
738            upstreams,
739            query: buf.to_vec(),
740            // All upstreams failing at runtime is also an inability to forward, not a non-existence
741            // (Go forwarder.go:1297-1307): hand the forwarder a SERVFAIL fallback, not NXDOMAIN.
742            servfail,
743            recursive,
744        }
745    }
746}
747
748/// The DNS query types Go's resolver explicitly leaves unimplemented for a tailnet-authoritative
749/// name, answering `RCodeNotImplemented` (NOTIMP) rather than NODATA (`net/dns/resolver/tsdns.go`
750/// `resolveLocal`: `case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO`). The numeric type
751/// codes: NS=2, SOA=6, HINFO=13, AXFR=252.
752fn is_unimplemented_tailnet_qtype(qtype: &ts_dns_wire::QType) -> bool {
753    matches!(qtype, ts_dns_wire::QType::Other(2 | 6 | 13 | 252))
754}
755
756/// For a query whose *qtype/qclass* we don't serve authoritatively (anything other than an IN-class
757/// A/AAAA/PTR — e.g. TXT, SRV, MX, HTTPS/SVCB, or a CHAOS-class query): forward it to upstream like
758/// any other name, but for a tailnet-authoritative name return an empty NOERROR (NODATA) instead of
759/// NXDOMAIN — except the NS/SOA/HINFO/AXFR types Go answers NOTIMP for
760/// ([`is_unimplemented_tailnet_qtype`]).
761///
762/// This mirrors Go's resolver: an authoritative name with no record of the requested type returns
763/// `RCodeSuccess` with no answers ("the name exists, but no records of that type"), NOT NXDOMAIN and
764/// NOT REFUSED; a non-authoritative name is forwarded verbatim regardless of qtype. The fork
765/// previously REFUSED every non-A/AAAA/PTR qtype (and every non-IN class) for *all* names, which a
766/// stub resolver reads as "this server won't serve me" — so it would abandon the resolver, breaking
767/// ordinary client lookups (HTTPS/SVCB type 65 issued routinely by browsers for HTTP/3 + ECH, plus
768/// MX/TXT/SRV) for off-tailnet names that A/AAAA queries already forward. Refusing these was never an
769/// anti-leak measure (the same name's A/AAAA already egresses); it was just broken interop.
770///
771/// Anti-leak is preserved: a tailnet-suffix name still never leaves this node (NODATA, not forward),
772/// exactly as the A/AAAA path keeps a positive overlay match authoritative.
773fn forward_or_nodata(
774    view: &DnsView,
775    canon: &str,
776    buf: &[u8],
777    id: u16,
778    q: &ts_dns_wire::Question,
779    rd: bool,
780) -> Decision {
781    // Authoritative tailnet name. For most unsupported types we answer NODATA (empty NOERROR) — the
782    // name exists, we just hold no record of that type. But a small set of types Go's resolver
783    // *explicitly* leaves unimplemented (`net/dns/resolver/tsdns.go` `resolveLocal`:
784    // `case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO: return RCodeNotImplemented`) must
785    // answer NOTIMP, not NODATA — a `dig NS`/`SOA`/`HINFO` against the tailnet zone is otherwise a
786    // clean fingerprint distinguishing this fork from real tailscaled. Off-tailnet names are
787    // unaffected (they forward below regardless of type); this NOTIMP applies only to a name we are
788    // authoritative for.
789    if is_tailnet_name(view, canon) {
790        let rcode = if is_unimplemented_tailnet_qtype(&q.qtype) {
791            Rcode::NotImpl
792        } else {
793            Rcode::NoError
794        };
795        // No SOA. Go sets `SOAZone` on a no-data answer only for an A/AAAA/ALL question; a TXT or
796        // SRV miss on a name we serve — and the NOTIMP types — go back bare, as they do upstream.
797        return Decision::Reply(encode_response(id, q, rd, rcode, &[], None));
798    }
799    // Anti-leak parity with the `QType::Ptr` arm: a reverse query for a tailnet CGNAT IPv4
800    // (100.64.0.0/10) or ANY `ip6.arpa` name must NEVER egress to an upstream resolver, regardless
801    // of qtype/class — forwarding it would reveal that a specific tailnet IP was probed. The PTR arm
802    // enforces this (NXDOMAIN) but its guards live only inside that arm; without re-checking here, an
803    // exotic-qtype (TXT/ANY/…) or non-IN-class query for a tailnet reverse name would slip through to
804    // the forward path below. Fail closed to NXDOMAIN, matching the PTR arm's disposition.
805    if is_ip6_arpa(canon) {
806        // No SOA: see the matching guard in [`decide`]'s PTR arm.
807        return Decision::Reply(encode_response(id, q, rd, Rcode::NxDomain, &[], None));
808    }
809    if let Some(octets) = q.name.ptr_to_ipv4()
810        && is_tailnet_cgnat(octets.into())
811    {
812        // Authoritative for the CGNAT reverse zone, so this NXDOMAIN carries its SOA — same
813        // disposition as the PTR arm, whatever the qtype or class that got us here.
814        return Decision::Reply(encode_response(
815            id,
816            q,
817            rd,
818            Rcode::NxDomain,
819            &[],
820            soa_for(view, &q.name, canon).as_ref(),
821        ));
822    }
823    // Off-tailnet, non-reverse-zone: forward verbatim. `forward_or_nxdomain` already forwards
824    // non-tailnet names and soft-fails (SERVFAIL) when no upstream is configured/routable; reuse it
825    // (the tailnet branch above is already handled, so its tailnet→NXDOMAIN and negative-route paths
826    // are unreachable here — this only exercises its off-tailnet forward / SERVFAIL dispositions).
827    forward_or_nxdomain(view, canon, buf, id, q, rd)
828}
829
830/// Client-side plan for a *recursive* forward: keep resolving over local UDP upstreams, or delegate
831/// the query to the active exit node's peerAPI DoH endpoint over the overlay.
832#[derive(Debug, PartialEq, Eq)]
833pub(crate) enum RecursivePlan {
834    /// Forward over UDP to these upstreams. Used when no exit node is active, or when the config
835    /// has `use_with_exit_node` resolvers (kept local even with an exit node selected).
836    Udp(Vec<SocketAddr>),
837    /// Delegate the query to the exit node's peerAPI DoH server at this overlay address.
838    Doh(SocketAddr),
839}
840
841/// Decide whether a recursive forward should stay on local UDP upstreams or be delegated to the
842/// active exit node's DoH endpoint. Pure (no I/O) so the delegation rule is unit-testable.
843///
844/// - No active exit node ([`DnsView::exit_doh`] is `None`) => keep `default_upstreams` (UDP).
845/// - Exit node active, but the config has [`use_with_exit_node`][ts_control::DnsResolver::use_with_exit_node]
846///   resolvers => those resolvers stay local (Go keeps `UseWithExitNode` resolvers when an exit node
847///   is selected); forward to them over UDP, do NOT delegate.
848/// - Exit node active, no kept-local resolvers => delegate to the exit node's DoH. Recursive DNS
849///   then egresses from the exit node, not this host (the whole point of routing through an exit
850///   node: this node's real IP is never used to resolve the peer's public names).
851pub(crate) fn recursive_plan(view: &DnsView, default_upstreams: Vec<SocketAddr>) -> RecursivePlan {
852    let Some(doh) = view.exit_doh else {
853        return RecursivePlan::Udp(default_upstreams);
854    };
855    let kept: Vec<SocketAddr> = view
856        .cfg
857        .resolvers_with_exit_node()
858        .map(DnsResolver::udp_addr)
859        // Anti-leak / IPv6-off: only ever resolve over IPv4 upstreams; never open a v6 socket.
860        .filter(SocketAddr::is_ipv4)
861        .collect();
862    if kept.is_empty() {
863        RecursivePlan::Doh(doh)
864    } else {
865        RecursivePlan::Udp(kept)
866    }
867}
868
869/// Which transport the *client* we are answering reached us over.
870///
871/// The only thing it changes is whether an answer may be marked `TC` for exceeding the
872/// UDP payload size the query advertised. That limit describes the **datagram** we would answer in
873/// (RFC 1035 §4.2.1, RFC 6891 §6.2.3); a client that reached us over TCP has no such bound
874/// (RFC 7766 §8), and marking its answer truncated sends a stub resolver that already retried over
875/// TCP — the retry `TC` asked it to make — straight back into another retry. Go draws the same line:
876/// `checkResponseSizeAndSetTC` is applied on the UDP answer path, while the TCP DNS handler
877/// installed by `acceptTCP`'s `hittingDNS` case writes the answer under a 2-byte length prefix with
878/// no size check (wgengine/netstack/netstack.go, net/dns/resolver/tsdns.go @
879/// 9ea7cba44591e0cd840c6c94d23274dd222059bf).
880#[derive(Clone, Copy, Debug, PartialEq, Eq)]
881pub(crate) enum ClientTransport {
882    /// A UDP client: the EDNS(0)-advertised (or 512-byte) datagram limit applies.
883    Udp,
884    /// A TCP client: no datagram limit applies, so no `TC` bit is added for size.
885    ///
886    /// Two things reach for it: `dns_over_tcp` (compiled with the `tun` feature — the application
887    /// netstack has no TCP listener on `100.100.100.100:53` yet), and the peerAPI DoH *server*,
888    /// which answers as `"tcp"` because the datagram budget belongs to the peer that asked, not to
889    /// us (Go `Resolver.HandlePeerDNSQuery`, see `peerapi_doh::PEER_CLIENT_TRANSPORT`).
890    Tcp,
891}
892
893/// Turn a [`Decision::Forward`] into the plan that will carry it: a recursive forward consults
894/// [`recursive_plan`] (which may delegate to the active exit node's DoH endpoint), a split-DNS
895/// forward always goes to its route's own upstreams over UDP.
896///
897/// One line of logic, but it is the point where "recursive" becomes "may egress from the exit node
898/// instead of this host", so every caller — the UDP serve loop, the `query_dns` handler, the TUN
899/// datapath's `plan_intercept` and the DNS-over-TCP connection loop — reads it from here rather than
900/// spelling the branch out again.
901pub(crate) fn forward_plan(
902    view: &DnsView,
903    upstreams: Vec<SocketAddr>,
904    recursive: bool,
905) -> RecursivePlan {
906    if recursive {
907        recursive_plan(view, upstreams)
908    } else {
909        RecursivePlan::Udp(upstreams)
910    }
911}
912
913/// Cap a forwarded upstream response to a single UDP datagram ([`MAX_UPSTREAM_RESPONSE`]) before
914/// relaying it, then — for a [`ClientTransport::Udp`] client only — mark it truncated if it is
915/// bigger than what `query`'s sender said it can receive ([`check_response_size_and_set_tc`]).
916///
917/// The two checks **compose**; they are not alternatives. The [`MAX_UPSTREAM_RESPONSE`] cap is this
918/// forwarder's own relay bound: when the response is too large it is truncated mid-message, so we
919/// set the `TC` (truncation) flag in the DNS header (byte 2, bit `0x02`) telling the stub resolver
920/// to retry over TCP — relaying a chopped answer without `TC` would surface a
921/// malformed-but-"complete" message. That flag is only set when truncation actually occurs. The
922/// second check is the *client's* bound, and never chops the body.
923///
924/// The cap runs *after* the whole message has been read (see [`MAX_UPSTREAM_RESPONSE`]), so it
925/// bounds what we relay, not what we allocate. The netstack's UDP receive ring (4096) is one byte
926/// wider than the cap (4095), so on the UDP hop the truncating branch is reachable by exactly one
927/// deliverable datagram size — the full-ring 4096-byte answer — which is the same size Go's
928/// `maxResponseBytes+1` read buffer exists to catch.
929///
930/// `via` is the hop the answer came over, and it is why the cap is not unconditional.
931/// [`MAX_UPSTREAM_RESPONSE`] is a **datagram** bound, so it applies to any answer that has to cross
932/// a datagram in either direction: one fetched over the UDP hop ([`UpstreamTransport::Udp`]), or one
933/// relayed to a [`ClientTransport::Udp`] client, which leaves here as a single UDP datagram whatever
934/// it arrived on. The one combination exempt from it is TCP end to end — an answer
935/// [`ask_upstream`] fetched over the TCP hop ([`UpstreamTransport::Tcp`]) *and* handed to a TCP
936/// client, which is the retry path RFC 1035 §4.2.1 exists to provide and the whole point of the
937/// upstream TCP hop: chopping there at 4095 would hand the stub resolver the same truncated answer
938/// it retried over TCP to escape, and the name would still not resolve. Such an answer is bounded
939/// instead by the two-byte length prefix it arrived under (64 KiB), the same bound
940/// `dns_over_tcp`'s framing and [`crate::peerapi_doh`]'s `MAX_CLIENT_RESPONSE` already impose on the
941/// way back out.
942fn cap_response(
943    query: &[u8],
944    mut resp: Vec<u8>,
945    client: ClientTransport,
946    via: UpstreamTransport,
947) -> Vec<u8> {
948    let crosses_a_datagram = client == ClientTransport::Udp || via == UpstreamTransport::Udp;
949    if crosses_a_datagram && resp.len() > MAX_UPSTREAM_RESPONSE {
950        resp.truncate(MAX_UPSTREAM_RESPONSE);
951        // The header is 12 bytes; the TC bit lives in the second flags byte (header byte 2). A
952        // capped datagram is always >= the header length, but guard anyway to never panic.
953        if let Some(flags_hi) = resp.get_mut(2) {
954            *flags_hi |= 0x02;
955        }
956    }
957    check_response_size_and_set_tc(query, resp, client)
958}
959
960/// The RFC 1035 §4.2.1 maximum size of a DNS message carried over UDP by a requestor that did not
961/// advertise an EDNS(0) buffer size. Go's `defaultUDPSize` in `checkResponseSizeAndSetTC`
962/// (net/dns/resolver/forwarder.go @ 9ea7cba44591e0cd840c6c94d23274dd222059bf).
963///
964/// It is **not** a floor under an advertised size. RFC 6891 §6.2.3 says a value below 512 "MUST be
965/// treated as equal to 512", but Go takes the advertised number verbatim (`maxSize = int(ednsSize)`)
966/// and only reaches for this constant when the request carries no usable OPT record at all. A stub
967/// that advertises 200 is told a 300-byte answer is truncated, and a Rust node on the same tailnet
968/// has to say the same thing.
969const NO_EDNS_UDP_LIMIT: usize = 512;
970
971/// The RR TYPE of an EDNS(0) OPT pseudo-record (RFC 6891 §6.1.2). In an OPT record the CLASS field
972/// is repurposed to carry the requestor's UDP payload size.
973const OPT_RR_TYPE: u16 = 41;
974
975/// Wire size of an EDNS(0) OPT record carrying **no** options: NAME (1 byte, the root label) +
976/// TYPE (2) + CLASS (2) + TTL (4) + RDLEN (2). Go's `optFixedBytes`.
977const OPT_FIXED_BYTES: usize = 11;
978
979/// Set the `TC` (truncated) bit on `resp` when it is larger than the UDP payload size the client's
980/// `query` advertised — the size in its EDNS(0) OPT record, or 512 bytes when it carries none
981/// (RFC 1035). The body is left **intact**: `TC` tells the stub resolver the answer may not fit the
982/// datagram it asked for, so it should retry over TCP; it is not a claim that we chopped anything.
983///
984/// This is Go's `checkResponseSizeAndSetTC` (net/dns/resolver/forwarder.go @
985/// 9ea7cba44591e0cd840c6c94d23274dd222059bf), including its first statement — `if family != "udp"
986/// { return response }`. `client` is that `family`: it names the transport the client **we answer**
987/// used, never the hop we fetched the answer over. A [`ClientTransport::Tcp`] client has no
988/// datagram to overflow (RFC 7766 §8) and setting `TC` for it would only send its resolver into
989/// another retry of a transport that already has no size bound.
990///
991/// It runs on every path that returns an answer to a client, exactly as upstream does: the UDP and
992/// DoH forwards (via [`cap_response`] / `forward_doh`, Go's `forwarder.send`) **and** the answers
993/// this node builds itself (Go calls it in `Resolver.Query` right after `respond` succeeds). The
994/// local path is not exempt: `ts_dns_wire` caps an authoritative response at 512 bytes, which only
995/// bounds it below a *default* client limit — a client that advertised less than that can still be
996/// overflowed by an answer we composed.
997pub(crate) fn check_response_size_and_set_tc(
998    query: &[u8],
999    mut resp: Vec<u8>,
1000    client: ClientTransport,
1001) -> Vec<u8> {
1002    if client == ClientTransport::Tcp {
1003        return resp;
1004    }
1005    // The header is 12 bytes and the TC bit lives in the second flags byte (header byte 2); a
1006    // response shorter than that is not something we can (or need to) mark. Re-setting a bit that
1007    // is already set is a no-op, so upstream's `truncatedFlagSet` early return needs no analogue.
1008    if resp.len() > client_udp_limit(query)
1009        && let Some(flags_hi) = resp.get_mut(2)
1010    {
1011        *flags_hi |= 0x02;
1012    }
1013    resp
1014}
1015
1016/// The largest UDP DNS response `query`'s sender is willing to receive: the EDNS(0) advertised size
1017/// verbatim, or [`NO_EDNS_UDP_LIMIT`] when the query carries no valid OPT record. Go's
1018/// `getEDNSBufferSize` plus the `hasEDNS` branch of `checkResponseSizeAndSetTC`.
1019fn client_udp_limit(query: &[u8]) -> usize {
1020    find_opt_record(query).map_or(NO_EDNS_UDP_LIMIT, usize::from)
1021}
1022
1023/// Return the requestor's UDP payload size from `query`'s EDNS(0) OPT record, or [`None`] when the
1024/// message carries no OPT record this node will act on.
1025///
1026/// A direct port of Go's `findOPTRecord` (net/dns/resolver/forwarder.go @
1027/// 9ea7cba44591e0cd840c6c94d23274dd222059bf), and deliberately as narrow as it is: the OPT record
1028/// must occupy the **final 11 bytes** of the message, and it must have a root NAME, TYPE `OPT`,
1029/// EDNS version 0 and `RDLEN == 0`. Upstream states the restriction outright — "Only OPT records at
1030/// the very end of the message with no option codes are addressed" — and everything else is
1031/// `(0, nil)`, i.e. *no EDNS*, i.e. the 512-byte RFC 1035 limit.
1032///
1033/// That matters far more often than "malformed query" suggests. A query carrying **any** EDNS
1034/// option — a DNS cookie (RFC 7873) or EDNS Client Subnet, both of which real stub resolvers send
1035/// routinely — has `RDLEN != 0`, so upstream ignores the 4096 it advertises and caps the answer at
1036/// 512. Walking the additional section properly and honouring that 4096 would leave `TC` clear on a
1037/// 900-byte reply that every Go node on the tailnet marks truncated, and the two nodes would hand
1038/// the same stub resolver different answers to the same question. Being generous here is the bug.
1039fn find_opt_record(packet: &[u8]) -> Option<u16> {
1040    /// The only EDNS version defined (RFC 6891 §6.1.3). Go: "Be conservative and don't touch
1041    /// unknown versions."
1042    const EDNS0_VERSION: u8 = 0;
1043
1044    if packet.len() < DNS_HEADER_LEN + OPT_FIXED_BYTES {
1045        return None;
1046    }
1047    // OPT lives in the additional section, so no additional records means no OPT.
1048    if u16::from_be_bytes([packet[10], packet[11]]) == 0 {
1049        return None;
1050    }
1051
1052    let opt = &packet[packet.len() - OPT_FIXED_BYTES..];
1053    if opt[0] != 0 {
1054        return None; // NAME must be the root domain (a single zero byte).
1055    }
1056    if u16::from_be_bytes([opt[1], opt[2]]) != OPT_RR_TYPE {
1057        return None;
1058    }
1059    // CLASS is repurposed as the requestor's UDP payload size (RFC 6891 §6.1.2).
1060    let requested_size = u16::from_be_bytes([opt[3], opt[4]]);
1061    // opt[5] is the extended RCODE: ignored, as upstream ignores it.
1062    if opt[6] != EDNS0_VERSION {
1063        return None;
1064    }
1065    // opt[7..9] are the EDNS flags (DO bit and friends): ignored.
1066    if u16::from_be_bytes([opt[9], opt[10]]) != 0 {
1067        return None; // RDLEN must be 0 — the record carries no options.
1068    }
1069    Some(requested_size)
1070}
1071
1072/// The byte length of a fixed DNS header.
1073const DNS_HEADER_LEN: usize = 12;
1074
1075/// Return the byte range of the first question section (QNAME + QTYPE + QCLASS) within `msg`,
1076/// starting just after the 12-byte header. Returns [`None`] if the name is malformed, uses a
1077/// compression pointer (illegal in a question), or runs past the buffer. Used to byte-compare a
1078/// forwarded query's question against the upstream response's question.
1079fn question_range(msg: &[u8]) -> Option<std::ops::Range<usize>> {
1080    let mut off = DNS_HEADER_LEN;
1081    // Walk the QNAME label sequence to the terminating root label (0x00).
1082    loop {
1083        let len = *msg.get(off)? as usize;
1084        // A compression pointer (top two bits set) is not valid in a question section.
1085        if len & 0xC0 != 0 {
1086            return None;
1087        }
1088        off += 1;
1089        if len == 0 {
1090            break; // root label: QNAME complete.
1091        }
1092        off = off.checked_add(len)?;
1093        if off > msg.len() {
1094            return None;
1095        }
1096    }
1097    // QTYPE (2) + QCLASS (2) follow the name.
1098    let end = off.checked_add(4)?;
1099    if end > msg.len() {
1100        return None;
1101    }
1102    Some(DNS_HEADER_LEN..end)
1103}
1104
1105/// Whether `resp` is a plausible DNS response to `query`: same 16-bit transaction id, the QR
1106/// (response) bit set, and a byte-identical question section (QNAME + QTYPE + QCLASS). Both buffers
1107/// carry the DNS header in the first 12 bytes (id at [0..2], flags at [2..4], QR is the high bit of
1108/// byte 2). Used to reject off-path/forged datagrams before relaying them back to the stub resolver
1109/// as authoritative: matching only the id + QR lets an injector that guesses the id swap in an
1110/// answer for a different question, so we also require the echoed question to match.
1111fn response_matches_query(query: &[u8], resp: &[u8]) -> bool {
1112    if query.len() < DNS_HEADER_LEN || resp.len() < DNS_HEADER_LEN {
1113        return false;
1114    }
1115    let id_matches = query[0..2] == resp[0..2];
1116    let is_response = resp[2] & 0x80 != 0;
1117    if !id_matches || !is_response {
1118        return false;
1119    }
1120    // The response must echo the exact question we asked. Parse both question sections and compare
1121    // their bytes; a parse failure on either side is treated as a non-match (fail closed).
1122    match (question_range(query), question_range(resp)) {
1123        (Some(q), Some(r)) => query[q] == resp[r],
1124        _ => false,
1125    }
1126}
1127
1128/// SERVFAIL (RCODE 2): the upstream could not process the query. A *soft* error to a forwarder — the
1129/// name may still resolve through another resolver.
1130const RCODE_SERVFAIL: u8 = 2;
1131/// REFUSED (RCODE 5): the upstream will not answer this query (policy, an ACL, a view it has no
1132/// data for). Soft for the same reason: another resolver may well serve it.
1133const RCODE_REFUSED: u8 = 5;
1134
1135/// The RCODE `msg` carries: the low 4 bits of header byte 3 (RFC 1035 §4.1.1), or `None` when `msg`
1136/// is too short to have a header. The EDNS(0) *extended* RCODE bits an OPT record can add are
1137/// ignored, as they are in [`find_opt_record`] and in Go's forwarder, which reads the 4-bit
1138/// `dnsmessage.Header.RCode`.
1139fn response_rcode(msg: &[u8]) -> Option<u8> {
1140    msg.get(3).map(|b| b & 0x0F)
1141}
1142
1143/// Whether `msg` carries an RCODE a forwarder must treat as a **soft** error — one that means "this
1144/// resolver could not answer", not "here is the answer": [`RCODE_SERVFAIL`] or [`RCODE_REFUSED`].
1145/// See [`forward_query`] for what that changes.
1146fn is_soft_error(msg: &[u8]) -> bool {
1147    matches!(response_rcode(msg), Some(RCODE_SERVFAIL | RCODE_REFUSED))
1148}
1149
1150/// Forward `query` to each upstream in order over the **overlay** netstack, returning the first
1151/// well-formed response that is not a *soft* error, or — once every upstream has failed — the
1152/// failure [`forward_walk`] chooses to relay, which is often the prebuilt `fallback` buffer.
1153///
1154/// Anti-leak: forwarding goes through the overlay netstack `channel` (a fresh `0.0.0.0:0` overlay
1155/// UDP socket per query, and — whenever the TCP hop runs — a fresh `0.0.0.0:0` overlay TCP
1156/// connection to the same resolver), NEVER a host socket: the real origin IP can't leak to the
1157/// resolver, and split-DNS upstreams reachable only over the tailnet/subnet-router work. Each
1158/// upstream is bounded by [`UPSTREAM_TIMEOUT`] per hop. `client` names the transport the *client* we
1159/// answer used, not the one we fetched over — it sets how long the UDP hop runs alone before TCP
1160/// joins it ([`UDP_RACE_TIMEOUT`]) as well as how the answer is capped; `retry` says whether the
1161/// upstream hop may use TCP at all ([`DnsView::upstream_tcp_retry`]).
1162///
1163/// The socket work is all this function does; which response is relayed to the client is
1164/// [`forward_walk`]'s decision.
1165pub(crate) async fn forward_query(
1166    channel: &Channel,
1167    upstreams: &[SocketAddr],
1168    query: &[u8],
1169    fallback: Vec<u8>,
1170    client: ClientTransport,
1171    retry: TcpRetry,
1172) -> Vec<u8> {
1173    forward_walk(upstreams, query, fallback, client, |upstream| {
1174        ask_upstream(channel, upstream, query, client, retry)
1175    })
1176    .await
1177}
1178
1179/// Which transport an upstream answer was fetched over. Not a property of the client we answer
1180/// (that is [`ClientTransport`]) — it is what [`cap_response`] needs in order to know whether the
1181/// answer has already survived a datagram.
1182#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1183enum UpstreamTransport {
1184    /// The UDP hop: one datagram, bounded by the netstack's UDP receive ring.
1185    Udp,
1186    /// The TCP hop: a length-prefixed message, bounded only by its two-byte prefix. Reached only
1187    /// through [`ask_upstream`]'s race: the retry of a truncated UDP answer, or the fallback from a
1188    /// slow or failed UDP hop.
1189    Tcp,
1190}
1191
1192/// One upstream's answer as [`ask_upstream`] hands it to [`forward_walk`]: the bytes, the address
1193/// they arrived from, and the hop they arrived over.
1194struct UpstreamAnswer {
1195    /// Where the answer came from, **unfiltered** — [`forward_walk`] is what checks it against the
1196    /// upstream we asked.
1197    from: SocketAddr,
1198    /// The DNS response bytes, verbatim.
1199    resp: Vec<u8>,
1200    /// The hop that carried them (see [`UpstreamTransport`]).
1201    via: UpstreamTransport,
1202}
1203
1204/// How long the UDP hop has the upstream to itself before the TCP hop joins the race, when the
1205/// client we answer asked over UDP. A client that asked over TCP gets no head start for UDP at all:
1206/// both hops start together.
1207///
1208/// Go's `udpRaceTimeout` (net/dns/resolver/forwarder.go @
1209/// `e2ed432399c9b0fda7aa14e9eb27784d2d893c55`). It is what lets a resolver whose UDP path drops or
1210/// stalls, but whose TCP path answers, still resolve — in about two seconds rather than not at all.
1211/// It is shorter than [`UPSTREAM_TIMEOUT`] on purpose: waiting that out first would spend the whole
1212/// UDP budget before TCP was ever tried.
1213const UDP_RACE_TIMEOUT: Duration = Duration::from_secs(2);
1214
1215/// Ask one `upstream` for `query` over the overlay and return what came back, or `None` when
1216/// nothing usable arrived on either hop (bind, connect, send or receive error, [`UPSTREAM_TIMEOUT`],
1217/// or an empty message).
1218///
1219/// The query is **raced** over UDP and TCP to the same resolver, as Go's forwarder does: `send`
1220/// hands a `firstUDP` and a `thenTCP` closure to `race.New(timeout, firstUDP, thenTCP)`, and
1221/// `util/race`'s `Race.Start` runs the first at once and the second when the timeout elapses or
1222/// the first fails, whichever comes first; the first good answer wins (net/dns/resolver/forwarder.go
1223/// and util/race/race.go @ `e2ed432399c9b0fda7aa14e9eb27784d2d893c55`). [`race_udp_and_tcp`] is
1224/// that race; what it means for one query:
1225///
1226/// - **A slow or failed UDP hop falls back to TCP.** The TCP hop starts after [`UDP_RACE_TIMEOUT`]
1227///   for a [`ClientTransport::Udp`] client, at once for a [`ClientTransport::Tcp`] one, and at once
1228///   whenever the UDP hop has already failed.
1229/// - **A truncated answer is re-asked over TCP**, RFC 1035 §4.2.1. `firstUDP` maps it to an error so
1230///   the race waits for the TCP hop.
1231/// - **The same resolver, never the next one.** Walking on to the next upstream would send the
1232///   query to a resolver the first one already answered — a second party learning a name it was
1233///   never asked about. [`forward_walk`] does the walking; this function only ever talks to the
1234///   `upstream` it was handed.
1235/// - **A failed TCP hop returns the truncated UDP answer**, not `SERVFAIL`. A truncated answer is
1236///   still an answer: its header and question are intact and a stub resolver can act on the `TC`
1237///   bit. Replacing it with a synthesized failure would be strictly worse than what we already had.
1238/// - **A TCP message that does not answer this query is a failed TCP hop**, not an answer. The
1239///   race checks it with [`response_matches_query`] exactly as it checks the datagram, because the
1240///   hop that loses the race is *dropped*: letting a mismatched message win would throw away the
1241///   UDP answer — pending, or held truncated — that [`forward_walk`] would otherwise have relayed,
1242///   and [`forward_walk`] would then discard the mismatch too and resolve nothing.
1243/// - **An answer that fit wins.** Once the UDP hop has one, the TCP hop is dropped (or, before the
1244///   head start runs out, never started) and its answer is never waited for.
1245///
1246/// One place differs from Go, deliberately: Go does **not** re-ask a truncated answer over TCP for
1247/// a UDP client (`firstUDP` returns it as it came "because the client can retry over TCP itself").
1248/// Here it is re-asked for every client, as before, because two of this tree's UDP-labelled clients
1249/// cannot make that retry: the application netstack serves `100.100.100.100:53` over UDP only (the
1250/// `dns_over_tcp` responder is TUN-mode), and `query_dns` is a programmatic caller with no TCP path
1251/// at all. Dropping the retry for them would turn names that resolve today into truncated answers.
1252///
1253/// `retry` is [`DnsView::upstream_tcp_retry`], and [`TcpRetry::Disabled`] shuts the TCP hop out of
1254/// the race entirely — no head-start timer, no fallback, no truncation retry — exactly as Go's
1255/// `thenTCP` waits on the context and never dials under `skipTCP`.
1256///
1257/// Anti-leak: both hops ride the overlay netstack `channel` — the TCP one via `channel.tcp_connect`
1258/// from `0.0.0.0:0`, exactly as the UDP one binds `0.0.0.0:0` there. A host socket would put this
1259/// node's real origin IP in front of the resolver and would not reach a split-DNS upstream that
1260/// only exists inside the tailnet, so the coupling is not a convenience.
1261///
1262/// This is the whole of [`forward_query`]'s I/O, split out from the walk so the policy above it —
1263/// anti-poisoning, the soft-error rules, which response is relayed — is decided (and tested) on
1264/// bytes rather than on sockets. It vouches for nothing about the answer it returns: the source
1265/// address comes back unfiltered precisely so [`forward_walk`] can check it. The checks the race
1266/// does make are not that check moved here — they only decide which hop *wins*, and every answer
1267/// that wins is still handed to [`forward_walk`] to accept or discard on its own terms.
1268async fn ask_upstream(
1269    channel: &Channel,
1270    upstream: SocketAddr,
1271    query: &[u8],
1272    client: ClientTransport,
1273    retry: TcpRetry,
1274) -> Option<UpstreamAnswer> {
1275    race_udp_and_tcp(
1276        upstream,
1277        query,
1278        client,
1279        retry,
1280        || ask_upstream_udp(channel, upstream, query),
1281        || ask_upstream_tcp(channel, upstream, query),
1282    )
1283    .await
1284}
1285
1286/// What the UDP hop produced, as the race reads it.
1287enum UdpOutcome {
1288    /// A good answer: it wins the race.
1289    Answer(UpstreamAnswer),
1290    /// A genuine answer that did not fit the datagram ([`did_not_fit_the_datagram`]): the race
1291    /// waits for the TCP hop, and falls back to this if that hop fails.
1292    Truncated(UpstreamAnswer),
1293    /// A datagram from the wrong source, or one that does not echo the question we asked. It is
1294    /// not an answer, and it does not start the TCP hop either — see [`race_udp_and_tcp`].
1295    Unmatched(UpstreamAnswer),
1296    /// Nothing usable came back.
1297    Failed,
1298}
1299
1300/// Read the UDP hop's `(source address, datagram)` as a [`UdpOutcome`].
1301fn classify_udp(
1302    upstream: SocketAddr,
1303    query: &[u8],
1304    got: Option<(SocketAddr, Vec<u8>)>,
1305) -> UdpOutcome {
1306    let Some((from, resp)) = got else {
1307        return UdpOutcome::Failed;
1308    };
1309    let answer = UpstreamAnswer {
1310        from,
1311        resp,
1312        via: UpstreamTransport::Udp,
1313    };
1314    // This repeats `forward_walk`'s check rather than moving it — the answer still goes back
1315    // unfiltered — so that an off-path injector cannot conscript this node into connecting to a
1316    // resolver by spraying forged `TC` datagrams it could never have matched the question of.
1317    if answer.from.ip() != upstream.ip() || !response_matches_query(query, &answer.resp) {
1318        return UdpOutcome::Unmatched(answer);
1319    }
1320    if did_not_fit_the_datagram(&answer.resp) {
1321        UdpOutcome::Truncated(answer)
1322    } else {
1323        UdpOutcome::Answer(answer)
1324    }
1325}
1326
1327/// The race [`ask_upstream`] runs, over the two hops as plain futures: `udp` returns the
1328/// `(source address, datagram)` of the UDP hop, `tcp` the message body of the TCP hop. `tcp` is
1329/// only called when the TCP hop actually starts, so "was a TCP connection opened" is "was `tcp`
1330/// called".
1331///
1332/// The shape is Go's `Race.Start`, with the UDP hop's result classified as `firstUDP` classifies
1333/// it:
1334///
1335/// 1. The UDP hop starts at once. For a [`ClientTransport::Udp`] client it runs alone for
1336///    [`UDP_RACE_TIMEOUT`]; if it settles inside that window, its answer is returned, except that a
1337///    **failed** hop starts TCP straight away (Go closes `startFallback`) and a **truncated** one
1338///    does too (Go's `truncatedResponseError`).
1339/// 2. Otherwise — the head start ran out, or the client asked over TCP and there is none — the TCP
1340///    hop starts and both run. The first good answer wins; a hop that fails leaves the race to the
1341///    other; a truncated UDP answer is held and returned only if TCP does not produce one.
1342///
1343/// A datagram that does not match the query ([`UdpOutcome::Unmatched`]) is the one divergence in
1344/// how a failed UDP hop is treated. Go's `sendUDP` turns a transaction-id mismatch into an error,
1345/// which starts the TCP hop at once; here it never *starts* the TCP hop, so a forged datagram buys
1346/// no connection that silence would not have bought anyway. If the TCP hop is already running it
1347/// is still waited for, and if it is not the datagram is handed on for [`forward_walk`] to discard,
1348/// as it always was.
1349///
1350/// Split from the sockets for the same reason [`forward_walk`] is: the decision — when TCP starts,
1351/// which answer wins — is the part that can regress silently, and here it is testable without a
1352/// netstack.
1353async fn race_udp_and_tcp<UdpFut, TcpFut>(
1354    upstream: SocketAddr,
1355    query: &[u8],
1356    client: ClientTransport,
1357    retry: TcpRetry,
1358    udp: impl FnOnce() -> UdpFut,
1359    tcp: impl FnOnce() -> TcpFut,
1360) -> Option<UpstreamAnswer>
1361where
1362    UdpFut: std::future::Future<Output = Option<(SocketAddr, Vec<u8>)>>,
1363    TcpFut: std::future::Future<Output = Option<Vec<u8>>>,
1364{
1365    // The TCP hop's message as an answer — or `None` when it is not one. A message that does not
1366    // echo this query is no more use than no message at all: `forward_walk` discards it either
1367    // way. The difference is what it costs on the way, because the hop that loses this race is
1368    // dropped unfinished: let a mismatched message win and the UDP answer it beat is gone, and a
1369    // query that would have resolved resolves as SERVFAIL. So it is scored as a failed hop, which
1370    // leaves the race to UDP — pending, or held truncated — exactly as a connect error would.
1371    let over_tcp = |resp: Vec<u8>| {
1372        if !response_matches_query(query, &resp) {
1373            tracing::debug!(%upstream, "magic dns dropping mismatched tcp response");
1374            return None;
1375        }
1376        Some(UpstreamAnswer {
1377            // The TCP peer is the resolver we connected to, by construction of the handshake, so
1378            // the source address is `upstream` itself and `forward_walk`'s source check passes on
1379            // a fact rather than on a claim in a datagram header.
1380            from: upstream,
1381            resp,
1382            via: UpstreamTransport::Tcp,
1383        })
1384    };
1385
1386    let udp = udp();
1387    if retry == TcpRetry::Disabled {
1388        // Control said no (`dns-forwarder-disable-tcp-retries`): the TCP hop never runs, on any
1389        // arm, so whatever UDP gives us — truncated, failed or late — is the whole answer.
1390        return udp.await.map(|(from, resp)| UpstreamAnswer {
1391            from,
1392            resp,
1393            via: UpstreamTransport::Udp,
1394        });
1395    }
1396    let mut udp = std::pin::pin!(udp);
1397
1398    // Step 1: the UDP hop's head start. A TCP client gets none.
1399    if client == ClientTransport::Udp {
1400        let settled = tokio::select! {
1401            biased;
1402            got = &mut udp => Some(classify_udp(upstream, query, got)),
1403            () = tokio::time::sleep(UDP_RACE_TIMEOUT) => None,
1404        };
1405        match settled {
1406            Some(UdpOutcome::Answer(answer) | UdpOutcome::Unmatched(answer)) => {
1407                return Some(answer);
1408            }
1409            Some(UdpOutcome::Truncated(truncated)) => {
1410                tracing::debug!(%upstream, "magic dns upstream answer truncated, retrying over tcp");
1411                return Some(tcp().await.and_then(over_tcp).unwrap_or(truncated));
1412            }
1413            Some(UdpOutcome::Failed) => {
1414                tracing::debug!(%upstream, "magic dns upstream udp hop failed, trying tcp");
1415                return tcp().await.and_then(over_tcp);
1416            }
1417            None => {
1418                tracing::debug!(%upstream, "magic dns upstream udp hop slow, racing tcp");
1419            }
1420        }
1421    }
1422
1423    // Step 2: both hops run; the first good answer wins.
1424    let mut tcp = std::pin::pin!(tcp());
1425    tokio::select! {
1426        biased;
1427        got = &mut udp => match classify_udp(upstream, query, got) {
1428            UdpOutcome::Answer(answer) => Some(answer),
1429            UdpOutcome::Truncated(held) | UdpOutcome::Unmatched(held) => {
1430                Some(tcp.await.and_then(over_tcp).unwrap_or_else(|| {
1431                    tracing::debug!(
1432                        %upstream,
1433                        "magic dns upstream tcp hop failed, relaying the udp answer"
1434                    );
1435                    held
1436                }))
1437            }
1438            UdpOutcome::Failed => tcp.await.and_then(over_tcp),
1439        },
1440        got = &mut tcp => match got.and_then(over_tcp) {
1441            Some(answer) => Some(answer),
1442            // The TCP hop failed first — or answered something we did not ask: the race is the UDP
1443            // hop's, whatever it brings — including a truncated answer, which Go returns in place
1444            // of the error.
1445            None => udp.await.map(|(from, resp)| UpstreamAnswer {
1446                from,
1447                resp,
1448                via: UpstreamTransport::Udp,
1449            }),
1450        },
1451    }
1452}
1453/// Whether `msg` is an upstream answer that did not fit the datagram it came in — the thing the TCP
1454/// retry exists for. Two ways to be one, and Go's forwarder retries on both:
1455///
1456/// - The resolver said so: the `TC` bit, header byte 2, bit `0x02` (RFC 1035 §4.1.1).
1457/// - It is longer than this forwarder will relay in one datagram ([`MAX_UPSTREAM_RESPONSE`]), so
1458///   [`cap_response`] is about to cut it and set `TC` itself. That is the same "did not fit" Go
1459///   detects by reading `sendUDP` into a `maxResponseBytes+1` buffer and then setting `TC` on the
1460///   reply it returns — which is the very bit its caller then tests. Retrying on it here keeps the
1461///   one deliverable oversize datagram the netstack's 4096-byte receive ring can hand us from being
1462///   a name that resolves only as a cut answer.
1463///
1464/// A message too short to hold a header is neither — it is garbage, and [`forward_walk`] discards
1465/// it.
1466fn did_not_fit_the_datagram(msg: &[u8]) -> bool {
1467    msg.len() > MAX_UPSTREAM_RESPONSE || msg.get(2).is_some_and(|flags_hi| flags_hi & 0x02 != 0)
1468}
1469
1470/// The UDP hop: one `0.0.0.0:0` overlay socket, one datagram out, the first datagram back as
1471/// `(source address, bytes)`. `None` on bind/send/recv failure, [`UPSTREAM_TIMEOUT`], or an empty
1472/// datagram.
1473async fn ask_upstream_udp(
1474    channel: &Channel,
1475    upstream: SocketAddr,
1476    query: &[u8],
1477) -> Option<(SocketAddr, Vec<u8>)> {
1478    let socket = match channel
1479        .udp_bind(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)))
1480        .await
1481    {
1482        Ok(s) => s,
1483        Err(e) => {
1484            tracing::warn!(error = %e, %upstream, "magic dns upstream bind failed");
1485            return None;
1486        }
1487    };
1488
1489    if let Err(e) = socket.send_to(upstream, query).await {
1490        tracing::warn!(error = %e, %upstream, "magic dns upstream send failed");
1491        return None;
1492    }
1493
1494    match timeout(UPSTREAM_TIMEOUT, socket.recv_from_bytes()).await {
1495        Ok(Ok((from, resp))) if !resp.is_empty() => Some((from, resp.to_vec())),
1496        Ok(Ok(_)) => None,
1497        Ok(Err(e)) => {
1498            tracing::warn!(error = %e, %upstream, "magic dns upstream recv failed");
1499            None
1500        }
1501        Err(_) => {
1502            tracing::debug!(%upstream, "magic dns upstream timed out");
1503            None
1504        }
1505    }
1506}
1507
1508/// The TCP hop: connect to `upstream` over the overlay from `0.0.0.0:0` and exchange one
1509/// length-prefixed message (RFC 1035 §4.2.2). `None` on any failure — connect refused, framing
1510/// error, EOF, or [`UPSTREAM_TIMEOUT`] over the whole exchange — which leaves [`race_udp_and_tcp`]
1511/// with whatever the UDP hop brings, a truncated answer included.
1512///
1513/// The timeout covers connect **and** transfer together, so a resolver that accepts the connection
1514/// and then stalls cannot hold the forward open for longer than a resolver that never answers at
1515/// all.
1516async fn ask_upstream_tcp(
1517    channel: &Channel,
1518    upstream: SocketAddr,
1519    query: &[u8],
1520) -> Option<Vec<u8>> {
1521    let local = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0));
1522    let exchange = async {
1523        let mut stream = channel
1524            .tcp_connect(local, upstream)
1525            .await
1526            .map_err(|e| std::io::Error::other(e.to_string()))?;
1527        tcp_exchange(&mut stream, query).await
1528    };
1529
1530    match timeout(UPSTREAM_TIMEOUT, exchange).await {
1531        Ok(Ok(resp)) if !resp.is_empty() => Some(resp),
1532        Ok(Ok(_)) => None,
1533        Ok(Err(e)) => {
1534            tracing::warn!(error = %e, %upstream, "magic dns upstream tcp retry failed");
1535            None
1536        }
1537        Err(_) => {
1538            tracing::debug!(%upstream, "magic dns upstream tcp retry timed out");
1539            None
1540        }
1541    }
1542}
1543
1544/// Write `query` and read one response on `stream`, both under the two-byte big-endian length
1545/// prefix DNS-over-TCP frames messages with (RFC 1035 §4.2.2).
1546///
1547/// Generic over the stream so the framing that actually ships is the framing the tests exercise,
1548/// without standing up a netstack — the same reason `dns_over_tcp::serve_conn`, which is this
1549/// framing read from the other end, is generic.
1550///
1551/// The prefix and the query go out in **one** write, as RFC 7766 §8 asks, so the length does not
1552/// leave as its own two-byte segment ahead of the body. The response allocation is bounded by the
1553/// prefix itself: at most 64 KiB, which is every DNS message that can be framed at all.
1554async fn tcp_exchange<S>(stream: &mut S, query: &[u8]) -> std::io::Result<Vec<u8>>
1555where
1556    S: AsyncRead + AsyncWrite + Unpin,
1557{
1558    let len = u16::try_from(query.len()).map_err(|_| {
1559        std::io::Error::new(
1560            std::io::ErrorKind::InvalidInput,
1561            "query too long to frame over tcp",
1562        )
1563    })?;
1564    let mut framed = Vec::with_capacity(2 + query.len());
1565    framed.extend_from_slice(&len.to_be_bytes());
1566    framed.extend_from_slice(query);
1567    stream.write_all(&framed).await?;
1568    stream.flush().await?;
1569
1570    let mut len_buf = [0u8; 2];
1571    stream.read_exact(&mut len_buf).await?;
1572    let len = usize::from(u16::from_be_bytes(len_buf));
1573    if len == 0 {
1574        return Err(std::io::Error::new(
1575            std::io::ErrorKind::InvalidData,
1576            "zero-length dns response over tcp",
1577        ));
1578    }
1579    let mut resp = vec![0u8; len];
1580    stream.read_exact(&mut resp).await?;
1581    Ok(resp)
1582}
1583
1584/// Walk `upstreams` in order, asking `ask` for each one's answer, and decide which response the
1585/// client gets. [`forward_query`] is the only caller; `ask` is its overlay socket exchange
1586/// ([`ask_upstream`]).
1587///
1588/// **REFUSED and SERVFAIL are soft errors, not answers** ([`is_soft_error`]). An upstream that
1589/// answers `REFUSED` (RCODE 5) or `SERVFAIL` (RCODE 2) does not end the forward: the walk continues
1590/// to the next upstream, and how the first failure looked is remembered for when the list is
1591/// exhausted with nothing better. Otherwise a broken or misconfigured resolver that refuses
1592/// instantly beats a healthy one still doing the work, and the stub resolver is handed the refusal
1593/// as though it were the answer — complete DNS failure exactly where a split-DNS route or a
1594/// fallback list names more than one resolver, which is the shape control commonly pushes. Every
1595/// other RCODE — including NXDOMAIN, which is a real answer — ends the walk on the spot.
1596///
1597/// The caller supplies `fallback` (a SERVFAIL response for a forwarded off-tailnet name — an
1598/// all-upstream failure is a soft "couldn't resolve", not a cacheable non-existence, matching Go
1599/// forwarder.go:1297-1307). Keeping it caller-supplied means this fn is rcode-agnostic. When every
1600/// upstream failed, the client gets an upstream's own bytes — which can carry an RFC 8914 extended
1601/// DNS error saying *why* it failed — in exactly the two cases Go's `forwardWithDestChan` relays
1602/// them (net/dns/resolver/forwarder.go @ e2ed432399c9b0fda7aa14e9eb27784d2d893c55, `firstErr` /
1603/// `sawNonRefused`):
1604///
1605/// - **every** upstream answered REFUSED: the first refusal is relayed;
1606/// - otherwise, the first failure is relayed only if it was itself a SERVFAIL answer.
1607///
1608/// Anything else gets `fallback`. So a REFUSED followed by a SERVFAIL, or by an upstream that never
1609/// answered, is a SERVFAIL — the refusal describes one resolver's policy, not why the forward as a
1610/// whole failed. "Never answered" is every upstream that timed out, errored, or only ever sent
1611/// datagrams the anti-poisoning check discarded; it counts as a failure that is not a refusal, as
1612/// Go's transport errors (and its transaction-id mismatch) do.
1613///
1614/// Every relayed response goes through [`cap_response`], which caps it at [`MAX_UPSTREAM_RESPONSE`]
1615/// unless it was TCP end to end, and — for a [`ClientTransport::Udp`] client — marks it truncated
1616/// when it exceeds what `query` advertised it can receive. That is why an [`UpstreamAnswer`] carries
1617/// the hop it arrived over all the way to here: the walk never inspects it, it only hands it on.
1618async fn forward_walk<F, Fut>(
1619    upstreams: &[SocketAddr],
1620    query: &[u8],
1621    fallback: Vec<u8>,
1622    client: ClientTransport,
1623    mut ask: F,
1624) -> Vec<u8>
1625where
1626    F: FnMut(SocketAddr) -> Fut,
1627    Fut: std::future::Future<Output = Option<UpstreamAnswer>>,
1628{
1629    /// How the first failing upstream failed: Go's `firstErr`.
1630    enum FirstFailure {
1631        Refused(UpstreamAnswer),
1632        ServFail(UpstreamAnswer),
1633        NoAnswer,
1634    }
1635
1636    let mut first_failure: Option<FirstFailure> = None;
1637    // Whether any upstream failed other than by answering REFUSED: Go's `sawNonRefused`.
1638    let mut saw_non_refused = false;
1639
1640    for upstream in upstreams {
1641        let Some(answer) = ask(*upstream).await else {
1642            first_failure.get_or_insert(FirstFailure::NoAnswer);
1643            saw_non_refused = true;
1644            continue;
1645        };
1646        let UpstreamAnswer { from, resp, via } = answer;
1647
1648        // Anti-poisoning: only accept a datagram that came from the upstream we queried and whose
1649        // DNS header matches this query (same transaction id, QR=response bit set). An off-path
1650        // injector racing the real answer is otherwise relayed straight back to the stub resolver
1651        // as authoritative — and this check runs FIRST, so an injected refusal is not even eligible
1652        // to become the soft error a fully-refused forward ends up relaying.
1653        if from.ip() != upstream.ip() || !response_matches_query(query, &resp) {
1654            tracing::debug!(%upstream, %from, "magic dns dropping unsolicited/mismatched response");
1655            first_failure.get_or_insert(FirstFailure::NoAnswer);
1656            saw_non_refused = true;
1657            continue;
1658        }
1659
1660        // A soft error (REFUSED/SERVFAIL) is not an answer: note how the first failure looked and
1661        // give the remaining upstreams their turn.
1662        if is_soft_error(&resp) {
1663            tracing::debug!(
1664                %upstream,
1665                rcode = response_rcode(&resp),
1666                "magic dns upstream soft error, trying the next upstream"
1667            );
1668            let answer = UpstreamAnswer { from, resp, via };
1669            if response_rcode(&answer.resp) == Some(RCODE_REFUSED) {
1670                first_failure.get_or_insert(FirstFailure::Refused(answer));
1671            } else {
1672                first_failure.get_or_insert(FirstFailure::ServFail(answer));
1673                saw_non_refused = true;
1674            }
1675            continue;
1676        }
1677
1678        return cap_response(query, resp, client, via);
1679    }
1680
1681    // Nothing better arrived. Relay an upstream's own bytes (extended DNS error and all) only where
1682    // Go does: the first refusal when every upstream refused, or a first failure that was itself a
1683    // SERVFAIL. A refusal mixed with any other failure, or nobody answering, is `fallback`.
1684    match first_failure {
1685        Some(FirstFailure::Refused(answer)) if !saw_non_refused => {
1686            cap_response(query, answer.resp, client, answer.via)
1687        }
1688        Some(FirstFailure::ServFail(answer)) => {
1689            cap_response(query, answer.resp, client, answer.via)
1690        }
1691        _ => fallback,
1692    }
1693}
1694
1695/// Run the receive/answer loop for the bound socket until it (or the netstack) goes away.
1696///
1697/// Authoritative answers are sent inline. Forwarded queries are handled on spawned tasks (each
1698/// cloning the overlay `channel`) so a slow upstream never blocks other queries.
1699async fn serve(
1700    socket: netstack::netsock::UdpSocket,
1701    rx: watch::Receiver<Arc<DnsView>>,
1702    channel: Channel,
1703) {
1704    let socket = Arc::new(socket);
1705    let mut forwards = JoinSet::new();
1706    // Bounds concurrent in-flight forwards (see `MAX_INFLIGHT_FORWARDS`); a permit is held for the
1707    // lifetime of each spawned forward task and released on completion.
1708    let inflight = Arc::new(Semaphore::new(MAX_INFLIGHT_FORWARDS));
1709    loop {
1710        let (src, buf) = match socket.recv_from_bytes().await {
1711            Ok(pkt) => pkt,
1712            Err(e) => {
1713                tracing::warn!(error = %e, "magic dns socket recv failed, stopping responder");
1714                return;
1715            }
1716        };
1717
1718        // Read the freshest view per packet.
1719        let view = rx.borrow().clone();
1720
1721        match decide(&view, &buf) {
1722            // Malformed query: drop silently.
1723            None => continue,
1724            Some(Decision::Reply(resp)) => {
1725                // Upstream runs the same size check on a locally-composed answer as on a forwarded
1726                // one (Go `Resolver.Query` calls `checkResponseSizeAndSetTC` right after `respond`).
1727                // An authoritative answer is capped at 512 bytes, but a client that advertised less
1728                // than that is still owed the `TC` bit.
1729                let resp = check_response_size_and_set_tc(&buf, resp, ClientTransport::Udp);
1730                if let Err(e) = socket.send_to(src, &resp).await {
1731                    tracing::warn!(error = %e, %src, "magic dns response send failed");
1732                }
1733            }
1734            Some(Decision::Forward {
1735                upstreams,
1736                query,
1737                servfail,
1738                recursive,
1739            }) => {
1740                // A recursive forward is eligible for exit-node DoH delegation; a split-DNS route
1741                // always stays on its configured upstreams. Decide the plan against the current
1742                // view so a query routed while an exit node is active egresses from that exit node.
1743                let plan = forward_plan(&view, upstreams, recursive);
1744                // Fail closed at the in-flight cap: drop the query (the stub resolver retries or
1745                // times out) rather than spawn an unbounded task that pins an overlay socket for up
1746                // to UPSTREAM_TIMEOUT. The permit is moved into the task as a named `_permit` binding
1747                // (NOT `let _ =`, which would drop it immediately) so it is released only when the
1748                // task body completes.
1749                let Ok(permit) = inflight.clone().try_acquire_owned() else {
1750                    tracing::warn!(
1751                        %src,
1752                        max = MAX_INFLIGHT_FORWARDS,
1753                        "magic dns drop: at max in-flight forwarded queries"
1754                    );
1755                    continue;
1756                };
1757                let socket = socket.clone();
1758                let channel = channel.clone();
1759                // Read the retry switch off the same view this query was decided against, before
1760                // the spawn takes the query away from it.
1761                let retry = view.upstream_tcp_retry();
1762                forwards.spawn(async move {
1763                    let _permit = permit;
1764                    let resp = match plan {
1765                        RecursivePlan::Udp(upstreams) => {
1766                            forward_query(
1767                                &channel,
1768                                &upstreams,
1769                                &query,
1770                                servfail,
1771                                ClientTransport::Udp,
1772                                retry,
1773                            )
1774                            .await
1775                        }
1776                        RecursivePlan::Doh(doh_addr) => {
1777                            crate::peerapi_doh::forward_doh(
1778                                &channel,
1779                                doh_addr,
1780                                &query,
1781                                servfail,
1782                                ClientTransport::Udp,
1783                            )
1784                            .await
1785                        }
1786                    };
1787                    if let Err(e) = socket.send_to(src, &resp).await {
1788                        tracing::warn!(error = %e, %src, "magic dns forwarded response send failed");
1789                    }
1790                });
1791            }
1792        }
1793
1794        // Reap finished forward tasks without blocking. The unreaped completed-handle backlog is
1795        // bounded by MAX_INFLIGHT_FORWARDS (a task spawns only after acquiring a permit, and there
1796        // are at most that many), so this bounds JoinSet memory too — not just the reap cadence.
1797        while forwards.try_join_next().is_some() {}
1798    }
1799}
1800
1801/// The MagicDNS responder actor.
1802///
1803/// Subscribes to control state (for the DNS config + self node) and peer state (for the peer
1804/// database), keeping a [`DnsView`] that the spawned answer loop reads for every query.
1805///
1806/// The peerAPI server task it owns also needs the live packet filter for its DoH source gate. That
1807/// one does **not** arrive on the bus: the `Args` carry a
1808/// [`LiveFilterRx`](crate::packetfilter::LiveFilterRx) written by the packet-filter updater itself,
1809/// because a fail-closed gate cannot use a lossy transport — see that alias for the two ways the bus
1810/// loses a filter, and what each one costs the gate.
1811pub struct MagicDnsActor {
1812    /// Keeps the socket-serving task alive for the lifetime of the actor.
1813    _joinset: JoinSet<()>,
1814    /// The latest view, shared with the answer loop.
1815    view_tx: watch::Sender<Arc<DnsView>>,
1816    /// The runtime [`Env`], retained so each view rebuild (the `StateUpdate` / `PeerState` handlers)
1817    /// can re-read the live [`Env::accept_dns`] cell. Unlike `enable_ipv6` (snapshotted once at
1818    /// spawn), `accept_dns` is runtime-settable via `Device::set_accept_dns`, so it must be read at
1819    /// rebuild time — not captured once — for a toggle to reach the served view.
1820    env: Env,
1821    /// The overlay channel, retained so the [`Query`] handler can run a query through the same
1822    /// forward path the serve loop uses ([`forward_query`] / [`forward_doh`], both binding
1823    /// `0.0.0.0:0` on this channel — never a host socket).
1824    channel: Channel,
1825}
1826
1827/// A programmatic DNS query routed through the live MagicDNS responder (the `100.100.100.100` path),
1828/// for [`Device::query_dns`](crate::Device::query_dns). The handler synthesizes a query packet and
1829/// drives it through the exact same [`decide`]/forward logic as an on-the-wire query, so the result
1830/// (and its anti-leak posture) matches what a tailnet client would observe.
1831pub struct Query {
1832    /// The canonical name to resolve (e.g. `example.com`, no trailing dot).
1833    pub name: String,
1834    /// The DNS query type (`1`=A, `28`=AAAA, `12`=PTR, or any other RFC 1035 TYPE).
1835    pub qtype: u16,
1836}
1837
1838/// The outcome of a `Query`: the raw DNS response bytes, the RCODE, and which upstream resolvers
1839/// (if any) were consulted. The response is returned as raw bytes (matching Go `LocalClient.QueryDNS`)
1840/// rather than parsed records — this fork's wire codec has no answer-record decoder.
1841///
1842/// (`Query` is the crate-internal actor message; not linked here as it is a private item — a
1843/// `pub` doc cannot intra-doc-link to it without erroring under the doc-lint gate.)
1844#[derive(Debug, Clone, kameo::Reply)]
1845pub struct DnsQueryResult {
1846    /// The raw DNS response datagram (header + question + any answer records).
1847    pub response: Vec<u8>,
1848    /// The RCODE from the response header's low 4 bits (`0`=NoError, `2`=SERVFAIL, `3`=NXDOMAIN,
1849    /// `5`=Refused, …).
1850    pub rcode: u8,
1851    /// The upstream resolver(s) the query was forwarded to. For a UDP forward this is the candidate
1852    /// list tried in order (the forwarder returns on the first that answers); for an exit-node DoH
1853    /// forward it is the single DoH endpoint. Empty for a locally-answered query (an authoritative
1854    /// tailnet name, a NODATA, or a fail-closed NXDOMAIN — nothing egressed).
1855    pub resolvers_consulted: Vec<SocketAddr>,
1856}
1857
1858impl kameo::Actor for MagicDnsActor {
1859    type Args = (Env, Channel, crate::packetfilter::LiveFilterRx);
1860    type Error = Error;
1861
1862    /// `filter_rx` is the live compiled packet filter for the peerAPI DoH source gate
1863    /// (`peerapi_doh::dns_source_allowed`, Go `isPeerAPIDNSAllowed`), handed in from
1864    /// `Runtime::spawn` rather than subscribed to here. It is deliberately not part of [`DnsView`]:
1865    /// it is not DNS data and it updates on its own cadence (a netmap can carry a new filter without
1866    /// a new DNS config, and the other way round).
1867    async fn on_start(
1868        (env, channel, filter_rx): Self::Args,
1869        slf: ActorRef<Self>,
1870    ) -> Result<Self, Self::Error> {
1871        env.subscribe::<Arc<ts_control::StateUpdate>>(&slf).await?;
1872        env.subscribe::<Arc<PeerState>>(&slf).await?;
1873        env.subscribe::<crate::route_updater::ActiveExitNode>(&slf)
1874            .await?;
1875
1876        // Seed the view with the runtime's IPv6 gate (default off) and the current accept-dns value.
1877        // Subsequent control/peer updates clone-and-modify this view: `enable_ipv6` (set once here)
1878        // is preserved, while `accept_dns` is re-read live from `Env` on every rebuild (it is
1879        // runtime-settable). The seed value is moot — no query is served before the first
1880        // StateUpdate — but seeding it keeps the pre-update view internally consistent.
1881        let (view_tx, view_rx) = watch::channel(Arc::new(DnsView {
1882            enable_ipv6: env.enable_ipv6,
1883            accept_dns: env.accept_dns(),
1884            ..DnsView::default()
1885        }));
1886
1887        let mut joinset = JoinSet::new();
1888
1889        // Bind the MagicDNS socket. If the bind fails we still start (fail closed: the actor just
1890        // never answers anything) so a transient bind error doesn't take down the runtime.
1891        let addr = SocketAddr::from((MAGIC_DNS_IP, MAGIC_DNS_PORT));
1892        match channel.udp_bind(addr).await {
1893            Ok(socket) => {
1894                tracing::debug!(%addr, "magic dns responder bound");
1895                joinset.spawn(serve(socket, view_rx.clone(), channel.clone()));
1896            }
1897            Err(e) => {
1898                tracing::error!(error = %e, %addr, "magic dns udp bind failed; responder inert");
1899            }
1900        }
1901
1902        // When this node advertises a peerAPI port, run the single peerAPI server on the same shared
1903        // view. It routes `/dns-query` to the exit-node DoH handler (recursive resolution gated by
1904        // `forward_exit_egress`, see `peerapi_doh`) and `/v0/put/<name>` to the Taildrop receive
1905        // handler when a store is configured (access-gated, fail-closed, see `peerapi`).
1906        if let Some(port) = env.peerapi_port {
1907            let channel = channel.clone();
1908            let view_rx = view_rx.clone();
1909            let forward_exit_egress = env.forward_exit_egress;
1910            let taildrop = env.taildrop_store.clone();
1911            let funnel_ingress = env.funnel_ingress.clone();
1912            joinset.spawn(crate::peerapi::serve(
1913                channel,
1914                port,
1915                view_rx,
1916                filter_rx,
1917                forward_exit_egress,
1918                taildrop,
1919                funnel_ingress,
1920            ));
1921        }
1922
1923        Ok(Self {
1924            _joinset: joinset,
1925            view_tx,
1926            env,
1927            channel,
1928        })
1929    }
1930}
1931
1932/// A bare SERVFAIL response header for a [`Query`] whose name could not be encoded into a
1933/// well-formed query (a non-ASCII label or an over-255-byte name). A 12-byte header with QR=1 (this
1934/// is a response) and RCODE=2 (server failure); no question or answer section (we never produced a
1935/// parseable question). Lets `query_dns` return a definite, honest RCODE instead of an empty buffer
1936/// that would read back as a fabricated NoError.
1937fn servfail_response() -> Vec<u8> {
1938    let mut resp = vec![0u8; 12];
1939    // Flags: QR=1 (byte 2, 0x80) + RCODE=2 (low nibble of byte 3). All other bits clear.
1940    resp[2] = 0x80;
1941    resp[3] = 0x02;
1942    resp
1943}
1944
1945impl Message<Query> for MagicDnsActor {
1946    type Reply = DnsQueryResult;
1947
1948    async fn handle(&mut self, query: Query, _ctx: &mut Context<Self, Self::Reply>) -> Self::Reply {
1949        // Synthesize a query packet and drive it through the SAME decide/forward path the serve loop
1950        // uses, against the freshest view — so the result and its anti-leak posture exactly match an
1951        // on-the-wire query. The id is fixed (0): a programmatic query has no concurrent-demux need,
1952        // and `response_matches_query` validates the echoed id against this same buffer.
1953        //
1954        // Normalize the name into labels: strip a single trailing dot (an FQDN's root marker — Go's
1955        // `dnsname.ToFQDN` does the same) and drop empty labels. An empty label would otherwise encode
1956        // as a lone `0x00`, identical to the QNAME root terminator, truncating the wire query and
1957        // corrupting the QTYPE/QCLASS that follow.
1958        let trimmed = query.name.strip_suffix('.').unwrap_or(&query.name);
1959        let labels: Vec<String> = trimmed
1960            .split('.')
1961            .filter(|label| !label.is_empty())
1962            .map(str::to_owned)
1963            .collect();
1964        let qtype = match query.qtype {
1965            1 => ts_dns_wire::QType::A,
1966            28 => ts_dns_wire::QType::Aaaa,
1967            12 => ts_dns_wire::QType::Ptr,
1968            other => ts_dns_wire::QType::Other(other),
1969        };
1970        // Class IN (1) — the only class the responder serves authoritatively (a non-IN class still
1971        // forwards via `forward_or_nodata`, matching the on-the-wire path).
1972        let buf = ts_dns_wire::encode_query(0, &ts_dns_wire::Name(labels), &qtype, 1);
1973
1974        let view = self.view_tx.borrow().clone();
1975
1976        let (response, resolvers_consulted) = match decide(&view, &buf) {
1977            // `decide` returns `None` only when `decode_query` rejects the buffer we just built. With
1978            // the name normalized above that can still happen for a name `encode_query` accepts but
1979            // `decode_query` rejects — a non-ASCII/IDN label (the caller must pass punycode) or a name
1980            // whose wire form exceeds 255 bytes. Surface a SERVFAIL (RCODE 2: "could not process")
1981            // rather than an empty buffer that would read back as a fabricated NoError. The serve loop
1982            // silently drops here (the on-wire client times out); a programmatic caller gets a
1983            // definite, honest error instead.
1984            None => (servfail_response(), Vec::new()),
1985            Some(Decision::Reply(resp)) => (
1986                check_response_size_and_set_tc(&buf, resp, ClientTransport::Udp),
1987                Vec::new(),
1988            ),
1989            Some(Decision::Forward {
1990                upstreams,
1991                query,
1992                servfail,
1993                recursive,
1994            }) => {
1995                let plan = forward_plan(&view, upstreams, recursive);
1996                match plan {
1997                    RecursivePlan::Udp(upstreams) => {
1998                        let resp = forward_query(
1999                            &self.channel,
2000                            &upstreams,
2001                            &query,
2002                            servfail,
2003                            ClientTransport::Udp,
2004                            view.upstream_tcp_retry(),
2005                        )
2006                        .await;
2007                        (resp, upstreams)
2008                    }
2009                    RecursivePlan::Doh(doh_addr) => {
2010                        let resp = crate::peerapi_doh::forward_doh(
2011                            &self.channel,
2012                            doh_addr,
2013                            &query,
2014                            servfail,
2015                            ClientTransport::Udp,
2016                        )
2017                        .await;
2018                        // The query egressed via the exit node's DoH endpoint, not a local UDP
2019                        // upstream — report the DoH address as the resolver consulted.
2020                        (resp, vec![doh_addr])
2021                    }
2022                }
2023            }
2024        };
2025
2026        // RCODE is the low 4 bits of the second flags byte (header byte 3).
2027        let rcode = response_rcode(&response).unwrap_or(0);
2028
2029        DnsQueryResult {
2030            response,
2031            rcode,
2032            resolvers_consulted,
2033        }
2034    }
2035}
2036
2037impl Message<Arc<ts_control::StateUpdate>> for MagicDnsActor {
2038    type Reply = ();
2039
2040    async fn handle(
2041        &mut self,
2042        update: Arc<ts_control::StateUpdate>,
2043        _ctx: &mut Context<Self, Self::Reply>,
2044    ) {
2045        // Re-read the live accept-dns cell on every rebuild (it is runtime-settable via
2046        // `Device::set_accept_dns`); `enable_ipv6` is preserved from the seed (set once at spawn).
2047        let accept_dns = self.env.accept_dns();
2048        self.view_tx.send_modify(|view| {
2049            let mut next = (**view).clone();
2050            next.cfg = update.dns_config.clone().unwrap_or_default();
2051            next.self_node = update.node.clone();
2052            next.accept_dns = accept_dns;
2053            *view = Arc::new(next);
2054        });
2055    }
2056}
2057
2058impl Message<Arc<PeerState>> for MagicDnsActor {
2059    type Reply = ();
2060
2061    async fn handle(&mut self, state: Arc<PeerState>, _ctx: &mut Context<Self, Self::Reply>) {
2062        // Re-read the live accept-dns cell on every rebuild: `Device::set_accept_dns` triggers a
2063        // `RepublishState` that lands here, so this is the path that re-applies the gate after a
2064        // runtime toggle (covers the netstack responder AND the peerAPI DoH server sharing the view).
2065        let accept_dns = self.env.accept_dns();
2066        self.view_tx.send_modify(|view| {
2067            let mut next = (**view).clone();
2068            next.peers = Some(state.peers.clone());
2069            next.accept_dns = accept_dns;
2070            *view = Arc::new(next);
2071        });
2072    }
2073}
2074
2075impl Message<crate::route_updater::ActiveExitNode> for MagicDnsActor {
2076    type Reply = ();
2077
2078    async fn handle(
2079        &mut self,
2080        active: crate::route_updater::ActiveExitNode,
2081        _ctx: &mut Context<Self, Self::Reply>,
2082    ) {
2083        // Cache the active exit node's DoH endpoint so the serve loop delegates recursive queries
2084        // to it. `None` (no exit node, or one that can't proxy DNS) keeps recursion local. Resolving
2085        // the address here — once, from the route updater's authoritative selection — means the
2086        // serve loop never re-resolves the selector.
2087        let exit_doh = active.node.as_ref().and_then(|n| n.peerapi_doh_addr());
2088        self.view_tx.send_modify(|view| {
2089            let mut next = (**view).clone();
2090            next.exit_doh = exit_doh;
2091            *view = Arc::new(next);
2092        });
2093    }
2094}
2095
2096#[cfg(test)]
2097mod tests {
2098    use ts_control::{StableNodeId, TailnetAddress};
2099
2100    use super::*;
2101
2102    /// Test wrapper: run [`decide`] and extract the reply bytes. These tests configure no
2103    /// upstream resolvers, so an unresolved name fails closed to a `Reply` (NXDOMAIN), never a
2104    /// `Forward`; a `Forward` here is a bug and panics.
2105    fn answer(view: &DnsView, buf: &[u8]) -> Option<Vec<u8>> {
2106        match decide(view, buf)? {
2107            Decision::Reply(resp) => Some(resp),
2108            Decision::Forward { .. } => panic!("unexpected forward in authoritative-only test"),
2109        }
2110    }
2111
2112    /// Build a `Node` named `host.user.ts.net` with a known v4/v6 tailnet address.
2113    fn test_node() -> Node {
2114        Node {
2115            id: 1,
2116            stable_id: StableNodeId("n1".to_string()),
2117            hostname: "host".to_string(),
2118            user_id: 0,
2119            tailnet: Some("user.ts.net".to_string()),
2120            tags: vec![],
2121            addresses: vec![
2122                "100.64.0.1/32".parse().unwrap(),
2123                "fd7a::1/128".parse().unwrap(),
2124            ],
2125            tailnet_address: TailnetAddress {
2126                ipv4: "100.64.0.1/32".parse().unwrap(),
2127                ipv6: "fd7a::1/128".parse().unwrap(),
2128            },
2129            node_key: [0u8; 32].into(),
2130            node_key_expiry: None,
2131            expired: false,
2132            online: None,
2133            last_seen: None,
2134            key_signature: vec![],
2135            machine_key: None,
2136            disco_key: None,
2137            accepted_routes: vec![],
2138            underlay_addresses: vec![],
2139            derp_region: None,
2140            cap: Default::default(),
2141            cap_map: Default::default(),
2142            peerapi_port: None,
2143            peerapi_dns_proxy: false,
2144            is_wireguard_only: false,
2145            exit_node_dns_resolvers: vec![],
2146            peer_relay: false,
2147            ssh_host_keys: vec![],
2148            service_vips: Default::default(),
2149            unsigned_peer_api_only: false,
2150        }
2151    }
2152
2153    /// A view with MagicDNS on and a single peer in the db.
2154    fn view_with_peer() -> DnsView {
2155        let mut db = PeerDb::default();
2156        db.upsert(&test_node());
2157
2158        DnsView {
2159            cfg: DnsConfig {
2160                magic_dns: true,
2161                search_domains: vec!["user.ts.net".to_string()],
2162                ..Default::default()
2163            },
2164            peers: Some(Arc::new(db)),
2165            self_node: None,
2166            exit_doh: None,
2167            enable_ipv6: false,
2168            accept_dns: true,
2169        }
2170    }
2171
2172    /// Build a raw DNS query buffer for `labels` with the given id, qtype, qclass.
2173    fn build_query(id: u16, labels: &[&str], qtype: u16, qclass: u16) -> Vec<u8> {
2174        let mut buf: Vec<u8> = Vec::new();
2175        buf.extend_from_slice(&id.to_be_bytes());
2176        buf.extend_from_slice(&0u16.to_be_bytes()); // flags: QR=0 (query)
2177        buf.extend_from_slice(&1u16.to_be_bytes()); // QDCOUNT
2178        buf.extend_from_slice(&0u16.to_be_bytes()); // ANCOUNT
2179        buf.extend_from_slice(&0u16.to_be_bytes()); // NSCOUNT
2180        buf.extend_from_slice(&0u16.to_be_bytes()); // ARCOUNT
2181        for label in labels {
2182            buf.push(label.len() as u8);
2183            buf.extend_from_slice(label.as_bytes());
2184        }
2185        buf.push(0); // root label
2186        buf.extend_from_slice(&qtype.to_be_bytes());
2187        buf.extend_from_slice(&qclass.to_be_bytes());
2188        buf
2189    }
2190
2191    /// `build_query` plus an EDNS(0) OPT record in the additional section advertising `udp_size` as
2192    /// the requestor's UDP payload size (RFC 6891: root NAME, TYPE 41, CLASS = the size), in the
2193    /// only shape Go's `findOPTRecord` accepts: last record in the message, version 0, `RDLEN` 0.
2194    fn build_edns_query(
2195        id: u16,
2196        labels: &[&str],
2197        qtype: u16,
2198        qclass: u16,
2199        udp_size: u16,
2200    ) -> Vec<u8> {
2201        let mut buf = build_query(id, labels, qtype, qclass);
2202        buf[11] = 1; // ARCOUNT = 1
2203        buf.push(0); // NAME: root
2204        buf.extend_from_slice(&41u16.to_be_bytes()); // TYPE: OPT
2205        buf.extend_from_slice(&udp_size.to_be_bytes()); // CLASS: requestor's UDP payload size
2206        buf.extend_from_slice(&0u32.to_be_bytes()); // TTL: extended rcode + flags
2207        buf.extend_from_slice(&0u16.to_be_bytes()); // RDLENGTH: no options
2208        buf
2209    }
2210
2211    /// Like [`build_edns_query`] but with one EDNS option in the OPT record's RDATA, so `RDLEN` is
2212    /// non-zero — the shape a stub resolver sending a DNS cookie (option code 10) produces.
2213    fn build_edns_query_with_option(
2214        id: u16,
2215        labels: &[&str],
2216        qtype: u16,
2217        qclass: u16,
2218        udp_size: u16,
2219        option_code: u16,
2220        option_data: &[u8],
2221    ) -> Vec<u8> {
2222        let mut buf = build_edns_query(id, labels, qtype, qclass, udp_size);
2223        let rdata_len = 4 + option_data.len();
2224        let rdlength_at = buf.len() - 2;
2225        buf[rdlength_at..].copy_from_slice(&(rdata_len as u16).to_be_bytes());
2226        buf.extend_from_slice(&option_code.to_be_bytes());
2227        buf.extend_from_slice(&(option_data.len() as u16).to_be_bytes());
2228        buf.extend_from_slice(option_data);
2229        buf
2230    }
2231
2232    /// Parse a response header: returns `(id, rcode, ancount)`.
2233    fn parse_header(resp: &[u8]) -> (u16, u8, u16) {
2234        let id = u16::from_be_bytes([resp[0], resp[1]]);
2235        let flags = u16::from_be_bytes([resp[2], resp[3]]);
2236        let ancount = u16::from_be_bytes([resp[6], resp[7]]);
2237        (id, (flags & 0x000F) as u8, ancount)
2238    }
2239
2240    #[test]
2241    fn a_query_for_known_peer_answers_v4() {
2242        let view = view_with_peer();
2243        let buf = build_query(0x1234, &["host", "user", "ts", "net"], 1, 1);
2244
2245        let resp = answer(&view, &buf).expect("answers");
2246        let (id, rcode, ancount) = parse_header(&resp);
2247        assert_eq!(id, 0x1234);
2248        assert_eq!(rcode, 0, "NoError");
2249        assert_eq!(ancount, 1);
2250
2251        // The trailing RDATA of the single A record is the peer's tailnet v4 octets.
2252        let tail = &resp[resp.len() - 4..];
2253        assert_eq!(tail, &[100, 64, 0, 1]);
2254    }
2255
2256    #[test]
2257    fn aaaa_query_for_known_peer_is_nodata_when_ipv6_off() {
2258        // Gate OFF (default): an AAAA query for a known overlay peer must return NoError with an
2259        // empty answer (NODATA) — NOT the overlay v6 address, which the IPv4-only client can't
2260        // route. This is the anti-fingerprint / no-dead-connections posture.
2261        let view = view_with_peer();
2262        assert!(!view.enable_ipv6, "default gate is off");
2263        let buf = build_query(0x5, &["host", "user", "ts", "net"], 28, 1);
2264
2265        let resp = answer(&view, &buf).expect("answers");
2266        let (_, rcode, ancount) = parse_header(&resp);
2267        assert_eq!(rcode, 0, "NoError (NODATA)");
2268        assert_eq!(ancount, 0, "empty answer: no AAAA handed out with IPv6 off");
2269    }
2270
2271    #[test]
2272    fn a_query_still_resolves_when_ipv6_off() {
2273        // Gate OFF must not touch the A (v4) path: the v4 answer is byte-for-byte unchanged.
2274        let view = view_with_peer();
2275        let buf = build_query(0x6, &["host", "user", "ts", "net"], 1, 1);
2276
2277        let resp = answer(&view, &buf).expect("answers");
2278        let (_, rcode, ancount) = parse_header(&resp);
2279        assert_eq!(rcode, 0, "NoError");
2280        assert_eq!(ancount, 1);
2281        let tail = &resp[resp.len() - 4..];
2282        assert_eq!(tail, &[100, 64, 0, 1]);
2283    }
2284
2285    #[test]
2286    fn aaaa_query_for_known_peer_answers_v6_when_ipv6_on() {
2287        // Gate ON: historical behavior — answer AAAA from the overlay v6 address.
2288        let mut view = view_with_peer();
2289        view.enable_ipv6 = true;
2290        let buf = build_query(0x5, &["host", "user", "ts", "net"], 28, 1);
2291
2292        let resp = answer(&view, &buf).expect("answers");
2293        let (_, rcode, ancount) = parse_header(&resp);
2294        assert_eq!(rcode, 0, "NoError");
2295        assert_eq!(ancount, 1);
2296
2297        let expected = "fd7a::1".parse::<std::net::Ipv6Addr>().unwrap().octets();
2298        let tail = &resp[resp.len() - 16..];
2299        assert_eq!(tail, expected);
2300    }
2301
2302    #[test]
2303    fn aaaa_for_unknown_tailnet_name_is_nxdomain_not_forwarded_with_ipv6_off() {
2304        // Anti-leak, unchanged by the gate: an AAAA for a name under the tailnet suffix that has no
2305        // overlay match still fails closed to NXDOMAIN — never forwarded to a recursive upstream,
2306        // even with resolvers configured. (Gate OFF only changes the *positive* overlay match into
2307        // NODATA; a non-match still routes through `forward_or_nxdomain`.)
2308        let mut db = PeerDb::default();
2309        db.upsert(&test_node());
2310        let view = DnsView {
2311            cfg: DnsConfig {
2312                magic_dns: true,
2313                search_domains: vec!["user.ts.net".to_string()],
2314                fallback_resolvers: vec![DnsResolver {
2315                    transport: ts_control::ResolverTransport::Udp("9.9.9.9:53".parse().unwrap()),
2316                    use_with_exit_node: false,
2317                }],
2318                ..Default::default()
2319            },
2320            peers: Some(Arc::new(db)),
2321            self_node: None,
2322            exit_doh: None,
2323            enable_ipv6: false,
2324            accept_dns: true,
2325        };
2326        let buf = build_query(0x5A, &["ghost", "user", "ts", "net"], 28, 1);
2327
2328        match decide(&view, &buf).expect("decides") {
2329            Decision::Reply(resp) => {
2330                let (_, rcode, _) = parse_header(&resp);
2331                assert_eq!(rcode, 3, "NxDomain: tailnet AAAA not leaked upstream");
2332            }
2333            Decision::Forward { .. } => panic!("tailnet AAAA must never be forwarded"),
2334        }
2335    }
2336
2337    #[test]
2338    fn bare_hostname_resolves() {
2339        // The name index also stores the bare hostname.
2340        let view = view_with_peer();
2341        let buf = build_query(0x7, &["host"], 1, 1);
2342
2343        let resp = answer(&view, &buf).expect("answers");
2344        let (_, rcode, ancount) = parse_header(&resp);
2345        assert_eq!(rcode, 0);
2346        assert_eq!(ancount, 1);
2347    }
2348
2349    #[test]
2350    fn unknown_off_tailnet_name_with_no_upstream_is_servfail() {
2351        // An off-tailnet name with no resolver configured cannot be forwarded. Go answers SERVFAIL
2352        // (a soft "couldn't resolve"), not NXDOMAIN — asserting non-existence of a real name we
2353        // simply have no upstream for would poison a downstream stub's negative cache. (A *tailnet*
2354        // name with no overlay match stays NXDOMAIN — see `tailnet_name_is_never_forwarded` — and a
2355        // negative split-DNS route stays NXDOMAIN — see `negative_route_is_nxdomain_not_forwarded`.)
2356        let view = view_with_peer();
2357        let buf = build_query(0x9, &["nope", "example", "com"], 1, 1);
2358
2359        let resp = answer(&view, &buf).expect("answers");
2360        let (_, rcode, ancount) = parse_header(&resp);
2361        assert_eq!(
2362            rcode, 2,
2363            "ServFail: off-tailnet name, nothing to forward to"
2364        );
2365        assert_eq!(ancount, 0);
2366    }
2367
2368    #[test]
2369    fn magic_dns_off_is_refused() {
2370        // Fail closed: with MagicDNS disabled, even a known name is refused.
2371        let mut view = view_with_peer();
2372        view.cfg.magic_dns = false;
2373        let buf = build_query(0xAB, &["host", "user", "ts", "net"], 1, 1);
2374
2375        let resp = answer(&view, &buf).expect("answers");
2376        let (_, rcode, ancount) = parse_header(&resp);
2377        assert_eq!(rcode, 5, "Refused");
2378        assert_eq!(ancount, 0);
2379    }
2380
2381    #[test]
2382    fn accept_dns_false_refuses_otherwise_answerable_query() {
2383        // The accept-dns gate (Go `CorpDNS`): with `accept_dns == false` the node ignores the
2384        // tailnet DNS config, so even a known peer name that would normally answer authoritatively is
2385        // REFUSED (the responder serves nothing) — mirroring Go applying an empty `dns.Config`.
2386        let mut view = view_with_peer();
2387        assert!(view.cfg.magic_dns, "MagicDNS itself is on");
2388        view.accept_dns = false;
2389        let buf = build_query(0xDD, &["host", "user", "ts", "net"], 1, 1);
2390
2391        let resp = answer(&view, &buf).expect("answers");
2392        let (_, rcode, ancount) = parse_header(&resp);
2393        assert_eq!(rcode, 5, "Refused: accept_dns off ⇒ serve nothing");
2394        assert_eq!(ancount, 0);
2395
2396        // Flip accept_dns back ON (the config was never destroyed, only gated): the same query now
2397        // answers authoritatively — proving the OFF→ON restore is automatic.
2398        view.accept_dns = true;
2399        let resp = answer(&view, &buf).expect("answers");
2400        let (_, rcode, ancount) = parse_header(&resp);
2401        assert_eq!(rcode, 0, "NoError: accept_dns on ⇒ the known peer answers");
2402        assert_eq!(ancount, 1);
2403        let tail = &resp[resp.len() - 4..];
2404        assert_eq!(tail, &[100, 64, 0, 1], "the peer's tailnet v4 is served");
2405    }
2406
2407    #[test]
2408    fn default_view_serves_nothing() {
2409        // The default (no dns_config seen) has magic_dns == false: fail closed.
2410        let view = DnsView::default();
2411        let buf = build_query(0x1, &["host", "user", "ts", "net"], 1, 1);
2412
2413        let resp = answer(&view, &buf).expect("answers");
2414        let (_, rcode, _) = parse_header(&resp);
2415        assert_eq!(rcode, 5, "Refused");
2416    }
2417
2418    #[test]
2419    fn unsupported_qtype_on_tailnet_name_is_nodata_not_refused() {
2420        // TXT (type 16) for a tailnet-authoritative name: the name exists but we hold no TXT, so —
2421        // like Go — return NODATA (empty NOERROR), NOT REFUSED (which would make a stub abandon the
2422        // resolver) and NOT NXDOMAIN (the name exists). The name is never forwarded (anti-leak).
2423        let view = view_with_peer();
2424        let buf = build_query(0x1, &["host", "user", "ts", "net"], 16, 1);
2425
2426        let resp = answer(&view, &buf).expect("answers");
2427        let (_, rcode, ancount) = parse_header(&resp);
2428        assert_eq!(rcode, 0, "NoError (NODATA), not Refused");
2429        assert_eq!(ancount, 0, "no answer records (NODATA)");
2430    }
2431
2432    #[test]
2433    fn unsupported_qtype_off_tailnet_forwards_or_servfails() {
2434        // A non-A/AAAA/PTR qtype for an OFF-tailnet name must be forwardable like A/AAAA — never
2435        // REFUSED. With no upstream configured in this view it soft-fails to SERVFAIL (the same
2436        // disposition an off-tailnet A query gets here), proving the qtype no longer short-circuits
2437        // to REFUSED. HTTPS/SVCB is type 65 (the browser HTTP/3 + ECH case the old REFUSED broke).
2438        let view = view_with_peer();
2439        let buf = build_query(0x1, &["example", "com"], 65, 1);
2440
2441        let resp = answer(&view, &buf).expect("answers");
2442        let (_, rcode, _) = parse_header(&resp);
2443        assert_eq!(
2444            rcode, 2,
2445            "off-tailnet, no upstream -> SERVFAIL (forwardable, not Refused)"
2446        );
2447    }
2448
2449    #[test]
2450    fn unimplemented_qtype_on_tailnet_name_is_notimp() {
2451        // NS (2), SOA (6), HINFO (13), AXFR (252) for a tailnet-authoritative name must answer NOTIMP
2452        // (rcode 4), matching Go `resolveLocal`'s `case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR,
2453        // dns.TypeHINFO: return RCodeNotImplemented`. Returning NODATA (rcode 0) here was a clean
2454        // fingerprint (a `dig SOA user.ts.net` answer differs from real tailscaled). The name is
2455        // still never forwarded (anti-leak).
2456        let view = view_with_peer();
2457        for qtype in [2u16, 6, 13, 252] {
2458            let buf = build_query(0x1, &["host", "user", "ts", "net"], qtype, 1);
2459            let resp = answer(&view, &buf).expect("answers");
2460            let (_, rcode, ancount) = parse_header(&resp);
2461            assert_eq!(rcode, 4, "qtype {qtype} on a tailnet name must be NOTIMP");
2462            assert_eq!(ancount, 0, "NOTIMP carries no answer records");
2463        }
2464    }
2465
2466    #[test]
2467    fn unimplemented_qtype_off_tailnet_still_forwards_not_notimp() {
2468        // The NOTIMP disposition is ONLY for a name we are authoritative for. An NS query for an
2469        // off-tailnet name must still forward (here: SERVFAIL, no upstream) — NOT NOTIMP — exactly
2470        // like the off-tailnet HTTPS/SVCB case above. Guards the NOTIMP change against over-reach.
2471        let view = view_with_peer();
2472        let buf = build_query(0x1, &["example", "com"], 2, 1); // NS, off-tailnet
2473        let resp = answer(&view, &buf).expect("answers");
2474        let (_, rcode, _) = parse_header(&resp);
2475        assert_eq!(
2476            rcode, 2,
2477            "off-tailnet NS -> SERVFAIL (forwardable), not NOTIMP"
2478        );
2479    }
2480
2481    #[test]
2482    fn malformed_query_is_dropped() {
2483        // A response (QR bit set) is not a query; we drop it (no answer).
2484        let mut buf = build_query(0x1, &["host"], 1, 1);
2485        buf[2] = 0x80; // set QR bit
2486        assert!(answer(&view_with_peer(), &buf).is_none());
2487    }
2488
2489    #[test]
2490    fn ptr_for_known_ip_answers_fqdn() {
2491        let view = view_with_peer();
2492        // Reverse name for 100.64.0.1 => 1.0.64.100.in-addr.arpa
2493        let buf = build_query(0x33, &["1", "0", "64", "100", "in-addr", "arpa"], 12, 1);
2494
2495        let resp = answer(&view, &buf).expect("answers");
2496        let (_, rcode, ancount) = parse_header(&resp);
2497        assert_eq!(rcode, 0, "NoError");
2498        assert_eq!(ancount, 1);
2499
2500        // The PTR rdata encodes the peer's fqdn "host.user.ts.net" as length-prefixed labels.
2501        let expected = {
2502            let mut out = Vec::new();
2503            for label in ["host", "user", "ts", "net"] {
2504                out.push(label.len() as u8);
2505                out.extend_from_slice(label.as_bytes());
2506            }
2507            out.push(0);
2508            out
2509        };
2510        let tail = &resp[resp.len() - expected.len()..];
2511        assert_eq!(tail, expected.as_slice());
2512    }
2513
2514    #[test]
2515    fn ptr_for_unknown_public_ip_off_tailnet_is_servfail() {
2516        let view = view_with_peer();
2517        // 9.9.9.9 is a public IP, not a known tailnet IP and not in the CGNAT reverse zone — so its
2518        // reverse query is an ordinary off-tailnet name. With no upstream to forward it to, that is
2519        // SERVFAIL (soft), not NXDOMAIN. (A CGNAT/ip6.arpa reverse for an unmatched tailnet IP still
2520        // fails closed to NXDOMAIN as an anti-leak guard — see `ptr_for_unknown_tailnet_ip_*`.)
2521        let buf = build_query(0x34, &["9", "9", "9", "9", "in-addr", "arpa"], 12, 1);
2522
2523        let resp = answer(&view, &buf).expect("answers");
2524        let (_, rcode, _) = parse_header(&resp);
2525        assert_eq!(
2526            rcode, 2,
2527            "ServFail: off-tailnet public-IP reverse, no upstream"
2528        );
2529    }
2530
2531    #[test]
2532    fn ptr_for_unknown_tailnet_ip_is_nxdomain_not_forwarded() {
2533        // A view WITH an upstream resolver: an off-tailnet reverse query would forward, but a
2534        // reverse query for an unmatched IP in the CGNAT range (100.64.0.0/10) must fail closed to
2535        // NXDOMAIN — the probed tailnet IP must never leak upstream.
2536        let mut db = PeerDb::default();
2537        db.upsert(&test_node());
2538        let view = DnsView {
2539            cfg: DnsConfig {
2540                magic_dns: true,
2541                search_domains: vec!["user.ts.net".to_string()],
2542                fallback_resolvers: vec![DnsResolver {
2543                    transport: ts_control::ResolverTransport::Udp("9.9.9.9:53".parse().unwrap()),
2544                    use_with_exit_node: false,
2545                }],
2546                ..Default::default()
2547            },
2548            peers: Some(Arc::new(db)),
2549            self_node: None,
2550            exit_doh: None,
2551            enable_ipv6: false,
2552            accept_dns: true,
2553        };
2554
2555        // 100.64.0.9 is in CGNAT range but owned by no peer => NXDOMAIN, never a Forward.
2556        let buf = build_query(0x35, &["9", "0", "64", "100", "in-addr", "arpa"], 12, 1);
2557        match decide(&view, &buf).expect("decides") {
2558            Decision::Reply(resp) => {
2559                let (_, rcode, _) = parse_header(&resp);
2560                assert_eq!(rcode, 3, "NxDomain");
2561            }
2562            Decision::Forward { .. } => {
2563                panic!("tailnet CGNAT PTR must never be forwarded upstream")
2564            }
2565        }
2566    }
2567
2568    /// Anti-leak regression for the exotic-qtype forward path: a NON-PTR query (TXT, type 16) for a
2569    /// tailnet CGNAT reverse name, with an upstream configured, must STILL fail closed to NXDOMAIN —
2570    /// never forward. The PTR arm guards this, but the `QType::Other` path routes through
2571    /// `forward_or_nodata`, which must re-apply the reverse-zone guard or the tailnet IP leaks.
2572    #[test]
2573    fn exotic_qtype_for_tailnet_cgnat_reverse_is_nxdomain_not_forwarded() {
2574        let mut db = PeerDb::default();
2575        db.upsert(&test_node());
2576        let view = DnsView {
2577            cfg: DnsConfig {
2578                magic_dns: true,
2579                search_domains: vec!["user.ts.net".to_string()],
2580                fallback_resolvers: vec![DnsResolver {
2581                    transport: ts_control::ResolverTransport::Udp("9.9.9.9:53".parse().unwrap()),
2582                    use_with_exit_node: false,
2583                }],
2584                ..Default::default()
2585            },
2586            peers: Some(Arc::new(db)),
2587            self_node: None,
2588            exit_doh: None,
2589            enable_ipv6: false,
2590            accept_dns: true,
2591        };
2592
2593        // TXT (16) for a CGNAT reverse name => NXDOMAIN, never a Forward (no tailnet-IP leak).
2594        let buf = build_query(0x36, &["9", "0", "64", "100", "in-addr", "arpa"], 16, 1);
2595        match decide(&view, &buf).expect("decides") {
2596            Decision::Reply(resp) => {
2597                let (_, rcode, _) = parse_header(&resp);
2598                assert_eq!(rcode, 3, "NxDomain");
2599            }
2600            Decision::Forward { .. } => {
2601                panic!("a non-PTR query for a tailnet CGNAT reverse name must never forward")
2602            }
2603        }
2604    }
2605
2606    /// Same anti-leak guard for an `ip6.arpa` reverse name under an exotic qtype: must NXDOMAIN, not
2607    /// forward (revealing a tailnet ULA was probed).
2608    #[test]
2609    fn exotic_qtype_for_ip6_arpa_is_nxdomain_not_forwarded() {
2610        let view = view_with_routes(
2611            std::collections::BTreeMap::new(),
2612            vec![udp("9.9.9.9:53")],
2613            vec![],
2614        );
2615        // An ip6.arpa reverse name with a TXT (16) qtype must fail closed.
2616        let buf = build_query(
2617            0x37,
2618            &[
2619                "1", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0",
2620                "a", "7", "d", "f", "ip6", "arpa",
2621            ],
2622            16,
2623            1,
2624        );
2625        match decide(&view, &buf).expect("decides") {
2626            Decision::Reply(resp) => {
2627                let (_, rcode, _) = parse_header(&resp);
2628                assert_eq!(rcode, 3, "NxDomain");
2629            }
2630            Decision::Forward { .. } => panic!("an ip6.arpa exotic-qtype query must never forward"),
2631        }
2632    }
2633
2634    #[test]
2635    fn is_tailnet_cgnat_classifies_range() {
2636        assert!(is_tailnet_cgnat("100.64.0.0".parse().unwrap()));
2637        assert!(is_tailnet_cgnat("100.64.0.1".parse().unwrap()));
2638        assert!(is_tailnet_cgnat("100.127.255.255".parse().unwrap()));
2639        // Outside the /10:
2640        assert!(!is_tailnet_cgnat("100.63.255.255".parse().unwrap()));
2641        assert!(!is_tailnet_cgnat("100.128.0.0".parse().unwrap()));
2642        assert!(!is_tailnet_cgnat("9.9.9.9".parse().unwrap()));
2643        // The MagicDNS resolver IP 100.100.100.100 is itself inside the /10.
2644        assert!(is_tailnet_cgnat("100.100.100.100".parse().unwrap()));
2645    }
2646
2647    #[test]
2648    fn response_matches_query_validates_id_and_qr() {
2649        // query id 0x1234, QR=0
2650        let query = build_query(0x1234, &["a", "com"], 1, 1);
2651
2652        // A well-formed response: same id, QR=1.
2653        let mut good = query.clone();
2654        good[2] |= 0x80;
2655        assert!(response_matches_query(&query, &good));
2656
2657        // Same id but QR still 0 (not a response): rejected.
2658        assert!(!response_matches_query(&query, &query));
2659
2660        // QR=1 but a different transaction id: rejected (off-path forgery).
2661        let mut wrong_id = good.clone();
2662        wrong_id[0] ^= 0xFF;
2663        assert!(!response_matches_query(&query, &wrong_id));
2664
2665        // Too-short buffers: rejected.
2666        assert!(!response_matches_query(&query, &[0u8; 2]));
2667        assert!(!response_matches_query(&[0u8; 3], &good));
2668    }
2669
2670    #[test]
2671    fn self_node_resolves_when_no_peer_match() {
2672        // With the peer db empty but a self node set, the self node answers for its own name.
2673        let view = DnsView {
2674            cfg: DnsConfig {
2675                magic_dns: true,
2676                search_domains: vec![],
2677                ..Default::default()
2678            },
2679            peers: None,
2680            self_node: Some(test_node()),
2681            exit_doh: None,
2682            enable_ipv6: false,
2683            accept_dns: true,
2684        };
2685        let buf = build_query(0x44, &["host", "user", "ts", "net"], 1, 1);
2686
2687        let resp = answer(&view, &buf).expect("answers");
2688        let (_, rcode, ancount) = parse_header(&resp);
2689        assert_eq!(rcode, 0);
2690        assert_eq!(ancount, 1);
2691        let tail = &resp[resp.len() - 4..];
2692        assert_eq!(tail, &[100, 64, 0, 1]);
2693    }
2694
2695    #[test]
2696    fn partially_qualified_name_resolves_via_search_domain() {
2697        // "host.user" is not indexed directly, but the "user.ts.net" search domain qualifies it
2698        // to "host.user.user.ts.net"... which does NOT match. The realistic case is "host" (bare,
2699        // already indexed) and "host.user.ts.net" (fqdn). Verify a name needing suffix expansion:
2700        // with search domain "ts.net" the partially-qualified "host.user" => "host.user.ts.net".
2701        let mut view = view_with_peer();
2702        view.cfg.search_domains = vec!["ts.net".to_string()];
2703        let buf = build_query(0x55, &["host", "user"], 1, 1);
2704
2705        let resp = answer(&view, &buf).expect("answers");
2706        let (_, rcode, ancount) = parse_header(&resp);
2707        assert_eq!(rcode, 0, "NoError via search-domain expansion");
2708        assert_eq!(ancount, 1);
2709        let tail = &resp[resp.len() - 4..];
2710        assert_eq!(tail, &[100, 64, 0, 1]);
2711    }
2712
2713    #[test]
2714    fn extra_record_a_answers_when_no_peer_match() {
2715        // A control-pushed static A record answers for a non-peer name, fail-closed otherwise.
2716        let mut view = view_with_peer();
2717        view.cfg.extra_records = vec![ts_control::ExtraRecord {
2718            name: "static.user.ts.net".to_string(),
2719            addr: IpAddr::V4(Ipv4Addr::new(100, 64, 0, 9)),
2720        }];
2721        let buf = build_query(0x77, &["static", "user", "ts", "net"], 1, 1);
2722
2723        let resp = answer(&view, &buf).expect("answers");
2724        let (_, rcode, ancount) = parse_header(&resp);
2725        assert_eq!(rcode, 0, "NoError from extra record");
2726        assert_eq!(ancount, 1);
2727        let tail = &resp[resp.len() - 4..];
2728        assert_eq!(tail, &[100, 64, 0, 9]);
2729    }
2730
2731    #[test]
2732    fn extra_record_matches_query_case_insensitively() {
2733        // The query name is canonicalized (lowercased) at decode time, so a mixed-case query
2734        // matches a lowercase extra record.
2735        let mut view = view_with_peer();
2736        view.cfg.extra_records = vec![ts_control::ExtraRecord {
2737            name: "static.user.ts.net".to_string(),
2738            addr: IpAddr::V4(Ipv4Addr::new(100, 64, 0, 9)),
2739        }];
2740        let buf = build_query(0x7A, &["Static", "User", "TS", "net"], 1, 1);
2741
2742        let resp = answer(&view, &buf).expect("answers");
2743        let (_, rcode, ancount) = parse_header(&resp);
2744        assert_eq!(rcode, 0, "NoError: case-insensitive match");
2745        assert_eq!(ancount, 1);
2746        let tail = &resp[resp.len() - 4..];
2747        assert_eq!(tail, &[100, 64, 0, 9]);
2748    }
2749
2750    #[test]
2751    fn extra_record_not_expanded_by_search_domain() {
2752        // Unlike peer names, an extra record is matched as an FQDN only: a bare query that would
2753        // need search-domain expansion to reach the record name must NOT resolve.
2754        let mut view = view_with_peer();
2755        view.cfg.extra_records = vec![ts_control::ExtraRecord {
2756            name: "static.user.ts.net".to_string(),
2757            addr: IpAddr::V4(Ipv4Addr::new(100, 64, 0, 9)),
2758        }];
2759        // "static" would only reach "static.user.ts.net" via the "user.ts.net" search domain.
2760        let buf = build_query(0x7B, &["static"], 1, 1);
2761
2762        let resp = answer(&view, &buf).expect("answers");
2763        let (_, rcode, _) = parse_header(&resp);
2764        // Not search-expanded → treated as the bare off-tailnet name "static", which has no upstream
2765        // here, so SERVFAIL (soft). The point of the test — that the extra record is NOT reachable
2766        // via search expansion — holds regardless of the failure rcode.
2767        assert_eq!(
2768            rcode, 2,
2769            "ServFail: bare 'static' is not search-expanded to the extra record"
2770        );
2771    }
2772
2773    #[test]
2774    fn extra_record_aaaa_family_is_isolated() {
2775        // An A-only extra record must NOT answer an AAAA query for the same name (NxDomain).
2776        let mut view = view_with_peer();
2777        view.cfg.extra_records = vec![ts_control::ExtraRecord {
2778            name: "v4only.user.ts.net".to_string(),
2779            addr: IpAddr::V4(Ipv4Addr::new(100, 64, 0, 9)),
2780        }];
2781        let buf = build_query(0x78, &["v4only", "user", "ts", "net"], 28, 1);
2782
2783        let resp = answer(&view, &buf).expect("answers");
2784        let (_, rcode, _) = parse_header(&resp);
2785        assert_eq!(rcode, 3, "NxDomain: A record does not satisfy AAAA");
2786    }
2787
2788    #[test]
2789    fn extra_record_ignored_when_magic_dns_off() {
2790        // Fail closed: extra records are never served while MagicDNS is disabled.
2791        let mut view = view_with_peer();
2792        view.cfg.magic_dns = false;
2793        view.cfg.extra_records = vec![ts_control::ExtraRecord {
2794            name: "static.user.ts.net".to_string(),
2795            addr: IpAddr::V4(Ipv4Addr::new(100, 64, 0, 9)),
2796        }];
2797        let buf = build_query(0x79, &["static", "user", "ts", "net"], 1, 1);
2798
2799        let resp = answer(&view, &buf).expect("answers");
2800        let (_, rcode, _) = parse_header(&resp);
2801        assert_eq!(rcode, 5, "Refused");
2802    }
2803
2804    /// The node attribute control sets to make every subdomain of a node resolve to it (Go
2805    /// `tailcfg/nodecap`'s `NodeAttrDNSSubdomainResolve`).
2806    const DNS_SUBDOMAIN_RESOLVE: &str = "dns-subdomain-resolve";
2807
2808    /// A view holding a single peer `host.user.ts.net` that carries the `dns-subdomain-resolve`
2809    /// node attribute, so control has declared every name under it to resolve to its addresses.
2810    fn view_with_subdomain_host() -> DnsView {
2811        let mut node = test_node();
2812        node.cap_map
2813            .insert(DNS_SUBDOMAIN_RESOLVE.to_string(), vec![]);
2814
2815        let mut db = PeerDb::default();
2816        db.upsert(&node);
2817
2818        let mut view = view_with_peer();
2819        view.peers = Some(Arc::new(db));
2820        view
2821    }
2822
2823    #[test]
2824    fn subdomain_of_a_subdomain_host_resolves_to_it() {
2825        // `my.host.user.ts.net` has no record of its own; its parent `host.user.ts.net` carries the
2826        // attribute, so it answers with the parent's address.
2827        let view = view_with_subdomain_host();
2828        let buf = build_query(0x90, &["my", "host", "user", "ts", "net"], 1, 1);
2829
2830        let resp = answer(&view, &buf).expect("answers");
2831        let (_, rcode, ancount) = parse_header(&resp);
2832        assert_eq!(rcode, 0, "NoError from the subdomain host");
2833        assert_eq!(ancount, 1);
2834        assert_eq!(&resp[resp.len() - 4..], &[100, 64, 0, 1]);
2835    }
2836
2837    #[test]
2838    fn a_multi_label_subdomain_of_a_subdomain_host_resolves() {
2839        // The walk climbs every parent, not one level: `be.my.host` reaches `host` just as
2840        // `my.host` does. One level of parent is not what upstream implements.
2841        let view = view_with_subdomain_host();
2842        let buf = build_query(0x91, &["be", "my", "host", "user", "ts", "net"], 1, 1);
2843
2844        let resp = answer(&view, &buf).expect("answers");
2845        let (_, rcode, ancount) = parse_header(&resp);
2846        assert_eq!(rcode, 0, "NoError: the walk is not depth-limited");
2847        assert_eq!(ancount, 1);
2848        assert_eq!(&resp[resp.len() - 4..], &[100, 64, 0, 1]);
2849    }
2850
2851    #[test]
2852    fn subdomain_of_a_peer_without_the_attribute_is_nxdomain() {
2853        // The attribute is what turns the walk on. Without it — the default for every node — a
2854        // subdomain of a peer name is still authoritatively absent.
2855        let view = view_with_peer();
2856        assert!(
2857            !view
2858                .node_by_name("host.user.ts.net")
2859                .expect("peer is present")
2860                .resolves_subdomains(),
2861            "the plain test peer carries no node attribute"
2862        );
2863        let buf = build_query(0x92, &["my", "host", "user", "ts", "net"], 1, 1);
2864
2865        let resp = answer(&view, &buf).expect("answers");
2866        let (_, rcode, ancount) = parse_header(&resp);
2867        assert_eq!(rcode, 3, "NxDomain: no attribute, no subdomain resolution");
2868        assert_eq!(ancount, 0);
2869    }
2870
2871    #[test]
2872    fn an_exact_match_beats_the_subdomain_host() {
2873        // The walk is the *miss* path: a name that resolves exactly — here a control-pushed extra
2874        // record — keeps its own answer, and never takes the parent's.
2875        let mut view = view_with_subdomain_host();
2876        view.cfg.extra_records = vec![ts_control::ExtraRecord {
2877            name: "my.host.user.ts.net".to_string(),
2878            addr: IpAddr::V4(Ipv4Addr::new(100, 64, 0, 9)),
2879        }];
2880        let buf = build_query(0x93, &["my", "host", "user", "ts", "net"], 1, 1);
2881
2882        let resp = answer(&view, &buf).expect("answers");
2883        let (_, rcode, ancount) = parse_header(&resp);
2884        assert_eq!(rcode, 0, "NoError");
2885        assert_eq!(ancount, 1);
2886        assert_eq!(
2887            &resp[resp.len() - 4..],
2888            &[100, 64, 0, 9],
2889            "the exact record answers, not the subdomain host's address"
2890        );
2891    }
2892
2893    #[test]
2894    fn the_subdomain_walk_stops_at_the_tailnet_zone() {
2895        // A node whose own FQDN *is* the search domain must not make the whole zone a wildcard:
2896        // the walk stops at the zone apex rather than climbing into names we do not serve.
2897        let mut zone_node = test_node();
2898        zone_node.hostname = "user".to_string();
2899        zone_node.tailnet = Some("ts.net".to_string());
2900        zone_node
2901            .cap_map
2902            .insert(DNS_SUBDOMAIN_RESOLVE.to_string(), vec![]);
2903        assert_eq!(zone_node.fqdn(false), "user.ts.net", "the zone apex itself");
2904
2905        let mut db = PeerDb::default();
2906        db.upsert(&zone_node);
2907        let mut view = view_with_peer();
2908        view.peers = Some(Arc::new(db));
2909
2910        for labels in [
2911            ["nothing", "user", "ts", "net"].as_slice(),
2912            ["deeper", "nothing", "user", "ts", "net"].as_slice(),
2913        ] {
2914            let buf = build_query(0x94, labels, 1, 1);
2915            let resp = answer(&view, &buf).expect("answers");
2916            let (_, rcode, ancount) = parse_header(&resp);
2917            assert_eq!(rcode, 3, "NxDomain: the walk stopped at {:?}", labels);
2918            assert_eq!(ancount, 0);
2919        }
2920    }
2921
2922    #[test]
2923    fn the_subdomain_walk_does_not_search_expand_a_bare_label() {
2924        // The peer-name index also holds bare hostnames, so a peer named after a public suffix must
2925        // not swallow every name under it: only a fully-qualified parent is a walk candidate. Go
2926        // cannot do this at all — its resolver does no search-list expansion.
2927        let mut suffix_node = test_node();
2928        suffix_node.hostname = "com".to_string();
2929        suffix_node
2930            .cap_map
2931            .insert(DNS_SUBDOMAIN_RESOLVE.to_string(), vec![]);
2932
2933        let mut db = PeerDb::default();
2934        db.upsert(&suffix_node);
2935        let mut view = view_with_peer();
2936        view.peers = Some(Arc::new(db));
2937
2938        let buf = build_query(0x95, &["www", "example", "com"], 1, 1);
2939        let resp = answer(&view, &buf).expect("answers");
2940        let (_, rcode, ancount) = parse_header(&resp);
2941        assert_eq!(
2942            rcode, 2,
2943            "ServFail: an off-tailnet name with no upstream, NOT the peer named 'com'"
2944        );
2945        assert_eq!(ancount, 0, "no answer manufactured from a bare hostname");
2946
2947        // The qualified form of the same peer still resolves its subdomains: the bound rejects the
2948        // bare label, not the subdomain host.
2949        let buf = build_query(0x96, &["www", "com", "user", "ts", "net"], 1, 1);
2950        let resp = answer(&view, &buf).expect("answers");
2951        let (_, rcode, ancount) = parse_header(&resp);
2952        assert_eq!(rcode, 0, "NoError from com.user.ts.net");
2953        assert_eq!(ancount, 1);
2954        assert_eq!(&resp[resp.len() - 4..], &[100, 64, 0, 1]);
2955    }
2956
2957    #[test]
2958    fn aaaa_for_a_subdomain_host_follows_the_ipv6_gate() {
2959        // The subdomain answer is the parent node's address, so it takes the same AAAA gate an
2960        // exact peer match does: NODATA with IPv6 off, the overlay v6 with it on.
2961        let mut view = view_with_subdomain_host();
2962        let buf = build_query(0x97, &["my", "host", "user", "ts", "net"], 28, 1);
2963
2964        let resp = answer(&view, &buf).expect("answers");
2965        let (_, rcode, ancount) = parse_header(&resp);
2966        assert_eq!(rcode, 0, "NoError (NODATA) with the gate off");
2967        assert_eq!(ancount, 0);
2968
2969        view.enable_ipv6 = true;
2970        let resp = answer(&view, &buf).expect("answers");
2971        let (_, rcode, ancount) = parse_header(&resp);
2972        assert_eq!(rcode, 0, "NoError");
2973        assert_eq!(ancount, 1);
2974        let expected = "fd7a::1".parse::<std::net::Ipv6Addr>().unwrap().octets();
2975        assert_eq!(&resp[resp.len() - 16..], expected);
2976    }
2977
2978    #[test]
2979    fn a_subdomain_of_the_self_node_resolves_when_it_has_the_attribute() {
2980        // The walk runs over the same name lookup the exact match uses, so the self node is a
2981        // subdomain host too when control sets the attribute on it.
2982        let mut self_node = test_node();
2983        self_node.hostname = "me".to_string();
2984        self_node
2985            .cap_map
2986            .insert(DNS_SUBDOMAIN_RESOLVE.to_string(), vec![]);
2987
2988        let mut view = view_with_peer();
2989        view.peers = None;
2990        view.self_node = Some(self_node);
2991
2992        let buf = build_query(0x98, &["a", "b", "me", "user", "ts", "net"], 1, 1);
2993        let resp = answer(&view, &buf).expect("answers");
2994        let (_, rcode, ancount) = parse_header(&resp);
2995        assert_eq!(rcode, 0, "NoError from the self node");
2996        assert_eq!(ancount, 1);
2997        assert_eq!(&resp[resp.len() - 4..], &[100, 64, 0, 1]);
2998    }
2999
3000    #[test]
3001    fn non_in_class_on_tailnet_name_is_nodata_not_answered_as_in() {
3002        // A CHAOS-class (3) query for a tailnet name must NOT be answered as IN (no overlay A), and
3003        // must NOT be REFUSED (Go does no class check on the local path). It's an unsupported
3004        // authoritative class -> NODATA (empty NOERROR), and never forwarded (tailnet name).
3005        let view = view_with_peer();
3006        let buf = build_query(0x66, &["host", "user", "ts", "net"], 1, 3);
3007
3008        let resp = answer(&view, &buf).expect("answers");
3009        let (_, rcode, ancount) = parse_header(&resp);
3010        assert_eq!(
3011            rcode, 0,
3012            "NoError (NODATA), not Refused and not an IN answer"
3013        );
3014        assert_eq!(
3015            ancount, 0,
3016            "must not hand out the overlay A for a non-IN class"
3017        );
3018    }
3019
3020    #[test]
3021    fn non_in_class_off_tailnet_forwards_or_servfails() {
3022        // A non-IN class for an OFF-tailnet name is forwardable (Go forwards it), never REFUSED.
3023        // No upstream here -> SERVFAIL, proving the class gate no longer short-circuits to Refused.
3024        let view = view_with_peer();
3025        let buf = build_query(0x66, &["example", "com"], 1, 3);
3026
3027        let resp = answer(&view, &buf).expect("answers");
3028        let (_, rcode, _) = parse_header(&resp);
3029        assert_eq!(
3030            rcode, 2,
3031            "off-tailnet non-IN class, no upstream -> SERVFAIL, not Refused"
3032        );
3033    }
3034
3035    /// A view with MagicDNS on, the `user.ts.net` search domain, and the given split-DNS routes
3036    /// + global resolvers.
3037    fn view_with_routes(
3038        routes: std::collections::BTreeMap<String, Vec<DnsResolver>>,
3039        resolvers: Vec<DnsResolver>,
3040        fallback: Vec<DnsResolver>,
3041    ) -> DnsView {
3042        DnsView {
3043            cfg: DnsConfig {
3044                magic_dns: true,
3045                search_domains: vec!["user.ts.net".to_string()],
3046                routes,
3047                resolvers,
3048                fallback_resolvers: fallback,
3049                ..Default::default()
3050            },
3051            peers: None,
3052            self_node: None,
3053            exit_doh: None,
3054            enable_ipv6: false,
3055            accept_dns: true,
3056        }
3057    }
3058
3059    fn udp(addr: &str) -> DnsResolver {
3060        DnsResolver {
3061            transport: ts_control::ResolverTransport::Udp(addr.parse().unwrap()),
3062            use_with_exit_node: false,
3063        }
3064    }
3065
3066    #[test]
3067    fn split_dns_route_forwards_to_matching_upstream() {
3068        let mut routes = std::collections::BTreeMap::new();
3069        routes.insert("corp.example".to_string(), vec![udp("10.0.0.53:53")]);
3070        let view = view_with_routes(routes, vec![], vec![]);
3071        let buf = build_query(0x100, &["api", "corp", "example"], 1, 1);
3072
3073        match decide(&view, &buf).expect("decides") {
3074            Decision::Forward { upstreams, .. } => {
3075                assert_eq!(upstreams, vec!["10.0.0.53:53".parse().unwrap()]);
3076            }
3077            Decision::Reply(_) => panic!("expected forward to the split-DNS upstream"),
3078        }
3079    }
3080
3081    #[test]
3082    fn exotic_qtype_off_tailnet_forwards_to_upstream() {
3083        // The core of the fix: an HTTPS/SVCB (type 65) query for an off-tailnet name with a matching
3084        // route must FORWARD to the upstream (verbatim), exactly like an A query would — not REFUSE
3085        // and not NXDOMAIN. This is the browser HTTP/3 + ECH case the old blanket-REFUSE broke.
3086        let mut routes = std::collections::BTreeMap::new();
3087        routes.insert("corp.example".to_string(), vec![udp("10.0.0.53:53")]);
3088        let view = view_with_routes(routes, vec![], vec![]);
3089        let buf = build_query(0x102, &["api", "corp", "example"], 65, 1);
3090
3091        match decide(&view, &buf).expect("decides") {
3092            Decision::Forward {
3093                upstreams, query, ..
3094            } => {
3095                assert_eq!(upstreams, vec!["10.0.0.53:53".parse().unwrap()]);
3096                assert_eq!(query, buf, "the exotic-qtype query is forwarded verbatim");
3097            }
3098            Decision::Reply(_) => {
3099                panic!("an off-tailnet HTTPS-record query must forward, not reply")
3100            }
3101        }
3102    }
3103
3104    #[test]
3105    fn non_in_class_off_tailnet_forwards_to_upstream() {
3106        // A non-IN class for an off-tailnet routed name forwards too (Go does no class check on the
3107        // local path). Proves the class gate no longer short-circuits to REFUSED before routing.
3108        let mut routes = std::collections::BTreeMap::new();
3109        routes.insert("corp.example".to_string(), vec![udp("10.0.0.53:53")]);
3110        let view = view_with_routes(routes, vec![], vec![]);
3111        let buf = build_query(0x103, &["api", "corp", "example"], 1, 3);
3112
3113        match decide(&view, &buf).expect("decides") {
3114            Decision::Forward { upstreams, .. } => {
3115                assert_eq!(upstreams, vec!["10.0.0.53:53".parse().unwrap()]);
3116            }
3117            Decision::Reply(_) => {
3118                panic!("an off-tailnet non-IN-class query must forward, not reply")
3119            }
3120        }
3121    }
3122
3123    /// The local responder bounds concurrent in-flight forwards: `serve` acquires one
3124    /// `MAX_INFLIGHT_FORWARDS` permit per spawned forward task and drops the query fail-closed when
3125    /// the pool is exhausted (a client spraying forwardable names can't open unbounded overlay
3126    /// sockets). This pins the gating semantics `serve` relies on — drained pool refuses a new
3127    /// permit; releasing one restores capacity — and the cap constant itself. (The async `serve`
3128    /// loop has no netstack-free test seam, so the semaphore behavior is exercised directly here, the
3129    /// same `Arc<Semaphore>::try_acquire_owned` the loop uses.)
3130    #[test]
3131    fn forward_inflight_cap_fails_closed_when_saturated() {
3132        use std::sync::Arc;
3133
3134        use tokio::sync::Semaphore;
3135
3136        let inflight = Arc::new(Semaphore::new(MAX_INFLIGHT_FORWARDS));
3137
3138        // Drain every permit (one per concurrently in-flight forward).
3139        let mut held = Vec::with_capacity(MAX_INFLIGHT_FORWARDS);
3140        for _ in 0..MAX_INFLIGHT_FORWARDS {
3141            held.push(
3142                inflight
3143                    .clone()
3144                    .try_acquire_owned()
3145                    .expect("permits available below the cap"),
3146            );
3147        }
3148
3149        // At the cap, the next forward is refused — `serve` would drop the query, not spawn.
3150        assert!(
3151            inflight.clone().try_acquire_owned().is_err(),
3152            "a saturated forward pool must refuse a new permit (fail closed)"
3153        );
3154
3155        // Completing an in-flight forward releases its permit and restores capacity.
3156        drop(held.pop());
3157        assert!(
3158            inflight.clone().try_acquire_owned().is_ok(),
3159            "releasing a permit must let the next forward proceed"
3160        );
3161    }
3162
3163    /// A permit moved into a spawned forward task (the `let _permit = permit;` shape `serve` uses)
3164    /// must stay held for the *whole* task body — across the `.await` on the upstream — and release
3165    /// only when the task completes. This guards the regression the saturation test above can't see:
3166    /// "tidying" `let _permit = permit;` to `let _ = permit;` would drop the permit immediately,
3167    /// re-opening unbounded concurrency while leaving the synchronous drain/restore test green. Here a
3168    /// 1-permit pool is consumed by a task that holds it across a yield; the pool must read empty
3169    /// while the task runs and refill once it finishes.
3170    #[tokio::test]
3171    async fn forward_permit_is_held_for_the_task_lifetime_not_dropped_early() {
3172        use std::sync::Arc;
3173
3174        use tokio::sync::Semaphore;
3175
3176        let inflight = Arc::new(Semaphore::new(1));
3177        let permit = inflight
3178            .clone()
3179            .try_acquire_owned()
3180            .expect("the sole permit is available");
3181
3182        let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3183        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
3184        let task = tokio::spawn(async move {
3185            // Same shape as `serve`'s spawned forward: the permit is a named binding moved into the
3186            // task, so it lives until the body ends — not dropped at the `let`.
3187            let _permit = permit;
3188            started_tx.send(()).unwrap();
3189            // Stand in for the `.await` on the upstream forward.
3190            release_rx.await.unwrap();
3191        });
3192
3193        started_rx.await.unwrap();
3194        // While the task runs, the permit it moved in is still held — the pool is empty.
3195        assert!(
3196            inflight.clone().try_acquire_owned().is_err(),
3197            "a permit moved into a running task must stay held across its await"
3198        );
3199
3200        // Let the task finish; its permit drops with the body and capacity returns.
3201        release_tx.send(()).unwrap();
3202        task.await.unwrap();
3203        assert!(
3204            inflight.clone().try_acquire_owned().is_ok(),
3205            "the permit must be released once the task body completes"
3206        );
3207    }
3208
3209    /// The address of the `n`th fake upstream resolver (RFC 5737 documentation range).
3210    fn upstream_addr(n: u8) -> SocketAddr {
3211        SocketAddr::from((Ipv4Addr::new(198, 51, 100, n), 53))
3212    }
3213
3214    /// Turn `query` into an upstream response: echo the header and question back with `QR` set and
3215    /// `rcode` in the header's low nibble, then append `tail` verbatim, counted as `ancount` answer
3216    /// records. The forwarder relays bytes and never parses past the question, so an opaque tail is
3217    /// what tells two upstreams' responses apart — and stands in for the RFC 8914 extended DNS error
3218    /// a real resolver puts in its own SERVFAIL/REFUSED.
3219    fn upstream_response(query: &[u8], rcode: u8, ancount: u16, tail: &[u8]) -> Vec<u8> {
3220        let mut resp = query.to_vec();
3221        resp[2] |= 0x80; // QR = 1 (this is a response)
3222        resp[3] = (resp[3] & 0xF0) | rcode;
3223        resp[6..8].copy_from_slice(&ancount.to_be_bytes());
3224        resp.extend_from_slice(tail);
3225        resp
3226    }
3227
3228    /// One scripted upstream for [`run_forward_walk`]: the upstream's address, and the
3229    /// `(source address, datagram)` it hands back — `None` when nothing came back at all.
3230    type ScriptedUpstream = (SocketAddr, Option<(SocketAddr, Vec<u8>)>);
3231
3232    /// Run the real [`forward_walk`] over a scripted set of upstreams: each entry is
3233    /// `(upstream, answer)`, where `answer` is the `(source address, datagram)` that upstream hands
3234    /// back (`None` = nothing came back — a timeout, a bind/send/recv failure). Returns the bytes
3235    /// the client would get **and** the upstreams the walk actually asked, so a test can tell "the
3236    /// second upstream answered" apart from "the walk stopped at the first".
3237    ///
3238    /// The script stands in for [`ask_upstream`]'s overlay socket exchange only; every decision
3239    /// under test — the source/transaction-id check, the REFUSED/SERVFAIL soft-error rules, which
3240    /// response is relayed — is made by the production code being called. Every scripted answer
3241    /// arrives over the UDP hop, which is the hop the walk's own rules are about; the TCP hop is
3242    /// [`race_udp_and_tcp`]'s business and is tested there.
3243    async fn run_forward_walk(
3244        script: &[ScriptedUpstream],
3245        query: &[u8],
3246        fallback: Vec<u8>,
3247    ) -> (Vec<u8>, Vec<SocketAddr>) {
3248        let upstreams: Vec<SocketAddr> = script.iter().map(|(upstream, _)| *upstream).collect();
3249        let asked = std::cell::RefCell::new(Vec::new());
3250
3251        let response = forward_walk(
3252            &upstreams,
3253            query,
3254            fallback,
3255            ClientTransport::Udp,
3256            |upstream| {
3257                asked.borrow_mut().push(upstream);
3258                let answer = script
3259                    .iter()
3260                    .find(|(scripted, _)| *scripted == upstream)
3261                    .and_then(|(_, answer)| answer.clone())
3262                    .map(|(from, resp)| UpstreamAnswer {
3263                        from,
3264                        resp,
3265                        via: UpstreamTransport::Udp,
3266                    });
3267                std::future::ready(answer)
3268            },
3269        )
3270        .await;
3271
3272        (response, asked.into_inner())
3273    }
3274
3275    /// A first upstream answering REFUSED must NOT end the forward. A broken or misconfigured
3276    /// resolver refuses instantly and would otherwise beat a healthy one that is still working,
3277    /// handing the stub resolver a refusal as though it were the answer — complete DNS failure
3278    /// wherever a split-DNS route or a fallback list names more than one resolver.
3279    #[tokio::test]
3280    async fn refused_first_upstream_does_not_end_the_walk() {
3281        let query = build_query(0x201, &["api", "example", "com"], 1, 1);
3282        let (first, second) = (upstream_addr(1), upstream_addr(2));
3283        let refusal = upstream_response(&query, RCODE_REFUSED, 0, b"refused");
3284        let answer = upstream_response(&query, 0, 1, b"the real answer");
3285        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3286
3287        let (got, asked) = run_forward_walk(
3288            &[
3289                (first, Some((first, refusal))),
3290                (second, Some((second, answer.clone()))),
3291            ],
3292            &query,
3293            fallback,
3294        )
3295        .await;
3296
3297        assert_eq!(
3298            asked,
3299            vec![first, second],
3300            "a REFUSED from the first upstream must not stop the walk"
3301        );
3302        assert_eq!(
3303            got, answer,
3304            "the healthy second upstream's answer is what reaches the client"
3305        );
3306    }
3307
3308    /// SERVFAIL is soft in the same way: the walk goes on and the healthy upstream's answer wins.
3309    #[tokio::test]
3310    async fn servfail_first_upstream_does_not_end_the_walk() {
3311        let query = build_query(0x202, &["api", "example", "com"], 1, 1);
3312        let (first, second) = (upstream_addr(1), upstream_addr(2));
3313        let soft_fail = upstream_response(&query, RCODE_SERVFAIL, 0, b"servfail");
3314        let answer = upstream_response(&query, 0, 1, b"the real answer");
3315        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3316
3317        let (got, asked) = run_forward_walk(
3318            &[
3319                (first, Some((first, soft_fail))),
3320                (second, Some((second, answer.clone()))),
3321            ],
3322            &query,
3323            fallback,
3324        )
3325        .await;
3326
3327        assert_eq!(asked, vec![first, second], "SERVFAIL is a soft error too");
3328        assert_eq!(
3329            got, answer,
3330            "the second upstream's answer reaches the client"
3331        );
3332    }
3333
3334    /// An RCODE that is *not* soft is an answer: NXDOMAIN ends the walk where it is found, and the
3335    /// upstreams after it are never asked. (Making everything soft would turn a legitimate
3336    /// "no such name" into a needless extra round trip — and, with a second refusing upstream, into
3337    /// a different answer entirely.)
3338    #[tokio::test]
3339    async fn nxdomain_ends_the_walk_at_the_first_upstream() {
3340        let query = build_query(0x203, &["nope", "example", "com"], 1, 1);
3341        let (first, second) = (upstream_addr(1), upstream_addr(2));
3342        let nxdomain = upstream_response(&query, 3, 0, b"no such name");
3343        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3344
3345        let (got, asked) = run_forward_walk(
3346            &[
3347                (first, Some((first, nxdomain.clone()))),
3348                (
3349                    second,
3350                    Some((second, upstream_response(&query, 0, 1, b"late"))),
3351                ),
3352            ],
3353            &query,
3354            fallback,
3355        )
3356        .await;
3357
3358        assert_eq!(asked, vec![first], "NXDOMAIN is an answer: stop asking");
3359        assert_eq!(got, nxdomain, "and it is what the client gets");
3360    }
3361
3362    /// When every upstream refuses, the client gets the FIRST refusal, byte for byte — not the
3363    /// caller's synthesized SERVFAIL. The upstream's own bytes can carry an RFC 8914 extended DNS
3364    /// error explaining the refusal; a locally built packet throws that away.
3365    #[tokio::test]
3366    async fn every_upstream_refusing_returns_the_first_refusal_verbatim() {
3367        let query = build_query(0x204, &["api", "example", "com"], 1, 1);
3368        let (first, second) = (upstream_addr(1), upstream_addr(2));
3369        let first_refusal =
3370            upstream_response(&query, RCODE_REFUSED, 0, b"first refusal + extended error");
3371        let second_refusal = upstream_response(&query, RCODE_REFUSED, 0, b"second refusal");
3372        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3373
3374        let (got, asked) = run_forward_walk(
3375            &[
3376                (first, Some((first, first_refusal.clone()))),
3377                (second, Some((second, second_refusal.clone()))),
3378            ],
3379            &query,
3380            fallback.clone(),
3381        )
3382        .await;
3383
3384        assert_eq!(
3385            asked,
3386            vec![first, second],
3387            "every upstream is given its turn"
3388        );
3389        assert_eq!(
3390            got, first_refusal,
3391            "an all-refused forward relays the first upstream's own REFUSED bytes"
3392        );
3393        assert_ne!(
3394            got, fallback,
3395            "the synthesized SERVFAIL must not replace an upstream's own response"
3396        );
3397        assert_ne!(got, second_refusal, "the FIRST refusal is the one kept");
3398    }
3399
3400    /// The first *soft* response is the one kept whichever code it carried: a SERVFAIL followed by a
3401    /// REFUSED relays the upstream's own SERVFAIL, extended error and all, rather than the
3402    /// synthesized one the caller supplied.
3403    #[tokio::test]
3404    async fn every_upstream_soft_failing_returns_the_upstream_servfail_not_the_fallback() {
3405        let query = build_query(0x205, &["api", "example", "com"], 1, 1);
3406        let (first, second) = (upstream_addr(1), upstream_addr(2));
3407        let upstream_servfail = upstream_response(
3408            &query,
3409            RCODE_SERVFAIL,
3410            0,
3411            b"upstream servfail + extended error",
3412        );
3413        let refusal = upstream_response(&query, RCODE_REFUSED, 0, b"second refusal");
3414        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3415
3416        let (got, _asked) = run_forward_walk(
3417            &[
3418                (first, Some((first, upstream_servfail.clone()))),
3419                (second, Some((second, refusal))),
3420            ],
3421            &query,
3422            fallback.clone(),
3423        )
3424        .await;
3425
3426        assert_eq!(
3427            got, upstream_servfail,
3428            "the upstream's own SERVFAIL is relayed verbatim, keeping any extended DNS error"
3429        );
3430        assert_ne!(got, fallback, "not the locally synthesized SERVFAIL");
3431    }
3432
3433    /// A lone upstream that refuses still has its refusal relayed: with nothing else to wait for,
3434    /// treating REFUSED as soft changes nothing about what the client is told.
3435    #[tokio::test]
3436    async fn lone_refusing_upstream_still_has_its_refusal_relayed() {
3437        let query = build_query(0x206, &["api", "example", "com"], 1, 1);
3438        let only = upstream_addr(1);
3439        let refusal = upstream_response(&query, RCODE_REFUSED, 0, b"refused");
3440        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3441
3442        let (got, asked) =
3443            run_forward_walk(&[(only, Some((only, refusal.clone())))], &query, fallback).await;
3444
3445        assert_eq!(asked, vec![only]);
3446        assert_eq!(
3447            got, refusal,
3448            "a single upstream's REFUSED is the client's answer"
3449        );
3450    }
3451
3452    /// The anti-poisoning check still runs BEFORE any of the soft-error handling: a datagram whose
3453    /// transaction id is not the one we asked with is discarded outright and never remembered as
3454    /// "the first REFUSED", so an off-path injector cannot plant the response an all-refused forward
3455    /// ends up relaying.
3456    #[tokio::test]
3457    async fn wrong_transaction_id_response_is_discarded_not_remembered_as_a_soft_error() {
3458        let query = build_query(0x207, &["api", "example", "com"], 1, 1);
3459        let only = upstream_addr(1);
3460        let mut poisoned = upstream_response(&query, RCODE_REFUSED, 0, b"injected");
3461        poisoned[0] ^= 0xFF; // a transaction id we never asked with
3462        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3463
3464        let (got, _asked) = run_forward_walk(
3465            &[(only, Some((only, poisoned.clone())))],
3466            &query,
3467            fallback.clone(),
3468        )
3469        .await;
3470
3471        assert_ne!(
3472            got, poisoned,
3473            "a mismatched transaction id must never be relayed"
3474        );
3475        assert_eq!(
3476            got, fallback,
3477            "with the datagram discarded nothing answered, so the synthesized fallback stands"
3478        );
3479    }
3480
3481    /// The same for the source check: a well-formed REFUSED that echoes the question and the
3482    /// transaction id but arrives from an address we did not query is discarded before it can become
3483    /// the forward's remembered soft error.
3484    #[tokio::test]
3485    async fn off_path_source_response_is_discarded_not_remembered_as_a_soft_error() {
3486        let query = build_query(0x208, &["api", "example", "com"], 1, 1);
3487        let (only, off_path) = (upstream_addr(1), upstream_addr(9));
3488        let poisoned = upstream_response(&query, RCODE_REFUSED, 0, b"injected");
3489        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3490
3491        let (got, _asked) = run_forward_walk(
3492            &[(only, Some((off_path, poisoned.clone())))],
3493            &query,
3494            fallback.clone(),
3495        )
3496        .await;
3497
3498        assert_ne!(
3499            got, poisoned,
3500            "a datagram from an unqueried source must never be relayed"
3501        );
3502        assert_eq!(
3503            got, fallback,
3504            "with the datagram discarded nothing answered, so the synthesized fallback stands"
3505        );
3506    }
3507
3508    /// An upstream that says nothing at all (timeout, bind/send/recv failure) is simply skipped, and
3509    /// the next upstream's answer is what the client gets.
3510    #[tokio::test]
3511    async fn silent_upstream_is_skipped_for_the_next_one() {
3512        let query = build_query(0x209, &["api", "example", "com"], 1, 1);
3513        let (first, second) = (upstream_addr(1), upstream_addr(2));
3514        let answer = upstream_response(&query, 0, 1, b"the real answer");
3515        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3516
3517        let (got, asked) = run_forward_walk(
3518            &[(first, None), (second, Some((second, answer.clone())))],
3519            &query,
3520            fallback,
3521        )
3522        .await;
3523
3524        assert_eq!(asked, vec![first, second]);
3525        assert_eq!(got, answer);
3526    }
3527
3528    /// A REFUSED is relayed only when EVERY upstream refused. Once a later upstream SERVFAILs, the
3529    /// forward did not fail by policy alone, and Go's forwarder answers SERVFAIL: its first error is
3530    /// not a SERVFAIL response, so it synthesizes one rather than relaying the refusal.
3531    #[tokio::test]
3532    async fn refusal_then_servfail_is_the_fallback_not_the_refusal() {
3533        let query = build_query(0x20A, &["api", "example", "com"], 1, 1);
3534        let (first, second) = (upstream_addr(1), upstream_addr(2));
3535        let refusal = upstream_response(&query, RCODE_REFUSED, 0, b"refused");
3536        let servfail = upstream_response(&query, RCODE_SERVFAIL, 0, b"second servfail");
3537        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3538
3539        let (got, asked) = run_forward_walk(
3540            &[
3541                (first, Some((first, refusal.clone()))),
3542                (second, Some((second, servfail.clone()))),
3543            ],
3544            &query,
3545            fallback.clone(),
3546        )
3547        .await;
3548
3549        assert_eq!(asked, vec![first, second]);
3550        assert_eq!(
3551            got, fallback,
3552            "a refusal mixed with a SERVFAIL is a SERVFAIL"
3553        );
3554        assert_ne!(got, refusal, "the first refusal must not be relayed");
3555        assert_ne!(
3556            got, servfail,
3557            "nor a later SERVFAIL that was not the first failure"
3558        );
3559    }
3560
3561    /// The same when the later upstream never answers: a transport failure is not a refusal, so the
3562    /// forward is no longer "every upstream refused" and the client gets the SERVFAIL fallback.
3563    #[tokio::test]
3564    async fn refusal_then_silence_is_the_fallback_not_the_refusal() {
3565        let query = build_query(0x20B, &["api", "example", "com"], 1, 1);
3566        let (first, second) = (upstream_addr(1), upstream_addr(2));
3567        let refusal = upstream_response(&query, RCODE_REFUSED, 0, b"refused");
3568        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3569
3570        let (got, asked) = run_forward_walk(
3571            &[(first, Some((first, refusal.clone()))), (second, None)],
3572            &query,
3573            fallback.clone(),
3574        )
3575        .await;
3576
3577        assert_eq!(asked, vec![first, second]);
3578        assert_eq!(got, fallback, "a refusal then no answer is a SERVFAIL");
3579    }
3580
3581    /// A datagram the anti-poisoning check discards counts as a non-refusal failure too (Go's
3582    /// transaction-id mismatch is an error that is not `errRefused`), so it also stops an earlier
3583    /// refusal from being relayed.
3584    #[tokio::test]
3585    async fn refusal_then_mismatched_response_is_the_fallback_not_the_refusal() {
3586        let query = build_query(0x20C, &["api", "example", "com"], 1, 1);
3587        let (first, second) = (upstream_addr(1), upstream_addr(2));
3588        let refusal = upstream_response(&query, RCODE_REFUSED, 0, b"refused");
3589        let mut mismatched = upstream_response(&query, RCODE_REFUSED, 0, b"wrong txid");
3590        mismatched[0] ^= 0xFF;
3591        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3592
3593        let (got, _asked) = run_forward_walk(
3594            &[
3595                (first, Some((first, refusal))),
3596                (second, Some((second, mismatched))),
3597            ],
3598            &query,
3599            fallback.clone(),
3600        )
3601        .await;
3602
3603        assert_eq!(got, fallback);
3604    }
3605
3606    /// An upstream SERVFAIL is relayed only when it was the FIRST failure. After an upstream that
3607    /// never answered, Go's first error is a transport error, not a SERVFAIL response, so it
3608    /// synthesizes the SERVFAIL rather than relaying the later upstream's.
3609    #[tokio::test]
3610    async fn silence_then_servfail_is_the_fallback_not_the_upstream_servfail() {
3611        let query = build_query(0x20D, &["api", "example", "com"], 1, 1);
3612        let (first, second) = (upstream_addr(1), upstream_addr(2));
3613        let servfail = upstream_response(&query, RCODE_SERVFAIL, 0, b"second servfail");
3614        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3615
3616        let (got, asked) = run_forward_walk(
3617            &[(first, None), (second, Some((second, servfail.clone())))],
3618            &query,
3619            fallback.clone(),
3620        )
3621        .await;
3622
3623        assert_eq!(asked, vec![first, second]);
3624        assert_eq!(got, fallback);
3625        assert_ne!(got, servfail);
3626    }
3627
3628    /// The mirror of the case above, and the only branch that still relays an upstream's bytes once
3629    /// a non-refusal has been seen: the FIRST failure is an upstream SERVFAIL and the second
3630    /// upstream is silent. The held SERVFAIL is relayed verbatim, so its RFC 8914 extended DNS error
3631    /// survives rather than being thrown away for the synthesized one.
3632    ///
3633    /// No other test reaches that branch with `saw_non_refused` set by an upstream that sent
3634    /// nothing at all, so this one pins that a later failure carrying no bytes neither replaces the
3635    /// held SERVFAIL nor demotes the forward to `fallback`.
3636    #[tokio::test]
3637    async fn servfail_then_silent_upstream_relays_the_upstream_servfail() {
3638        let query = build_query(0x20e, &["api", "example", "com"], 1, 1);
3639        let (first, second) = (upstream_addr(1), upstream_addr(2));
3640        let servfail = upstream_response(&query, RCODE_SERVFAIL, 0, b"first servfail");
3641        let fallback = upstream_response(&query, RCODE_SERVFAIL, 0, b"synthesized");
3642
3643        let (got, asked) = run_forward_walk(
3644            &[(first, Some((first, servfail.clone()))), (second, None)],
3645            &query,
3646            fallback,
3647        )
3648        .await;
3649
3650        assert_eq!(
3651            asked,
3652            vec![first, second],
3653            "the walk must ask both upstreams"
3654        );
3655        assert_eq!(
3656            got, servfail,
3657            "the first failure's own SERVFAIL bytes are relayed, not the synthesized fallback"
3658        );
3659    }
3660
3661    #[test]
3662    fn longest_suffix_route_wins() {
3663        let mut routes = std::collections::BTreeMap::new();
3664        routes.insert("example".to_string(), vec![udp("10.0.0.1:53")]);
3665        routes.insert("corp.example".to_string(), vec![udp("10.0.0.2:53")]);
3666        let view = view_with_routes(routes, vec![], vec![]);
3667        let buf = build_query(0x101, &["api", "corp", "example"], 1, 1);
3668
3669        match decide(&view, &buf).expect("decides") {
3670            Decision::Forward { upstreams, .. } => {
3671                assert_eq!(
3672                    upstreams,
3673                    vec!["10.0.0.2:53".parse().unwrap()],
3674                    "longer suffix wins"
3675                );
3676            }
3677            Decision::Reply(_) => panic!("expected forward"),
3678        }
3679    }
3680
3681    #[test]
3682    fn negative_route_is_nxdomain_not_forwarded() {
3683        // An empty upstream list is a negative route: fail closed, never forward.
3684        let mut routes = std::collections::BTreeMap::new();
3685        routes.insert("blocked.example".to_string(), vec![]);
3686        let view = view_with_routes(routes, vec![udp("8.8.8.8:53")], vec![]);
3687        let buf = build_query(0x102, &["x", "blocked", "example"], 1, 1);
3688
3689        match decide(&view, &buf).expect("decides") {
3690            Decision::Reply(resp) => {
3691                let (_, rcode, _) = parse_header(&resp);
3692                assert_eq!(rcode, 3, "NxDomain: negative route is not forwarded");
3693            }
3694            Decision::Forward { .. } => panic!("negative route must not forward"),
3695        }
3696    }
3697
3698    #[test]
3699    fn unrouted_name_forwards_to_fallback_then_global() {
3700        // No route matches: fallback resolvers are preferred over global resolvers.
3701        let view = view_with_routes(
3702            std::collections::BTreeMap::new(),
3703            vec![udp("8.8.8.8:53")],
3704            vec![udp("1.1.1.1:53")],
3705        );
3706        let buf = build_query(0x103, &["example", "com"], 1, 1);
3707
3708        match decide(&view, &buf).expect("decides") {
3709            Decision::Forward { upstreams, .. } => {
3710                assert_eq!(
3711                    upstreams,
3712                    vec!["1.1.1.1:53".parse().unwrap()],
3713                    "fallback preferred"
3714                );
3715            }
3716            Decision::Reply(_) => panic!("expected forward to fallback"),
3717        }
3718    }
3719
3720    #[test]
3721    fn unrouted_name_forwards_to_global_when_no_fallback() {
3722        let view = view_with_routes(
3723            std::collections::BTreeMap::new(),
3724            vec![udp("8.8.8.8:53")],
3725            vec![],
3726        );
3727        let buf = build_query(0x104, &["example", "com"], 1, 1);
3728
3729        match decide(&view, &buf).expect("decides") {
3730            Decision::Forward { upstreams, .. } => {
3731                assert_eq!(upstreams, vec!["8.8.8.8:53".parse().unwrap()]);
3732            }
3733            Decision::Reply(_) => panic!("expected forward to global resolver"),
3734        }
3735    }
3736
3737    #[test]
3738    fn tailnet_name_is_never_forwarded() {
3739        // Anti-leak: a name under a tailnet search domain that has no overlay match must fail
3740        // closed to NXDOMAIN, never leak to an upstream resolver, even with resolvers configured.
3741        let view = view_with_routes(
3742            std::collections::BTreeMap::new(),
3743            vec![udp("8.8.8.8:53")],
3744            vec![udp("1.1.1.1:53")],
3745        );
3746        // "ghost.user.ts.net" is under the tailnet suffix but matches no peer.
3747        let buf = build_query(0x105, &["ghost", "user", "ts", "net"], 1, 1);
3748
3749        match decide(&view, &buf).expect("decides") {
3750            Decision::Reply(resp) => {
3751                let (_, rcode, _) = parse_header(&resp);
3752                assert_eq!(rcode, 3, "NxDomain: tailnet name not leaked upstream");
3753            }
3754            Decision::Forward { .. } => panic!("tailnet name must never be forwarded"),
3755        }
3756    }
3757
3758    #[test]
3759    fn no_resolvers_off_tailnet_is_servfail_not_nxdomain() {
3760        // No route, no resolvers: an OFF-tailnet name cannot be forwarded. Go answers SERVFAIL
3761        // (forwarder.go:1207 "no upstream resolvers set, returning SERVFAIL"), NOT NXDOMAIN — a
3762        // cacheable non-existence for a real name we merely couldn't forward would poison downstream
3763        // stub caches. We still never forward (the name does not leak); we just soft-fail.
3764        let view = view_with_routes(std::collections::BTreeMap::new(), vec![], vec![]);
3765        let buf = build_query(0x106, &["example", "com"], 1, 1);
3766
3767        match decide(&view, &buf).expect("decides") {
3768            Decision::Reply(resp) => {
3769                let (_, rcode, _) = parse_header(&resp);
3770                assert_eq!(
3771                    rcode, 2,
3772                    "ServFail: off-tailnet name with no upstream to forward to"
3773                );
3774            }
3775            Decision::Forward { .. } => panic!("must not forward with no resolvers"),
3776        }
3777    }
3778
3779    #[test]
3780    fn route_with_only_ipv6_upstreams_off_tailnet_is_servfail() {
3781        // A split-DNS route exists but every resolver is IPv6 (filtered out under the IPv4-only
3782        // egress): we have a route yet nowhere to forward. That is an inability to forward an
3783        // off-tailnet name, so SERVFAIL (soft), not a fabricated NXDOMAIN.
3784        let mut routes = std::collections::BTreeMap::new();
3785        routes.insert("corp.example".to_string(), vec![udp("[2001:db8::53]:53")]);
3786        let view = view_with_routes(routes, vec![], vec![]);
3787        let buf = build_query(0x108, &["host", "corp", "example"], 1, 1);
3788
3789        match decide(&view, &buf).expect("decides") {
3790            Decision::Reply(resp) => {
3791                let (_, rcode, _) = parse_header(&resp);
3792                assert_eq!(
3793                    rcode, 2,
3794                    "ServFail: route's resolvers all filtered out (IPv6-only), cannot forward"
3795                );
3796            }
3797            Decision::Forward { .. } => panic!("must not forward when all upstreams are filtered"),
3798        }
3799    }
3800
3801    #[test]
3802    fn overlay_match_wins_over_forwarding() {
3803        // A known peer name resolves authoritatively even when upstream resolvers are configured.
3804        let mut db = PeerDb::default();
3805        db.upsert(&test_node());
3806        let view = DnsView {
3807            cfg: DnsConfig {
3808                magic_dns: true,
3809                search_domains: vec!["user.ts.net".to_string()],
3810                resolvers: vec![udp("8.8.8.8:53")],
3811                ..Default::default()
3812            },
3813            peers: Some(Arc::new(db)),
3814            self_node: None,
3815            exit_doh: None,
3816            enable_ipv6: false,
3817            accept_dns: true,
3818        };
3819        let buf = build_query(0x107, &["host", "user", "ts", "net"], 1, 1);
3820
3821        match decide(&view, &buf).expect("decides") {
3822            Decision::Reply(resp) => {
3823                let (_, rcode, ancount) = parse_header(&resp);
3824                assert_eq!(rcode, 0, "authoritative answer wins");
3825                assert_eq!(ancount, 1);
3826            }
3827            Decision::Forward { .. } => panic!("overlay match must not forward"),
3828        }
3829    }
3830
3831    #[test]
3832    fn ipv6_reverse_ptr_is_nxdomain_not_forwarded() {
3833        // Anti-leak: an `ip6.arpa` reverse PTR for a tailnet ULA (fd7a:…) must fail closed to
3834        // NXDOMAIN, never be forwarded — even with an upstream resolver configured. This fork is
3835        // IPv4-only on the tailnet; forwarding would reveal that a v6 address was probed.
3836        let view = view_with_routes(
3837            std::collections::BTreeMap::new(),
3838            vec![udp("8.8.8.8:53")],
3839            vec![udp("1.1.1.1:53")],
3840        );
3841        // Reverse name for fd7a::1 (nibble-reversed) under ip6.arpa. The exact nibble labels don't
3842        // matter to the guard — any name ending in ip6.arpa must fail closed.
3843        let labels = vec![
3844            "1", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0",
3845            "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "a", "7", "d", "f", "ip6",
3846            "arpa",
3847        ];
3848        let buf = build_query(0x200, &labels, 12, 1);
3849
3850        match decide(&view, &buf).expect("decides") {
3851            Decision::Reply(resp) => {
3852                let (_, rcode, _) = parse_header(&resp);
3853                assert_eq!(
3854                    rcode, 3,
3855                    "NxDomain: ip6.arpa reverse must not leak upstream"
3856                );
3857            }
3858            Decision::Forward { .. } => panic!("ip6.arpa PTR must never be forwarded"),
3859        }
3860    }
3861
3862    /// Both transports a client can reach this node over, for the race rules that hold for either.
3863    const BOTH_CLIENTS: [ClientTransport; 2] = [ClientTransport::Udp, ClientTransport::Tcp];
3864
3865    /// A UDP hop that hears nothing and gives up the way [`ask_upstream_udp`] does: after
3866    /// [`UPSTREAM_TIMEOUT`], with `None`. A resolver whose UDP path drops or stalls.
3867    async fn silent_udp_hop() -> Option<(SocketAddr, Vec<u8>)> {
3868        tokio::time::sleep(UPSTREAM_TIMEOUT).await;
3869        None
3870    }
3871
3872    /// The retry this module exists for: an upstream answer with the `TC` bit set is re-asked over
3873    /// TCP, and the TCP answer — not the truncated datagram — is what the walk relays.
3874    ///
3875    /// Two of Go's bounds are pinned here alongside it. The retry goes to the **same** resolver
3876    /// (walking on would hand the query to a resolver the first one already answered), which is why
3877    /// this asserts on `from`; and it re-sends the **same** query, which is why the TCP hop records
3878    /// the bytes it was asked for.
3879    #[tokio::test]
3880    async fn a_truncated_udp_answer_is_retried_over_tcp_to_the_same_resolver() {
3881        let query = build_query(0x400, &["big", "example", "com"], 16, 1);
3882        let upstream = upstream_addr(1);
3883        // What the UDP hop can carry: the resolver cut the record set and said so.
3884        let mut truncated = upstream_response(&query, 0, 1, b"the part that fit");
3885        truncated[2] |= 0x02; // TC
3886        // What TCP can carry: the whole set, far past what any datagram here could hold.
3887        let whole = upstream_response(&query, 0, 40, &[0xAB; 8000]);
3888
3889        for client in BOTH_CLIENTS {
3890            let retries = std::cell::Cell::new(0);
3891            let answer = race_udp_and_tcp(
3892                upstream,
3893                &query,
3894                client,
3895                TcpRetry::Enabled,
3896                || std::future::ready(Some((upstream, truncated.clone()))),
3897                || {
3898                    retries.set(retries.get() + 1);
3899                    std::future::ready(Some(whole.clone()))
3900                },
3901            )
3902            .await
3903            .expect("the upstream answered");
3904
3905            assert_eq!(
3906                answer.resp, whole,
3907                "{client:?}: the TCP answer replaces the truncated datagram"
3908            );
3909            assert_eq!(
3910                answer.via,
3911                UpstreamTransport::Tcp,
3912                "{client:?}: and is labelled as having come over TCP, so the relay cap knows not to \
3913                 chop it"
3914            );
3915            assert_eq!(
3916                answer.from, upstream,
3917                "{client:?}: the retry goes to the SAME resolver, never on to the next one"
3918            );
3919            assert_eq!(
3920                retries.get(),
3921                1,
3922                "{client:?}: one retry, not one per upstream and not one per attempt"
3923            );
3924        }
3925    }
3926
3927    /// Go falls back to the truncated UDP response when the TCP retry fails, rather than to
3928    /// SERVFAIL. A truncated answer still has an intact header and question and a `TC` bit the stub
3929    /// resolver can act on; a synthesized failure is strictly less than we already had.
3930    #[tokio::test]
3931    async fn a_failed_tcp_retry_relays_the_truncated_udp_answer() {
3932        let query = build_query(0x401, &["big", "example", "com"], 16, 1);
3933        let upstream = upstream_addr(1);
3934        let mut truncated = upstream_response(&query, 0, 1, b"the part that fit");
3935        truncated[2] |= 0x02; // TC
3936
3937        for client in BOTH_CLIENTS {
3938            let answer = race_udp_and_tcp(
3939                upstream,
3940                &query,
3941                client,
3942                TcpRetry::Enabled,
3943                || std::future::ready(Some((upstream, truncated.clone()))),
3944                // The resolver refused the connection, reset it, or never framed an answer.
3945                || std::future::ready(None),
3946            )
3947            .await
3948            .expect("a failed retry must not lose the answer we already had");
3949
3950            assert_eq!(
3951                answer.resp, truncated,
3952                "{client:?}: the truncated UDP answer is relayed, not a synthesized failure"
3953            );
3954            assert_eq!(
3955                answer.via,
3956                UpstreamTransport::Udp,
3957                "{client:?}: and it is still a datagram answer, so the relay cap still applies to it"
3958            );
3959        }
3960    }
3961
3962    /// A reply that fit is never retried: the retry exists for the answer that did not, and for
3963    /// nothing else. For a UDP client, whose UDP hop has the resolver to itself for
3964    /// [`UDP_RACE_TIMEOUT`], a prompt answer that fit means no TCP connection at all — one per
3965    /// forwarded query would double the work this node makes every upstream resolver do.
3966    #[tokio::test]
3967    async fn an_answer_that_is_not_truncated_is_never_retried() {
3968        let query = build_query(0x402, &["example", "com"], 1, 1);
3969        let upstream = upstream_addr(1);
3970        let fits = upstream_response(&query, 0, 1, b"a small answer");
3971
3972        let retried = std::cell::Cell::new(false);
3973        let answer = race_udp_and_tcp(
3974            upstream,
3975            &query,
3976            ClientTransport::Udp,
3977            TcpRetry::Enabled,
3978            || std::future::ready(Some((upstream, fits.clone()))),
3979            || {
3980                retried.set(true);
3981                std::future::ready(None)
3982            },
3983        )
3984        .await
3985        .expect("the upstream answered");
3986
3987        assert!(
3988            !retried.get(),
3989            "an untruncated answer must not open a TCP connection"
3990        );
3991        assert_eq!(answer.resp, fits);
3992        assert_eq!(answer.via, UpstreamTransport::Udp);
3993    }
3994
3995    /// A TCP client gets no UDP head start — Go's race timeout is 0 for it — so its TCP hop is
3996    /// already running when the UDP answer lands. An answer that fit still wins: the TCP hop is
3997    /// dropped, not waited for, and its answer never replaces the one UDP brought.
3998    #[tokio::test(start_paused = true)]
3999    async fn a_tcp_client_races_both_hops_and_an_answer_that_fit_still_wins() {
4000        let query = build_query(0x409, &["example", "com"], 1, 1);
4001        let upstream = upstream_addr(1);
4002        let fits = upstream_response(&query, 0, 1, b"a small answer");
4003        let started = tokio::time::Instant::now();
4004
4005        let tcp_started_at = std::cell::Cell::new(None);
4006        let answer = race_udp_and_tcp(
4007            upstream,
4008            &query,
4009            ClientTransport::Tcp,
4010            TcpRetry::Enabled,
4011            async || {
4012                tokio::time::sleep(Duration::from_millis(100)).await;
4013                Some((upstream, fits.clone()))
4014            },
4015            || {
4016                tcp_started_at.set(Some(started.elapsed()));
4017                // A TCP hop still connecting when UDP answers.
4018                std::future::pending()
4019            },
4020        )
4021        .await
4022        .expect("the upstream answered");
4023
4024        assert_eq!(
4025            tcp_started_at.get(),
4026            Some(Duration::ZERO),
4027            "a TCP client's TCP hop starts alongside the UDP hop, not after a head start"
4028        );
4029        assert_eq!(answer.resp, fits, "the answer that fit wins the race");
4030        assert_eq!(answer.via, UpstreamTransport::Udp);
4031        assert_eq!(
4032            started.elapsed(),
4033            Duration::from_millis(100),
4034            "and the race ends when it lands, without waiting on the TCP hop"
4035        );
4036    }
4037
4038    /// Behaviour (1) of Go's race: a resolver that never answers over UDP but answers over TCP
4039    /// still resolves — once the UDP hop's head start runs out, not once [`UPSTREAM_TIMEOUT`] does.
4040    /// Before the race, the UDP hop was waited out in full and returned nothing, so the walk moved on
4041    /// to the next resolver or ended in SERVFAIL without TCP ever being tried.
4042    #[tokio::test(start_paused = true)]
4043    async fn a_silent_udp_hop_falls_back_to_tcp_after_the_race_timeout() {
4044        let query = build_query(0x40A, &["example", "com"], 1, 1);
4045        let upstream = upstream_addr(1);
4046        let over_tcp = upstream_response(&query, 0, 1, b"answered over tcp");
4047
4048        for (client, head_start) in [
4049            (ClientTransport::Udp, UDP_RACE_TIMEOUT),
4050            (ClientTransport::Tcp, Duration::ZERO),
4051        ] {
4052            let started = tokio::time::Instant::now();
4053            let tcp_started_at = std::cell::Cell::new(None);
4054            let answer = race_udp_and_tcp(
4055                upstream,
4056                &query,
4057                client,
4058                TcpRetry::Enabled,
4059                silent_udp_hop,
4060                || {
4061                    tcp_started_at.set(Some(started.elapsed()));
4062                    std::future::ready(Some(over_tcp.clone()))
4063                },
4064            )
4065            .await
4066            .expect("a resolver that answers over TCP resolves");
4067
4068            assert_eq!(answer.resp, over_tcp, "{client:?}");
4069            assert_eq!(answer.via, UpstreamTransport::Tcp, "{client:?}");
4070            assert_eq!(answer.from, upstream, "{client:?}: the same resolver");
4071            assert_eq!(
4072                tcp_started_at.get(),
4073                Some(head_start),
4074                "{client:?}: TCP joins the race when the UDP hop's head start runs out"
4075            );
4076            assert!(
4077                started.elapsed() < UPSTREAM_TIMEOUT,
4078                "{client:?}: the answer must not wait out the UDP hop's own timeout ({:?})",
4079                started.elapsed()
4080            );
4081        }
4082    }
4083
4084    /// A UDP hop that has already **failed** does not sit out its head start: Go's `Race.Start`
4085    /// starts the fallback the moment the first function returns an error, and so does this.
4086    #[tokio::test(start_paused = true)]
4087    async fn a_failed_udp_hop_starts_tcp_at_once() {
4088        let query = build_query(0x40B, &["example", "com"], 1, 1);
4089        let upstream = upstream_addr(1);
4090        let over_tcp = upstream_response(&query, 0, 1, b"answered over tcp");
4091        let started = tokio::time::Instant::now();
4092
4093        let answer = race_udp_and_tcp(
4094            upstream,
4095            &query,
4096            ClientTransport::Udp,
4097            TcpRetry::Enabled,
4098            // Bind or send failed: nothing will come back.
4099            || std::future::ready(None),
4100            || std::future::ready(Some(over_tcp.clone())),
4101        )
4102        .await
4103        .expect("the TCP hop answered");
4104
4105        assert_eq!(answer.resp, over_tcp);
4106        assert_eq!(answer.via, UpstreamTransport::Tcp);
4107        assert_eq!(
4108            started.elapsed(),
4109            Duration::ZERO,
4110            "no head start is waited out for a hop that already failed"
4111        );
4112
4113        // And when both hops fail there is no answer, so the walk moves on.
4114        let answer = race_udp_and_tcp(
4115            upstream,
4116            &query,
4117            ClientTransport::Udp,
4118            TcpRetry::Enabled,
4119            || std::future::ready(None),
4120            || std::future::ready(None),
4121        )
4122        .await;
4123        assert!(answer.is_none(), "both hops failed: nothing to relay");
4124    }
4125
4126    /// The race is symmetric: a TCP hop that fails first leaves it to the UDP hop, which is waited
4127    /// for rather than abandoned.
4128    #[tokio::test(start_paused = true)]
4129    async fn a_failed_tcp_hop_leaves_the_race_to_the_udp_hop() {
4130        let query = build_query(0x40C, &["example", "com"], 1, 1);
4131        let upstream = upstream_addr(1);
4132        let over_udp = upstream_response(&query, 0, 1, b"answered over udp");
4133
4134        let answer = race_udp_and_tcp(
4135            upstream,
4136            &query,
4137            ClientTransport::Tcp,
4138            TcpRetry::Enabled,
4139            async || {
4140                tokio::time::sleep(Duration::from_secs(1)).await;
4141                Some((upstream, over_udp.clone()))
4142            },
4143            || std::future::ready(None),
4144        )
4145        .await
4146        .expect("the UDP hop answered after the TCP hop failed");
4147
4148        assert_eq!(answer.resp, over_udp);
4149        assert_eq!(answer.via, UpstreamTransport::Udp);
4150    }
4151
4152    /// The race sits inside the walk, not beside it: a first resolver that is silent over UDP but
4153    /// answers over TCP is the resolver that answers. The walk never reaches the second one, and the
4154    /// client has its answer after the head start, not after the UDP hop's full timeout.
4155    #[tokio::test(start_paused = true)]
4156    async fn a_resolver_silent_over_udp_answers_the_walk_over_tcp() {
4157        let query = build_query(0x40D, &["example", "com"], 1, 1);
4158        let first = upstream_addr(1);
4159        let second = upstream_addr(2);
4160        let over_tcp = upstream_response(&query, 0, 1, b"the first resolver over tcp");
4161        let started = tokio::time::Instant::now();
4162
4163        let asked = std::cell::RefCell::new(Vec::new());
4164        let response = forward_walk(
4165            &[first, second],
4166            &query,
4167            servfail_response(),
4168            ClientTransport::Udp,
4169            |upstream| {
4170                asked.borrow_mut().push(upstream);
4171                let over_tcp = over_tcp.clone();
4172                race_udp_and_tcp(
4173                    upstream,
4174                    &query,
4175                    ClientTransport::Udp,
4176                    TcpRetry::Enabled,
4177                    silent_udp_hop,
4178                    move || std::future::ready(Some(over_tcp)),
4179                )
4180            },
4181        )
4182        .await;
4183
4184        assert_eq!(response, over_tcp);
4185        assert_eq!(
4186            asked.into_inner(),
4187            vec![first],
4188            "the resolver that answered over TCP ends the walk"
4189        );
4190        assert_eq!(started.elapsed(), UDP_RACE_TIMEOUT);
4191    }
4192
4193    /// The other way an answer did not fit: the upstream sent one this forwarder cannot relay in a
4194    /// single datagram ([`MAX_UPSTREAM_RESPONSE`]), with `TC` clear because it fit the resolver's
4195    /// own idea of a datagram. [`cap_response`] is about to cut it and set `TC` — so the retry has
4196    /// to fire on it too, or that name resolves only ever as a cut answer. Go detects the same
4197    /// case by reading into a `maxResponseBytes+1` buffer and setting `TC` on the reply it hands
4198    /// its own truncation check.
4199    #[tokio::test]
4200    async fn an_oversize_answer_is_retried_even_with_tc_clear() {
4201        let query = build_edns_query(0x407, &["big", "example", "com"], 16, 1, 4096);
4202        let upstream = upstream_addr(1);
4203        // The one oversize datagram the netstack's 4096-byte receive ring can deliver, TC clear.
4204        let mut full_ring = upstream_response(&query, 0, 1, b"");
4205        full_ring.resize(MAX_UPSTREAM_RESPONSE + 1, 0xAB);
4206        assert_eq!(
4207            full_ring[2] & 0x02,
4208            0,
4209            "the upstream did not mark it truncated"
4210        );
4211        let whole = upstream_response(&query, 0, 40, &[0xAB; 8000]);
4212
4213        for client in BOTH_CLIENTS {
4214            let answer = race_udp_and_tcp(
4215                upstream,
4216                &query,
4217                client,
4218                TcpRetry::Enabled,
4219                || std::future::ready(Some((upstream, full_ring.clone()))),
4220                || std::future::ready(Some(whole.clone())),
4221            )
4222            .await
4223            .expect("the upstream answered");
4224
4225            assert_eq!(
4226                answer.resp, whole,
4227                "{client:?}: an answer too big for one datagram is re-asked over TCP, TC bit or no \
4228                 TC bit"
4229            );
4230            assert_eq!(answer.via, UpstreamTransport::Tcp);
4231        }
4232    }
4233
4234    /// `dns-forwarder-disable-tcp-retries` is the TCP hop's **off** switch, and the polarity is the
4235    /// whole point: a node control never set it on keeps using TCP. Read from the self node through
4236    /// the view, and honoured at the one place that would open the connection — on **every** arm of
4237    /// the race: not for a truncated answer, not for a silent UDP hop once its head start runs out,
4238    /// and not alongside UDP for a TCP client.
4239    #[tokio::test(start_paused = true)]
4240    async fn the_node_attribute_turns_the_tcp_retry_off() {
4241        let mut view = view_with_peer();
4242        assert_eq!(
4243            view.upstream_tcp_retry(),
4244            TcpRetry::Enabled,
4245            "no self node yet ⇒ the retry is on, because on is the default"
4246        );
4247
4248        let mut node = test_node();
4249        view.self_node = Some(node.clone());
4250        assert_eq!(
4251            view.upstream_tcp_retry(),
4252            TcpRetry::Enabled,
4253            "a self node without the attribute ⇒ still on"
4254        );
4255
4256        node.cap_map
4257            .insert("dns-forwarder-disable-tcp-retries".to_string(), vec![]);
4258        view.self_node = Some(node);
4259        assert_eq!(
4260            view.upstream_tcp_retry(),
4261            TcpRetry::Disabled,
4262            "the attribute control sets is what turns it off"
4263        );
4264
4265        let query = build_query(0x403, &["big", "example", "com"], 16, 1);
4266        let upstream = upstream_addr(1);
4267        let mut truncated = upstream_response(&query, 0, 1, b"the part that fit");
4268        truncated[2] |= 0x02; // TC
4269
4270        for client in BOTH_CLIENTS {
4271            // With it off, a truncated answer is relayed as it came — no TCP connection at all.
4272            let retried = std::cell::Cell::new(false);
4273            let answer = race_udp_and_tcp(
4274                upstream,
4275                &query,
4276                client,
4277                view.upstream_tcp_retry(),
4278                || std::future::ready(Some((upstream, truncated.clone()))),
4279                || {
4280                    retried.set(true);
4281                    std::future::ready(None)
4282                },
4283            )
4284            .await
4285            .expect("the upstream answered");
4286
4287            assert!(
4288                !retried.get(),
4289                "{client:?}: the attribute must stop the TCP retry being made at all"
4290            );
4291            assert_eq!(
4292                answer.resp, truncated,
4293                "{client:?}: relayed truncated, as control asked"
4294            );
4295            assert_eq!(answer.via, UpstreamTransport::Udp);
4296
4297            // And a UDP hop that never answers is waited out, not raced: no fallback either.
4298            let started = tokio::time::Instant::now();
4299            let raced = std::cell::Cell::new(false);
4300            let answer = race_udp_and_tcp(
4301                upstream,
4302                &query,
4303                client,
4304                view.upstream_tcp_retry(),
4305                silent_udp_hop,
4306                || {
4307                    raced.set(true);
4308                    std::future::ready(Some(truncated.clone()))
4309                },
4310            )
4311            .await;
4312
4313            assert!(
4314                !raced.get(),
4315                "{client:?}: the attribute must keep the TCP hop out of the race too"
4316            );
4317            assert!(answer.is_none(), "{client:?}: UDP said nothing, so nothing");
4318            assert_eq!(
4319                started.elapsed(),
4320                UPSTREAM_TIMEOUT,
4321                "{client:?}: the UDP hop runs to its own timeout, as it did before the race"
4322            );
4323        }
4324    }
4325
4326    /// An off-path injector must not be able to buy a TCP connection to a resolver with a datagram
4327    /// it forged. A `TC`-marked datagram from the wrong source, or one that does not echo the
4328    /// question we asked, is relayed to [`forward_walk`] exactly as before — which discards it — and
4329    /// costs no connection on the way.
4330    #[tokio::test]
4331    async fn a_forged_truncated_datagram_does_not_buy_a_tcp_connection() {
4332        let query = build_query(0x404, &["example", "com"], 1, 1);
4333        let upstream = upstream_addr(1);
4334
4335        let retried = std::cell::Cell::new(false);
4336        let mut tcp_hop = || {
4337            retried.set(true);
4338            std::future::ready(None)
4339        };
4340
4341        let mut from_elsewhere = upstream_response(&query, 0, 1, b"injected");
4342        from_elsewhere[2] |= 0x02; // TC
4343        let answer = race_udp_and_tcp(
4344            upstream,
4345            &query,
4346            ClientTransport::Udp,
4347            TcpRetry::Enabled,
4348            || std::future::ready(Some((upstream_addr(9), from_elsewhere.clone()))),
4349            &mut tcp_hop,
4350        )
4351        .await
4352        .expect("the datagram is still handed on for the walk to discard");
4353        assert!(
4354            !retried.get(),
4355            "a datagram from the wrong source must not be retried"
4356        );
4357        assert_eq!(answer.via, UpstreamTransport::Udp);
4358
4359        let other_question = build_query(0x404, &["other", "example", "com"], 1, 1);
4360        let mut wrong_question = upstream_response(&other_question, 0, 1, b"injected");
4361        wrong_question[2] |= 0x02; // TC
4362        let answer = race_udp_and_tcp(
4363            upstream,
4364            &query,
4365            ClientTransport::Udp,
4366            TcpRetry::Enabled,
4367            || std::future::ready(Some((upstream, wrong_question.clone()))),
4368            &mut tcp_hop,
4369        )
4370        .await
4371        .expect("the datagram is still handed on for the walk to discard");
4372        assert!(
4373            !retried.get(),
4374            "a datagram answering another question must not be retried"
4375        );
4376        assert_eq!(answer.via, UpstreamTransport::Udp);
4377    }
4378
4379    /// Where the TCP hop is already running — a TCP client's, which starts with the UDP hop — a
4380    /// forged datagram does not end the race either: it is not an answer, so the resolver's own
4381    /// TCP answer is what comes back.
4382    #[tokio::test]
4383    async fn a_forged_datagram_does_not_cut_short_a_running_tcp_hop() {
4384        let query = build_query(0x40E, &["example", "com"], 1, 1);
4385        let upstream = upstream_addr(1);
4386        let injected = upstream_response(&query, 0, 1, b"injected");
4387        let over_tcp = upstream_response(&query, 0, 1, b"the resolver over tcp");
4388
4389        let answer = race_udp_and_tcp(
4390            upstream,
4391            &query,
4392            ClientTransport::Tcp,
4393            TcpRetry::Enabled,
4394            || std::future::ready(Some((upstream_addr(9), injected.clone()))),
4395            || std::future::ready(Some(over_tcp.clone())),
4396        )
4397        .await
4398        .expect("the TCP hop answered");
4399
4400        assert_eq!(answer.resp, over_tcp);
4401        assert_eq!(answer.via, UpstreamTransport::Tcp);
4402    }
4403
4404    /// The TCP hop gets the same scrutiny as the datagram, and for a reason the datagram check does
4405    /// not have: the hop that loses this race is **dropped**. A TCP message that does not echo the
4406    /// query is one `forward_walk` will discard, so letting it win takes the UDP answer down with
4407    /// it — the future is cancelled and the answer never arrives — and a query that resolves over
4408    /// UDP comes back SERVFAIL instead. Scored as a failed hop, the race stays with UDP.
4409    #[tokio::test(start_paused = true)]
4410    async fn a_mismatched_tcp_answer_leaves_the_race_to_the_udp_hop() {
4411        let query = build_query(0x410, &["example", "com"], 1, 1);
4412        let upstream = upstream_addr(1);
4413        let over_udp = upstream_response(&query, 0, 1, b"the resolver over udp");
4414        // A resolver answering a question we never asked, framed over the connection we opened for
4415        // this one: `response_matches_query` is what tells the two apart.
4416        let other_question = build_query(0x410, &["other", "example", "com"], 1, 1);
4417        let mismatched = upstream_response(&other_question, 0, 1, b"an answer to something else");
4418
4419        // A TCP client races both hops from the start, so the mismatched message is ready while the
4420        // UDP hop is still in flight — the case where winning would cost the answer.
4421        let answer = race_udp_and_tcp(
4422            upstream,
4423            &query,
4424            ClientTransport::Tcp,
4425            TcpRetry::Enabled,
4426            async || {
4427                tokio::time::sleep(Duration::from_millis(100)).await;
4428                Some((upstream, over_udp.clone()))
4429            },
4430            || std::future::ready(Some(mismatched.clone())),
4431        )
4432        .await
4433        .expect("the UDP hop's answer must survive a mismatched TCP message");
4434
4435        assert_eq!(
4436            answer.resp, over_udp,
4437            "the mismatched TCP message must not win a race the UDP hop was still running"
4438        );
4439        assert_eq!(answer.via, UpstreamTransport::Udp);
4440
4441        // And with nothing else running, a mismatched message is no answer at all: the walk moves
4442        // on to the next resolver rather than relaying it.
4443        let answer = race_udp_and_tcp(
4444            upstream,
4445            &query,
4446            ClientTransport::Udp,
4447            TcpRetry::Enabled,
4448            || std::future::ready(None),
4449            || std::future::ready(Some(mismatched.clone())),
4450        )
4451        .await;
4452
4453        assert!(
4454            answer.is_none(),
4455            "a failed UDP hop plus a mismatched TCP message is not an answer"
4456        );
4457    }
4458
4459    /// The truncation retry has the same hole and the worst outcome of it: a truncated answer is a
4460    /// real answer being *held*, and a mismatched TCP reply that replaced it would lose it outright
4461    /// — the same loss a failed TCP retry is already careful not to cause.
4462    #[tokio::test]
4463    async fn a_mismatched_tcp_retry_relays_the_truncated_udp_answer() {
4464        let query = build_query(0x411, &["big", "example", "com"], 16, 1);
4465        let upstream = upstream_addr(1);
4466        let mut truncated = upstream_response(&query, 0, 1, b"the part that fit");
4467        truncated[2] |= 0x02; // TC
4468        // Same question, wrong transaction id: a stale answer to the previous query on a reused
4469        // connection is the everyday way to get one of these without an attacker.
4470        let mut stale = upstream_response(&query, 0, 40, &[0xAB; 600]);
4471        stale[0] ^= 0xFF;
4472
4473        for client in BOTH_CLIENTS {
4474            let answer = race_udp_and_tcp(
4475                upstream,
4476                &query,
4477                client,
4478                TcpRetry::Enabled,
4479                || std::future::ready(Some((upstream, truncated.clone()))),
4480                || std::future::ready(Some(stale.clone())),
4481            )
4482            .await
4483            .expect("a mismatched retry must not lose the answer we already had");
4484
4485            assert_eq!(
4486                answer.resp, truncated,
4487                "{client:?}: the truncated UDP answer is relayed, not the mismatched TCP message"
4488            );
4489            assert_eq!(answer.via, UpstreamTransport::Udp, "{client:?}");
4490        }
4491    }
4492
4493    /// The other side of the fallback, in the shape of Go's `TestForwarderNetstackUpstream`, which
4494    /// bounds its elapsed time by `udpRaceTimeout`: a resolver whose UDP path works must be answered
4495    /// over UDP, inside [`UDP_RACE_TIMEOUT`], with no TCP hop started. The race would otherwise hide
4496    /// a broken UDP hop behind the right bytes arriving two seconds late over TCP — which is why the
4497    /// clock is checked, not only the bytes.
4498    #[tokio::test(start_paused = true)]
4499    async fn a_working_udp_hop_answers_inside_the_race_timeout_without_tcp() {
4500        let query = build_query(0x40F, &["example", "com"], 1, 1);
4501        let upstream = upstream_addr(1);
4502        let over_udp = upstream_response(&query, 0, 1, b"the resolver over udp");
4503        let started = tokio::time::Instant::now();
4504
4505        let tcp_started = std::cell::Cell::new(false);
4506        let answer = race_udp_and_tcp(
4507            upstream,
4508            &query,
4509            ClientTransport::Udp,
4510            TcpRetry::Enabled,
4511            async || {
4512                // A round trip to a resolver that is up.
4513                tokio::time::sleep(Duration::from_millis(50)).await;
4514                Some((upstream, over_udp.clone()))
4515            },
4516            || {
4517                tcp_started.set(true);
4518                std::future::ready(Some(b"never wanted".to_vec()))
4519            },
4520        )
4521        .await
4522        .expect("the upstream answered");
4523
4524        assert_eq!(answer.resp, over_udp);
4525        assert_eq!(answer.via, UpstreamTransport::Udp);
4526        assert!(
4527            started.elapsed() < UDP_RACE_TIMEOUT,
4528            "a working UDP path answers before TCP would ever be tried ({:?})",
4529            started.elapsed()
4530        );
4531        assert!(!tcp_started.get(), "and no TCP connection is opened");
4532    }
4533
4534    /// The relay cap is a datagram bound, so the one path it must not chop is the one that never
4535    /// touches a datagram: fetched over the TCP hop, handed to a TCP client. Chopping there would
4536    /// hand the stub resolver the same truncated answer it retried over TCP to escape, and the name
4537    /// would still not resolve through this node.
4538    ///
4539    /// The same answer on its way to a **UDP** client is still cut to one datagram and marked `TC`
4540    /// — it has to fit the datagram it leaves in — which is what sends that client to
4541    /// `dns_over_tcp`, where it is served whole.
4542    #[test]
4543    fn a_tcp_fetched_answer_is_relayed_whole_to_a_tcp_client() {
4544        let query = build_edns_query(0x405, &["big", "example", "com"], 16, 1, 4096);
4545        let mut big = query.clone();
4546        big[2] |= 0x80; // QR = 1
4547        big.resize(MAX_UPSTREAM_RESPONSE + 5000, 0xAB);
4548
4549        let out = cap_response(
4550            &query,
4551            big.clone(),
4552            ClientTransport::Tcp,
4553            UpstreamTransport::Tcp,
4554        );
4555        assert_eq!(
4556            out, big,
4557            "TCP end to end: the whole answer, unchopped and unmarked"
4558        );
4559
4560        let out = cap_response(&query, big, ClientTransport::Udp, UpstreamTransport::Tcp);
4561        assert_eq!(
4562            out.len(),
4563            MAX_UPSTREAM_RESPONSE,
4564            "the same answer still has to fit the datagram a UDP client is answered in"
4565        );
4566        assert_ne!(
4567            out[2] & 0x02,
4568            0,
4569            "and is marked truncated, which is what sends that client to the TCP responder"
4570        );
4571    }
4572
4573    /// The TCP hop's framing, driven end to end over an in-memory stream: the query goes out under
4574    /// a two-byte big-endian length prefix (RFC 1035 §4.2.2) in ONE write, and the answer is read
4575    /// back out from under its own prefix. Framing is what a DNS-over-TCP resolver rejects a
4576    /// connection for, so it is checked against a reader rather than asserted about.
4577    #[tokio::test]
4578    async fn tcp_exchange_frames_the_query_and_reads_the_framed_answer() {
4579        let query = build_query(0x406, &["big", "example", "com"], 16, 1);
4580        let answer = upstream_response(&query, 0, 40, &[0xAB; 8000]);
4581
4582        let (mut client, mut server) = tokio::io::duplex(64 * 1024);
4583        let resolver = {
4584            let query = query.clone();
4585            let answer = answer.clone();
4586            tokio::spawn(async move {
4587                let mut len_buf = [0u8; 2];
4588                server.read_exact(&mut len_buf).await.unwrap();
4589                let len = usize::from(u16::from_be_bytes(len_buf));
4590                assert_eq!(len, query.len(), "the prefix declares the query's length");
4591                let mut got = vec![0u8; len];
4592                server.read_exact(&mut got).await.unwrap();
4593                assert_eq!(got, query, "and the query follows it verbatim");
4594
4595                let mut framed = u16::try_from(answer.len()).unwrap().to_be_bytes().to_vec();
4596                framed.extend_from_slice(&answer);
4597                server.write_all(&framed).await.unwrap();
4598            })
4599        };
4600
4601        let got = tcp_exchange(&mut client, &query)
4602            .await
4603            .expect("a well-framed answer is read back");
4604        resolver
4605            .await
4606            .expect("the resolver side saw a framed query");
4607
4608        assert_eq!(
4609            got, answer,
4610            "the answer comes back whole — 8000 bytes no datagram here could have carried"
4611        );
4612    }
4613
4614    /// The `TC` bit a truncated UDP answer sets is what sends a stub resolver to TCP (RFC 1035
4615    /// §4.2.1). Setting it *again* on the TCP answer sends that resolver straight back into another
4616    /// retry, so the client's advertised UDP payload size — a property of the datagram it would
4617    /// have been answered in, and one RFC 7766 §8 gives a TCP client no equivalent of — is applied
4618    /// only to a [`ClientTransport::Udp`] client. Same query, same answer, two transports.
4619    #[test]
4620    fn client_udp_limit_is_not_applied_to_a_tcp_client() {
4621        // No EDNS OPT record, so the client's limit is the classic 512 bytes.
4622        let query = build_query(0x310, &["example", "com"], 1, 1);
4623        let mut answer = query.clone();
4624        answer[2] |= 0x80; // make it a response (QR=1)
4625        answer.resize(900, 0xAB); // over 512, under MAX_UPSTREAM_RESPONSE: only the client limit bites
4626
4627        let udp = cap_response(
4628            &query,
4629            answer.clone(),
4630            ClientTransport::Udp,
4631            UpstreamTransport::Udp,
4632        );
4633        assert_ne!(
4634            udp[2] & 0x02,
4635            0,
4636            "a UDP client that advertised 512 bytes is told the 900-byte answer is truncated"
4637        );
4638        assert_eq!(udp.len(), 900, "and the body is left intact either way");
4639
4640        let tcp = cap_response(&query, answer, ClientTransport::Tcp, UpstreamTransport::Udp);
4641        assert_eq!(
4642            tcp[2] & 0x02,
4643            0,
4644            "the same answer over TCP is NOT marked: the client already did the TCP retry"
4645        );
4646        assert_eq!(tcp.len(), 900, "and is relayed whole");
4647    }
4648
4649    /// The relay cap is a different claim from the client's datagram size, and for an answer that
4650    /// crossed the UDP hop it holds on both client transports: when [`MAX_UPSTREAM_RESPONSE`] really
4651    /// did cut the message, `TC` says so. Handing a TCP client a chopped body with `TC` clear would
4652    /// be a malformed-but-"complete" answer.
4653    #[test]
4654    fn a_chopped_answer_is_marked_truncated_on_both_transports() {
4655        let query = build_edns_query(0x311, &["example", "com"], 1, 1, 4096);
4656        let mut big = query.clone();
4657        big[2] |= 0x80;
4658        big.resize(MAX_UPSTREAM_RESPONSE + 500, 0xAB);
4659
4660        let out = cap_response(&query, big, ClientTransport::Tcp, UpstreamTransport::Udp);
4661        assert_eq!(out.len(), MAX_UPSTREAM_RESPONSE, "capped to one datagram");
4662        assert_ne!(
4663            out[2] & 0x02,
4664            0,
4665            "we really did chop the body, so TC is set for a TCP client too"
4666        );
4667    }
4668
4669    #[test]
4670    fn cap_response_sets_tc_when_truncated() {
4671        // An oversize upstream answer is capped to a single datagram AND marked truncated (TC bit)
4672        // so the stub resolver retries over TCP rather than trusting a chopped message. The query
4673        // advertises a big EDNS buffer so only the relay cap can be what fires here.
4674        let query = build_edns_query(0x300, &["example", "com"], 1, 1, 4096);
4675        let mut big = query.clone();
4676        big[2] |= 0x80; // make it a response (QR=1)
4677        big.resize(MAX_UPSTREAM_RESPONSE + 500, 0xAB);
4678
4679        let out = cap_response(&query, big, ClientTransport::Udp, UpstreamTransport::Udp);
4680        assert_eq!(out.len(), MAX_UPSTREAM_RESPONSE, "capped to one datagram");
4681        assert_ne!(out[2] & 0x02, 0, "TC bit set on truncation");
4682    }
4683
4684    #[test]
4685    fn cap_response_leaves_small_response_untouched() {
4686        // A response that fits both bounds is returned verbatim with no TC bit forced on.
4687        let query = build_query(0x301, &["example", "com"], 1, 1);
4688        let mut small = query.clone();
4689        small[2] |= 0x80;
4690        let before = small.clone();
4691
4692        let out = cap_response(&query, small, ClientTransport::Udp, UpstreamTransport::Udp);
4693        assert_eq!(out, before, "small response unchanged");
4694        assert_eq!(out[2] & 0x02, 0, "TC bit not set when no truncation");
4695    }
4696
4697    #[test]
4698    fn cap_is_a_relay_bound_not_the_read_bound() {
4699        // `forward_query` reads with `recv_from_bytes`, which issues `Recv { max_len: None }`, so
4700        // the netstack has already copied the whole datagram out before `cap_response` runs: the
4701        // cap bounds what we relay, not what we read or allocate. What bounds the read is the
4702        // netstack UDP socket's receive ring (`udp_buffer_size`, which `ts_runtime` leaves at the
4703        // `netcore` default) -- smoltcp drops a datagram larger than that ring at enqueue instead
4704        // of delivering it, and hands us everything up to and including the ring whole. The ring
4705        // being *wider* than the cap is what shows the two are different bounds: the read can put
4706        // more bytes in front of `cap_response` than the cap will relay.
4707        let ring = netstack::netcore::Config::default().udp_buffer_size;
4708        assert!(
4709            ring > MAX_UPSTREAM_RESPONSE,
4710            "the netstack udp receive ring ({ring}) no longer exceeds the relay cap \
4711             ({MAX_UPSTREAM_RESPONSE}): the cap would then be unreachable through this socket, and \
4712             the doc describing it as a relay bound the read can overrun is wrong"
4713        );
4714
4715        // The largest answer the cap passes is relayed byte-for-byte. Ask with an EDNS buffer that
4716        // covers the whole datagram, so the client-limit check (the other half of `cap_response`)
4717        // is not what we are measuring.
4718        let query = build_edns_query(0x302, &["example", "com"], 1, 1, 4096);
4719        let mut largest = query.clone();
4720        largest[2] |= 0x80; // QR=1
4721        largest.resize(MAX_UPSTREAM_RESPONSE, 0xAB);
4722        let before = largest.clone();
4723
4724        let out = cap_response(
4725            &query,
4726            largest,
4727            ClientTransport::Udp,
4728            UpstreamTransport::Udp,
4729        );
4730        assert_eq!(out, before, "an answer at the cap must be relayed verbatim");
4731        assert_eq!(
4732            out[2] & 0x02,
4733            0,
4734            "TC must not be set on a datagram that was never chopped"
4735        );
4736    }
4737
4738    #[test]
4739    fn full_ring_datagram_is_chopped_and_marked_truncated() {
4740        // Upstream's bound is `const maxResponseBytes = 4095` (net/dns/resolver/tsdns.go @
4741        // 9ea7cba44591e0cd840c6c94d23274dd222059bf). `sendUDP` reads into `maxResponseBytes+1`
4742        // bytes exactly so a 4096-byte answer is detectable as "did not fit", then cuts it to 4095
4743        // and sets TC. Here the netstack's 4096-byte receive ring plays the part of Go's `+1`: a
4744        // full-ring datagram is the one deliverable size the cap does not pass, and it must come
4745        // back with the same shape a Go forwarder would have produced. With the cap at 4096 this
4746        // datagram was relayed whole with TC clear, while a Go client on the same tailnet answering
4747        // the same query returned 4095 bytes marked truncated.
4748        let ring = netstack::netcore::Config::default().udp_buffer_size;
4749        let query = build_edns_query(0x303, &["example", "com"], 1, 1, 4096);
4750        let mut full_ring = query.clone();
4751        full_ring[2] |= 0x80; // QR=1
4752        full_ring.resize(ring, 0xAB);
4753
4754        let out = cap_response(
4755            &query,
4756            full_ring,
4757            ClientTransport::Udp,
4758            UpstreamTransport::Udp,
4759        );
4760        assert_eq!(
4761            out.len(),
4762            4095,
4763            "a full-ring answer must be cut to upstream's maxResponseBytes"
4764        );
4765        assert_ne!(out[2] & 0x02, 0, "TC bit set on the chopped answer");
4766    }
4767
4768    #[test]
4769    fn forwarded_reply_over_512_sets_tc_for_a_plain_query() {
4770        // A query with no EDNS OPT record is limited to 512 bytes (RFC 1035), so a 900-byte
4771        // forwarded reply -- well under the 4095 relay cap, and therefore relayed with TC clear
4772        // before this check existed -- must come back marked truncated, body intact.
4773        let query = build_query(0x400, &["example", "com"], 1, 1);
4774        let mut reply = query.clone();
4775        reply[2] |= 0x80; // QR=1
4776        reply.resize(900, 0xAB);
4777
4778        let out = cap_response(
4779            &query,
4780            reply.clone(),
4781            ClientTransport::Udp,
4782            UpstreamTransport::Udp,
4783        );
4784
4785        assert_ne!(
4786            out[2] & 0x02,
4787            0,
4788            "a 900-byte reply to a non-EDNS query must have TC set"
4789        );
4790        assert_eq!(out.len(), 900, "the body is left intact, not chopped");
4791        assert_eq!(
4792            out[3..],
4793            reply[3..],
4794            "only the flags byte carrying TC may differ"
4795        );
4796    }
4797
4798    #[test]
4799    fn forwarded_reply_under_advertised_edns_size_leaves_tc_clear() {
4800        // The same 900-byte reply, but the client advertised a 4096-byte EDNS buffer: it fits, so
4801        // TC must stay clear and the datagram must be relayed byte-for-byte.
4802        let query = build_edns_query(0x401, &["example", "com"], 1, 1, 4096);
4803        let mut reply = query.clone();
4804        reply[2] |= 0x80; // QR=1
4805        reply.resize(900, 0xAB);
4806        let before = reply.clone();
4807
4808        let out = cap_response(&query, reply, ClientTransport::Udp, UpstreamTransport::Udp);
4809
4810        assert_eq!(
4811            out, before,
4812            "a reply within the advertised buffer is verbatim"
4813        );
4814        assert_eq!(out[2] & 0x02, 0, "TC must stay clear");
4815    }
4816
4817    /// Go's `findOPTRecord` accepts an OPT record only in the final 11 bytes of the message, with a
4818    /// root NAME, EDNS version 0 and `RDLEN == 0`; anything else is "no EDNS", i.e. the 512-byte
4819    /// RFC 1035 limit. Every rejection below is a case where a laxer reader would honour a large
4820    /// advertised buffer and leave `TC` clear on an answer a Go node marks truncated.
4821    #[test]
4822    fn client_udp_limit_reads_the_opt_record() {
4823        // No OPT record => the RFC 1035 512-byte limit.
4824        let plain = build_query(0x402, &["example", "com"], 1, 1);
4825        assert_eq!(client_udp_limit(&plain), NO_EDNS_UDP_LIMIT);
4826
4827        // An OPT record's CLASS field carries the advertised size.
4828        let edns = build_edns_query(0x403, &["example", "com"], 1, 1, 1232);
4829        assert_eq!(client_udp_limit(&edns), 1232);
4830
4831        // A value below 512 is taken verbatim. RFC 6891 6.2.3 would floor it at 512, but Go does
4832        // not (`maxSize = int(ednsSize)`), so a Rust node that did would leave `TC` clear where a
4833        // Go node on the same tailnet sets it.
4834        let tiny = build_edns_query(0x404, &["example", "com"], 1, 1, 64);
4835        assert_eq!(client_udp_limit(&tiny), 64);
4836
4837        // An OPT record that is not the last record in the message is not read at all: upstream
4838        // only ever looks at the final 11 bytes.
4839        let mut trailing_rr = build_edns_query(0x405, &["example", "com"], 1, 1, 2048);
4840        // A 1-byte-RDATA TXT (type 16) record for the root name, appended after the OPT.
4841        trailing_rr.extend_from_slice(&[0, 0, 16, 0, 1, 0, 0, 0, 0, 0, 1, 0]);
4842        trailing_rr[11] = 2; // ARCOUNT = 2
4843        assert_eq!(client_udp_limit(&trailing_rr), NO_EDNS_UDP_LIMIT);
4844
4845        // An OPT record carrying options — a DNS cookie, EDNS Client Subnet — has RDLEN != 0 and is
4846        // rejected. This is the common case, not a corner: stub resolvers send cookies routinely.
4847        let cookie =
4848            build_edns_query_with_option(0x406, &["example", "com"], 1, 1, 4096, 10, &[0; 8]);
4849        assert_eq!(client_udp_limit(&cookie), NO_EDNS_UDP_LIMIT);
4850
4851        // An unknown EDNS version is left alone rather than guessed at.
4852        let mut future_version = build_edns_query(0x407, &["example", "com"], 1, 1, 4096);
4853        let ttl_at = future_version.len() - 6; // TTL = extended RCODE (1) | VERSION (1) | flags (2)
4854        future_version[ttl_at + 1] = 1; // EDNS version 1
4855        assert_eq!(client_udp_limit(&future_version), NO_EDNS_UDP_LIMIT);
4856
4857        // A non-root OPT NAME is rejected.
4858        let mut named = build_edns_query(0x408, &["example", "com"], 1, 1, 4096);
4859        let name_at = named.len() - 11;
4860        named[name_at] = 0xC0; // a compression pointer where the root label must be
4861        assert_eq!(client_udp_limit(&named), NO_EDNS_UDP_LIMIT);
4862
4863        // ARCOUNT == 0 means there is no additional section to hold an OPT, whatever the trailing
4864        // bytes happen to look like.
4865        let mut no_ar = build_edns_query(0x409, &["example", "com"], 1, 1, 4096);
4866        no_ar[11] = 0;
4867        assert_eq!(client_udp_limit(&no_ar), NO_EDNS_UDP_LIMIT);
4868
4869        // A truncated message falls back to the conservative limit, never a larger one.
4870        let mut chopped = build_edns_query(0x40A, &["example", "com"], 1, 1, 4096);
4871        chopped.truncate(chopped.len() - 8);
4872        assert_eq!(client_udp_limit(&chopped), NO_EDNS_UDP_LIMIT);
4873    }
4874
4875    /// The whole point of the narrow OPT reader, end to end: a stub resolver that advertises 4096
4876    /// **and** sends a DNS cookie is capped at 512, so the 900-byte forwarded reply comes back with
4877    /// `TC` set. A reader that walked the additional section properly would honour the 4096 and
4878    /// leave `TC` clear — which is the answer no Go node on the tailnet would have produced.
4879    #[test]
4880    fn an_opt_record_carrying_options_is_not_honoured() {
4881        let query =
4882            build_edns_query_with_option(0x40B, &["example", "com"], 1, 1, 4096, 10, &[0; 8]);
4883        let mut reply = query.clone();
4884        reply[2] |= 0x80; // QR=1
4885        reply.resize(900, 0xAB);
4886
4887        let out = cap_response(&query, reply, ClientTransport::Udp, UpstreamTransport::Udp);
4888        assert_ne!(
4889            out[2] & 0x02,
4890            0,
4891            "an OPT record with options is no EDNS at all upstream: the 512-byte limit applies"
4892        );
4893        assert_eq!(out.len(), 900, "the body is left intact, not chopped");
4894    }
4895
4896    /// An advertised size below 512 is honoured as-is. Go floors nothing: `maxSize = int(ednsSize)`
4897    /// whenever an OPT record is present, and only a request with no OPT record falls back to 512.
4898    #[test]
4899    fn an_advertised_size_below_512_is_not_floored() {
4900        let query = build_edns_query(0x40C, &["example", "com"], 1, 1, 200);
4901        let mut reply = query.clone();
4902        reply[2] |= 0x80; // QR=1
4903        reply.resize(300, 0xAB);
4904
4905        let out = cap_response(&query, reply, ClientTransport::Udp, UpstreamTransport::Udp);
4906        assert_ne!(
4907            out[2] & 0x02,
4908            0,
4909            "300 bytes overflows the 200 the client asked for, so TC is set"
4910        );
4911        assert_eq!(out.len(), 300, "the body is left intact, not chopped");
4912    }
4913
4914    /// Upstream runs the size check on answers the resolver builds itself, not only on forwarded
4915    /// ones (`Resolver.Query` calls `checkResponseSizeAndSetTC` right after `respond` succeeds). An
4916    /// authoritative answer is capped at 512 bytes, which says nothing about a client that
4917    /// advertised less than that.
4918    #[test]
4919    fn an_authoritative_answer_over_the_advertised_size_is_marked() {
4920        let view = view_with_peer();
4921        let buf = build_edns_query(0x40D, &["host", "user", "ts", "net"], 1, 1, 20);
4922
4923        let resp = answer(&view, &buf).expect("answers");
4924        assert!(
4925            resp.len() > 20,
4926            "the fixture only works if the answer overflows the advertised 20 bytes"
4927        );
4928
4929        let marked = check_response_size_and_set_tc(&buf, resp.clone(), ClientTransport::Udp);
4930        assert_ne!(
4931            marked[2] & 0x02,
4932            0,
4933            "an answer we composed ourselves can still overflow a small advertised buffer"
4934        );
4935        assert_eq!(marked.len(), resp.len(), "the body is left intact");
4936        assert_eq!(
4937            marked[3..],
4938            resp[3..],
4939            "only the flags byte carrying TC may differ"
4940        );
4941    }
4942
4943    #[test]
4944    fn response_matches_query_rejects_mismatched_question() {
4945        // id + QR match but the echoed question differs (different QNAME) => rejected. This guards
4946        // against an off-path injector that guesses the id but answers a different question.
4947        let query = build_query(0x1234, &["a", "com"], 1, 1);
4948
4949        let mut wrong_question = build_query(0x1234, &["b", "com"], 1, 1);
4950        wrong_question[2] |= 0x80; // QR=1, same id
4951        assert!(
4952            !response_matches_query(&query, &wrong_question),
4953            "different QNAME must be rejected"
4954        );
4955
4956        // A different QTYPE with the same name is also rejected.
4957        let mut wrong_qtype = build_query(0x1234, &["a", "com"], 28, 1);
4958        wrong_qtype[2] |= 0x80;
4959        assert!(
4960            !response_matches_query(&query, &wrong_qtype),
4961            "different QTYPE must be rejected"
4962        );
4963
4964        // The exact echoed question with QR=1 is accepted.
4965        let mut good = query.clone();
4966        good[2] |= 0x80;
4967        assert!(
4968            response_matches_query(&query, &good),
4969            "matching question accepted"
4970        );
4971    }
4972
4973    #[test]
4974    fn suffix_matches_handles_boundaries_and_empty() {
4975        // Exact and label-boundary matches.
4976        assert!(suffix_matches("corp", "corp"));
4977        assert!(suffix_matches("a.corp", "corp"));
4978        assert!(suffix_matches("a.b.corp", "corp"));
4979        // Not a label boundary.
4980        assert!(!suffix_matches("acorp", "corp"));
4981        // Empty suffix never matches (defense-in-depth against `ends_with("")`).
4982        assert!(!suffix_matches("anything.example", ""));
4983        assert!(!suffix_matches("", ""));
4984    }
4985
4986    #[test]
4987    fn empty_search_domain_does_not_capture_everything() {
4988        // Defense-in-depth: an empty search domain must NOT make every name look like a tailnet
4989        // name (which would fail-close legitimate recursive queries / mis-route). With an empty
4990        // suffix present alongside a real resolver, an off-tailnet name still forwards.
4991        let mut view = view_with_routes(
4992            std::collections::BTreeMap::new(),
4993            vec![udp("8.8.8.8:53")],
4994            vec![],
4995        );
4996        view.cfg.search_domains = vec![String::new()];
4997        let buf = build_query(0x400, &["example", "com"], 1, 1);
4998
4999        match decide(&view, &buf).expect("decides") {
5000            Decision::Forward { upstreams, .. } => {
5001                assert_eq!(upstreams, vec!["8.8.8.8:53".parse().unwrap()]);
5002            }
5003            Decision::Reply(_) => {
5004                panic!("empty search domain must not treat every name as tailnet")
5005            }
5006        }
5007    }
5008
5009    #[test]
5010    fn empty_route_suffix_does_not_capture_everything() {
5011        // Defense-in-depth: an empty route suffix must not match every name (which would route all
5012        // queries to that route's upstreams). With an empty-suffix route present, an unrelated name
5013        // still falls through to the global resolver.
5014        let mut routes = std::collections::BTreeMap::new();
5015        routes.insert(String::new(), vec![udp("10.9.9.9:53")]);
5016        let view = view_with_routes(routes, vec![udp("8.8.8.8:53")], vec![]);
5017        let buf = build_query(0x401, &["example", "com"], 1, 1);
5018
5019        match decide(&view, &buf).expect("decides") {
5020            Decision::Forward { upstreams, .. } => {
5021                assert_eq!(
5022                    upstreams,
5023                    vec!["8.8.8.8:53".parse().unwrap()],
5024                    "empty route suffix must not capture; falls through to global"
5025                );
5026            }
5027            Decision::Reply(_) => panic!("expected forward to global resolver"),
5028        }
5029    }
5030
5031    fn udp_exit(addr: &str) -> DnsResolver {
5032        DnsResolver {
5033            transport: ts_control::ResolverTransport::Udp(addr.parse().unwrap()),
5034            use_with_exit_node: true,
5035        }
5036    }
5037
5038    #[test]
5039    fn recursive_forward_is_flagged_route_forward_is_not() {
5040        // A recursive (global/fallback) forward sets `recursive = true` (eligible for DoH
5041        // delegation); a deliberately-configured split-DNS route sets `recursive = false`.
5042        let mut routes = std::collections::BTreeMap::new();
5043        routes.insert("corp.example".to_string(), vec![udp("10.0.0.53:53")]);
5044        let view = view_with_routes(routes, vec![udp("8.8.8.8:53")], vec![]);
5045
5046        let routed = build_query(0x500, &["api", "corp", "example"], 1, 1);
5047        match decide(&view, &routed).expect("decides") {
5048            Decision::Forward { recursive, .. } => {
5049                assert!(!recursive, "split-DNS route is not a recursive forward")
5050            }
5051            Decision::Reply(_) => panic!("expected route forward"),
5052        }
5053
5054        let global = build_query(0x501, &["example", "com"], 1, 1);
5055        match decide(&view, &global).expect("decides") {
5056            Decision::Forward { recursive, .. } => {
5057                assert!(recursive, "unrouted name is a recursive forward")
5058            }
5059            Decision::Reply(_) => panic!("expected recursive forward"),
5060        }
5061    }
5062
5063    #[test]
5064    fn recursive_plan_keeps_udp_without_exit_node() {
5065        // No active exit node: a recursive forward stays on its default UDP upstreams.
5066        let view = view_with_routes(
5067            std::collections::BTreeMap::new(),
5068            vec![udp("8.8.8.8:53")],
5069            vec![],
5070        );
5071        let default = vec!["8.8.8.8:53".parse().unwrap()];
5072        assert_eq!(
5073            recursive_plan(&view, default.clone()),
5074            RecursivePlan::Udp(default)
5075        );
5076    }
5077
5078    #[test]
5079    fn recursive_plan_delegates_to_doh_with_exit_node() {
5080        // Exit node active, no kept-local resolvers: recursive queries delegate to the exit node's
5081        // DoH endpoint so resolution egresses from the exit node, not this host.
5082        let mut view = view_with_routes(
5083            std::collections::BTreeMap::new(),
5084            vec![udp("8.8.8.8:53")],
5085            vec![],
5086        );
5087        let doh: SocketAddr = "100.64.0.5:8080".parse().unwrap();
5088        view.exit_doh = Some(doh);
5089        assert_eq!(
5090            recursive_plan(&view, vec!["8.8.8.8:53".parse().unwrap()]),
5091            RecursivePlan::Doh(doh)
5092        );
5093    }
5094
5095    #[test]
5096    fn recursive_plan_keeps_use_with_exit_node_resolvers_local() {
5097        // Even with an exit node active, resolvers flagged `use_with_exit_node` stay local (Go keeps
5098        // UseWithExitNode resolvers). The plan forwards to those over UDP, never delegating to DoH.
5099        let mut view = view_with_routes(
5100            std::collections::BTreeMap::new(),
5101            vec![udp_exit("10.0.0.53:53"), udp("8.8.8.8:53")],
5102            vec![],
5103        );
5104        view.exit_doh = Some("100.64.0.5:8080".parse().unwrap());
5105        // The default upstreams the caller computed are irrelevant when kept-local resolvers exist;
5106        // the plan must use the kept-local ones.
5107        assert_eq!(
5108            recursive_plan(&view, vec!["8.8.8.8:53".parse().unwrap()]),
5109            RecursivePlan::Udp(vec!["10.0.0.53:53".parse().unwrap()])
5110        );
5111    }
5112
5113    // --- SOA on authoritative negative answers (RFC 2308) -----------------------------------
5114
5115    /// Read an uncompressed name at `off`, returning it dotted and the offset just past it.
5116    fn read_name(resp: &[u8], mut off: usize) -> (String, usize) {
5117        let mut labels: Vec<String> = Vec::new();
5118        loop {
5119            let len = resp[off] as usize;
5120            assert_eq!(len & 0xC0, 0, "no compression pointer expected here");
5121            off += 1;
5122            if len == 0 {
5123                break;
5124            }
5125            labels.push(String::from_utf8(resp[off..off + len].to_vec()).expect("ascii label"));
5126            off += len;
5127        }
5128        (labels.join("."), off)
5129    }
5130
5131    /// The number of records in a response's authority section (NSCOUNT).
5132    fn nscount(resp: &[u8]) -> u16 {
5133        u16::from_be_bytes([resp[8], resp[9]])
5134    }
5135
5136    /// Walk an answer-less response to its authority section and read the SOA there, returning
5137    /// `(zone, record TTL, SERIAL, MINIMUM)`. `None` when the authority section is empty.
5138    ///
5139    /// Also asserts the record's shape as it goes: TYPE=SOA, CLASS=IN, and MNAME/RNAME both equal
5140    /// the owner name (the placeholders Go writes).
5141    fn parse_soa(resp: &[u8]) -> Option<(String, u32, u32, u32)> {
5142        let (.., ancount) = parse_header(resp);
5143        assert_eq!(ancount, 0, "parse_soa only walks answer-less responses");
5144        if nscount(resp) == 0 {
5145            return None;
5146        }
5147        assert_eq!(nscount(resp), 1, "at most one SOA");
5148
5149        // Question: QNAME then QTYPE + QCLASS.
5150        let (_, off) = read_name(resp, 12);
5151        // Authority record: NAME, TYPE, CLASS, TTL, RDLENGTH, RDATA.
5152        let (zone, off) = read_name(resp, off + 4);
5153        let u16_at = |at: usize| u16::from_be_bytes([resp[at], resp[at + 1]]);
5154        let u32_at = |at: usize| u32::from_be_bytes(resp[at..at + 4].try_into().unwrap());
5155        assert_eq!(u16_at(off), 6, "TYPE = SOA");
5156        assert_eq!(u16_at(off + 2), 1, "CLASS = IN");
5157        let ttl = u32_at(off + 4);
5158        let rdlength = u16_at(off + 8) as usize;
5159
5160        // RDATA: MNAME, RNAME, SERIAL, REFRESH, RETRY, EXPIRE, MINIMUM.
5161        let rdata_start = off + 10;
5162        let (mname, off) = read_name(resp, rdata_start);
5163        let (rname, off) = read_name(resp, off);
5164        assert_eq!(mname, zone, "MNAME is the zone (placeholder)");
5165        assert_eq!(rname, zone, "RNAME is the zone (placeholder)");
5166        let serial = u32_at(off);
5167        let minimum = u32_at(off + 16);
5168        assert_eq!(
5169            off + 20 - rdata_start,
5170            rdlength,
5171            "RDLENGTH covers exactly the SOA fields"
5172        );
5173        assert_eq!(resp.len(), off + 20, "the SOA is the last record");
5174        Some((zone, ttl, serial, minimum))
5175    }
5176
5177    /// Roughly-now, for asserting the SOA SERIAL is a unix timestamp rather than a constant.
5178    fn now_unix() -> u32 {
5179        std::time::SystemTime::now()
5180            .duration_since(std::time::UNIX_EPOCH)
5181            .expect("clock after the epoch")
5182            .as_secs() as u32
5183    }
5184
5185    /// An NXDOMAIN for a name under a tailnet search domain is authoritative, so it carries that
5186    /// search domain's SOA with the 10-second negative TTL. Without it a downstream cache picks its
5187    /// own (much longer) negative lifetime and a node renamed to that name stays unresolvable.
5188    #[test]
5189    fn nxdomain_for_tailnet_name_carries_the_search_domain_soa() {
5190        let view = view_with_peer();
5191        let buf = build_query(0x1111, &["nope", "user", "ts", "net"], 1, 1);
5192
5193        let resp = answer(&view, &buf).expect("answers");
5194        let (_, rcode, ancount) = parse_header(&resp);
5195        assert_eq!(rcode, 3, "NXDOMAIN");
5196        assert_eq!(ancount, 0);
5197
5198        let (zone, ttl, serial, minimum) =
5199            parse_soa(&resp).expect("an SOA in the authority section");
5200        assert_eq!(zone, "user.ts.net", "the search domain containing the name");
5201        assert_eq!(ttl, 10, "negative TTL");
5202        assert_eq!(minimum, 10, "MINIMUM also bounds negative caching");
5203        // The serial is the response time in unix seconds, not a fixed placeholder.
5204        assert!(
5205            serial.abs_diff(now_unix()) < 60,
5206            "SERIAL should be about now, got {serial}"
5207        );
5208    }
5209
5210    /// A NODATA — the name exists but we hold no address of the queried family, which is what an
5211    /// AAAA query for a peer becomes with the IPv6 gate off — is negative too, and takes the SOA.
5212    #[test]
5213    fn nodata_aaaa_for_known_peer_carries_the_soa() {
5214        let view = view_with_peer();
5215        assert!(!view.enable_ipv6, "default gate is off");
5216        let buf = build_query(0x2222, &["host", "user", "ts", "net"], 28, 1);
5217
5218        let resp = answer(&view, &buf).expect("answers");
5219        let (_, rcode, ancount) = parse_header(&resp);
5220        assert_eq!(rcode, 0, "NoError (NODATA)");
5221        assert_eq!(ancount, 0);
5222        let (zone, ttl, _, minimum) = parse_soa(&resp).expect("an SOA in the authority section");
5223        assert_eq!(zone, "user.ts.net");
5224        assert_eq!((ttl, minimum), (10, 10));
5225    }
5226
5227    /// A reverse query for an unmatched IP in the tailnet CGNAT range is authoritatively absent, so
5228    /// it carries the SOA of the reverse zone that covers it — the same per-/16 `in-addr.arpa`
5229    /// chunk real tailscaled advertises, not the search domain.
5230    #[test]
5231    fn cgnat_reverse_miss_carries_the_reverse_zone_soa() {
5232        let view = view_with_peer();
5233        // Reverse name for an unclaimed 100.64.0.0/10 address, least-significant octet first.
5234        let buf = build_query(0x3333, &["9", "0", "64", "100", "in-addr", "arpa"], 12, 1);
5235
5236        let resp = answer(&view, &buf).expect("answers");
5237        let (_, rcode, ancount) = parse_header(&resp);
5238        assert_eq!(rcode, 3, "NXDOMAIN");
5239        assert_eq!(ancount, 0);
5240        let (zone, ttl, _, minimum) = parse_soa(&resp).expect("an SOA in the authority section");
5241        assert_eq!(zone, "64.100.in-addr.arpa", "the CGNAT reverse zone");
5242        assert_eq!((ttl, minimum), (10, 10));
5243    }
5244
5245    /// The exotic-qtype path re-applies the CGNAT reverse guard, and its NXDOMAIN is just as
5246    /// authoritative — so it carries the same reverse-zone SOA the PTR arm does.
5247    #[test]
5248    fn exotic_qtype_cgnat_reverse_nxdomain_carries_the_soa() {
5249        let view = view_with_peer();
5250        // TXT (16) for a CGNAT reverse name.
5251        let buf = build_query(0x4444, &["9", "0", "64", "100", "in-addr", "arpa"], 16, 1);
5252
5253        let resp = answer(&view, &buf).expect("answers");
5254        assert_eq!(parse_header(&resp).1, 3, "NXDOMAIN");
5255        let (zone, ..) = parse_soa(&resp).expect("an SOA in the authority section");
5256        assert_eq!(zone, "64.100.in-addr.arpa");
5257    }
5258
5259    /// A negative split-DNS route (a route with no resolvers) is Go's `localDomains` verbatim: the
5260    /// NXDOMAIN it produces is authoritative and names the route's own suffix as its zone.
5261    #[test]
5262    fn negative_route_nxdomain_carries_the_route_zone_soa() {
5263        let mut routes = std::collections::BTreeMap::new();
5264        routes.insert("corp.example".to_string(), vec![]);
5265        let view = view_with_routes(routes, vec![], vec![]);
5266        let buf = build_query(0x5555, &["intranet", "corp", "example"], 1, 1);
5267
5268        let resp = answer(&view, &buf).expect("answers");
5269        assert_eq!(parse_header(&resp).1, 3, "NXDOMAIN");
5270        let (zone, ttl, _, minimum) = parse_soa(&resp).expect("an SOA in the authority section");
5271        assert_eq!(zone, "corp.example");
5272        assert_eq!((ttl, minimum), (10, 10));
5273    }
5274
5275    /// Answers we are NOT authoritative for carry no SOA: a SERVFAIL is a soft failure with nothing
5276    /// to cache, and an `ip6.arpa` NXDOMAIN is this fork's blanket anti-leak refusal, not a claim to
5277    /// serve the IPv6 reverse tree.
5278    #[test]
5279    fn non_authoritative_negative_answers_carry_no_soa() {
5280        let view = view_with_peer();
5281
5282        // Off-tailnet name, no upstream configured => SERVFAIL.
5283        let servfail =
5284            answer(&view, &build_query(0x6, &["example", "com"], 1, 1)).expect("answers");
5285        assert_eq!(parse_header(&servfail).1, 2, "ServFail");
5286        assert_eq!(nscount(&servfail), 0, "SERVFAIL carries no SOA");
5287
5288        // An ip6.arpa reverse name. The exact nibble labels do not matter to the guard.
5289        let mut labels: Vec<&str> = vec!["1"; 32];
5290        labels.push("ip6");
5291        labels.push("arpa");
5292        let ip6 = answer(&view, &build_query(0x7, &labels, 12, 1)).expect("answers");
5293        assert_eq!(parse_header(&ip6).1, 3, "NXDOMAIN");
5294        assert_eq!(nscount(&ip6), 0, "ip6.arpa NXDOMAIN carries no SOA");
5295
5296        // MagicDNS off => REFUSED, which asserts nothing about the name.
5297        let mut off = view_with_peer();
5298        off.cfg.magic_dns = false;
5299        let refused = answer(
5300            &off,
5301            &build_query(0x8, &["host", "user", "ts", "net"], 1, 1),
5302        )
5303        .expect("answers");
5304        assert_eq!(parse_header(&refused).1, 5, "Refused");
5305        assert_eq!(nscount(&refused), 0, "REFUSED carries no SOA");
5306    }
5307
5308    /// A NODATA for a type we simply do not serve on a name we do (TXT on a tailnet name) carries
5309    /// no SOA: Go sets `SOAZone` on a no-data answer only for an A/AAAA/ALL question.
5310    #[test]
5311    fn nodata_for_an_unserved_qtype_carries_no_soa() {
5312        let view = view_with_peer();
5313        let resp = answer(
5314            &view,
5315            &build_query(0x9, &["host", "user", "ts", "net"], 16, 1),
5316        )
5317        .expect("answers");
5318        let (_, rcode, ancount) = parse_header(&resp);
5319        assert_eq!((rcode, ancount), (0, 0), "NODATA");
5320        assert_eq!(nscount(&resp), 0);
5321    }
5322
5323    /// A positive answer has an empty authority section and a 5-second TTL. The short TTL is the
5324    /// positive half of the same argument: the netmap is local and in-memory, so a re-query is
5325    /// nearly free, while a downstream cache would otherwise hide a node rename for the full TTL.
5326    #[test]
5327    fn positive_answer_has_ttl_5_and_no_authority_section() {
5328        let view = view_with_peer();
5329        let resp = answer(
5330            &view,
5331            &build_query(0xA, &["host", "user", "ts", "net"], 1, 1),
5332        )
5333        .expect("answers");
5334        let (_, rcode, ancount) = parse_header(&resp);
5335        assert_eq!((rcode, ancount), (0, 1), "one A record");
5336        assert_eq!(nscount(&resp), 0, "a positive answer claims no zone");
5337        // The single A record's tail is TTL, RDLENGTH, RDATA.
5338        let ttl_at = resp.len() - 10;
5339        let ttl = u32::from_be_bytes(resp[ttl_at..ttl_at + 4].try_into().unwrap());
5340        assert_eq!(ttl, 5, "positive TTL");
5341    }
5342
5343    /// An authoritative negative answer with its SOA attached must still fit the classic 512-byte
5344    /// UDP limit, so the client-limit check leaves TC clear on it for a client that advertised no
5345    /// EDNS buffer. (A client that advertises *less* than 512 is a different case and is marked —
5346    /// see `an_authoritative_answer_over_the_advertised_size_is_marked`.)
5347    #[test]
5348    fn nxdomain_with_soa_stays_within_the_client_udp_limit() {
5349        let view = view_with_peer();
5350        let long = "a".repeat(63);
5351        let buf = build_query(0xB, &[&long, "user", "ts", "net"], 1, 1);
5352
5353        let resp = answer(&view, &buf).expect("answers");
5354        assert_eq!(nscount(&resp), 1, "the SOA fits beside this question");
5355        assert!(resp.len() <= 512, "still one classic UDP datagram");
5356
5357        let marked = check_response_size_and_set_tc(&buf, resp.clone(), ClientTransport::Udp);
5358        assert_eq!(marked, resp, "nothing to mark: an authoritative reply fits");
5359        assert_eq!(
5360            u16::from_be_bytes([marked[2], marked[3]]) & 0x0200,
5361            0,
5362            "TC must stay clear"
5363        );
5364    }
5365
5366    /// When the zone is so long that its SOA no longer fits under the 512-byte cap, the SOA is
5367    /// dropped rather than the answer being truncated: the NXDOMAIN goes back complete, with an
5368    /// empty authority section, TC clear, and still within a client's UDP limit. Losing the SOA
5369    /// only means a resolver falls back to its own negative-cache policy.
5370    #[test]
5371    fn an_soa_that_will_not_fit_is_dropped_and_the_nxdomain_still_answers() {
5372        let long = "a".repeat(63);
5373        let zone = [long.as_str(), long.as_str(), long.as_str()].join(".");
5374        let mut view = view_with_peer();
5375        view.cfg.search_domains = vec![zone.clone()];
5376
5377        let buf = build_query(0xC, &["x", &long, &long, &long], 1, 1);
5378        let resp = answer(&view, &buf).expect("answers");
5379
5380        assert_eq!(parse_header(&resp).1, 3, "NXDOMAIN");
5381        assert_eq!(nscount(&resp), 0, "the SOA did not fit and was dropped");
5382        assert!(resp.len() <= 512, "response stays within the UDP limit");
5383        let marked = check_response_size_and_set_tc(&buf, resp.clone(), ClientTransport::Udp);
5384        assert_eq!(
5385            u16::from_be_bytes([marked[2], marked[3]]) & 0x0200,
5386            0,
5387            "a dropped SOA must not set TC: the fork cannot serve the TCP retry it would ask for"
5388        );
5389    }
5390
5391    /// The zone is the *longest* authoritative suffix containing the name, so a name under a
5392    /// sub-zone gets the sub-zone's SOA rather than the shorter search domain's.
5393    #[test]
5394    fn the_longest_authoritative_zone_wins() {
5395        let mut routes = std::collections::BTreeMap::new();
5396        routes.insert("sub.user.ts.net".to_string(), vec![]);
5397        let mut view = view_with_routes(routes, vec![], vec![]);
5398        view.cfg.search_domains = vec!["user.ts.net".to_string()];
5399
5400        let buf = build_query(0xD, &["nope", "sub", "user", "ts", "net"], 1, 1);
5401        let resp = answer(&view, &buf).expect("answers");
5402        assert_eq!(parse_header(&resp).1, 3, "NXDOMAIN");
5403        let (zone, ..) = parse_soa(&resp).expect("an SOA in the authority section");
5404        assert_eq!(zone, "sub.user.ts.net");
5405    }
5406
5407    /// A name we resolved only by search-domain qualification (a short name like `host`) is not
5408    /// itself inside a zone we serve, so its negative answer names no zone — matching Go, whose
5409    /// `authoritativeZoneFor` is given the query name as asked.
5410    #[test]
5411    fn a_short_name_outside_every_zone_gets_no_soa() {
5412        let mut view = view_with_peer();
5413        view.enable_ipv6 = false;
5414        // `host` resolves to the peer via search-domain qualification, and with IPv6 off the AAAA
5415        // is a NODATA — but `host` sits under no zone we serve.
5416        let resp = answer(&view, &build_query(0xE, &["host"], 28, 1)).expect("answers");
5417        let (_, rcode, ancount) = parse_header(&resp);
5418        assert_eq!((rcode, ancount), (0, 0), "NODATA");
5419        assert_eq!(nscount(&resp), 0, "no zone contains a single-label name");
5420    }
5421}