Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-server/src/estate_order/domain.rs

//! **The customer's own domain** (owner, 2026-10-04 18:00): besides
//! `<company>.gunnar.rs`, whose DNS we write, a company may run its gunnar
//! under a name of its own — `gunnar.company1.com`, `gunnar.company2.se`.
//!
//! We do not hold that DNS, so the customer sets two records and the job
//! waits for them (`waiting_dns`), with a deadline:
//!
//! * `CNAME <domain> → <company>.<apex>` — the name leads to the estate's
//!   front (whose own records we wrote), so Let's Encrypt's HTTP-01 reaches
//!   the front and gunnar-front's existing ACME makes the certificate;
//! * `TXT _<proof>.<domain> = <proof>=<token>` — the proof that whoever ordered
//!   controls the name; `<proof>` is the product's
//!   ([`super::lifecycle::Lifecycle::proof_name`], Gunnar's `gunnar-proof`). The token is random and public: it is
//!   a claim to be published, not a secret.
//!
//! The records are read through a [`DomainCheck`]: the account's is a real
//! resolver, [`DnsResolver`] — the DNS protocol itself over UDP (TCP when an
//! answer is truncated), asking the nameservers `resolver = "dns"` reads from
//! `/etc/resolv.conf` or the ones `resolver = "dns:<ip:port>,…"` names; the
//! mock's is a file ([`FileResolver`]), and the tests' resolver is a fake
//! nameserver on loopback that [`DnsResolver`] itself asks.

use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

/// The label the ownership proof named `proof` is published under (`_gunnar-proof`).
pub fn proof_label(proof: &str) -> String {
    format!("_{proof}")
}

/// **Why `domain` cannot be a customer's estate name**, or `None`. A host
/// name of at least two labels, lowercase letters, digits and hyphens, not
/// under our own apex (those are ours, not the customer's).
pub fn domain_refusal(domain: &str, apex: &str) -> Option<String> {
    if domain.len() > 253 || !domain.contains('.') {
        return Some(format!(
            "{domain:?} is not a domain name like shop.example.com"
        ));
    }
    if domain == apex || domain.ends_with(&format!(".{apex}")) {
        return Some(format!(
            "{domain} is under {apex}: order <company>.{apex} instead, whose DNS we write"
        ));
    }
    for label in domain.split('.') {
        let ok = !label.is_empty()
            && label.len() <= 63
            && !label.starts_with('-')
            && !label.ends_with('-')
            && label
                .chars()
                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
        if !ok {
            return Some(format!("{domain:?} is not a domain name: lowercase letters, digits and hyphens, label by label"));
        }
    }
    None
}

/// One record the customer must set, as the page shows it.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CustomerRecord {
    pub name: String,
    pub kind: String,
    pub value: String,
}

/// **The two records the customer sets**, the proof named `proof` (the product's).
pub fn required_as(
    proof: &str,
    domain: &str,
    company: &str,
    apex: &str,
    token: &str,
) -> Vec<CustomerRecord> {
    vec![
        CustomerRecord {
            name: domain.into(),
            kind: "CNAME".into(),
            value: format!("{company}.{apex}"),
        },
        CustomerRecord {
            name: format!("{}.{domain}", proof_label(proof)),
            kind: "TXT".into(),
            value: format!("{proof}={token}"),
        },
    ]
}

/// **How the world's DNS is read.**
pub trait DomainCheck: Send + Sync {
    /// The CNAME target of `name`, without a trailing dot; `None` when it has none.
    fn cname(&self, name: &str) -> Result<Option<String>, String>;
    /// Every TXT string at `name`.
    fn txt(&self, name: &str) -> Result<Vec<String>, String>;
    /// Every A record's address at `name` (the shop's own names, set by hand: `ManualDns`).
    fn a(&self, name: &str) -> Result<Vec<String>, String> {
        let _ = name;
        Err("this resolver reads no A records".into())
    }
    /// Whether this checker can read a customer's domain at all — `false`
    /// for [`NoResolver`], so `Start` refuses an own domain at once instead
    /// of taking the money for an order that can only time out (A57).
    fn reads(&self) -> bool {
        true
    }
}

/// No resolver here: a customer's domain cannot be checked, and the job says so.
pub struct NoResolver;

