blob: eb6092edbd33d7cfa9fde4fec9a1f0d3849f5df0 [file] [log] [blame]
// Copyright 2015-2017 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
//! System configuration loading
//!
//! This module is responsible for parsing and returning the configuration from
//! the host system. It will read from the default location on each operating
//! system, e.g. most Unixes have this written to `/etc/resolv.conf`
use std::fs::File;
use std::io;
use std::io::Read;
use std::net::SocketAddr;
use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
use resolv_conf;
use crate::config::*;
use crate::proto::rr::Name;
const DEFAULT_PORT: u16 = 53;
pub fn read_system_conf() -> io::Result<(ResolverConfig, ResolverOpts)> {
Ok(read_resolv_conf("/etc/resolv.conf")?)
}
fn read_resolv_conf<P: AsRef<Path>>(path: P) -> io::Result<(ResolverConfig, ResolverOpts)> {
let mut data = String::new();
let mut file = File::open(path)?;
file.read_to_string(&mut data)?;
parse_resolv_conf(&data)
}
fn parse_resolv_conf<T: AsRef<[u8]>>(data: T) -> io::Result<(ResolverConfig, ResolverOpts)> {
let parsed_conf = resolv_conf::Config::parse(&data).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Error parsing resolv.conf: {:?}", e),
)
})?;
into_resolver_config(parsed_conf)
}
// TODO: use a custom parsing error type maybe?
fn into_resolver_config(
parsed_config: resolv_conf::Config,
) -> io::Result<(ResolverConfig, ResolverOpts)> {
let domain = if let Some(domain) = parsed_config.get_system_domain() {
Some(Name::from_str(domain.as_str())?)
} else {
None
};
// nameservers
let mut nameservers = Vec::<NameServerConfig>::with_capacity(parsed_config.nameservers.len());
for ip in &parsed_config.nameservers {
nameservers.push(NameServerConfig {
socket_addr: SocketAddr::new(ip.into(), DEFAULT_PORT),
protocol: Protocol::Udp,
tls_dns_name: None,
#[cfg(feature = "dns-over-rustls")]
tls_config: None,
});
nameservers.push(NameServerConfig {
socket_addr: SocketAddr::new(ip.into(), DEFAULT_PORT),
protocol: Protocol::Tcp,
tls_dns_name: None,
#[cfg(feature = "dns-over-rustls")]
tls_config: None,
});
}
if nameservers.is_empty() {
warn!("no nameservers found in config");
}
// search
let mut search = vec![];
for search_domain in parsed_config.get_last_search_or_domain() {
search.push(Name::from_str_relaxed(&search_domain).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("Error parsing resolv.conf: {:?}", e),
)
})?);
}
let config = ResolverConfig::from_parts(domain, search, nameservers);
let mut options = ResolverOpts::default();
options.ndots = parsed_config.ndots as usize;
options.timeout = Duration::from_secs(u64::from(parsed_config.timeout));
options.attempts = parsed_config.attempts as usize;
Ok((config, options))
}
#[cfg(test)]
mod tests {
use super::*;
use proto::rr::Name;
use std::env;
use std::net::*;
use std::str::FromStr;
fn empty_config() -> ResolverConfig {
ResolverConfig::from_parts(None, vec![], vec![])
}
fn nameserver_config(ip: &str) -> [NameServerConfig; 2] {
let addr = SocketAddr::new(IpAddr::from_str(ip).unwrap(), 53);
[
NameServerConfig {
socket_addr: addr,
protocol: Protocol::Udp,
tls_dns_name: None,
#[cfg(feature = "dns-over-rustls")]
tls_config: None,
},
NameServerConfig {
socket_addr: addr,
protocol: Protocol::Tcp,
tls_dns_name: None,
#[cfg(feature = "dns-over-rustls")]
tls_config: None,
},
]
}
fn tests_dir() -> String {
let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned());
format!("{}/crates/resolver/tests", server_path)
}
#[test]
#[allow(clippy::redundant_clone)]
fn test_name_server() {
let parsed = parse_resolv_conf("nameserver 127.0.0.1").expect("failed");
let mut cfg = empty_config();
let nameservers = nameserver_config("127.0.0.1");
cfg.add_name_server(nameservers[0].clone());
cfg.add_name_server(nameservers[1].clone());
assert_eq!(cfg.name_servers(), parsed.0.name_servers());
assert_eq!(ResolverOpts::default(), parsed.1);
}
#[test]
fn test_search() {
let parsed = parse_resolv_conf("search localnet.").expect("failed");
let mut cfg = empty_config();
cfg.add_search(Name::from_str("localnet.").unwrap());
assert_eq!(cfg.search(), parsed.0.search());
assert_eq!(ResolverOpts::default(), parsed.1);
}
#[test]
fn test_underscore_in_search() {
let parsed = parse_resolv_conf("search Speedport_000").expect("failed");
let mut cfg = empty_config();
cfg.add_search(Name::from_str_relaxed("Speedport_000.").unwrap());
assert_eq!(cfg.search(), parsed.0.search());
assert_eq!(ResolverOpts::default(), parsed.1);
}
#[test]
fn test_domain() {
let parsed = parse_resolv_conf("domain example.com").expect("failed");
let mut cfg = empty_config();
cfg.set_domain(Name::from_str("example.com").unwrap());
assert_eq!(cfg, parsed.0);
assert_eq!(ResolverOpts::default(), 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");
}
}