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");
}
}