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

nordisk/edda

crates/estate-front/src/web/dns.rs

//! **Does the name point here?** — asked before every ACME attempt (owner 2026-10-25:
//! "certificates must fix themselves").
//!
//! Let's Encrypt validates HTTP-01 by resolving each name and fetching the token from
//! port 80 of what it finds. A name with no A record, or one that points at another box,
//! can only fail, and each such failure is one of the five failed validations per name per
//! hour Let's Encrypt allows. So the front resolves its names first (A and AAAA, the
//! resolver libc reads) and compares them with its own public address. A name that does not
//! point here is NOT an ACME failure: Let's Encrypt is not asked, the persisted backoff
//! (`acme-next`) does not grow, and the name is looked at again every [`DNS_EVERY`]. The
//! moment it points here, the front asks at once. MEASURED 2026-10-25: test2's ONE box had
//! no A record for hours; every look was an ACME failure, so when the owner added the record
//! the box was inside a six-hour backoff.
//!
//! **This front's public address** is the fed `public_address` (comma-separated, for a
//! front behind NAT), or else the source address the kernel picks for a public
//! destination (a UDP `connect`, which sends no packet). When the front knows no public
//! address of its own (only private ones: a bench behind NAT), it cannot tell, and ACME is
//! asked as before.
//!
//! The check is part of ACME: it runs only where ACME would be asked (nothing valid, or
//! renewal due, and the installer has fed this front). A front that never asks ACME never
//! resolves anything for it.

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, ToSocketAddrs, UdpSocket};
use std::time::Duration;

/// How often a name that does not point here is resolved again.
pub const DNS_EVERY: Duration = Duration::from_secs(60);

/// The feed key of this front's public address(es), when it cannot see its own (NAT).
pub const FEED_PUBLIC_ADDRESS: &str = "public_address";

/// **The resolver and this front's own addresses** — injected, so tests never ask live DNS.
pub trait Dns: Send + Sync {
    /// The A and AAAA records of `host`, or the resolver's own words.
    fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, String>;
    /// This front's public addresses (empty: it cannot tell).
    fn own(&self) -> Vec<IpAddr>;
}

/// The resolver libc reads (`getaddrinfo`, `/etc/resolv.conf` on the appliance) and the
/// kernel's own source address.
#[derive(Debug, Default, Clone, Copy)]
pub struct SystemDns;

impl Dns for SystemDns {
    fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, String> {
        let mut out: Vec<IpAddr> = (host, 80u16).to_socket_addrs().map_err(|e| e.to_string())?.map(|a| a.ip()).collect();
        out.sort();
        out.dedup();
        Ok(out)
    }
    fn own(&self) -> Vec<IpAddr> {
        // A UDP `connect` only picks a route: no packet leaves. The address is that of the
        // interface the box's traffic to the internet leaves by.
        let probe = |bind: IpAddr, to: IpAddr| -> Option<IpAddr> {
            let s = UdpSocket::bind((bind, 0)).ok()?;
            s.connect((to, 53)).ok()?;
            s.local_addr().ok().map(|a| a.ip())
        };
        [probe(Ipv4Addr::UNSPECIFIED.into(), Ipv4Addr::new(192, 0, 2, 1).into()), probe(Ipv6Addr::UNSPECIFIED.into(), "2001:db8::1".parse::<Ipv6Addr>().unwrap_or(Ipv6Addr::LOCALHOST).into())]
            .into_iter()
            .flatten()
            .filter(is_public)
            .collect()
    }
}

/// Pure: an address another box on the internet could reach this one at. Private,
/// shared (CGNAT), loopback, link-local, unspecified and unique-local addresses are not.
pub fn is_public(ip: &IpAddr) -> bool {
    match ip {
        IpAddr::V4(v) => {
            let o = v.octets();
            !(v.is_private() || v.is_loopback() || v.is_link_local() || v.is_unspecified() || v.is_broadcast() || (o[0] == 100 && (o[1] & 0xc0) == 64))
        }
        IpAddr::V6(v) => {
            let s = v.segments()[0];
            !(v.is_loopback() || v.is_unspecified() || (s & 0xfe00) == 0xfc00 || (s & 0xffc0) == 0xfe80)
        }
    }
}

/// Pure: the fed `public_address` (comma-separated), the addresses that parse.
pub fn fed_public(value: Option<&str>) -> Vec<IpAddr> {
    value.unwrap_or_default().split(',').filter_map(|s| s.trim().parse().ok()).collect()
}

/// What [`check`] found.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Points {
    /// Every name resolves to (at least) one of this front's addresses.
    Here,
    /// `host` does not: it resolves to `to` (empty: to nothing), or the resolver said `error`.
    Elsewhere { host: String, to: Vec<IpAddr>, error: Option<String> },
    /// This front knows no public address of its own; ACME is asked as before.
    CannotTell,
}

