Skip to main content

seer_core/dns/
trace.rs

1//! Iterative resolution from the root servers down to the answer, one hop
2//! per delegation level — what `dig +trace` shows.
3//!
4//! Flow (see [`DnsTracer::trace`]):
5//! 1. Start at the root zone (`.`) with the built-in root hints
6//!    ([`ROOT_SERVERS`], IANA's `named.root`).
7//! 2. Ask one server of the current zone for the query name and type,
8//!    directly and with recursion disabled (RD=0). A server that does not
9//!    respond, answers with an error RCODE (SERVFAIL, REFUSED, …), or gives a
10//!    lame server's reply — AA clear, no answer, no SOA and no referral — is
11//!    noted in the hop's `failed_servers` and the next server of the zone is
12//!    asked — up to [`MAX_SERVERS_PER_LEVEL`] servers per level. A server is
13//!    asked on one address, IPv4 first: an address this host has no route to
14//!    (IPv4 on an IPv6-only host, or the reverse) or cannot open a socket
15//!    for (IPv6 when the kernel has IPv6 disabled) fails at once, is noted,
16//!    and the server's next address is tried instead, without counting
17//!    toward the limit, since no query left the host.
18//! 3. A referral — NOERROR, AA clear, no answer, no SOA, NS records in
19//!    AUTHORITY — names the next zone, which must lie strictly below the
20//!    current zone and at or above the query name (bailiwick); an upward or
21//!    sideways referral is unusable, like a lame reply: it is noted and the
22//!    zone's next server asked, and only when no server of the zone does
23//!    better does the walk stop with an error. The next servers'
24//!    addresses are the referral's glue: ADDITIONAL A/AAAA records for the
25//!    NS names, trusted only for names inside the current zone, as a
26//!    resolver would. Glueless NS names are resolved through the recursive
27//!    resolver (Google Public DNS), at most [`MAX_GLUELESS_PER_LEVEL`] per
28//!    level.
29//! 4. The walk stops at the first answer — authoritative or not; a CNAME is
30//!    reported, not chased, as `dig +trace` does — at NXDOMAIN or NODATA
31//!    (authoritative, or carrying the zone's SOA), or on an error, which
32//!    [`DnsTrace::error`] reports with the hops so far.
33//!
34//! **One raw exchange per hop.** Each query goes to one server address over
35//! UDP (repeated over TCP when the reply is truncated) through the same
36//! transport as [`DnsResolver::query`](crate::dns::DnsResolver::query)
37//! (`dns::transport`), not through a resolver lookup, because the resolver
38//! cannot report a hop as the server sent it (verified against hickory
39//! 0.26): its name-server layer (`DnsError::from_response`) turns every
40//! referral, NXDOMAIN and NODATA into a `NoRecordsFound` error, which drops
41//! the response header (the AA bit) and all of ADDITIONAL but the glue it
42//! matched itself, and its caching layer chases a CNAME by sending a
43//! follow-up query to the same server. Unlike `query`, a hop asks with
44//! recursion off, of the one vetted address the walk chose — the walk, not
45//! the transport, decides which server and address to ask next.
46//! ([`crate::dns::DelegationChecker`] reads only NS sets, so the resolver
47//! serves it.)
48//!
49//! **SSRF:** every server address — root hint, glue or resolved — passes
50//! [`crate::validation::describe_reserved_ip`] (via
51//! `delegation::partition_reserved`) before a query is sent; a reserved one
52//! is skipped and noted in the hop's `failed_servers`. Tests reach loopback
53//! fixtures only through `#[cfg(test)]` seams (`allowing_private_hosts`,
54//! `with_root_hints`, `with_port_map`, `with_recursive_upstream`, and
55//! `with_unroutable` to simulate a local routing failure); the production
56//! validation path is never weakened.
57//!
58//! **Retry boundary (deliberate):** like the rest of `dns/`, no
59//! [`crate::retry::RetryPolicy`]. hickory retransmits a UDP query within the
60//! per-query timeout; asking the next server of a zone after one fails is
61//! how iterative resolution proceeds, not a retry of the same query.
62//!
63//! **Bounded work:** at most [`MAX_HOPS`] delegation levels and, per level,
64//! at most [`MAX_SERVERS_PER_LEVEL`] queries and [`MAX_GLUELESS_PER_LEVEL`]
65//! glueless lookups, each query and each lookup under the per-query timeout
66//! (the config file's DNS timeout); an address with no local route fails
67//! without waiting. The walk as a whole has one deadline of
68//! [`TRACE_BUDGET_TIMEOUTS`] per-query timeouts (30s at the default 5s), as
69//! long as a single level may take at worst. The levels run one after
70//! another, and every level below a zone its owner controls is theirs to
71//! slow down, so without the deadline a chain of levels each answered only
72//! by its last server, after timed-out glueless lookups, would hold the
73//! caller for `MAX_HOPS × (MAX_SERVERS_PER_LEVEL + MAX_GLUELESS_PER_LEVEL)`
74//! = 96 timeouts (8 minutes at the default). A walk that runs out of time
75//! stops with the hops so far, and [`DnsTrace::error`] says so. Each
76//! referral must descend toward the query name, so the walk cannot loop.
77
78use std::collections::BTreeMap;
79use std::net::IpAddr;
80use std::time::{Duration, Instant};
81
82use hickory_resolver::config::ConnectionConfig;
83use hickory_resolver::net::NetError;
84use hickory_resolver::proto::op::{Message, ResponseCode};
85use hickory_resolver::proto::rr::{Name, RData as HickoryRData, RecordType as HickoryRecordType};
86use hickory_resolver::TokioResolver;
87use serde::{Deserialize, Serialize};
88use tracing::{debug, instrument};
89
90use super::delegation::{build_recursive_resolver, is_local_no_route, partition_reserved};
91use super::query::{duration_ms, DnsStatus};
92use super::records::{DnsRecord, RecordType};
93use super::resolver::{fqdn, prepare_query, to_dns_record, wire_query_name, wire_type};
94use super::transport::{transport_reason, Transport};
95use super::DEFAULT_DNS_TIMEOUT;
96use crate::error::{Result, SeerError};
97
98/// Most delegation levels walked (root included) before giving up. Real
99/// names resolve in 3–5; the bound matters only for pathological chains.
100const MAX_HOPS: usize = 16;
101
102/// Servers queried per delegation level before the level is given up.
103const MAX_SERVERS_PER_LEVEL: usize = 3;
104
105/// Glueless nameserver names resolved (through the recursive resolver) per
106/// delegation level.
107const MAX_GLUELESS_PER_LEVEL: usize = 3;
108
109/// The whole walk's deadline, in per-query timeouts: as long as a single
110/// level may take at worst (every query and glueless lookup of it timing
111/// out). Any one slow level can still finish; a chain of them cannot hold
112/// the caller for [`MAX_HOPS`] times that.
113const TRACE_BUDGET_TIMEOUTS: u32 = (MAX_SERVERS_PER_LEVEL + MAX_GLUELESS_PER_LEVEL) as u32;
114
115/// The root zone's servers — name, IPv4, IPv6 — from IANA's root hints
116/// (`https://www.internic.net/domain/named.root`, last updated 2026-09-24;
117/// b.root-servers.net's addresses changed on 2023-11-27). The one copy: the
118/// walk starts here. Literals, parsed by [`root_hints`]; a test proves
119/// every entry parses and is public.
120const ROOT_SERVERS: [(&str, &str, &str); 13] = [
121    ("a.root-servers.net.", "198.41.0.4", "2001:503:ba3e::2:30"),
122    ("b.root-servers.net.", "170.247.170.2", "2801:1b8:10::b"),
123    ("c.root-servers.net.", "192.33.4.12", "2001:500:2::c"),
124    ("d.root-servers.net.", "199.7.91.13", "2001:500:2d::d"),
125    ("e.root-servers.net.", "192.203.230.10", "2001:500:a8::e"),
126    ("f.root-servers.net.", "192.5.5.241", "2001:500:2f::f"),
127    ("g.root-servers.net.", "192.112.36.4", "2001:500:12::d0d"),
128    ("h.root-servers.net.", "198.97.190.53", "2001:500:1::53"),
129    ("i.root-servers.net.", "192.36.148.17", "2001:7fe::53"),
130    ("j.root-servers.net.", "192.58.128.30", "2001:503:c27::2:30"),
131    ("k.root-servers.net.", "193.0.14.129", "2001:7fd::1"),
132    ("l.root-servers.net.", "199.7.83.42", "2001:500:9f::42"),
133    ("m.root-servers.net.", "202.12.27.33", "2001:dc3::35"),
134];
135
136/// One delegation level of a [`DnsTrace`]: the server that answered for
137/// `zone`, and what it said.
138///
139/// Zone and server names are fully qualified, lowercase ASCII (IDNs as
140/// `xn--` A-labels), as `dig +trace` prints them: `"."` is the root, then
141/// e.g. `"com."` and `"example.com."`.
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct TraceHop {
144    /// The zone this server was asked as an authority for (`"."` = root).
145    pub zone: String,
146    /// The nameserver's host name, e.g. `"a.root-servers.net."`.
147    pub server: String,
148    /// The address actually queried.
149    pub address: String,
150    /// Round-trip time of the query, in milliseconds.
151    pub query_time_ms: u64,
152    /// The response code.
153    pub status: DnsStatus,
154    /// The response's AA (authoritative answer) bit.
155    pub authoritative: bool,
156    /// The zone the server delegated to — the next hop's `zone` — or `None`
157    /// when it did not refer onward. A referral the walk refused (upward or
158    /// sideways) is recorded here too when it ends the trace, with an error.
159    pub referral_zone: Option<String>,
160    /// The NS names delegated to, sorted; empty when there is no referral.
161    pub referral: Vec<String>,
162    /// This response's ANSWER section, every record under its real owner
163    /// name (normally only the final hop has one).
164    pub answers: Vec<DnsRecord>,
165    /// Servers of this zone — or single addresses of one — that were skipped
166    /// or failed before this one answered, as `"host (ip): reason"`, or
167    /// `"host: reason"` when no address was found for the host.
168    pub failed_servers: Vec<String>,
169}
170
171/// The result of [`DnsTracer::trace`]: every hop from the root down.
172///
173/// ```json
174/// {
175///   "name": "www.example.com",
176///   "record_type": "A",
177///   "hops": [
178///     { "zone": ".", "server": "a.root-servers.net.", "address": "198.41.0.4",
179///       "query_time_ms": 21, "status": "NOERROR", "authoritative": false,
180///       "referral_zone": "com.", "referral": ["a.gtld-servers.net.", "…"],
181///       "answers": [], "failed_servers": [] },
182///     { "zone": "com.", "…": "…" },
183///     { "zone": "example.com.", "server": "a.iana-servers.net.",
184///       "address": "199.43.135.53", "query_time_ms": 88, "status": "NOERROR",
185///       "authoritative": true, "referral_zone": null, "referral": [],
186///       "answers": [{ "name": "www.example.com", "record_type": "A",
187///                     "ttl": 300, "data": { "…": "…" } }],
188///       "failed_servers": [] }
189///   ],
190///   "status": "NOERROR",
191///   "answers": [{ "name": "www.example.com", "…": "…" }],
192///   "error": null
193/// }
194/// ```
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct DnsTrace {
197    /// The queried name, exactly as [`DnsQueryResult::name`] reports it: a
198    /// PTR query for an IP literal is named by its reverse-DNS name.
199    ///
200    /// [`DnsQueryResult::name`]: crate::dns::DnsQueryResult::name
201    pub name: String,
202    /// The queried record type.
203    pub record_type: RecordType,
204    /// The hops walked, root first.
205    pub hops: Vec<TraceHop>,
206    /// The final status: the last hop's response code.
207    pub status: DnsStatus,
208    /// The final ANSWER section (the last hop's), every record under its real
209    /// owner name. A CNAME answer is reported as-is, not chased. The server
210    /// may have followed an in-zone chain itself, though: beside NXDOMAIN
211    /// the chain leads to the name that does not exist, since the response
212    /// code is about the chain's last name (RFC 6604 §2).
213    pub answers: Vec<DnsRecord>,
214    /// Why the walk stopped before a final response, if it did: every server
215    /// of a zone failed (a referral upward or sideways counts as a failure),
216    /// the chain was too long, or the walk ran out of time.
217    pub error: Option<String>,
218}
219
220/// Walks a name's delegation chain from the root servers down, the way
221/// `dig +trace` does.
222///
223/// Construct with [`DnsTracer::new`] (defaults) or
224/// [`DnsTracer::from_config`] (honors `~/.seer/config.toml`
225/// `timeouts.dns_secs`), then call [`trace`](DnsTracer::trace).
226pub struct DnsTracer {
227    /// Per-query timeout, for direct queries and glueless lookups alike.
228    timeout: Duration,
229    /// The direct queries' transport, under the same timeout.
230    transport: Transport,
231    /// Recursive resolver for glueless nameserver names.
232    recursive: TokioResolver,
233    /// Test-only: pin the recursive resolver to a loopback mock.
234    #[cfg(test)]
235    recursive_upstream: Option<(IpAddr, u16)>,
236    /// Test-only: root servers to start from instead of [`ROOT_SERVERS`].
237    #[cfg(test)]
238    root_hints: Option<Vec<NsCandidate>>,
239    /// Test-only: per-host port override for direct queries, so each mock
240    /// "server" can live on its own ephemeral loopback port.
241    #[cfg(test)]
242    port_map: Option<std::collections::HashMap<String, u16>>,
243    /// Test-only: skip the SSRF/reserved-IP validation on server addresses.
244    #[cfg(test)]
245    allow_private_hosts: bool,
246    /// Test-only: addresses a direct query fails for at once, before
247    /// anything is sent, each with the local socket error its function makes
248    /// (see `with_unroutable`).
249    #[cfg(test)]
250    unroutable: Vec<(IpAddr, fn() -> std::io::Error)>,
251}
252
253impl std::fmt::Debug for DnsTracer {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        f.debug_struct("DnsTracer")
256            .field("timeout", &self.timeout)
257            .finish()
258    }
259}
260
261impl Default for DnsTracer {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267impl DnsTracer {
268    /// Creates a tracer with default settings (5s per-query timeout, Google
269    /// DNS for glueless nameserver lookups).
270    pub fn new() -> Self {
271        Self {
272            timeout: DEFAULT_DNS_TIMEOUT,
273            transport: Transport::new(DEFAULT_DNS_TIMEOUT),
274            recursive: build_recursive_resolver(DEFAULT_DNS_TIMEOUT, None),
275            #[cfg(test)]
276            recursive_upstream: None,
277            #[cfg(test)]
278            root_hints: None,
279            #[cfg(test)]
280            port_map: None,
281            #[cfg(test)]
282            allow_private_hosts: false,
283            #[cfg(test)]
284            unroutable: Vec::new(),
285        }
286    }
287
288    /// Builds a tracer honoring `~/.seer/config.toml` settings.
289    ///
290    /// Reads `timeouts.dns_secs` (already clamped to 1–60s by
291    /// [`crate::config::SeerConfig::load`]). Sugar over
292    /// [`DnsTracer::with_timeout`].
293    pub fn from_config(config: &crate::config::SeerConfig) -> Self {
294        Self::new().with_timeout(config.dns_timeout())
295    }
296
297    /// Sets the per-query timeout (each hop's query, including a TCP retry
298    /// of a truncated reply, and each glueless nameserver lookup).
299    pub fn with_timeout(mut self, timeout: Duration) -> Self {
300        self.timeout = timeout;
301        self.transport = Transport::new(timeout);
302        self.recursive = build_recursive_resolver(timeout, self.recursive_upstream());
303        self
304    }
305
306    /// Test-only: point the recursive resolver at a loopback mock server.
307    #[cfg(test)]
308    fn with_recursive_upstream(mut self, ip: IpAddr, port: u16) -> Self {
309        self.recursive_upstream = Some((ip, port));
310        self.recursive = build_recursive_resolver(self.timeout, self.recursive_upstream);
311        self
312    }
313
314    /// Test-only: start the walk at these `(host, address)` root servers.
315    #[cfg(test)]
316    fn with_root_hints(mut self, hints: &[(&str, IpAddr)]) -> Self {
317        self.root_hints = Some(
318            hints
319                .iter()
320                .map(|(host, ip)| NsCandidate {
321                    host: Name::from_ascii(fqdn(host)).expect("valid test root name"),
322                    addrs: vec![*ip],
323                })
324                .collect(),
325        );
326        self
327    }
328
329    /// Test-only: route direct queries for the given host names (with or
330    /// without the trailing dot) to per-host mock ports instead of port 53.
331    #[cfg(test)]
332    fn with_port_map(mut self, map: std::collections::HashMap<String, u16>) -> Self {
333        self.port_map = Some(
334            map.into_iter()
335                .map(|(host, port)| (host.trim_end_matches('.').to_ascii_lowercase(), port))
336                .collect(),
337        );
338        self
339    }
340
341    /// Test-only: allow queries to loopback/private addresses (mock servers).
342    #[cfg(test)]
343    fn allowing_private_hosts(mut self) -> Self {
344        self.allow_private_hosts = true;
345        self
346    }
347
348    /// Test-only: fail every direct query to these addresses locally, before
349    /// anything is sent, with the socket error `error` makes — "network
350    /// unreachable" is what an IPv4 server address gives on an IPv6-only
351    /// host, "address family not supported" what an IPv6 one gives when the
352    /// kernel has IPv6 disabled.
353    #[cfg(test)]
354    fn with_unroutable(mut self, addrs: &[IpAddr], error: fn() -> std::io::Error) -> Self {
355        self.unroutable.extend(addrs.iter().map(|&ip| (ip, error)));
356        self
357    }
358
359    #[cfg(test)]
360    fn recursive_upstream(&self) -> Option<(IpAddr, u16)> {
361        self.recursive_upstream
362    }
363
364    #[cfg(not(test))]
365    fn recursive_upstream(&self) -> Option<(IpAddr, u16)> {
366        None
367    }
368
369    #[cfg(test)]
370    fn allow_private(&self) -> bool {
371        self.allow_private_hosts
372    }
373
374    #[cfg(not(test))]
375    fn allow_private(&self) -> bool {
376        false
377    }
378
379    /// The servers the walk starts from: the root hints, or the test seam's
380    /// override.
381    #[cfg(test)]
382    fn root_servers(&self) -> Vec<NsCandidate> {
383        self.root_hints.clone().unwrap_or_else(root_hints)
384    }
385
386    #[cfg(not(test))]
387    fn root_servers(&self) -> Vec<NsCandidate> {
388        root_hints()
389    }
390
391    /// Port used for a direct query to `host`. Always 53 in production; the
392    /// `#[cfg(test)]` port map lets each mock server bind its own ephemeral
393    /// loopback port.
394    #[cfg(test)]
395    fn direct_port(&self, host: &Name) -> u16 {
396        let key = name_text(host);
397        self.port_map
398            .as_ref()
399            .and_then(|map| map.get(key.trim_end_matches('.')).copied())
400            .unwrap_or(53)
401    }
402
403    #[cfg(not(test))]
404    fn direct_port(&self, _host: &Name) -> u16 {
405        53
406    }
407
408    /// The local routing failure the `#[cfg(test)]` seam simulates for `ip`,
409    /// if any. Never one in production: there the socket reports it.
410    #[cfg(test)]
411    fn simulated_no_route(&self, ip: IpAddr) -> Option<NetError> {
412        self.unroutable
413            .iter()
414            .find(|(addr, _)| *addr == ip)
415            .map(|(_, error)| NetError::from(error()))
416    }
417
418    #[cfg(not(test))]
419    fn simulated_no_route(&self, _ip: IpAddr) -> Option<NetError> {
420        None
421    }
422
423    /// Traces the resolution of `name` from the root servers down.
424    ///
425    /// Each hop asks one server of the current zone directly, with recursion
426    /// disabled, and follows its referral to the next zone, until a server
427    /// answers (a CNAME is reported, not chased), returns NXDOMAIN or NODATA,
428    /// or the walk fails. A server that does not respond, returns an error
429    /// RCODE or gives a lame server's empty non-authoritative reply is passed
430    /// over for the zone's next one. A referral must lead toward `name`; an
431    /// upward or sideways one is passed over the same way. Server addresses
432    /// come from the root hints, the referral's glue, or a recursive lookup
433    /// for glueless nameservers, and each one is refused when it is private
434    /// or reserved.
435    /// Up to 16 delegation levels are walked and up to 3 servers asked per
436    /// level, each query and glueless lookup under the configured DNS
437    /// timeout, and the whole walk under six times that timeout: a walk that
438    /// runs out of time returns the hops so far with an error.
439    ///
440    /// # Arguments
441    /// * `name` - The name to trace, prepared exactly as
442    ///   [`DnsResolver::query`](crate::dns::DnsResolver::query) prepares it
443    ///   (`www.` is kept, a leading `*` label is accepted, SRV needs the
444    ///   `_service._proto.name` form, and a PTR query may name an IP literal)
445    /// * `record_type` - The type to ask for: any single type (not `ANY`)
446    ///
447    /// # Returns
448    /// * `Ok(DnsTrace)` - the hops walked; a walk that stopped before a final
449    ///   response carries the reason in [`DnsTrace::error`]
450    /// * `Err(SeerError)` - invalid input, `ANY`, or no root server answered
451    #[instrument(skip(self), fields(name = %name, record_type = %record_type))]
452    pub async fn trace(&self, name: &str, record_type: RecordType) -> Result<DnsTrace> {
453        let qtype = trace_wire_type(record_type)?;
454        let (name, qname) = query_target(name, record_type)?;
455
456        let mut zone = Name::root();
457        let mut servers = self.root_servers();
458        let mut hops: Vec<TraceHop> = Vec::new();
459        let mut error = None;
460        // One deadline for the whole walk: each level is bounded on its own,
461        // but the levels run one after another.
462        let budget = self.timeout.saturating_mul(TRACE_BUDGET_TIMEOUTS);
463        let started = Instant::now();
464
465        loop {
466            let remaining = budget.saturating_sub(started.elapsed());
467            let level = self.ask_level(&zone, &servers, &qname, qtype);
468            let Ok(outcome) = tokio::time::timeout(remaining, level).await else {
469                let why = format!(
470                    "gave up after {budget:?} without a final response, while asking the \
471                     nameservers of {}",
472                    name_text(&zone)
473                );
474                if hops.is_empty() {
475                    return Err(SeerError::DnsError(format!(
476                        "trace of {name} failed: {why}"
477                    )));
478                }
479                error = Some(why);
480                break;
481            };
482            let (hop, step) = match outcome {
483                LevelOutcome::Answered { hop, step } => (hop, step),
484                LevelOutcome::Failed { final_hop, reason } => {
485                    let why = format!(
486                        "no nameserver for {} gave a usable response: {}",
487                        name_text(&zone),
488                        reason
489                    );
490                    match final_hop {
491                        Some(hop) => hops.push(hop),
492                        // Without one root response there is nothing to trace.
493                        None if hops.is_empty() => {
494                            return Err(SeerError::DnsError(format!(
495                                "trace of {name} failed: {why}"
496                            )))
497                        }
498                        None => {}
499                    }
500                    error = Some(why);
501                    break;
502                }
503            };
504
505            // `ask_level` accepts only a referral inside the bailiwick.
506            let referral = match step {
507                Step::Final => {
508                    hops.push(hop);
509                    break;
510                }
511                Step::Referral(referral) => referral,
512            };
513            hops.push(hop);
514            if hops.len() >= MAX_HOPS {
515                error = Some(format!(
516                    "gave up after {MAX_HOPS} delegation levels without a final response"
517                ));
518                break;
519            }
520            zone = referral.zone;
521            servers = referral.servers;
522        }
523
524        let (status, answers) = match hops.last() {
525            Some(last) => (last.status, last.answers.clone()),
526            // Unreachable: a walk that recorded no hop returned early above.
527            None => {
528                return Err(SeerError::DnsError(format!(
529                    "trace of {name} produced no response"
530                )))
531            }
532        };
533        Ok(DnsTrace {
534            name,
535            record_type,
536            hops,
537            status,
538            answers,
539            error,
540        })
541    }
542
543    /// Asks the servers of `zone` in turn until one gives a usable response
544    /// (a final one or a referral), recording every skipped or failed server.
545    async fn ask_level(
546        &self,
547        zone: &Name,
548        servers: &[NsCandidate],
549        qname: &Name,
550        qtype: HickoryRecordType,
551    ) -> LevelOutcome {
552        let (picks, mut failures) = plan_servers(servers, self.allow_private());
553        let mut queried = 0;
554        let mut resolved = 0;
555        // The last unusable response (an error RCODE or a lame reply), and
556        // the index of its own note in `failures`: it becomes the final hop
557        // if no server does better.
558        let mut unusable_hop: Option<(TraceHop, usize)> = None;
559
560        for pick in picks {
561            if queried == MAX_SERVERS_PER_LEVEL {
562                break;
563            }
564            let (host, addrs) = match pick {
565                ServerPick::Addrs(host, addrs) => (host, addrs),
566                ServerPick::Glueless(host) => {
567                    // Glueless picks come last, so none remain to try.
568                    if resolved == MAX_GLUELESS_PER_LEVEL {
569                        break;
570                    }
571                    resolved += 1;
572                    let (addrs, notes) = self.resolve_glueless(&host).await;
573                    failures.extend(notes);
574                    (host, addrs)
575                }
576            };
577            let Some((ip, elapsed, result)) = self
578                .ask_server(&host, &addrs, qname, qtype, &mut failures)
579                .await
580            else {
581                continue;
582            };
583            queried += 1;
584            match result {
585                Ok(response) => {
586                    let mut hop = hop_from_response(zone, &host, ip, elapsed, &response);
587                    let reason = match classify_response(&response, zone) {
588                        Ok(Step::Referral(referral)) => {
589                            hop.referral_zone = Some(name_text(&referral.zone));
590                            hop.referral = referral
591                                .servers
592                                .iter()
593                                .map(|s| name_text(&s.host))
594                                .collect();
595                            // A referral off the path to the name — one lame
596                            // server's upward or sideways pointer — is as
597                            // unusable as a lame reply: the next server of
598                            // the zone may still refer correctly.
599                            match check_bailiwick(zone, &referral.zone, qname) {
600                                Ok(()) => {
601                                    hop.failed_servers = failures;
602                                    let step = Step::Referral(referral);
603                                    return LevelOutcome::Answered { hop, step };
604                                }
605                                Err(fault) => format!(
606                                    "referred {fault} to {} (not toward the name)",
607                                    name_text(&referral.zone)
608                                ),
609                            }
610                        }
611                        Ok(step) => {
612                            hop.failed_servers = failures;
613                            return LevelOutcome::Answered { hop, step };
614                        }
615                        Err(reason) => reason,
616                    };
617                    unusable_hop = Some((hop, failures.len()));
618                    failures.push(server_note(&host, ip, &reason));
619                }
620                Err(e) => failures.push(server_note(&host, ip, &transport_reason(&e))),
621            }
622        }
623
624        let reason = if failures.is_empty() {
625            "no nameserver address to query".to_string()
626        } else {
627            failures.join("; ")
628        };
629        let final_hop = unusable_hop.map(|(mut hop, own_note)| {
630            failures.remove(own_note);
631            hop.failed_servers = failures;
632            hop
633        });
634        LevelOutcome::Failed { final_hop, reason }
635    }
636
637    /// Queries one server on its addresses in turn until a query leaves this
638    /// host: an address with no local route fails at once, is noted in
639    /// `failures`, and the next one is tried. Returns the address queried,
640    /// the round-trip time and the exchange's result, or `None` when no
641    /// address could be reached.
642    async fn ask_server(
643        &self,
644        host: &Name,
645        addrs: &[IpAddr],
646        qname: &Name,
647        qtype: HickoryRecordType,
648        failures: &mut Vec<String>,
649    ) -> Option<(IpAddr, Duration, std::result::Result<Message, NetError>)> {
650        for &ip in addrs {
651            debug!(server = %name_text(host), %ip, "trace: querying");
652            let started = Instant::now();
653            match self.exchange(host, ip, qname, qtype).await {
654                Err(e) if is_no_route(&e) => {
655                    failures.push(server_note(host, ip, &transport_reason(&e)));
656                }
657                result => return Some((ip, started.elapsed(), result)),
658            }
659        }
660        None
661    }
662
663    /// Resolves a glueless nameserver's addresses through the recursive
664    /// resolver, under the per-query timeout, vetting every address it
665    /// returns. Returns the addresses to query, IPv4 first (none when the
666    /// lookup failed or every address was refused), and notes for the failed
667    /// lookup or the refused addresses.
668    async fn resolve_glueless(&self, host: &Name) -> (Vec<IpAddr>, Vec<String>) {
669        let text = name_text(host);
670        // The resolver re-sends a query that timed out (`attempts`), so only
671        // this deadline holds the lookup to one per-query timeout.
672        let lookup = tokio::time::timeout(self.timeout, self.recursive.lookup_ip(text.as_str()))
673            .await
674            .unwrap_or(Err(NetError::Timeout));
675        let ips: Vec<IpAddr> = match lookup {
676            Ok(lookup) => lookup.iter().collect(),
677            Err(e) if e.is_no_records_found() => Vec::new(),
678            Err(e) => {
679                return (
680                    Vec::new(),
681                    vec![format!(
682                        "{text}: glueless nameserver lookup failed: {}",
683                        transport_reason(&e)
684                    )],
685                )
686            }
687        };
688        let (usable, refused) = partition_reserved(&ips, self.allow_private());
689        let mut notes: Vec<String> = refused
690            .iter()
691            .map(|(ip, reason)| server_note(host, *ip, &format!("refused, {reason}")))
692            .collect();
693        if usable.is_empty() && notes.is_empty() {
694            notes.push(format!("{text}: glueless nameserver has no address"));
695        }
696        let mut usable = usable;
697        crate::net::ipv4_first(&mut usable, |ip| *ip);
698        (usable, notes)
699    }
700
701    /// Sends one non-recursive query to `ip` and returns the response as the
702    /// server sent it (see [`Transport::exchange`]): over UDP, then once more
703    /// over TCP when the UDP reply is truncated, the whole exchange under one
704    /// per-query deadline.
705    async fn exchange(
706        &self,
707        host: &Name,
708        ip: IpAddr,
709        qname: &Name,
710        qtype: HickoryRecordType,
711    ) -> std::result::Result<Message, NetError> {
712        if let Some(no_route) = self.simulated_no_route(ip) {
713            return Err(no_route);
714        }
715        let mut udp = ConnectionConfig::udp();
716        udp.port = self.direct_port(host);
717        // Delegation data must come from each server's own authority, not
718        // from recursion or a forwarder's cache.
719        let request = self.transport.request(qname.clone(), qtype, false);
720        self.transport.exchange(ip, &udp, &request).await
721    }
722}
723
724/// A nameserver of the zone being asked, and the addresses known for it
725/// (glue or root hint). No addresses means glueless.
726#[derive(Debug, Clone, PartialEq)]
727struct NsCandidate {
728    host: Name,
729    addrs: Vec<IpAddr>,
730}
731
732/// A referral: the child zone and its nameservers (sorted by name).
733#[derive(Debug, PartialEq)]
734struct Referral {
735    zone: Name,
736    servers: Vec<NsCandidate>,
737}
738
739/// What a usable response means for the walk.
740#[derive(Debug, PartialEq)]
741enum Step {
742    /// A final response: an answer, NXDOMAIN, or NODATA.
743    Final,
744    /// A referral to follow (once its bailiwick is checked).
745    Referral(Referral),
746}
747
748/// The outcome of asking one delegation level.
749enum LevelOutcome {
750    /// A server gave a usable response: a final one or a referral.
751    Answered { hop: TraceHop, step: Step },
752    /// No server did. `final_hop` is the last unusable response (an error
753    /// RCODE or a lame reply), if any server responded at all; `reason`
754    /// lists every failure.
755    Failed {
756        final_hop: Option<TraceHop>,
757        reason: String,
758    },
759}
760
761/// Where to send a level's queries, in order.
762#[derive(Debug, PartialEq)]
763enum ServerPick {
764    /// A server with vetted addresses (glue or root hints), IPv4 first.
765    Addrs(Name, Vec<IpAddr>),
766    /// A glueless server, whose address must be looked up first.
767    Glueless(Name),
768}
769
770/// Why a referral was refused.
771#[derive(Debug, Clone, Copy, PartialEq)]
772enum ReferralFault {
773    /// The referred zone is the current zone or one of its ancestors.
774    Upward,
775    /// The referred zone is not on the path from the current zone to the
776    /// query name.
777    Sideways,
778}
779
780impl std::fmt::Display for ReferralFault {
781    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
782        f.write_str(match self {
783            ReferralFault::Upward => "upward",
784            ReferralFault::Sideways => "sideways",
785        })
786    }
787}
788
789/// The root hints as query candidates.
790///
791/// The `expect`s are invariants over literals, like a compiled regex:
792/// `root_hints_are_complete_and_public` parses every entry.
793fn root_hints() -> Vec<NsCandidate> {
794    ROOT_SERVERS
795        .iter()
796        .map(|(host, v4, v6)| NsCandidate {
797            host: Name::from_ascii(host).expect("root hint names are valid"),
798            addrs: [v4, v6]
799                .iter()
800                .map(|addr| addr.parse().expect("root hint addresses are valid"))
801                .collect(),
802        })
803        .collect()
804}
805
806/// The wire type a trace asks for: that of any single type. ANY — the one
807/// type without a wire type — is a fan-out over several queries that no
808/// single walk can follow.
809fn trace_wire_type(record_type: RecordType) -> Result<HickoryRecordType> {
810    wire_type(record_type).ok_or_else(|| {
811        SeerError::InvalidInput(
812            "trace needs a single record type; ANY is a fan-out over several queries".to_string(),
813        )
814    })
815}
816
817/// The query name, prepared by the resolver's own steps (`prepare_query`,
818/// then `wire_query_name`'s SRV and PTR rules) so a trace asks for — and
819/// reports — exactly the name `DnsResolver::query` would. Returned as
820/// reported (no trailing dot) and as the wire name; a PTR query for an IP
821/// literal is its reverse-DNS name.
822fn query_target(name: &str, record_type: RecordType) -> Result<(String, Name)> {
823    let reported = wire_query_name(&prepare_query(name, record_type)?, record_type)?;
824    let qname = Name::from_ascii(fqdn(&reported))
825        .map_err(|e| SeerError::InvalidDomain(format!("{reported}: {e}")))?;
826    Ok((reported, qname))
827}
828
829/// Orders a level's servers for querying — every server with a known
830/// address first, then the glueless ones — and vets every known address,
831/// keeping a server's usable ones IPv4 first. Returns the picks and a note
832/// per refused address; a server whose addresses are all refused is dropped
833/// rather than looked up again, since the zone itself pointed it at
834/// reserved space.
835fn plan_servers(servers: &[NsCandidate], allow_private: bool) -> (Vec<ServerPick>, Vec<String>) {
836    let mut picks = Vec::new();
837    let mut glueless = Vec::new();
838    let mut notes = Vec::new();
839    for server in servers {
840        if server.addrs.is_empty() {
841            glueless.push(ServerPick::Glueless(server.host.clone()));
842            continue;
843        }
844        let (usable, refused) = partition_reserved(&server.addrs, allow_private);
845        notes.extend(
846            refused
847                .iter()
848                .map(|(ip, reason)| server_note(&server.host, *ip, &format!("refused, {reason}"))),
849        );
850        if !usable.is_empty() {
851            let mut usable = usable;
852            crate::net::ipv4_first(&mut usable, |ip| *ip);
853            picks.push(ServerPick::Addrs(server.host.clone(), usable));
854        }
855    }
856    picks.extend(glueless);
857    (picks, notes)
858}
859
860/// Classifies a server's response for the walk, or says why it is unusable.
861///
862/// Any answer, an authoritative response or an SOA in AUTHORITY is final —
863/// an answer, NXDOMAIN or NODATA (a referral never carries the AA bit, and a
864/// negative answer carries the zone's SOA, RFC 2308); otherwise NS records
865/// in AUTHORITY are a referral. An error RCODE is unusable, and so is what
866/// remains — AA clear, no answer, no SOA, no referral: a lame server's
867/// reply, which proves nothing about the name, so the zone's next server is
868/// asked.
869fn classify_response(response: &Message, zone: &Name) -> std::result::Result<Step, String> {
870    let code = response.metadata.response_code;
871    if !matches!(code, ResponseCode::NoError | ResponseCode::NXDomain) {
872        return Err(DnsStatus::from(code).to_string());
873    }
874    if !response.answers.is_empty()
875        || response.metadata.authoritative
876        || response
877            .authorities
878            .iter()
879            .any(|record| record.record_type() == HickoryRecordType::SOA)
880    {
881        return Ok(Step::Final);
882    }
883    if code == ResponseCode::NXDomain {
884        return Err("non-authoritative NXDOMAIN without an SOA".to_string());
885    }
886    extract_referral(response, zone)
887        .map(Step::Referral)
888        .ok_or_else(|| "empty non-authoritative response (no answer, referral or SOA)".to_string())
889}
890
891/// Reads a referral from AUTHORITY: the child zone is the owner of the
892/// first NS record, and its nameservers are every NS target under that
893/// owner, deduplicated and sorted by name. Glue is taken from ADDITIONAL for
894/// NS names inside `zone` (the responding server's own zone) only — glue for
895/// a name elsewhere is not the server's to vouch for, so such a nameserver
896/// is treated as glueless, as a resolver would.
897fn extract_referral(response: &Message, zone: &Name) -> Option<Referral> {
898    let child = response
899        .authorities
900        .iter()
901        .find(|record| matches!(record.data, HickoryRData::NS(_)))?
902        .name
903        .to_lowercase();
904    let targets: BTreeMap<String, Name> = response
905        .authorities
906        .iter()
907        .filter(|record| record.name == child)
908        .filter_map(|record| match &record.data {
909            HickoryRData::NS(ns) => {
910                let host = ns.0.to_lowercase();
911                Some((name_text(&host), host))
912            }
913            _ => None,
914        })
915        .collect();
916    let servers = targets
917        .into_values()
918        .map(|host| {
919            let mut addrs: Vec<IpAddr> = Vec::new();
920            if zone.zone_of(&host) {
921                for record in response.additionals.iter().filter(|r| r.name == host) {
922                    let ip = match &record.data {
923                        HickoryRData::A(a) => IpAddr::V4(a.0),
924                        HickoryRData::AAAA(aaaa) => IpAddr::V6(aaaa.0),
925                        _ => continue,
926                    };
927                    if !addrs.contains(&ip) {
928                        addrs.push(ip);
929                    }
930                }
931            }
932            NsCandidate { host, addrs }
933        })
934        .collect();
935    Some(Referral {
936        zone: child,
937        servers,
938    })
939}
940
941/// Checks a referral from `zone` to `child` against the bailiwick rule: the
942/// child must lie strictly below `zone` (anything else could loop) and at or
943/// above `qname` (anything else leads away from the answer).
944fn check_bailiwick(
945    zone: &Name,
946    child: &Name,
947    qname: &Name,
948) -> std::result::Result<(), ReferralFault> {
949    if child.zone_of(zone) {
950        return Err(ReferralFault::Upward);
951    }
952    if !zone.zone_of(child) || !child.zone_of(qname) {
953        return Err(ReferralFault::Sideways);
954    }
955    Ok(())
956}
957
958/// Builds a hop from a server's response (referral fields are filled in by
959/// the caller once the response is classified).
960fn hop_from_response(
961    zone: &Name,
962    host: &Name,
963    ip: IpAddr,
964    elapsed: Duration,
965    response: &Message,
966) -> TraceHop {
967    TraceHop {
968        zone: name_text(zone),
969        server: name_text(host),
970        address: ip.to_string(),
971        query_time_ms: duration_ms(elapsed),
972        status: DnsStatus::from(response.metadata.response_code),
973        authoritative: response.metadata.authoritative,
974        referral_zone: None,
975        referral: Vec::new(),
976        answers: response.answers.iter().filter_map(to_dns_record).collect(),
977        failed_servers: Vec::new(),
978    }
979}
980
981/// A `failed_servers` entry: `"host (ip): reason"`.
982fn server_note(host: &Name, ip: IpAddr, reason: &str) -> String {
983    format!("{} ({}): {}", name_text(host), ip, reason)
984}
985
986/// True when an exchange failed before its query left this host: the
987/// kernel had no route to the server, or no socket of its address family
988/// (see `delegation::is_local_no_route`).
989fn is_no_route(err: &NetError) -> bool {
990    matches!(err, NetError::Io(io) if is_local_no_route(io))
991}
992
993/// A name as the trace reports zones and servers: fully qualified,
994/// lowercase ASCII (A-labels); the root is `"."`.
995fn name_text(name: &Name) -> String {
996    name.to_ascii().to_ascii_lowercase()
997}
998
999#[cfg(test)]
1000mod tests {
1001    //! Hermetic tests: every server of a scenario (each zone's nameserver,
1002    //! and the recursive resolver for glueless names) is a loopback mock on
1003    //! its own ephemeral port from the shared [`crate::dns::test_support`]
1004    //! fixture, reached through the `#[cfg(test)]` seams (`with_root_hints`,
1005    //! `with_port_map`, `allowing_private_hosts`, `with_recursive_upstream`).
1006
1007    use std::collections::HashMap;
1008    use std::net::{Ipv4Addr, Ipv6Addr};
1009    use std::sync::{Arc, Mutex};
1010
1011    use hickory_resolver::proto::op::OpCode;
1012    use hickory_resolver::proto::rr::rdata as wire;
1013    use hickory_resolver::proto::rr::Record;
1014
1015    use super::*;
1016    use crate::dns::delegation::ADDRESS_FAMILY_UNSUPPORTED;
1017    use crate::dns::records::RecordData;
1018    use crate::dns::test_support::{
1019        a_rdata, cname_rdata, fq_name, record, soa_rdata, spawn_mock_dns, spawn_mock_dns_fn,
1020        spawn_mock_dns_fn_with_tcp, MockMode, MockReply,
1021    };
1022
1023    const LOOPBACK: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
1024
1025    /// The local failure an IPv4 address gives on an IPv6-only host.
1026    fn network_unreachable() -> std::io::Error {
1027        std::io::Error::new(
1028            std::io::ErrorKind::NetworkUnreachable,
1029            "Network is unreachable",
1030        )
1031    }
1032
1033    /// The local failure an IPv6 address gives when the kernel has IPv6
1034    /// disabled: EAFNOSUPPORT, by its raw OS code, as the socket call
1035    /// reports it.
1036    fn address_family_unsupported() -> std::io::Error {
1037        let code = ADDRESS_FAMILY_UNSUPPORTED.expect("a Unix or Windows target");
1038        std::io::Error::from_raw_os_error(code)
1039    }
1040
1041    // --- pure helpers ----------------------------------------------------
1042
1043    #[test]
1044    fn root_hints_are_complete_and_public() {
1045        let hints = root_hints();
1046        assert_eq!(hints.len(), 13);
1047        // One entry per root server, a through m.
1048        for (letter, hint) in ('a'..='m').zip(&hints) {
1049            assert_eq!(name_text(&hint.host), format!("{letter}.root-servers.net."));
1050            assert_eq!(hint.addrs.len(), 2);
1051            assert!(hint.addrs[0].is_ipv4() && hint.addrs[1].is_ipv6());
1052            // The production vetting must never refuse a root server.
1053            let (usable, refused) = partition_reserved(&hint.addrs, false);
1054            assert_eq!(usable, hint.addrs);
1055            assert!(refused.is_empty(), "{refused:?}");
1056        }
1057    }
1058
1059    #[test]
1060    fn name_text_is_fully_qualified_lowercase_ascii() {
1061        assert_eq!(name_text(&Name::root()), ".");
1062        assert_eq!(name_text(&fq_name("Example.COM")), "example.com.");
1063        assert_eq!(
1064            name_text(&Name::from_utf8("münchen.de.").unwrap()),
1065            "xn--mnchen-3ya.de."
1066        );
1067    }
1068
1069    #[test]
1070    fn bailiwick_accepts_only_strict_descendants_on_the_path() {
1071        let qname = fq_name("www.example.com");
1072        // Root → com → example.com: each step descends toward the name.
1073        assert_eq!(
1074            check_bailiwick(&Name::root(), &fq_name("com"), &qname),
1075            Ok(())
1076        );
1077        assert_eq!(
1078            check_bailiwick(&fq_name("com"), &fq_name("example.com"), &qname),
1079            Ok(())
1080        );
1081        // A zone cut at the name itself is on the path.
1082        assert_eq!(
1083            check_bailiwick(&fq_name("example.com"), &fq_name("www.example.com"), &qname),
1084            Ok(())
1085        );
1086        // Skipping levels is fine (a parent may serve several cuts).
1087        assert_eq!(
1088            check_bailiwick(&Name::root(), &fq_name("example.com"), &qname),
1089            Ok(())
1090        );
1091        // Case differences are not a new zone.
1092        assert_eq!(
1093            check_bailiwick(&fq_name("com"), &fq_name("EXAMPLE.com"), &qname),
1094            Ok(())
1095        );
1096
1097        // Back to the root, to the same zone, or to an ancestor: upward.
1098        assert_eq!(
1099            check_bailiwick(&fq_name("com"), &Name::root(), &qname),
1100            Err(ReferralFault::Upward)
1101        );
1102        assert_eq!(
1103            check_bailiwick(&fq_name("com"), &fq_name("com"), &qname),
1104            Err(ReferralFault::Upward)
1105        );
1106        assert_eq!(
1107            check_bailiwick(&fq_name("example.com"), &fq_name("com"), &qname),
1108            Err(ReferralFault::Upward)
1109        );
1110        // Below the zone but off the path, or outside the zone: sideways.
1111        assert_eq!(
1112            check_bailiwick(&fq_name("com"), &fq_name("other.com"), &qname),
1113            Err(ReferralFault::Sideways)
1114        );
1115        assert_eq!(
1116            check_bailiwick(&fq_name("com"), &fq_name("example.net"), &qname),
1117            Err(ReferralFault::Sideways)
1118        );
1119        // Below the query name is past the answer.
1120        assert_eq!(
1121            check_bailiwick(
1122                &fq_name("example.com"),
1123                &fq_name("a.www.example.com"),
1124                &qname
1125            ),
1126            Err(ReferralFault::Sideways)
1127        );
1128    }
1129
1130    fn message() -> Message {
1131        Message::response(1, OpCode::Query)
1132    }
1133
1134    fn ns(owner: &str, target: &str) -> Record {
1135        record(owner, 300, HickoryRData::NS(wire::NS(fq_name(target))))
1136    }
1137
1138    fn a(owner: &str, ip: Ipv4Addr) -> Record {
1139        record(owner, 300, a_rdata(ip))
1140    }
1141
1142    fn aaaa(owner: &str, ip: Ipv6Addr) -> Record {
1143        record(owner, 300, HickoryRData::AAAA(wire::AAAA(ip)))
1144    }
1145
1146    #[test]
1147    fn referral_extraction_reads_ns_and_in_bailiwick_glue() {
1148        let mut response = message();
1149        // Out of order and mixed case, with a duplicate: sorted and deduped.
1150        response.add_authority(ns("Example.COM", "ns2.example.com"));
1151        response.add_authority(ns("example.com", "NS1.example.com"));
1152        response.add_authority(ns("example.com", "ns1.example.com"));
1153        response.add_authority(ns("example.com", "ns.dns-host.net"));
1154        // An NS record for another owner is not part of this referral.
1155        response.add_authority(ns("other.com", "ns.other.com"));
1156        let v4 = Ipv4Addr::new(192, 0, 2, 1);
1157        let v6 = "2001:db8::1".parse().unwrap();
1158        response.add_additional(aaaa("ns1.example.com", v6));
1159        response.add_additional(a("ns1.example.com", v4));
1160        response.add_additional(a("ns1.example.com", v4));
1161        // Glue for a name outside the responding server's zone (`com`) is
1162        // not trusted: that nameserver counts as glueless.
1163        response.add_additional(a("ns.dns-host.net", Ipv4Addr::new(198, 51, 100, 7)));
1164        // Additional records for names that are not NS targets are ignored.
1165        response.add_additional(a("unrelated.example.com", v4));
1166
1167        let referral = extract_referral(&response, &fq_name("com")).expect("a referral");
1168        assert_eq!(referral.zone, fq_name("example.com"));
1169        assert_eq!(name_text(&referral.zone), "example.com.");
1170        let servers: Vec<(String, Vec<IpAddr>)> = referral
1171            .servers
1172            .iter()
1173            .map(|s| (name_text(&s.host), s.addrs.clone()))
1174            .collect();
1175        assert_eq!(
1176            servers,
1177            vec![
1178                ("ns.dns-host.net.".to_string(), vec![]),
1179                (
1180                    "ns1.example.com.".to_string(),
1181                    vec![IpAddr::V6(v6), IpAddr::V4(v4)]
1182                ),
1183                ("ns2.example.com.".to_string(), vec![]),
1184            ]
1185        );
1186        // The root may vouch for any glue.
1187        let from_root = extract_referral(&response, &Name::root()).expect("a referral");
1188        assert_eq!(from_root.servers[0].addrs.len(), 1);
1189
1190        // No NS in AUTHORITY: no referral.
1191        assert_eq!(extract_referral(&message(), &fq_name("com")), None);
1192    }
1193
1194    #[test]
1195    fn classification_separates_answers_negatives_referrals_and_lame_replies() {
1196        let zone = fq_name("com");
1197        let mut referral = message();
1198        referral.add_authority(ns("example.com", "ns1.example.com"));
1199        assert!(matches!(
1200            classify_response(&referral, &zone),
1201            Ok(Step::Referral(_))
1202        ));
1203
1204        // The same NS set with the AA bit is an authoritative NODATA.
1205        let mut authoritative = referral.clone();
1206        authoritative.metadata.authoritative = true;
1207        assert_eq!(classify_response(&authoritative, &zone), Ok(Step::Final));
1208
1209        // An SOA in AUTHORITY is a negative answer, NS records or not.
1210        let mut nodata = referral.clone();
1211        nodata.add_authority(record("com", 300, soa_rdata("com")));
1212        assert_eq!(classify_response(&nodata, &zone), Ok(Step::Final));
1213        let mut nxdomain = nodata.clone();
1214        nxdomain.metadata.response_code = ResponseCode::NXDomain;
1215        assert_eq!(classify_response(&nxdomain, &zone), Ok(Step::Final));
1216        // An authoritative NXDOMAIN is final even without its SOA.
1217        let mut aa_nxdomain = message();
1218        aa_nxdomain.metadata.authoritative = true;
1219        aa_nxdomain.metadata.response_code = ResponseCode::NXDomain;
1220        assert_eq!(classify_response(&aa_nxdomain, &zone), Ok(Step::Final));
1221
1222        // Any answer is final, authoritative or not (a CNAME included).
1223        let mut answer = referral.clone();
1224        answer.add_answer(record("www.example.com", 300, cname_rdata("edge.cdn.net")));
1225        assert_eq!(classify_response(&answer, &zone), Ok(Step::Final));
1226
1227        // An error RCODE is unusable, whatever it carries.
1228        let mut servfail = answer;
1229        servfail.metadata.response_code = ResponseCode::ServFail;
1230        assert_eq!(
1231            classify_response(&servfail, &zone),
1232            Err("SERVFAIL".to_string())
1233        );
1234
1235        // Nothing at all from a server that is not authoritative: a lame
1236        // reply, not NODATA — it proves nothing about the name.
1237        let lame = classify_response(&message(), &zone).expect_err("a lame reply");
1238        assert!(lame.contains("empty non-authoritative"), "{lame}");
1239        // Nor is a non-authoritative NXDOMAIN without the zone's SOA a
1240        // negative answer, even beside NS records.
1241        let mut bare_nxdomain = referral;
1242        bare_nxdomain.metadata.response_code = ResponseCode::NXDomain;
1243        let lame = classify_response(&bare_nxdomain, &zone).expect_err("a lame reply");
1244        assert!(lame.contains("NXDOMAIN without an SOA"), "{lame}");
1245    }
1246
1247    #[test]
1248    fn server_plan_orders_glued_first_prefers_ipv4_and_refuses_reserved() {
1249        let public_v4: IpAddr = "192.5.6.30".parse().unwrap();
1250        let public_v6: IpAddr = "2001:503:a83e::2:30".parse().unwrap();
1251        let private: IpAddr = "10.0.0.53".parse().unwrap();
1252        let metadata: IpAddr = "169.254.169.254".parse().unwrap();
1253        let servers = vec![
1254            NsCandidate {
1255                host: fq_name("glueless.example.net"),
1256                addrs: vec![],
1257            },
1258            // AAAA listed first: the IPv4 address is still the one asked.
1259            NsCandidate {
1260                host: fq_name("ns1.example.com"),
1261                addrs: vec![public_v6, public_v4],
1262            },
1263            // Reserved glue beside a public address: the public one is used
1264            // and the reserved one is noted.
1265            NsCandidate {
1266                host: fq_name("ns2.example.com"),
1267                addrs: vec![private, public_v6],
1268            },
1269            // Only reserved glue: dropped, not looked up again.
1270            NsCandidate {
1271                host: fq_name("ns3.example.com"),
1272                addrs: vec![metadata],
1273            },
1274        ];
1275
1276        let (picks, notes) = plan_servers(&servers, false);
1277        assert_eq!(
1278            picks,
1279            vec![
1280                // Both addresses are kept, IPv4 first: the IPv6 one is the
1281                // fallback on a host with no IPv4 route.
1282                ServerPick::Addrs(fq_name("ns1.example.com"), vec![public_v4, public_v6]),
1283                ServerPick::Addrs(fq_name("ns2.example.com"), vec![public_v6]),
1284                ServerPick::Glueless(fq_name("glueless.example.net")),
1285            ]
1286        );
1287        assert_eq!(
1288            notes,
1289            vec![
1290                "ns2.example.com. (10.0.0.53): refused, private network (RFC 1918)",
1291                "ns3.example.com. (169.254.169.254): refused, cloud metadata endpoint (169.254.169.254)",
1292            ]
1293        );
1294
1295        // The test seam's flag is the only way past the vetting.
1296        let (picks, notes) = plan_servers(&servers, true);
1297        assert!(picks.contains(&ServerPick::Addrs(
1298            fq_name("ns3.example.com"),
1299            vec![metadata]
1300        )));
1301        assert!(notes.is_empty());
1302    }
1303
1304    #[test]
1305    fn query_target_follows_record_query_normalization() {
1306        // `www.` is kept and the name lowercased, as for every record query.
1307        let (name, qname) = query_target("WWW.Example.com", RecordType::A).unwrap();
1308        assert_eq!(name, "www.example.com");
1309        assert_eq!(qname, fq_name("www.example.com"));
1310        assert!(qname.is_fqdn());
1311        // A PTR query for an IP literal is named by the reverse-DNS name,
1312        // exactly as the resolver reports it.
1313        let (name, qname) = query_target("192.0.2.1", RecordType::PTR).unwrap();
1314        assert_eq!(name, "1.2.0.192.in-addr.arpa");
1315        assert_eq!(qname, fq_name("1.2.0.192.in-addr.arpa"));
1316        let (name, _) = query_target("2606:4700:4700::1111", RecordType::PTR).unwrap();
1317        assert_eq!(
1318            name,
1319            "1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa"
1320        );
1321        // SRV keeps the resolver's `_service._proto.name` rule.
1322        assert!(query_target("_sip._tcp.example.com", RecordType::SRV).is_ok());
1323        assert!(matches!(
1324            query_target("example.com", RecordType::SRV),
1325            Err(SeerError::InvalidInput(_))
1326        ));
1327        assert!(query_target("not a name", RecordType::A).is_err());
1328    }
1329
1330    #[test]
1331    fn query_target_applies_the_resolvers_srv_label_rule() {
1332        // `sip_x` passes the name normalizer but is no valid service label:
1333        // the trace refuses it exactly as `DnsResolver::query` does.
1334        let err = query_target("_sip_x._tcp.example.com", RecordType::SRV).unwrap_err();
1335        assert!(
1336            matches!(&err, SeerError::InvalidInput(m) if m.contains("invalid SRV service")),
1337            "{err:?}"
1338        );
1339    }
1340
1341    #[test]
1342    fn any_is_not_traceable() {
1343        assert!(matches!(
1344            trace_wire_type(RecordType::ANY),
1345            Err(SeerError::InvalidInput(_))
1346        ));
1347        assert_eq!(
1348            trace_wire_type(RecordType::SRV).unwrap(),
1349            HickoryRecordType::SRV
1350        );
1351        assert_eq!(
1352            trace_wire_type(RecordType::MX).unwrap(),
1353            HickoryRecordType::MX
1354        );
1355    }
1356
1357    #[test]
1358    fn trace_serializes_to_the_documented_shape() {
1359        let hop = TraceHop {
1360            zone: ".".to_string(),
1361            server: "a.root-servers.net.".to_string(),
1362            address: "198.41.0.4".to_string(),
1363            query_time_ms: 21,
1364            status: DnsStatus::NoError,
1365            authoritative: false,
1366            referral_zone: Some("com.".to_string()),
1367            referral: vec!["a.gtld-servers.net.".to_string()],
1368            answers: vec![],
1369            failed_servers: vec![],
1370        };
1371        let trace = DnsTrace {
1372            name: "www.example.com".to_string(),
1373            record_type: RecordType::A,
1374            hops: vec![hop],
1375            status: DnsStatus::NxDomain,
1376            answers: vec![],
1377            error: None,
1378        };
1379        let json = serde_json::to_value(&trace).unwrap();
1380        assert_eq!(
1381            json,
1382            serde_json::json!({
1383                "name": "www.example.com",
1384                "record_type": "A",
1385                "hops": [{
1386                    "zone": ".",
1387                    "server": "a.root-servers.net.",
1388                    "address": "198.41.0.4",
1389                    "query_time_ms": 21,
1390                    "status": "NOERROR",
1391                    "authoritative": false,
1392                    "referral_zone": "com.",
1393                    "referral": ["a.gtld-servers.net."],
1394                    "answers": [],
1395                    "failed_servers": []
1396                }],
1397                "status": "NXDOMAIN",
1398                "answers": [],
1399                "error": null
1400            })
1401        );
1402        // And it reads back.
1403        let back: DnsTrace = serde_json::from_value(json).unwrap();
1404        assert_eq!(back.hops[0].referral_zone.as_deref(), Some("com."));
1405        assert_eq!(back.status, DnsStatus::NxDomain);
1406    }
1407
1408    #[test]
1409    fn transport_reasons_are_short_and_name_local_routing_failures() {
1410        assert_eq!(transport_reason(&NetError::Timeout), "timed out");
1411        // An IPv6-only server asked from an IPv4-only host: the packet never
1412        // left, which the note must not blame on the server.
1413        let no_route = NetError::from(network_unreachable());
1414        assert!(is_no_route(&no_route));
1415        assert_eq!(
1416            transport_reason(&no_route),
1417            "no route from this host (Network is unreachable)"
1418        );
1419        // Nor when no socket of the server's address family can be opened
1420        // (IPv6 disabled in the kernel), whatever the OS calls it.
1421        let no_socket = NetError::from(address_family_unsupported());
1422        assert!(is_no_route(&no_socket));
1423        assert_eq!(
1424            transport_reason(&no_socket),
1425            format!("no route from this host ({})", address_family_unsupported())
1426        );
1427        let refused = NetError::from(std::io::Error::new(
1428            std::io::ErrorKind::ConnectionRefused,
1429            "refused",
1430        ));
1431        assert!(transport_reason(&refused).contains("refused"));
1432    }
1433
1434    #[test]
1435    fn from_config_applies_dns_timeout() {
1436        let mut config = crate::config::SeerConfig::default();
1437        config.timeouts.dns_secs = 9;
1438        let tracer = DnsTracer::from_config(&config);
1439        assert_eq!(tracer.timeout, Duration::from_secs(9));
1440    }
1441
1442    // --- end-to-end walks over loopback mocks ------------------------------
1443
1444    /// A referral to `zone` served by the given nameservers, each glued to
1445    /// the loopback address.
1446    fn delegation(zone: &str, servers: &[&str]) -> MockReply {
1447        MockReply::Delegation {
1448            zone: zone.to_string(),
1449            servers: servers
1450                .iter()
1451                .map(|s| (s.to_string(), vec![LOOPBACK]))
1452                .collect(),
1453        }
1454    }
1455
1456    /// A tracer over loopback mocks: `roots` are the root servers and
1457    /// `ports` maps every nameserver host to its mock.
1458    fn tracer(roots: &[&str], ports: &[(&str, u16)]) -> DnsTracer {
1459        let hints: Vec<(&str, IpAddr)> = roots.iter().map(|root| (*root, LOOPBACK)).collect();
1460        DnsTracer::new()
1461            .with_timeout(Duration::from_millis(500))
1462            .allowing_private_hosts()
1463            .with_root_hints(&hints)
1464            .with_port_map(
1465                ports
1466                    .iter()
1467                    .map(|(host, port)| (host.to_string(), *port))
1468                    .collect(),
1469            )
1470    }
1471
1472    /// Root → `test` → `example.test`, whose server answers with `answer`.
1473    async fn three_level_tracer(answer: fn(&str) -> MockReply) -> DnsTracer {
1474        let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
1475        let tld = spawn_mock_dns_fn(|_, _| {
1476            delegation("example.test", &["ns2.example.test", "ns1.example.test"])
1477        })
1478        .await;
1479        let auth = spawn_mock_dns_fn(move |qname, _| answer(qname)).await;
1480        tracer(
1481            &["a.root.test"],
1482            &[
1483                ("a.root.test", root),
1484                ("ns1.nic.test", tld),
1485                ("ns1.example.test", auth),
1486                ("ns2.example.test", auth),
1487            ],
1488        )
1489    }
1490
1491    #[tokio::test]
1492    async fn walks_root_tld_and_authoritative_server() {
1493        let tracer = three_level_tracer(|_| {
1494            MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 10))])
1495        })
1496        .await;
1497        let trace = tracer
1498            .trace("WWW.example.test", RecordType::A)
1499            .await
1500            .unwrap();
1501
1502        assert_eq!(trace.name, "www.example.test");
1503        assert_eq!(trace.record_type, RecordType::A);
1504        assert_eq!(trace.hops.len(), 3, "{trace:#?}");
1505        assert!(trace.error.is_none(), "{trace:#?}");
1506
1507        let root = &trace.hops[0];
1508        assert_eq!(root.zone, ".");
1509        assert_eq!(root.server, "a.root.test.");
1510        assert_eq!(root.address, "127.0.0.1");
1511        assert_eq!(root.status, DnsStatus::NoError);
1512        assert!(!root.authoritative);
1513        assert_eq!(root.referral_zone.as_deref(), Some("test."));
1514        assert_eq!(root.referral, vec!["ns1.nic.test."]);
1515        assert!(root.answers.is_empty());
1516
1517        let tld = &trace.hops[1];
1518        assert_eq!(tld.zone, "test.");
1519        assert_eq!(tld.server, "ns1.nic.test.");
1520        assert_eq!(tld.referral_zone.as_deref(), Some("example.test."));
1521        assert_eq!(tld.referral, vec!["ns1.example.test.", "ns2.example.test."]);
1522
1523        let auth = &trace.hops[2];
1524        assert_eq!(auth.zone, "example.test.");
1525        // Sorted referral: ns1 is asked first.
1526        assert_eq!(auth.server, "ns1.example.test.");
1527        assert!(auth.authoritative);
1528        assert_eq!(auth.status, DnsStatus::NoError);
1529        assert_eq!(auth.referral_zone, None);
1530        assert!(auth.referral.is_empty());
1531        assert!(auth.failed_servers.is_empty());
1532
1533        assert_eq!(trace.status, DnsStatus::NoError);
1534        assert_eq!(trace.answers.len(), 1);
1535        assert_eq!(trace.answers[0].name, "www.example.test");
1536        assert_eq!(trace.answers[0].record_type, RecordType::A);
1537        assert!(matches!(
1538            &trace.answers[0].data,
1539            RecordData::A { address } if address == "192.0.2.10"
1540        ));
1541    }
1542
1543    #[tokio::test]
1544    async fn nxdomain_at_the_authoritative_level_ends_the_walk() {
1545        let tracer = three_level_tracer(|_| MockReply::AuthoritativeNxDomain("example.test")).await;
1546        let trace = tracer
1547            .trace("gone.example.test", RecordType::A)
1548            .await
1549            .unwrap();
1550
1551        assert_eq!(trace.hops.len(), 3);
1552        assert_eq!(trace.status, DnsStatus::NxDomain);
1553        assert!(trace.answers.is_empty());
1554        assert!(trace.error.is_none(), "a negative answer is not an error");
1555        let last = &trace.hops[2];
1556        assert_eq!(last.status, DnsStatus::NxDomain);
1557        assert!(last.authoritative, "the AA bit survives a negative answer");
1558        assert_eq!(last.referral_zone, None);
1559    }
1560
1561    #[tokio::test]
1562    async fn nodata_at_the_authoritative_level_ends_the_walk() {
1563        let tracer = three_level_tracer(|_| MockReply::AuthoritativeAnswer(vec![])).await;
1564        let trace = tracer
1565            .trace("www.example.test", RecordType::MX)
1566            .await
1567            .unwrap();
1568
1569        assert_eq!(trace.hops.len(), 3);
1570        assert_eq!(trace.status, DnsStatus::NoError);
1571        assert!(trace.answers.is_empty());
1572        assert!(trace.hops[2].authoritative);
1573        assert!(trace.error.is_none());
1574    }
1575
1576    #[tokio::test]
1577    async fn cname_answer_is_reported_not_chased() {
1578        let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
1579        let tld = spawn_mock_dns_fn(|_, _| delegation("example.test", &["ns1.example.test"])).await;
1580        let asked = Arc::new(Mutex::new(Vec::new()));
1581        let log = Arc::clone(&asked);
1582        let auth = spawn_mock_dns_fn(move |qname, qtype| {
1583            log.lock().unwrap().push((qname.to_string(), qtype));
1584            MockReply::AuthoritativeAnswer(vec![cname_rdata("edge.cdn.test")])
1585        })
1586        .await;
1587        let trace = tracer(
1588            &["a.root.test"],
1589            &[
1590                ("a.root.test", root),
1591                ("ns1.nic.test", tld),
1592                ("ns1.example.test", auth),
1593            ],
1594        )
1595        .trace("www.example.test", RecordType::A)
1596        .await
1597        .unwrap();
1598
1599        assert_eq!(trace.hops.len(), 3);
1600        assert!(trace.error.is_none());
1601        assert_eq!(trace.answers.len(), 1);
1602        let cname = &trace.answers[0];
1603        assert_eq!(cname.name, "www.example.test");
1604        assert_eq!(cname.record_type, RecordType::CNAME);
1605        assert!(matches!(
1606            &cname.data,
1607            RecordData::CNAME { target } if target == "edge.cdn.test."
1608        ));
1609        // The server was only ever asked the original question — the
1610        // target was never looked up (UDP retransmits may repeat it).
1611        let asked = asked.lock().unwrap();
1612        assert!(!asked.is_empty());
1613        assert!(
1614            asked
1615                .iter()
1616                .all(|q| *q == ("www.example.test".to_string(), HickoryRecordType::A)),
1617            "{asked:?}"
1618        );
1619    }
1620
1621    #[tokio::test]
1622    async fn glueless_referral_is_resolved_through_the_recursive_resolver() {
1623        let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
1624        // `example.test` is served by a nameserver the referral gives no
1625        // glue for.
1626        let tld = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
1627            zone: "example.test".to_string(),
1628            servers: vec![("ns.dns-host.test".to_string(), vec![])],
1629        })
1630        .await;
1631        let auth = spawn_mock_dns_fn(|_, _| {
1632            MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 20))])
1633        })
1634        .await;
1635        let recursive = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
1636            ("ns.dns-host.test", HickoryRecordType::A) => {
1637                MockReply::Answer(vec![a_rdata(Ipv4Addr::LOCALHOST)])
1638            }
1639            _ => MockReply::NoData,
1640        })
1641        .await;
1642        let trace = tracer(
1643            &["a.root.test"],
1644            &[
1645                ("a.root.test", root),
1646                ("ns1.nic.test", tld),
1647                ("ns.dns-host.test", auth),
1648            ],
1649        )
1650        .with_recursive_upstream(LOOPBACK, recursive)
1651        .trace("www.example.test", RecordType::A)
1652        .await
1653        .unwrap();
1654
1655        assert_eq!(trace.hops.len(), 3, "{trace:#?}");
1656        assert_eq!(trace.hops[1].referral, vec!["ns.dns-host.test."]);
1657        assert_eq!(trace.hops[2].server, "ns.dns-host.test.");
1658        assert_eq!(trace.hops[2].address, "127.0.0.1");
1659        assert_eq!(trace.answers.len(), 1);
1660        assert!(trace.error.is_none());
1661    }
1662
1663    #[tokio::test]
1664    async fn unresolvable_glueless_nameserver_ends_the_walk_with_an_error() {
1665        let root = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
1666            zone: "test".to_string(),
1667            servers: vec![("ns.nowhere.test".to_string(), vec![])],
1668        })
1669        .await;
1670        let recursive = spawn_mock_dns(MockMode::Nxdomain).await;
1671        let trace = tracer(&["a.root.test"], &[("a.root.test", root)])
1672            .with_recursive_upstream(LOOPBACK, recursive)
1673            .trace("www.example.test", RecordType::A)
1674            .await
1675            .unwrap();
1676
1677        assert_eq!(trace.hops.len(), 1);
1678        let error = trace.error.expect("the walk must stop with an error");
1679        assert!(error.contains("test."), "{error}");
1680        assert!(error.contains("ns.nowhere.test."), "{error}");
1681    }
1682
1683    #[tokio::test]
1684    async fn reserved_root_hints_are_refused_without_the_test_seam() {
1685        // Production vetting end to end: nothing may be sent to a loopback
1686        // server, so the walk has no root to start from.
1687        let asked = Arc::new(Mutex::new(0usize));
1688        let count = Arc::clone(&asked);
1689        let root = spawn_mock_dns_fn(move |_, _| {
1690            *count.lock().unwrap() += 1;
1691            MockReply::AuthoritativeAnswer(vec![])
1692        })
1693        .await;
1694        let err = DnsTracer::new()
1695            .with_timeout(Duration::from_millis(500))
1696            .with_root_hints(&[("a.root.test", LOOPBACK)])
1697            .with_port_map(HashMap::from([("a.root.test".to_string(), root)]))
1698            .trace("www.example.test", RecordType::A)
1699            .await
1700            .expect_err("a loopback root must be refused");
1701        assert!(matches!(err, SeerError::DnsError(_)), "{err:?}");
1702        assert!(err.to_string().contains("loopback"), "{err}");
1703        assert_eq!(*asked.lock().unwrap(), 0, "no query may reach the server");
1704    }
1705
1706    #[tokio::test]
1707    async fn glueless_addresses_are_vetted_without_the_test_seam() {
1708        // Production vetting of what a glueless NS name resolves to. The
1709        // recursive upstream is a test seam the walk never sends a direct
1710        // query to; the addresses it returns come from the (attacker's) zone.
1711        let public_v4 = Ipv4Addr::new(9, 9, 9, 9);
1712        let public_v6: Ipv6Addr = "2620:fe::fe".parse().unwrap();
1713        let private = Ipv4Addr::new(10, 0, 0, 53);
1714        let recursive = spawn_mock_dns_fn(move |qname, qtype| match (qname, qtype) {
1715            ("ns.evil.test", HickoryRecordType::A) => MockReply::Answer(vec![
1716                a_rdata(private),
1717                a_rdata(Ipv4Addr::new(169, 254, 169, 254)),
1718            ]),
1719            ("ns.evil.test", HickoryRecordType::AAAA) => {
1720                MockReply::Answer(vec![HickoryRData::AAAA(wire::AAAA(Ipv6Addr::LOCALHOST))])
1721            }
1722            ("ns.mixed.test", HickoryRecordType::A) => {
1723                MockReply::Answer(vec![a_rdata(private), a_rdata(public_v4)])
1724            }
1725            ("ns.mixed.test", HickoryRecordType::AAAA) => {
1726                MockReply::Answer(vec![HickoryRData::AAAA(wire::AAAA(public_v6))])
1727            }
1728            _ => MockReply::NoData,
1729        })
1730        .await;
1731        let tracer = DnsTracer::new()
1732            .with_timeout(Duration::from_millis(500))
1733            .with_recursive_upstream(LOOPBACK, recursive);
1734
1735        // Only reserved addresses: nothing to query, each refusal noted.
1736        let (addrs, mut notes) = tracer.resolve_glueless(&fq_name("ns.evil.test")).await;
1737        assert!(addrs.is_empty(), "{addrs:?}");
1738        notes.sort();
1739        assert_eq!(
1740            notes,
1741            vec![
1742                "ns.evil.test. (10.0.0.53): refused, private network (RFC 1918)",
1743                "ns.evil.test. (169.254.169.254): refused, cloud metadata endpoint (169.254.169.254)",
1744                "ns.evil.test. (::1): refused, IPv6 loopback (::1)",
1745            ]
1746        );
1747
1748        // Reserved beside public: only the public ones are kept, IPv4 first.
1749        let (addrs, notes) = tracer.resolve_glueless(&fq_name("ns.mixed.test")).await;
1750        assert_eq!(addrs, vec![IpAddr::V4(public_v4), IpAddr::V6(public_v6)]);
1751        assert_eq!(
1752            notes,
1753            vec!["ns.mixed.test. (10.0.0.53): refused, private network (RFC 1918)"]
1754        );
1755    }
1756
1757    #[tokio::test]
1758    async fn a_glueless_lookup_takes_at_most_one_timeout() {
1759        // The recursive resolver re-sends a query that timed out, so an
1760        // unanswered lookup took several timeouts; it must end within one.
1761        let recursive = spawn_mock_dns(MockMode::Ignore).await;
1762        let timeout = Duration::from_millis(300);
1763        let tracer = DnsTracer::new()
1764            .with_timeout(timeout)
1765            .with_recursive_upstream(LOOPBACK, recursive);
1766
1767        let started = Instant::now();
1768        let (addrs, notes) = tracer.resolve_glueless(&fq_name("ns.dark.test")).await;
1769        let elapsed = started.elapsed();
1770        assert!(addrs.is_empty(), "{addrs:?}");
1771        assert_eq!(
1772            notes,
1773            vec!["ns.dark.test.: glueless nameserver lookup failed: timed out"]
1774        );
1775        assert!(elapsed < timeout * 2, "took {elapsed:?}");
1776    }
1777
1778    #[tokio::test]
1779    async fn addresses_with_no_route_are_skipped_without_using_up_the_level() {
1780        // As on an IPv6-only host, where every IPv4 address fails locally at
1781        // once: no query left the host, so the server's next address is
1782        // tried, and the failure does not count toward the servers asked per
1783        // level (three here, before the fourth root answers).
1784        let unroutable: Vec<IpAddr> = (1..=4)
1785            .map(|i| IpAddr::V4(Ipv4Addr::new(192, 0, 2, i)))
1786            .collect();
1787        let first_glue = unroutable[3];
1788        let root = spawn_mock_dns_fn(move |_, _| MockReply::Delegation {
1789            zone: "example.test".to_string(),
1790            servers: vec![("ns1.example.test".to_string(), vec![first_glue, LOOPBACK])],
1791        })
1792        .await;
1793        let auth = spawn_mock_dns_fn(|_, _| {
1794            MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 50))])
1795        })
1796        .await;
1797        let trace = DnsTracer::new()
1798            .with_timeout(Duration::from_millis(500))
1799            .allowing_private_hosts()
1800            .with_root_hints(&[
1801                ("a.root.test", unroutable[0]),
1802                ("b.root.test", unroutable[1]),
1803                ("c.root.test", unroutable[2]),
1804                ("d.root.test", LOOPBACK),
1805            ])
1806            .with_port_map(HashMap::from([
1807                ("d.root.test".to_string(), root),
1808                ("ns1.example.test".to_string(), auth),
1809            ]))
1810            .with_unroutable(&unroutable, network_unreachable)
1811            .trace("www.example.test", RecordType::A)
1812            .await
1813            .unwrap();
1814
1815        assert!(trace.error.is_none(), "{trace:#?}");
1816        assert_eq!(trace.hops.len(), 2, "{trace:#?}");
1817        let no_route = "no route from this host (Network is unreachable)";
1818        let root = &trace.hops[0];
1819        assert_eq!(root.server, "d.root.test.");
1820        assert_eq!(
1821            root.failed_servers,
1822            vec![
1823                format!("a.root.test. (192.0.2.1): {no_route}"),
1824                format!("b.root.test. (192.0.2.2): {no_route}"),
1825                format!("c.root.test. (192.0.2.3): {no_route}"),
1826            ]
1827        );
1828        let auth = &trace.hops[1];
1829        assert_eq!(auth.server, "ns1.example.test.");
1830        assert_eq!(auth.address, "127.0.0.1", "the server's next address");
1831        assert_eq!(
1832            auth.failed_servers,
1833            vec![format!("ns1.example.test. (192.0.2.4): {no_route}")]
1834        );
1835        assert_eq!(trace.answers.len(), 1);
1836    }
1837
1838    #[tokio::test]
1839    async fn ipv6_only_servers_without_local_ipv6_do_not_use_up_the_level() {
1840        // As on a host whose kernel has IPv6 disabled, where no IPv6 socket
1841        // can be opened (EAFNOSUPPORT): three IPv6-only roots fail locally
1842        // without sending anything, so none of them counts toward the three
1843        // servers asked per level, and the fourth root still answers.
1844        let v6_only: Vec<IpAddr> = (1..=3)
1845            .map(|i| IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, i)))
1846            .collect();
1847        let root =
1848            spawn_mock_dns_fn(|_, _| delegation("example.test", &["ns1.example.test"])).await;
1849        let auth = spawn_mock_dns_fn(|_, _| {
1850            MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 50))])
1851        })
1852        .await;
1853        let trace = DnsTracer::new()
1854            .with_timeout(Duration::from_millis(500))
1855            .allowing_private_hosts()
1856            .with_root_hints(&[
1857                ("a.root.test", v6_only[0]),
1858                ("b.root.test", v6_only[1]),
1859                ("c.root.test", v6_only[2]),
1860                ("d.root.test", LOOPBACK),
1861            ])
1862            .with_port_map(HashMap::from([
1863                ("d.root.test".to_string(), root),
1864                ("ns1.example.test".to_string(), auth),
1865            ]))
1866            .with_unroutable(&v6_only, address_family_unsupported)
1867            .trace("www.example.test", RecordType::A)
1868            .await
1869            .unwrap();
1870
1871        assert!(trace.error.is_none(), "{trace:#?}");
1872        assert_eq!(trace.hops.len(), 2, "{trace:#?}");
1873        let no_socket = format!("no route from this host ({})", address_family_unsupported());
1874        let root = &trace.hops[0];
1875        assert_eq!(root.server, "d.root.test.");
1876        assert_eq!(
1877            root.failed_servers,
1878            vec![
1879                format!("a.root.test. (2001:db8::1): {no_socket}"),
1880                format!("b.root.test. (2001:db8::2): {no_socket}"),
1881                format!("c.root.test. (2001:db8::3): {no_socket}"),
1882            ]
1883        );
1884        assert_eq!(trace.answers.len(), 1);
1885    }
1886
1887    #[tokio::test]
1888    async fn a_lame_servers_empty_reply_falls_back_to_the_next_server() {
1889        let root = spawn_mock_dns_fn(|_, _| {
1890            delegation("example.test", &["ns1.example.test", "ns2.example.test"])
1891        })
1892        .await;
1893        // ns1 is lame: NOERROR, AA clear, every section empty — not NODATA.
1894        let lame = spawn_mock_dns_fn(|_, _| MockReply::NoData).await;
1895        let auth = spawn_mock_dns_fn(|_, _| {
1896            MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 60))])
1897        })
1898        .await;
1899        let trace = tracer(
1900            &["a.root.test"],
1901            &[
1902                ("a.root.test", root),
1903                ("ns1.example.test", lame),
1904                ("ns2.example.test", auth),
1905            ],
1906        )
1907        .trace("www.example.test", RecordType::A)
1908        .await
1909        .unwrap();
1910
1911        assert!(trace.error.is_none(), "{trace:#?}");
1912        assert_eq!(trace.hops.len(), 2);
1913        let last = &trace.hops[1];
1914        assert_eq!(last.server, "ns2.example.test.");
1915        assert!(last.authoritative);
1916        assert_eq!(
1917            last.failed_servers,
1918            vec![
1919                "ns1.example.test. (127.0.0.1): empty non-authoritative response (no answer, referral or SOA)"
1920            ]
1921        );
1922        assert_eq!(trace.status, DnsStatus::NoError);
1923        assert_eq!(trace.answers.len(), 1);
1924    }
1925
1926    #[tokio::test]
1927    async fn a_zone_of_lame_servers_is_an_error_not_a_negative_answer() {
1928        let root = spawn_mock_dns_fn(|_, _| {
1929            delegation("example.test", &["ns1.example.test", "ns2.example.test"])
1930        })
1931        .await;
1932        let empty = spawn_mock_dns_fn(|_, _| MockReply::NoData).await;
1933        // NXDOMAIN with AA clear and no SOA: no authority said so.
1934        let bare_nxdomain = spawn_mock_dns_fn(|_, _| MockReply::NxDomain).await;
1935        let trace = tracer(
1936            &["a.root.test"],
1937            &[
1938                ("a.root.test", root),
1939                ("ns1.example.test", empty),
1940                ("ns2.example.test", bare_nxdomain),
1941            ],
1942        )
1943        .trace("www.example.test", RecordType::A)
1944        .await
1945        .unwrap();
1946
1947        assert_eq!(trace.hops.len(), 2);
1948        let last = &trace.hops[1];
1949        assert_eq!(last.server, "ns2.example.test.");
1950        assert_eq!(last.status, DnsStatus::NxDomain);
1951        assert_eq!(
1952            last.failed_servers,
1953            vec![
1954                "ns1.example.test. (127.0.0.1): empty non-authoritative response (no answer, referral or SOA)"
1955            ]
1956        );
1957        let error = trace
1958            .error
1959            .expect("a lame zone proves nothing about the name");
1960        assert!(error.contains("example.test."), "{error}");
1961        assert!(error.contains("NXDOMAIN without an SOA"), "{error}");
1962    }
1963
1964    #[tokio::test]
1965    async fn upward_referral_from_every_server_stops_the_walk() {
1966        let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
1967        // The zone's only server is lame and sends the resolver back to the root.
1968        let tld = spawn_mock_dns_fn(|_, _| delegation(".", &["a.root.test"])).await;
1969        let trace = tracer(
1970            &["a.root.test"],
1971            &[("a.root.test", root), ("ns1.nic.test", tld)],
1972        )
1973        .trace("www.example.test", RecordType::A)
1974        .await
1975        .unwrap();
1976
1977        assert_eq!(trace.hops.len(), 2);
1978        assert_eq!(trace.hops[1].referral_zone.as_deref(), Some("."));
1979        assert_eq!(trace.status, DnsStatus::NoError);
1980        assert!(trace.answers.is_empty());
1981        let error = trace.error.expect("an upward referral is an error");
1982        assert!(error.contains("upward"), "{error}");
1983        assert!(error.contains("ns1.nic.test."), "{error}");
1984    }
1985
1986    /// Regression: one lame server's upward referral ended the trace,
1987    /// although the zone's next server would have referred correctly. It
1988    /// is passed over like any other unusable reply.
1989    #[tokio::test]
1990    async fn upward_referral_from_a_lame_server_asks_the_next() {
1991        let root =
1992            spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test", "ns2.nic.test"])).await;
1993        let lame = spawn_mock_dns_fn(|_, _| delegation(".", &["a.root.test"])).await;
1994        let healthy =
1995            spawn_mock_dns_fn(|_, _| delegation("example.test", &["ns1.example.test"])).await;
1996        let auth =
1997            spawn_mock_dns_fn(|_, _| MockReply::AuthoritativeAnswer(vec![a_rdata([192, 0, 2, 1])]))
1998                .await;
1999        let trace = tracer(
2000            &["a.root.test"],
2001            &[
2002                ("a.root.test", root),
2003                ("ns1.nic.test", lame),
2004                ("ns2.nic.test", healthy),
2005                ("ns1.example.test", auth),
2006            ],
2007        )
2008        .trace("www.example.test", RecordType::A)
2009        .await
2010        .unwrap();
2011
2012        assert!(trace.error.is_none(), "{:?}", trace.error);
2013        assert_eq!(trace.hops.len(), 3);
2014        let tld_hop = &trace.hops[1];
2015        assert_eq!(tld_hop.server, "ns2.nic.test.");
2016        assert_eq!(tld_hop.referral_zone.as_deref(), Some("example.test."));
2017        assert!(
2018            tld_hop
2019                .failed_servers
2020                .iter()
2021                .any(|f| f.starts_with("ns1.nic.test.") && f.contains("referred upward")),
2022            "{:?}",
2023            tld_hop.failed_servers
2024        );
2025        assert_eq!(trace.answers.len(), 1);
2026    }
2027
2028    #[tokio::test]
2029    async fn sideways_referral_stops_the_walk() {
2030        let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
2031        let tld = spawn_mock_dns_fn(|_, _| delegation("other.test", &["ns1.other.test"])).await;
2032        let trace = tracer(
2033            &["a.root.test"],
2034            &[("a.root.test", root), ("ns1.nic.test", tld)],
2035        )
2036        .trace("www.example.test", RecordType::A)
2037        .await
2038        .unwrap();
2039
2040        assert_eq!(trace.hops.len(), 2);
2041        assert_eq!(trace.hops[1].referral_zone.as_deref(), Some("other.test."));
2042        let error = trace.error.expect("a sideways referral is an error");
2043        assert!(error.contains("sideways"), "{error}");
2044    }
2045
2046    #[tokio::test]
2047    async fn walk_gives_up_after_the_hop_limit() {
2048        // A 21-label name and one server that refers one label deeper on
2049        // every query: a legitimate-looking chain longer than MAX_HOPS.
2050        let labels: Vec<String> = (1..=20)
2051            .map(|i| format!("l{i}"))
2052            .chain(["test".into()])
2053            .collect();
2054        let qname = labels.join(".");
2055        let zone_at = |depth: usize| labels[labels.len() - depth..].join(".");
2056        let zones: Vec<String> = (1..=labels.len()).map(zone_at).collect();
2057
2058        let mut depth = 0;
2059        let chain = zones.clone();
2060        let server = spawn_mock_dns_fn(move |_, _| {
2061            let zone = chain[depth.min(chain.len() - 1)].clone();
2062            depth += 1;
2063            MockReply::Delegation {
2064                servers: vec![(format!("ns.{zone}"), vec![LOOPBACK])],
2065                zone,
2066            }
2067        })
2068        .await;
2069        let ns_hosts: Vec<String> = zones.iter().map(|zone| format!("ns.{zone}")).collect();
2070        let mut ports: Vec<(&str, u16)> = ns_hosts.iter().map(|h| (h.as_str(), server)).collect();
2071        ports.push(("a.root.test", server));
2072
2073        let trace = tracer(&["a.root.test"], &ports)
2074            .trace(&qname, RecordType::A)
2075            .await
2076            .unwrap();
2077
2078        assert_eq!(trace.hops.len(), MAX_HOPS);
2079        let error = trace.error.expect("the walk must give up");
2080        assert!(error.contains(&MAX_HOPS.to_string()), "{error}");
2081        // Every hop descended one level.
2082        assert_eq!(trace.hops[0].zone, ".");
2083        assert_eq!(trace.hops[1].zone, "test.");
2084        assert_eq!(trace.hops[2].zone, "l20.test.");
2085    }
2086
2087    #[tokio::test]
2088    async fn the_whole_walk_is_held_to_one_deadline() {
2089        // A chain delegated one label at a time, every level answered only
2090        // by its second server after the first times out: each level costs
2091        // one timeout, and the chain is longer than the walk's budget.
2092        // Without the deadline the walk ran on to the answer.
2093        let timeout = Duration::from_millis(100);
2094        let depth = TRACE_BUDGET_TIMEOUTS as usize + 2;
2095        let labels: Vec<String> = (1..=depth)
2096            .map(|i| format!("l{i}"))
2097            .chain(["test".into()])
2098            .collect();
2099        let qname = labels.join(".");
2100        let zones: Vec<String> = (1..=labels.len())
2101            .map(|n| labels[labels.len() - n..].join("."))
2102            .collect();
2103
2104        let dark = spawn_mock_dns(MockMode::Ignore).await;
2105        let mut asked = 0;
2106        let chain = zones.clone();
2107        let live = spawn_mock_dns_fn(move |_, _| {
2108            let reply = match chain.get(asked) {
2109                Some(zone) => MockReply::Delegation {
2110                    zone: zone.clone(),
2111                    servers: vec![
2112                        (format!("ns1.{zone}"), vec![LOOPBACK]),
2113                        (format!("ns2.{zone}"), vec![LOOPBACK]),
2114                    ],
2115                },
2116                None => MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 40))]),
2117            };
2118            asked += 1;
2119            reply
2120        })
2121        .await;
2122        let ns_hosts: Vec<(String, u16)> = zones
2123            .iter()
2124            .flat_map(|zone| [(format!("ns1.{zone}"), dark), (format!("ns2.{zone}"), live)])
2125            .collect();
2126        let mut ports: Vec<(&str, u16)> = ns_hosts.iter().map(|(h, p)| (h.as_str(), *p)).collect();
2127        ports.extend([("a.root.test", dark), ("b.root.test", live)]);
2128
2129        let started = Instant::now();
2130        let trace = tracer(&["a.root.test", "b.root.test"], &ports)
2131            .with_timeout(timeout)
2132            .trace(&qname, RecordType::A)
2133            .await
2134            .unwrap();
2135        let elapsed = started.elapsed();
2136
2137        let budget = timeout * TRACE_BUDGET_TIMEOUTS;
2138        assert!(elapsed < budget + timeout, "took {elapsed:?}");
2139        let error = trace.error.expect("the walk must run out of time");
2140        assert!(error.starts_with("gave up after 600ms"), "{error}");
2141        // The hops walked so far are kept, each one a referral that the
2142        // deadline cut short of the answer.
2143        assert!(!trace.hops.is_empty());
2144        assert!(trace.hops.iter().all(|hop| hop.referral_zone.is_some()));
2145        assert!(trace.answers.is_empty());
2146        let asking = zones[trace.hops.len() - 1].clone();
2147        assert!(
2148            error.ends_with(&format!("nameservers of {asking}.")),
2149            "{error}"
2150        );
2151    }
2152
2153    #[tokio::test]
2154    async fn unresponsive_server_falls_back_to_the_next_one() {
2155        let dark = spawn_mock_dns(MockMode::Ignore).await;
2156        let live = spawn_mock_dns_fn(|_, _| {
2157            MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 30))])
2158        })
2159        .await;
2160        let trace = tracer(
2161            &["a.root.test", "b.root.test"],
2162            &[("a.root.test", dark), ("b.root.test", live)],
2163        )
2164        .trace("example.test", RecordType::A)
2165        .await
2166        .unwrap();
2167
2168        assert_eq!(trace.hops.len(), 1);
2169        let hop = &trace.hops[0];
2170        assert_eq!(hop.server, "b.root.test.");
2171        assert_eq!(
2172            hop.failed_servers,
2173            vec!["a.root.test. (127.0.0.1): timed out"]
2174        );
2175        assert_eq!(trace.answers.len(), 1);
2176        assert!(trace.error.is_none());
2177    }
2178
2179    #[tokio::test]
2180    async fn every_server_refusing_ends_with_the_refusal_as_the_final_hop() {
2181        let root = spawn_mock_dns_fn(|_, _| {
2182            delegation("example.test", &["ns1.example.test", "ns2.example.test"])
2183        })
2184        .await;
2185        let refused = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::Refused)).await;
2186        let servfail = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::ServFail)).await;
2187        let trace = tracer(
2188            &["a.root.test"],
2189            &[
2190                ("a.root.test", root),
2191                ("ns1.example.test", servfail),
2192                ("ns2.example.test", refused),
2193            ],
2194        )
2195        .trace("www.example.test", RecordType::A)
2196        .await
2197        .unwrap();
2198
2199        assert_eq!(trace.hops.len(), 2);
2200        let last = &trace.hops[1];
2201        assert_eq!(last.server, "ns2.example.test.");
2202        assert_eq!(last.status, DnsStatus::Refused);
2203        assert_eq!(
2204            last.failed_servers,
2205            vec!["ns1.example.test. (127.0.0.1): SERVFAIL"]
2206        );
2207        assert_eq!(trace.status, DnsStatus::Refused);
2208        let error = trace.error.expect("no usable response is an error");
2209        assert!(error.contains("example.test."), "{error}");
2210        assert!(
2211            error.contains("REFUSED") && error.contains("SERVFAIL"),
2212            "{error}"
2213        );
2214    }
2215
2216    #[tokio::test]
2217    async fn no_responsive_root_is_an_error() {
2218        let dark = spawn_mock_dns(MockMode::Ignore).await;
2219        let err = tracer(&["a.root.test"], &[("a.root.test", dark)])
2220            .trace("example.test", RecordType::A)
2221            .await
2222            .expect_err("nothing to trace without a root response");
2223        assert!(matches!(err, SeerError::DnsError(_)), "{err:?}");
2224        assert!(err.to_string().contains("timed out"), "{err}");
2225    }
2226
2227    #[tokio::test]
2228    async fn ptr_trace_of_an_ip_literal_uses_the_reverse_name() {
2229        let asked = Arc::new(Mutex::new(Vec::new()));
2230        let log = Arc::clone(&asked);
2231        let root = spawn_mock_dns_fn(move |qname, _| {
2232            log.lock().unwrap().push(qname.to_string());
2233            MockReply::AuthoritativeAnswer(vec![HickoryRData::PTR(wire::PTR(fq_name(
2234                "ptr.seer.test",
2235            )))])
2236        })
2237        .await;
2238        let trace = tracer(&["a.root.test"], &[("a.root.test", root)])
2239            .trace("192.0.2.1", RecordType::PTR)
2240            .await
2241            .unwrap();
2242
2243        assert_eq!(trace.name, "1.2.0.192.in-addr.arpa");
2244        assert_eq!(trace.answers.len(), 1);
2245        assert_eq!(trace.answers[0].name, "1.2.0.192.in-addr.arpa");
2246        assert_eq!(trace.answers[0].record_type, RecordType::PTR);
2247        assert!(asked
2248            .lock()
2249            .unwrap()
2250            .iter()
2251            .all(|q| q == "1.2.0.192.in-addr.arpa"));
2252    }
2253
2254    #[tokio::test]
2255    async fn truncated_reply_is_retried_over_tcp() {
2256        let mut served = 0;
2257        let root = spawn_mock_dns_fn_with_tcp(move |_, _| {
2258            served += 1;
2259            if served == 1 {
2260                MockReply::Truncated
2261            } else {
2262                MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 40))])
2263            }
2264        })
2265        .await;
2266        let trace = tracer(&["a.root.test"], &[("a.root.test", root)])
2267            .trace("example.test", RecordType::A)
2268            .await
2269            .unwrap();
2270
2271        assert_eq!(trace.hops.len(), 1);
2272        assert!(trace.hops[0].authoritative);
2273        assert_eq!(trace.answers.len(), 1, "{trace:#?}");
2274    }
2275
2276    #[tokio::test]
2277    async fn answers_convert_the_service_binding_and_child_dnssec_types() {
2278        // Hops go through the resolver's one conversion table, so every
2279        // modeled type — HTTPS/SVCB and CDS/CDNSKEY included — comes through
2280        // under its owner.
2281        use hickory_resolver::proto::dnssec::rdata::{DNSSECRData, CDS};
2282        use hickory_resolver::proto::dnssec::{Algorithm, DigestType};
2283        use hickory_resolver::proto::rr::rdata::svcb::{Alpn, SvcParamKey, SvcParamValue, SVCB};
2284
2285        let tracer = three_level_tracer(|qname| match qname {
2286            "www.example.test" => {
2287                MockReply::AuthoritativeAnswer(vec![HickoryRData::HTTPS(wire::HTTPS(SVCB::new(
2288                    1,
2289                    Name::root(),
2290                    vec![(
2291                        SvcParamKey::Alpn,
2292                        SvcParamValue::Alpn(Alpn(vec!["h2".to_string()])),
2293                    )],
2294                )))])
2295            }
2296            _ => MockReply::AuthoritativeAnswer(vec![HickoryRData::DNSSEC(DNSSECRData::CDS(
2297                CDS::new(
2298                    2371,
2299                    Some(Algorithm::ED25519),
2300                    DigestType::SHA256,
2301                    vec![0xAB],
2302                ),
2303            ))]),
2304        })
2305        .await;
2306
2307        let https = tracer
2308            .trace("www.example.test", RecordType::HTTPS)
2309            .await
2310            .unwrap();
2311        assert_eq!(https.answers.len(), 1, "{https:#?}");
2312        assert_eq!(https.answers[0].name, "www.example.test");
2313        assert_eq!(https.answers[0].record_type, RecordType::HTTPS);
2314        assert_eq!(https.answers[0].data.to_string(), "1 . alpn=\"h2\"");
2315        assert_eq!(https.hops[2].answers, https.answers);
2316
2317        let cds = tracer.trace("example.test", RecordType::CDS).await.unwrap();
2318        assert_eq!(cds.answers.len(), 1, "{cds:#?}");
2319        assert_eq!(cds.answers[0].record_type, RecordType::CDS);
2320        assert_eq!(cds.answers[0].data.to_string(), "2371 15 2 AB");
2321    }
2322
2323    #[tokio::test]
2324    async fn at_most_three_servers_are_asked_per_level() {
2325        // Four silent roots: three are asked (and time out), the fourth is
2326        // never sent a packet.
2327        let roots = ["a.root.test", "b.root.test", "c.root.test", "d.root.test"];
2328        let mut ports = Vec::new();
2329        let mut hits = Vec::new();
2330        for _ in roots {
2331            let count = Arc::new(Mutex::new(0usize));
2332            let log = Arc::clone(&count);
2333            ports.push(
2334                spawn_mock_dns_fn(move |_, _| {
2335                    *log.lock().unwrap() += 1;
2336                    MockReply::NoReply
2337                })
2338                .await,
2339            );
2340            hits.push(count);
2341        }
2342        let map: Vec<(&str, u16)> = roots.iter().copied().zip(ports).collect();
2343        let err = tracer(&roots, &map)
2344            .with_timeout(Duration::from_millis(200))
2345            .trace("example.test", RecordType::A)
2346            .await
2347            .expect_err("no root answered");
2348
2349        let message = err.to_string();
2350        for root in &roots[..3] {
2351            assert!(message.contains(root), "{message}");
2352        }
2353        assert!(!message.contains("d.root.test"), "{message}");
2354        let hits: Vec<usize> = hits.iter().map(|count| *count.lock().unwrap()).collect();
2355        assert!(hits[..3].iter().all(|&n| n > 0), "{hits:?}");
2356        assert_eq!(hits[3], 0, "the fourth server must not be asked: {hits:?}");
2357    }
2358
2359    #[tokio::test]
2360    async fn at_most_three_glueless_nameservers_are_looked_up_per_level() {
2361        let root = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
2362            zone: "test".to_string(),
2363            servers: (1..=4)
2364                .map(|i| (format!("ns{i}.nowhere.test"), vec![]))
2365                .collect(),
2366        })
2367        .await;
2368        let looked_up = Arc::new(Mutex::new(std::collections::BTreeSet::new()));
2369        let log = Arc::clone(&looked_up);
2370        let recursive = spawn_mock_dns_fn(move |qname, _| {
2371            log.lock().unwrap().insert(qname.to_string());
2372            MockReply::NxDomain
2373        })
2374        .await;
2375        let trace = tracer(&["a.root.test"], &[("a.root.test", root)])
2376            .with_recursive_upstream(LOOPBACK, recursive)
2377            .trace("www.example.test", RecordType::A)
2378            .await
2379            .unwrap();
2380
2381        assert_eq!(trace.hops.len(), 1);
2382        let error = trace
2383            .error
2384            .expect("no nameserver of `test.` has an address");
2385        assert!(!error.contains("ns4.nowhere.test"), "{error}");
2386        let looked_up: Vec<String> = looked_up.lock().unwrap().iter().cloned().collect();
2387        assert_eq!(
2388            looked_up,
2389            ["ns1.nowhere.test", "ns2.nowhere.test", "ns3.nowhere.test"],
2390            "sorted referral order, capped at three"
2391        );
2392    }
2393
2394    #[tokio::test]
2395    async fn trace_rejects_any_and_bad_names_before_sending() {
2396        let tracer = tracer(&["a.root.test"], &[]);
2397        assert!(matches!(
2398            tracer.trace("example.test", RecordType::ANY).await,
2399            Err(SeerError::InvalidInput(_))
2400        ));
2401        assert!(tracer.trace("not a name", RecordType::A).await.is_err());
2402    }
2403
2404    #[tokio::test]
2405    #[ignore = "live network; run with --ignored or SEER_LIVE_TESTS=1"]
2406    async fn test_live_trace_example_com() {
2407        let trace = DnsTracer::new()
2408            .trace("example.com", RecordType::A)
2409            .await
2410            .unwrap();
2411        assert!(trace.error.is_none(), "{trace:#?}");
2412        assert!(trace.hops.len() >= 3, "{trace:#?}");
2413        assert_eq!(trace.hops[0].zone, ".");
2414        assert_eq!(trace.hops[1].zone, "com.");
2415        let last = trace.hops.last().unwrap();
2416        assert_eq!(last.zone, "example.com.");
2417        assert!(last.authoritative);
2418        assert_eq!(trace.status, DnsStatus::NoError);
2419        assert!(!trace.answers.is_empty());
2420    }
2421}