impl DomainCheck for NoResolver {
    fn cname(&self, _name: &str) -> Result<Option<String>, String> {
        Err("no resolver is configured here ([estate_orders] resolver)".into())
    }
    fn txt(&self, _name: &str) -> Result<Vec<String>, String> {
        Err("no resolver is configured here ([estate_orders] resolver)".into())
    }
    fn reads(&self) -> bool {
        false
    }
}

/// **The mock's world DNS**: a JSON list of `{name, kind, value}` the test —
/// the customer — writes.
pub struct FileResolver(pub PathBuf);

impl FileResolver {
    fn rows(&self) -> Result<Vec<CustomerRecord>, String> {
        match std::fs::read(&self.0) {
            Ok(b) if b.is_empty() => Ok(Vec::new()),
            Ok(b) => serde_json::from_slice(&b).map_err(|e| format!("{}: {e}", self.0.display())),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
            Err(e) => Err(format!("{}: {e}", self.0.display())),
        }
    }

    /// The customer sets a record (the mock's side of their DNS provider).
    pub fn set(&self, r: CustomerRecord) -> Result<(), String> {
        let mut rows = self.rows()?;
        rows.retain(|x| !(x.name == r.name && x.kind == r.kind));
        rows.push(r);
        std::fs::write(
            &self.0,
            serde_json::to_vec_pretty(&rows).map_err(|e| e.to_string())?,
        )
        .map_err(|e| format!("{}: {e}", self.0.display()))
    }
}

impl DomainCheck for FileResolver {
    fn cname(&self, name: &str) -> Result<Option<String>, String> {
        Ok(self
            .rows()?
            .into_iter()
            .find(|r| r.name == name && r.kind == "CNAME")
            .map(|r| r.value.trim_end_matches('.').to_string()))
    }
    fn txt(&self, name: &str) -> Result<Vec<String>, String> {
        Ok(self
            .rows()?
            .into_iter()
            .filter(|r| r.name == name && r.kind == "TXT")
            .map(|r| r.value)
            .collect())
    }
    fn a(&self, name: &str) -> Result<Vec<String>, String> {
        Ok(self
            .rows()?
            .into_iter()
            .filter(|r| r.name == name && r.kind == "A")
            .map(|r| r.value)
            .collect())
    }
}

/// **The resolver `[estate_orders] resolver` names**: `file:<path>` (the
/// mock's world DNS), `dns` (the nameservers of `/etc/resolv.conf`),
/// `dns:<ip:port>,…` (these), or empty (none: a customer's domain is refused
/// at `Start`).
pub fn resolver_of(word: &str) -> Result<Arc<dyn DomainCheck>, String> {
    let w = word.trim();
    if let Some(p) = w.strip_prefix("file:") {
        return Ok(Arc::new(FileResolver(PathBuf::from(p))));
    }
    if w.is_empty() {
        return Ok(Arc::new(NoResolver));
    }
    if w == "dns" {
        let text = std::fs::read_to_string("/etc/resolv.conf")
            .map_err(|e| format!("[estate_orders] resolver = \"dns\": /etc/resolv.conf: {e}"))?;
        return Ok(Arc::new(DnsResolver::new(DnsResolver::system(&text)?)));
    }
    if let Some(list) = w.strip_prefix("dns:") {
        let servers = list
            .split(',')
            .map(|a| {
                a.trim().parse::<SocketAddr>().map_err(|e| {
                    format!("[estate_orders] resolver {w:?}: {a:?} is not ip:port ({e})")
                })
            })
            .collect::<Result<Vec<_>, _>>()?;
        if servers.is_empty() {
            return Err(format!(
                "[estate_orders] resolver {w:?} names no nameserver"
            ));
        }
        return Ok(Arc::new(DnsResolver::new(servers)));
    }
    Err(format!(
        "[estate_orders] resolver {w:?} is not `file:<path>`, `dns`, `dns:<ip:port>,…` or empty"
    ))
}

