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,
}
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)
}
pub fn reqwest_resolver() -> Result<Arc<HickoryReqwestResolver>, DnsError> {
runtime().map(|r| Arc::clone(&r.reqwest))
}
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())
}
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);
}
}