use std::net::IpAddr;
use hickory_resolver::config::{LookupIpStrategy, NameServerConfig, ResolverConfig, ResolverOpts};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DnsSource {
Env,
System,
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,
}
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)),
];
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)
}
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());
}
}