/// **A real resolver**: the DNS protocol (RFC 1035) to recursive
/// nameservers — a question with recursion desired, the answer read for the
/// CNAME or the TXT strings asked for. UDP first; an answer marked truncated
/// is asked again over TCP. Each nameserver in turn until one answers; an
/// NXDOMAIN or an empty answer is "no record", never an error.
pub struct DnsResolver {
    pub servers: Vec<SocketAddr>,
    pub timeout: Duration,
}

/// The record types asked for.
const T_A: u16 = 1;
const T_CNAME: u16 = 5;
const T_TXT: u16 = 16;

impl DnsResolver {
    pub fn new(servers: Vec<SocketAddr>) -> DnsResolver {
        DnsResolver {
            servers,
            timeout: Duration::from_secs(3),
        }
    }

    /// The `nameserver` lines of a resolv.conf, port 53.
    pub fn system(resolv_conf: &str) -> Result<Vec<SocketAddr>, String> {
        let out: Vec<SocketAddr> = resolv_conf
            .lines()
            .filter_map(|l| l.trim().strip_prefix("nameserver"))
            .filter_map(|ip| ip.trim().parse::<std::net::IpAddr>().ok())
            .map(|ip| SocketAddr::new(ip, 53))
            .collect();
        if out.is_empty() {
            return Err("/etc/resolv.conf names no nameserver".into());
        }
        Ok(out)
    }

    /// Ask every nameserver in turn; the answer's records of `qtype`.
    fn ask(&self, name: &str, qtype: u16) -> Result<Vec<wire::Record>, String> {
        let mut last = format!("no nameserver to ask for {name}");
        for at in &self.servers {
            match self.ask_one(*at, name, qtype) {
                Ok(rows) => return Ok(rows),
                Err(e) => last = format!("{at}: {e}"),
            }
        }
        Err(format!("the DNS could not be asked about {name}: {last}"))
    }

    fn ask_one(&self, at: SocketAddr, name: &str, qtype: u16) -> Result<Vec<wire::Record>, String> {
        let id = (std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.subsec_nanos())
            .unwrap_or(7)
            & 0xffff) as u16;
        let q = wire::query(id, name, qtype)?;
        let bind = if at.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" };
        let sock = std::net::UdpSocket::bind(bind).map_err(|e| e.to_string())?;
        sock.set_read_timeout(Some(self.timeout))
            .map_err(|e| e.to_string())?;
        sock.connect(at).map_err(|e| e.to_string())?;
        let mut answer = None;
        for _ in 0..2 {
            sock.send(&q).map_err(|e| e.to_string())?;
            let mut buf = vec![0u8; 4096];
            match sock.recv(&mut buf) {
                Ok(n) => {
                    buf.truncate(n);
                    if buf.len() >= 2 && u16::from_be_bytes([buf[0], buf[1]]) == id {
                        answer = Some(buf);
                        break;
                    }
                }
                Err(e)
                    if matches!(
                        e.kind(),
                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
                    ) =>
                {
                    continue
                }
                Err(e) => return Err(e.to_string()),
            }
        }
        let mut msg = answer
            .ok_or_else(|| format!("no answer within {} s, twice", self.timeout.as_secs()))?;
        if wire::truncated(&msg) {
            msg = self.ask_tcp(at, &q, id)?;
        }
        wire::answers(&msg, id)
    }

    fn ask_tcp(&self, at: SocketAddr, q: &[u8], id: u16) -> Result<Vec<u8>, String> {
        use std::io::{Read, Write};
        let mut s = std::net::TcpStream::connect_timeout(&at, self.timeout)
            .map_err(|e| format!("over TCP: {e}"))?;
        s.set_read_timeout(Some(self.timeout))
            .map_err(|e| e.to_string())?;
        let mut framed = (q.len() as u16).to_be_bytes().to_vec();
        framed.extend_from_slice(q);
        s.write_all(&framed).map_err(|e| format!("over TCP: {e}"))?;
        let mut len = [0u8; 2];
        s.read_exact(&mut len)
            .map_err(|e| format!("over TCP: {e}"))?;
        let mut msg = vec![0u8; u16::from_be_bytes(len) as usize];
        s.read_exact(&mut msg)
            .map_err(|e| format!("over TCP: {e}"))?;
        if msg.len() < 2 || u16::from_be_bytes([msg[0], msg[1]]) != id {
            return Err("over TCP: an answer to another question".into());
        }
        Ok(msg)
    }
}

