nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! DNS configuration from environment and defaults.

use std::net::IpAddr;

use hickory_resolver::config::{LookupIpStrategy, NameServerConfig, ResolverConfig, ResolverOpts};
use thiserror::Error;

/// Where upstream nameservers came from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DnsSource {
    /// **`NAUTALID_DNS`**
    Env,
    /// **`/etc/resolv.conf`** when present on the host
    System,
    /// Built-in public resolvers when nothing else is available
    PublicFallback,
}

#[derive(Debug, Error)]
pub enum DnsConfigError {
    #[error("invalid nameserver `{0}` in NAUTALID_DNS")]
    InvalidNameserver(String),
    #[error("NAUTALID_DNS is set but contains no nameservers")]
    EmptyNameservers,
}

/// Default public resolvers used only when env and system config are unavailable.
pub const PUBLIC_FALLBACK: &[IpAddr] = &[
    IpAddr::V4(std::net::Ipv4Addr::new(1, 1, 1, 1)),
    IpAddr::V4(std::net::Ipv4Addr::new(8, 8, 8, 8)),
];

/// Parse comma-separated IP addresses (e.g. `8.8.8.8,8.8.4.4`).
pub fn parse_nameserver_list(raw: &str) -> Result<Vec<IpAddr>, DnsConfigError> {
    let mut out = Vec::new();
    for part in raw.split(',') {
        let trimmed = part.trim();
        if trimmed.is_empty() {
            continue;
        }
        let ip: IpAddr = trimmed
            .parse()
            .map_err(|_| DnsConfigError::InvalidNameserver(trimmed.to_string()))?;
        out.push(ip);
    }

    if out.is_empty() {
        return Err(DnsConfigError::EmptyNameservers);
    }
    Ok(out)
}

/// Parse **`NAUTALID_DNS`**: comma-separated IP addresses (e.g. `8.8.8.8,8.8.4.4`).
pub fn nameservers_from_env() -> Result<Option<Vec<IpAddr>>, DnsConfigError> {
    let raw = match std::env::var("NAUTALID_DNS") {
        Ok(v) if !v.trim().is_empty() => v,
        _ => return Ok(None),
    };
    parse_nameserver_list(&raw).map(Some)
}

pub fn resolver_config_from_ips(ips: &[IpAddr]) -> ResolverConfig {
    let name_servers: Vec<NameServerConfig> = ips
        .iter()
        .copied()
        .map(NameServerConfig::udp_and_tcp)
        .collect();
    ResolverConfig::from_parts(None, Vec::new(), name_servers)
}

pub fn resolver_opts() -> ResolverOpts {
    let mut opts = ResolverOpts::default();
    opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
    opts
}

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

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

    #[test]
    fn rejects_invalid_ip() {
        assert!(parse_nameserver_list("not-an-ip").is_err());
    }

    #[test]
    fn rejects_empty_list() {
        assert!(parse_nameserver_list("  ,  ").is_err());
    }
}