impl Points {
    /// The reason the status and the log carry, for a name that does not point here.
    pub fn reason(&self) -> Option<String> {
        match self {
            Points::Elsewhere { host, to, error } => {
                let x = if !to.is_empty() {
                    to.iter().map(|a| a.to_string()).collect::<Vec<_>>().join(", ")
                } else if let Some(e) = error {
                    format!("nothing: {e}")
                } else {
                    "nothing".to_string()
                };
                Some(format!("dns: {host} does not point at this box (it resolves to {x})"))
            }
            _ => None,
        }
    }
}

/// **Judge the names**, pure: `resolved` is each host's answer, `own` this front's public
/// addresses. A name points here when one of its addresses is ours; a name whose answer
/// has no address of a family we know (only AAAA, and we know only our IPv4) cannot be
/// told and is let through.
pub fn judge(resolved: &[(String, Result<Vec<IpAddr>, String>)], own: &[IpAddr]) -> Points {
    if own.is_empty() {
        return Points::CannotTell;
    }
    let knows = |a: &IpAddr| own.iter().any(|o| o.is_ipv4() == a.is_ipv4());
    for (host, r) in resolved {
        match r {
            Ok(to) if to.iter().any(|a| own.contains(a)) => {}
            Ok(to) if !to.is_empty() && !to.iter().any(knows) => {}
            Ok(to) => return Points::Elsewhere { host: host.clone(), to: to.clone(), error: None },
            Err(e) => return Points::Elsewhere { host: host.clone(), to: vec![], error: Some(e.clone()) },
        }
    }
    Points::Here
}

/// **Resolve every one of `hosts` and judge them** against `fed` (the fed public address)
/// or, when none is fed, the front's own.
pub fn check(dns: &dyn Dns, hosts: &[String], fed: &[IpAddr]) -> Points {
    let own = if fed.is_empty() { dns.own() } else { fed.to_vec() };
    if own.is_empty() {
        return Points::CannotTell;
    }
    let resolved: Vec<(String, Result<Vec<IpAddr>, String>)> = hosts.iter().map(|h| (h.clone(), dns.resolve(h))).collect();
    judge(&resolved, &own)
}

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

    fn ip(s: &str) -> IpAddr {
        s.parse().unwrap()
    }

    #[test]
    fn a_name_points_here_only_when_one_of_its_addresses_is_ours() {
        let own = [ip("198.51.100.7")];
        let ok = |v: &[&str]| Ok(v.iter().map(|s| ip(s)).collect::<Vec<_>>());
        assert_eq!(judge(&[("a.test".into(), ok(&["198.51.100.7"]))], &own), Points::Here);
        assert_eq!(judge(&[("a.test".into(), ok(&["203.0.113.9", "198.51.100.7"]))], &own), Points::Here);
        let p = judge(&[("a.test".into(), ok(&["198.51.100.7"])), ("www.a.test".into(), ok(&["203.0.113.9"]))], &own);
        assert_eq!(p.reason().unwrap(), "dns: www.a.test does not point at this box (it resolves to 203.0.113.9)");
        let p = judge(&[("a.test".into(), Err("Name does not resolve".into()))], &own);
        assert_eq!(p.reason().unwrap(), "dns: a.test does not point at this box (it resolves to nothing: Name does not resolve)");
        assert!(judge(&[("a.test".into(), ok(&[]))], &own).reason().unwrap().ends_with("(it resolves to nothing)"));
        assert_eq!(judge(&[("a.test".into(), ok(&["2001:db8::5"]))], &own), Points::Here, "only AAAA, and we know only our IPv4: cannot be told, let through");
        assert_eq!(judge(&[("a.test".into(), ok(&["203.0.113.9"]))], &[]), Points::CannotTell);
        assert_eq!(Points::Here.reason(), None);
    }

    #[test]
    fn only_a_public_address_is_this_fronts_own() {
        for p in ["198.51.100.7", "94.237.1.2", "2a04:3540::1"] {
            assert!(is_public(&ip(p)), "{p}");
        }
        for p in ["10.1.2.3", "172.16.0.1", "192.168.1.1", "100.64.0.1", "127.0.0.1", "169.254.1.1", "0.0.0.0", "::1", "fd00::1", "fe80::1"] {
            assert!(!is_public(&ip(p)), "{p}");
        }
        assert_eq!(fed_public(Some(" 198.51.100.7, nonsense ,2001:db8::1")), vec![ip("198.51.100.7"), ip("2001:db8::1")]);
        assert!(fed_public(None).is_empty());
    }

    struct Fixed;
    impl Dns for Fixed {
        fn resolve(&self, h: &str) -> Result<Vec<IpAddr>, String> {
            Ok(if h == "a.test" { vec![ip("198.51.100.7")] } else { vec![] })
        }
        fn own(&self) -> Vec<IpAddr> {
            vec![ip("203.0.113.1")]
        }
    }

    #[test]
    fn a_fed_public_address_wins_over_the_one_the_front_sees() {
        let hosts = ["a.test".to_string()];
        assert!(matches!(check(&Fixed, &hosts, &[]), Points::Elsewhere { .. }), "the kernel's address is not the one DNS names");
        assert_eq!(check(&Fixed, &hosts, &[ip("198.51.100.7")]), Points::Here, "behind NAT: the fed address is ours");
    }
}