impl DomainCheck for DnsResolver {
    fn cname(&self, name: &str) -> Result<Option<String>, String> {
        let rows = self.ask(name, T_CNAME)?;
        let want = name.trim_end_matches('.').to_ascii_lowercase();
        // A CNAME's rdata is its target, decoded by `wire::answers`.
        Ok(rows
            .iter()
            .find(|(owner, kind, _, _)| *kind == T_CNAME && owner.eq_ignore_ascii_case(&want))
            .map(|r| {
                String::from_utf8_lossy(&r.2)
                    .trim_end_matches('.')
                    .to_ascii_lowercase()
            }))
    }
    fn txt(&self, name: &str) -> Result<Vec<String>, String> {
        let rows = self.ask(name, T_TXT)?;
        Ok(rows
            .iter()
            .filter(|r| r.1 == T_TXT)
            .map(|r| wire::txt(&r.2))
            .collect())
    }
    fn a(&self, name: &str) -> Result<Vec<String>, String> {
        let rows = self.ask(name, T_A)?;
        Ok(rows
            .iter()
            .filter(|r| r.1 == T_A && r.2.len() == 4)
            .map(|r| std::net::Ipv4Addr::new(r.2[0], r.2[1], r.2[2], r.2[3]).to_string())
            .collect())
    }
}

/// **The DNS message, written and read** (RFC 1035 §4): only what the two
/// questions need.
pub mod wire {
    /// One answer record: `(owner, type, rdata, rdata offset)`.
    pub type Record = (String, u16, Vec<u8>, usize);

    /// A question for `name`/`qtype`, class IN, recursion desired.
    pub fn query(id: u16, name: &str, qtype: u16) -> Result<Vec<u8>, String> {
        let mut m = Vec::with_capacity(64);
        m.extend_from_slice(&id.to_be_bytes());
        m.extend_from_slice(&0x0100u16.to_be_bytes()); // RD
        m.extend_from_slice(&1u16.to_be_bytes()); // QDCOUNT
        m.extend_from_slice(&[0, 0, 0, 0, 0, 0]);
        put_name(&mut m, name)?;
        m.extend_from_slice(&qtype.to_be_bytes());
        m.extend_from_slice(&1u16.to_be_bytes());
        Ok(m)
    }

    pub fn put_name(m: &mut Vec<u8>, name: &str) -> Result<(), String> {
        for label in name.trim_end_matches('.').split('.') {
            if label.is_empty() || label.len() > 63 {
                return Err(format!("{name:?} is not a DNS name"));
            }
            m.push(label.len() as u8);
            m.extend_from_slice(label.as_bytes());
        }
        m.push(0);
        Ok(())
    }

    /// The TC bit.
    pub fn truncated(m: &[u8]) -> bool {
        m.len() >= 4 && m[2] & 0x02 != 0
    }

    /// A (possibly compressed) name at `at`; answers it and where it ends.
    pub fn name(m: &[u8], mut at: usize) -> Result<(String, usize), String> {
        let mut labels: Vec<String> = Vec::new();
        let mut end = None;
        for _ in 0..128 {
            let len = *m.get(at).ok_or("a name runs past the message")? as usize;
            if len == 0 {
                return Ok((labels.join("."), end.unwrap_or(at + 1)));
            }
            if len & 0xc0 == 0xc0 {
                let lo = *m.get(at + 1).ok_or("a pointer runs past the message")? as usize;
                end.get_or_insert(at + 2);
                at = ((len & 0x3f) << 8) | lo;
                continue;
            }
            let label = m
                .get(at + 1..at + 1 + len)
                .ok_or("a label runs past the message")?;
            labels.push(String::from_utf8_lossy(label).to_ascii_lowercase());
            at += 1 + len;
        }
        Err("a name loops".into())
    }

