pub mod parse;
#[cfg(target_os = "macos")]
pub(crate) mod scdynamicstore;
pub use parse::{Nameserver, ParseNameserverError};
use std::net::{IpAddr, SocketAddr};
use std::path::Path;
use resolv_conf::Config as ResolvConfig;
const DNS_PORT: u16 = 53;
const RESOLV_CONF_PATH: &str = "/etc/resolv.conf";
pub(super) async fn resolve_nameservers(
nameservers: &[Nameserver],
) -> std::io::Result<Vec<SocketAddr>> {
let mut out = Vec::with_capacity(nameservers.len());
let mut last_err: Option<std::io::Error> = None;
for ns in nameservers {
match ns.resolve().await {
Ok(sa) => out.push(sa),
Err(e) => {
tracing::warn!(nameserver = %ns, error = %e, "failed to resolve nameserver");
last_err = Some(e);
}
}
}
if out.is_empty()
&& let Some(e) = last_err
{
return Err(e);
}
Ok(out)
}
pub(super) async fn read_host_dns_servers() -> std::io::Result<Vec<SocketAddr>> {
#[cfg(target_os = "macos")]
if let Some(servers) = try_read_scdynamicstore() {
return Ok(servers);
}
read_resolv_conf(Path::new(RESOLV_CONF_PATH)).await
}
#[cfg(target_os = "macos")]
fn try_read_scdynamicstore() -> Option<Vec<SocketAddr>> {
match self::scdynamicstore::read_dns_servers() {
Ok(servers) if !servers.is_empty() => {
tracing::debug!(
count = servers.len(),
"loaded nameservers from SCDynamicStore"
);
Some(servers)
}
Ok(_) => {
tracing::debug!(
"SCDynamicStore returned no nameservers; falling back to /etc/resolv.conf"
);
None
}
Err(e) => {
tracing::debug!(
error = %e,
"SCDynamicStore lookup failed; falling back to /etc/resolv.conf"
);
None
}
}
}
async fn read_resolv_conf(path: &Path) -> std::io::Result<Vec<SocketAddr>> {
let bytes = tokio::fs::read(path).await?;
let cfg = ResolvConfig::parse(&bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
Ok(cfg
.nameservers
.into_iter()
.map(|ns| SocketAddr::new(IpAddr::from(ns), DNS_PORT))
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn read_resolv_conf_parses_nameservers() {
let dir = std::env::temp_dir();
let path = dir.join(format!("msb-resolv-{}.conf", std::process::id()));
std::fs::write(
&path,
"# comment line\n\
nameserver 1.1.1.1\n\
nameserver 8.8.8.8 # inline comment\n\
search example.com\n\
options ndots:5\n\
nameserver 2606:4700:4700::1111\n\
\n",
)
.unwrap();
let servers = read_resolv_conf(&path).await.expect("read ok");
std::fs::remove_file(&path).ok();
assert_eq!(servers.len(), 3);
assert_eq!(servers[0], "1.1.1.1:53".parse().unwrap());
assert_eq!(servers[1], "8.8.8.8:53".parse().unwrap());
assert_eq!(servers[2], "[2606:4700:4700::1111]:53".parse().unwrap());
}
#[tokio::test]
async fn read_resolv_conf_missing_file_errs() {
assert!(
read_resolv_conf(Path::new("/nonexistent/path/to/resolv.conf"))
.await
.is_err()
);
}
}