use std::fs;
use std::io;
use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
use crate::config::{NameServerConfig, ResolverConfig, ResolverOpts};
use crate::net::NetError;
use crate::proto::rr::Name;
pub fn read_system_conf() -> Result<(ResolverConfig, ResolverOpts), NetError> {
read_resolv_conf("/etc/resolv.conf")
}
fn read_resolv_conf<P: AsRef<Path>>(path: P) -> Result<(ResolverConfig, ResolverOpts), NetError> {
parse_resolv_conf(fs::read(path)?)
}
pub fn parse_resolv_conf<T: AsRef<[u8]>>(
data: T,
) -> Result<(ResolverConfig, ResolverOpts), NetError> {
let parsed_conf = resolv_conf::Config::parse(&data)
.map_err(|e| io::Error::other(format!("Error parsing resolv.conf: {e}")))?;
into_resolver_config(parsed_conf)
}
fn into_resolver_config(
parsed_config: resolv_conf::Config,
) -> Result<(ResolverConfig, ResolverOpts), NetError> {
let domain = if let Some(domain) = parsed_config.get_system_domain() {
Name::from_str(domain.as_str()).ok()
} else {
None
};
let nameservers = parsed_config
.nameservers
.iter()
.map(|ip| NameServerConfig::udp_and_tcp(ip.into()))
.collect::<Vec<_>>();
if nameservers.is_empty() {
Err(io::Error::other("no nameservers found in config"))?;
}
let mut search = vec![];
for search_domain in parsed_config.get_last_search_or_domain() {
if search_domain == "--" {
continue;
}
search.push(
Name::from_str_relaxed(search_domain)
.map_err(|e| io::Error::other(format!("Error parsing resolv.conf: {e}")))?,
);
}
let config = ResolverConfig::from_parts(domain, search, nameservers);
let options = ResolverOpts {
ndots: parsed_config.ndots as usize,
timeout: Duration::from_secs(u64::from(parsed_config.timeout)),
attempts: parsed_config.attempts as usize,
edns0: parsed_config.edns0,
..ResolverOpts::default()
};
Ok((config, options))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::rr::Name;
use std::env;
use std::net::*;
use std::str::FromStr;
fn empty_config(name_servers: NameServerConfig) -> ResolverConfig {
ResolverConfig::from_parts(None, vec![], vec![name_servers])
}
fn nameserver_config(ip: &str) -> NameServerConfig {
let mut server = NameServerConfig::udp_and_tcp(IpAddr::from_str(ip).unwrap());
server.trust_negative_responses = false;
server
}
fn tests_dir() -> String {
let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned());
format!("{server_path}/crates/resolver/tests")
}
#[test]
fn test_name_server() {
let parsed = parse_resolv_conf("nameserver 127.0.0.1").expect("failed");
let cfg = empty_config(nameserver_config("127.0.0.1"));
assert_eq!(cfg.name_servers()[0].ip, parsed.0.name_servers()[0].ip);
is_default_opts(parsed.1);
}
#[test]
fn test_search() {
let parsed = parse_resolv_conf("search localnet.\nnameserver 127.0.0.1").expect("failed");
let mut cfg = empty_config(nameserver_config("127.0.0.1"));
cfg.add_search(Name::from_str("localnet.").unwrap());
assert_eq!(cfg.search(), parsed.0.search());
is_default_opts(parsed.1);
}
#[test]
fn test_skips_invalid_search() {
let parsed =
parse_resolv_conf("\n\nnameserver 127.0.0.53\noptions edns0 trust-ad\nsearch -- lan\n")
.expect("failed");
let mut cfg = empty_config(nameserver_config("127.0.0.53"));
{
assert_eq!(cfg.name_servers()[0].ip, parsed.0.name_servers()[0].ip);
is_default_opts(parsed.1);
}
{
cfg.add_search(Name::from_str("lan").unwrap());
assert_eq!(cfg.search(), parsed.0.search());
}
}
#[test]
fn test_underscore_in_search() {
let parsed =
parse_resolv_conf("search Speedport_000\nnameserver 127.0.0.1").expect("failed");
let mut cfg = empty_config(nameserver_config("127.0.0.1"));
cfg.add_search(Name::from_str_relaxed("Speedport_000").unwrap());
assert_eq!(cfg.search(), parsed.0.search());
is_default_opts(parsed.1);
}
#[test]
fn test_domain() {
let parsed = parse_resolv_conf("domain example.com\nnameserver 127.0.0.1").expect("failed");
let mut cfg = empty_config(nameserver_config("127.0.0.1"));
cfg.set_domain(Name::from_str("example.com").unwrap());
assert_eq!(cfg.name_servers()[0].ip, parsed.0.name_servers()[0].ip);
assert_eq!(cfg.domain(), parsed.0.domain());
is_default_opts(parsed.1);
}
#[test]
fn test_read_resolv_conf() {
read_resolv_conf(format!("{}/resolv.conf-simple", tests_dir())).expect("simple failed");
read_resolv_conf(format!("{}/resolv.conf-macos", tests_dir())).expect("macos failed");
read_resolv_conf(format!("{}/resolv.conf-linux", tests_dir())).expect("linux failed");
}
fn is_default_opts(opts: ResolverOpts) {
assert_eq!(opts.ndots, 1);
assert_eq!(opts.timeout, Duration::from_secs(5));
assert_eq!(opts.attempts, 2);
}
}