    /// **The answer section**: `(owner, type, rdata, rdata offset)` per
    /// record — a CNAME's rdata decoded to its target's text. NXDOMAIN is
    /// no records; any other error code is an error.
    pub fn answers(m: &[u8], id: u16) -> Result<Vec<Record>, String> {
        if m.len() < 12 || u16::from_be_bytes([m[0], m[1]]) != id {
            return Err("an answer to another question".into());
        }
        if m[2] & 0x80 == 0 {
            return Err("not an answer".into());
        }
        match m[3] & 0x0f {
            0 => {}
            3 => return Ok(Vec::new()),
            rc => return Err(format!("the nameserver answered error {rc}")),
        }
        let qd = u16::from_be_bytes([m[4], m[5]]) as usize;
        let an = u16::from_be_bytes([m[6], m[7]]) as usize;
        let mut at = 12;
        for _ in 0..qd {
            at = name(m, at)?.1 + 4;
        }
        let mut out = Vec::with_capacity(an);
        for _ in 0..an {
            let (owner, next) = name(m, at)?;
            let head = m
                .get(next..next + 10)
                .ok_or("a record runs past the message")?;
            let kind = u16::from_be_bytes([head[0], head[1]]);
            let len = u16::from_be_bytes([head[8], head[9]]) as usize;
            let start = next + 10;
            let rdata = m
                .get(start..start + len)
                .ok_or("a record's data runs past the message")?;
            let rdata = if kind == super::T_CNAME {
                name(m, start)?.0.into_bytes()
            } else {
                rdata.to_vec()
            };
            out.push((owner, kind, rdata, start));
            at = start + len;
        }
        Ok(out)
    }

    /// A TXT record's strings, joined (RFC 7208 §3.3).
    pub fn txt(rdata: &[u8]) -> String {
        let mut out = String::new();
        let mut i = 0;
        while i < rdata.len() {
            let n = rdata[i] as usize;
            let end = (i + 1 + n).min(rdata.len());
            out.push_str(&String::from_utf8_lossy(&rdata[i + 1..end]));
            i = end;
        }
        out
    }
}

