nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! Hickory resolver lifecycle (install once, share across the process).

use std::net::IpAddr;
use std::sync::{Arc, OnceLock};

use hickory_resolver::Resolver;
use hickory_resolver::TokioResolver;
use hickory_resolver::config::LookupIpStrategy;
use hickory_resolver::net::runtime::TokioRuntimeProvider;
use tracing::{info, warn};

use super::config::{
    DnsSource, PUBLIC_FALLBACK, nameservers_from_env, resolver_config_from_ips, resolver_opts,
};
use super::error::DnsError;
use super::reqwest_resolve::HickoryReqwestResolver;

static RUNTIME: OnceLock<DnsRuntime> = OnceLock::new();

struct DnsRuntime {
    resolver: Arc<TokioResolver>,
    reqwest: Arc<HickoryReqwestResolver>,
    source: DnsSource,
}

/// Build and install the process-wide Hickory resolver (idempotent).
pub fn install_from_env() -> Result<(), DnsError> {
    if RUNTIME.get().is_some() {
        return Ok(());
    }

    let (resolver, source) = build_resolver()?;
    info!(?source, "nautalid DNS resolver installed (Hickory)");
    let resolver = Arc::new(resolver);
    let reqwest = Arc::new(HickoryReqwestResolver::new(Arc::clone(&resolver)));
    let _ = RUNTIME.set(DnsRuntime {
        resolver,
        reqwest,
        source,
    });
    Ok(())
}

fn build_resolver() -> Result<(TokioResolver, DnsSource), DnsError> {
    if let Some(ips) = nameservers_from_env()? {
        let config = resolver_config_from_ips(&ips);
        let resolver = Resolver::builder_with_config(config, TokioRuntimeProvider::default())
            .with_options(resolver_opts())
            .build()
            .map_err(|e| DnsError::Hickory(e.to_string()))?;
        return Ok((resolver, DnsSource::Env));
    }

    match TokioResolver::builder_tokio() {
        Ok(builder) => {
            let mut builder = builder;
            builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
            let resolver = builder
                .build()
                .map_err(|e| DnsError::Hickory(e.to_string()))?;
            Ok((resolver, DnsSource::System))
        }
        Err(e) => {
            warn!(
                %e,
                "no NAUTALID_DNS and no system resolver config; using public fallback"
            );
            let config = resolver_config_from_ips(PUBLIC_FALLBACK);
            let resolver = Resolver::builder_with_config(config, TokioRuntimeProvider::default())
                .with_options(resolver_opts())
                .build()
                .map_err(|e| DnsError::Hickory(e.to_string()))?;
            Ok((resolver, DnsSource::PublicFallback))
        }
    }
}

fn runtime() -> Result<&'static DnsRuntime, DnsError> {
    RUNTIME.get().ok_or(DnsError::NotInstalled)
}

#[must_use]
pub fn source() -> Option<DnsSource> {
    RUNTIME.get().map(|r| r.source)
}

/// Resolver for [`reqwest`](https://docs.rs/reqwest) HTTP clients.
pub fn reqwest_resolver() -> Result<Arc<HickoryReqwestResolver>, DnsError> {
    runtime().map(|r| Arc::clone(&r.reqwest))
}

/// Resolve a host to IP addresses (A/AAAA via Hickory).
pub async fn lookup_ip(host: &str) -> Result<Vec<IpAddr>, DnsError> {
    let resolver = &runtime()?.resolver;
    let lookup = resolver
        .lookup_ip(host)
        .await
        .map_err(|e| DnsError::Hickory(e.to_string()))?;
    Ok(lookup.iter().collect())
}

/// Startup warm-up: log a sample lookup through the installed resolver.
pub async fn log_sample_resolution() {
    match install_from_env() {
        Ok(()) => {}
        Err(e) => {
            warn!(%e, "DNS install failed");
            return;
        }
    }

    match lookup_ip("example.com.").await {
        Ok(ips) => info!(?ips, source = ?source(), "hickory lookup example.com"),
        Err(e) => warn!(%e, "hickory lookup failed (non-fatal)"),
    }
}

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

    #[test]
    fn resolver_config_from_parsed_nameservers() {
        let ips = parse_nameserver_list("8.8.8.8,1.1.1.1").unwrap();
        let config = resolver_config_from_ips(&ips);
        assert_eq!(config.name_servers().len(), 2);
    }
}