/// **Are both records in place?**, the proof named `proof` (the product's). `Ok(None)`: yes.
/// `Ok(Some(what))`: not yet, and what is missing. `Err`: the resolver could not be asked.
pub fn verify_as(
    proof_name: &str,
    check: &dyn DomainCheck,
    domain: &str,
    company: &str,
    apex: &str,
    token: &str,
) -> Result<Option<String>, String> {
    let want = format!("{company}.{apex}");
    let mut missing = Vec::new();
    match check.cname(domain)? {
        Some(t) if t == want => {}
        Some(t) => missing.push(format!("{domain} is a CNAME to {t}, not to {want}")),
        None => missing.push(format!("{domain} has no CNAME to {want}")),
    }
    let proof = format!("{proof_name}={token}");
    let label = proof_label(proof_name);
    if !check
        .txt(&format!("{label}.{domain}"))?
        .iter()
        .any(|t| t == &proof)
    {
        missing.push(format!("{label}.{domain} has no TXT {proof}"));
    }
    Ok((!missing.is_empty()).then(|| missing.join("; ")))
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The records a Gunnar-kind estate's own domain carries (its proof `gunnar-proof`).
    fn required(domain: &str, company: &str, apex: &str, token: &str) -> Vec<CustomerRecord> {
        required_as("gunnar-proof", domain, company, apex, token)
    }

    /// [`verify_as`] for a Gunnar-kind estate.
    fn verify(
        check: &dyn DomainCheck,
        domain: &str,
        company: &str,
        apex: &str,
        token: &str,
    ) -> Result<Option<String>, String> {
        verify_as("gunnar-proof", check, domain, company, apex, token)
    }

    #[test]
    fn a_customer_domain_is_a_host_name_not_under_our_apex() {
        for ok in [
            "gunnar.company1.com",
            "gunnar.company2.se",
            "git-host.example.org",
        ] {
            assert_eq!(domain_refusal(ok, "gunnar.rs"), None, "{ok}");
        }
        for bad in [
            "company",
            "acme.gunnar.rs",
            "gunnar.rs",
            "Gunnar.Example.com",
            "a..b",
            "-a.com",
            "a_b.com",
        ] {
            assert!(domain_refusal(bad, "gunnar.rs").is_some(), "{bad}");
        }
    }

    #[test]
    fn both_records_must_be_in_place() {
        let f = std::env::temp_dir().join(format!("monetize-domain-{}.json", std::process::id()));
        let _ = std::fs::remove_file(&f);
        let r = FileResolver(f.clone());
        let missing = verify(&r, "gunnar.company1.com", "acme", "gunnar.rs", "t0k")
            .unwrap()
            .unwrap();
        assert!(
            missing.contains("no CNAME") && missing.contains("no TXT"),
            "{missing}"
        );
        r.set(CustomerRecord {
            name: "gunnar.company1.com".into(),
            kind: "CNAME".into(),
            value: "acme.gunnar.rs.".into(),
        })
        .unwrap();
        assert!(
            verify(&r, "gunnar.company1.com", "acme", "gunnar.rs", "t0k")
                .unwrap()
                .unwrap()
                .contains("no TXT")
        );
        for rec in required("gunnar.company1.com", "acme", "gunnar.rs", "t0k") {
            r.set(rec).unwrap();
        }
        assert_eq!(
            verify(&r, "gunnar.company1.com", "acme", "gunnar.rs", "t0k").unwrap(),
            None
        );
        assert!(verify(&NoResolver, "x.com", "acme", "gunnar.rs", "t").is_err());
        let _ = std::fs::remove_file(&f);
    }

    // ── the real resolver, against a fake nameserver on loopback ────────────

    /// **A nameserver that knows a few records** — the tests' "world DNS":
    /// answers a question for one of `records` (name, type, rdata) with it,
    /// anything else NXDOMAIN. `truncate_udp`: every UDP answer is empty with
    /// the TC bit set, and the full answer is served over TCP on the same port.
    pub(crate) fn fake_nameserver(
        records: Vec<(&'static str, u16, Vec<u8>)>,
        truncate_udp: bool,
    ) -> SocketAddr {
        let udp = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
        let at = udp.local_addr().unwrap();
        let records = std::sync::Arc::new(records);
        let answer = {
            let records = records.clone();
            move |q: &[u8], truncated: bool| -> Vec<u8> {
                let (qname, end) = wire::name(q, 12).unwrap();
                let qtype = u16::from_be_bytes([q[end], q[end + 1]]);
                let hits: Vec<&(&str, u16, Vec<u8>)> = records
                    .iter()
                    .filter(|r| r.0 == qname && (r.1 == qtype || r.1 == T_CNAME))
                    .collect();
                let mut m = q[..2].to_vec();
                let rcode = if hits.is_empty() { 3 } else { 0 };
                m.extend_from_slice(&[0x81 | if truncated { 0x02 } else { 0 }, 0x80 | rcode]);
                m.extend_from_slice(&1u16.to_be_bytes());
                m.extend_from_slice(
                    &(if truncated { 0u16 } else { hits.len() as u16 }).to_be_bytes(),
                );
                m.extend_from_slice(&[0, 0, 0, 0]);
                m.extend_from_slice(&q[12..end + 4]);
                if !truncated {
                    for (_, kind, rdata) in hits {
                        m.extend_from_slice(&[0xc0, 12]);
                        m.extend_from_slice(&kind.to_be_bytes());
                        m.extend_from_slice(&1u16.to_be_bytes());
                        m.extend_from_slice(&300u32.to_be_bytes());
                        m.extend_from_slice(&(rdata.len() as u16).to_be_bytes());
                        m.extend_from_slice(rdata);
                    }
                }
                m
            }
        };
        let udp_answer = answer.clone();
        std::thread::spawn(move || {
            let mut buf = [0u8; 512];
            while let Ok((n, from)) = udp.recv_from(&mut buf) {
                let _ = udp.send_to(&udp_answer(&buf[..n], truncate_udp), from);
            }
        });
        if truncate_udp {
            let tcp = std::net::TcpListener::bind(at).unwrap();
            std::thread::spawn(move || {
                use std::io::{Read, Write};
                for s in tcp.incoming().flatten() {
                    let mut s = s;
                    let mut len = [0u8; 2];
                    if s.read_exact(&mut len).is_err() {
                        continue;
                    }
                    let mut q = vec![0u8; u16::from_be_bytes(len) as usize];
                    if s.read_exact(&mut q).is_err() {
                        continue;
                    }
                    let a = answer(&q, false);
                    let mut framed = (a.len() as u16).to_be_bytes().to_vec();
                    framed.extend_from_slice(&a);
                    let _ = s.write_all(&framed);
                }
            });
        }
        at
    }

    /// A name in wire form (an uncompressed CNAME target).
    fn name_rdata(n: &str) -> Vec<u8> {
        let mut v = Vec::new();
        wire::put_name(&mut v, n).unwrap();
        v
    }

    /// TXT rdata of these strings.
    fn txt_rdata(parts: &[&str]) -> Vec<u8> {
        parts
            .iter()
            .flat_map(|p| std::iter::once(p.len() as u8).chain(p.bytes()))
            .collect()
    }

    /// ★ **The account's resolver is the DNS itself**: the CNAME and the
    /// proof are read from a nameserver (a TXT split into two strings is one
    /// value), a name nobody set is NXDOMAIN — "missing", not an error — and
    /// the check passes only with both in place.
    #[test]
    fn a_real_resolver_reads_the_cname_and_the_proof_from_a_nameserver() {
        let at = fake_nameserver(
            vec![
                (
                    "gunnar.company1.com",
                    T_CNAME,
                    name_rdata("acme.gunnar.rs."),
                ),
                (
                    "_gunnar-proof.gunnar.company1.com",
                    T_TXT,
                    txt_rdata(&["gunnar-proof=", "t0k"]),
                ),
                (
                    "gunnar.company2.se",
                    T_CNAME,
                    name_rdata("someone-else.example."),
                ),
            ],
            false,
        );
        let r = resolver_of(&format!("dns:{at}")).unwrap();
        assert_eq!(
            r.cname("gunnar.company1.com").unwrap().as_deref(),
            Some("acme.gunnar.rs")
        );
        assert_eq!(
            r.txt("_gunnar-proof.gunnar.company1.com").unwrap(),
            vec!["gunnar-proof=t0k".to_string()]
        );
        assert_eq!(
            r.cname("nobody.example.org").unwrap(),
            None,
            "NXDOMAIN is no record"
        );
        assert_eq!(
            verify(
                r.as_ref(),
                "gunnar.company1.com",
                "acme",
                "gunnar.rs",
                "t0k"
            )
            .unwrap(),
            None
        );
        let missing = verify(r.as_ref(), "gunnar.company2.se", "acme", "gunnar.rs", "t0k")
            .unwrap()
            .unwrap();
        assert!(
            missing.contains("CNAME to someone-else.example, not to acme.gunnar.rs")
                && missing.contains("no TXT"),
            "{missing}"
        );
    }

    /// An answer too big for UDP is marked truncated; the resolver asks the
    /// same nameserver again over TCP.
    #[test]
    fn a_truncated_answer_is_asked_again_over_tcp() {
        let at = fake_nameserver(
            vec![(
                "_gunnar-proof.gunnar.company1.com",
                T_TXT,
                txt_rdata(&["gunnar-proof=t0k"]),
            )],
            true,
        );
        let r = DnsResolver::new(vec![at]);
        assert_eq!(
            r.txt("_gunnar-proof.gunnar.company1.com").unwrap(),
            vec!["gunnar-proof=t0k".to_string()]
        );
    }

    /// A nameserver that does not answer is an ERROR (the job keeps waiting
    /// and says so), never "no record"; the config words are refused by name.
    #[test]
    fn a_silent_nameserver_is_an_error_and_the_config_words_are_checked() {
        let dead = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
        let mut r = DnsResolver::new(vec![dead.local_addr().unwrap()]);
        r.timeout = Duration::from_millis(200);
        assert!(r.cname("gunnar.company1.com").is_err());
        assert!(
            resolver_of("dns:").is_err()
                && resolver_of("dns:not-an-address").is_err()
                && resolver_of("bind9").is_err()
        );
        assert!(resolver_of("").is_ok() && resolver_of("file:/tmp/x.json").is_ok());
        assert_eq!(
            DnsResolver::system("# c\nnameserver 127.0.0.53\noptions edns0\nnameserver ::1\n")
                .unwrap(),
            vec![
                "127.0.0.53:53".parse::<SocketAddr>().unwrap(),
                "[::1]:53".parse().unwrap()
            ]
        );
        assert!(DnsResolver::system("search lan\n").is_err());
    }
}