#![warn(missing_debug_implementations, missing_docs, unreachable_pub)]
#![warn(clippy::use_self)]
use std::fmt;
use std::iter::Iterator;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::slice::Iter;
use std::str::{self, from_utf8, FromStr, Utf8Error};
mod ip;
pub use ip::{AddrParseError, Network, ScopedIp};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Config {
pub nameservers: Vec<ScopedIp>,
last_search: LastSearch,
domain: Option<String>,
search: Option<Vec<String>>,
pub sortlist: Vec<Network>,
pub debug: bool,
pub ndots: u32,
pub timeout: u32,
pub attempts: u32,
pub rotate: bool,
pub no_check_names: bool,
pub inet6: bool,
pub ip6_bytestring: bool,
pub ip6_dotint: bool,
pub edns0: bool,
pub single_request: bool,
pub single_request_reopen: bool,
pub no_tld_query: bool,
pub use_vc: bool,
pub no_reload: bool,
pub trust_ad: bool,
pub lookup: Vec<Lookup>,
pub family: Vec<Family>,
pub no_aaaa: bool,
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn parse<T: AsRef<[u8]>>(buf: T) -> Result<Self, ParseError> {
let (new, mut errors) = Self::parse_with_errors(buf.as_ref());
let mut iter = errors.drain(..);
match iter.next() {
Some(err) => Err(err),
None => Ok(new),
}
}
pub fn parse_with_errors(bytes: &[u8]) -> (Self, Vec<ParseError>) {
use ParseError::*;
let mut cfg = Self::new();
let mut errors = Vec::new();
'lines: for (lineno, line) in bytes.split(|&x| x == b'\n').enumerate() {
for &c in line.iter() {
if c != b'\t' && c != b' ' {
if c == b';' || c == b'#' {
continue 'lines;
} else {
break;
}
}
}
let str = match from_utf8(line) {
Ok(str) => str,
Err(e) => {
errors.push(InvalidUtf8(lineno, e));
continue;
}
};
let text = match str.split([';', '#']).next() {
Some(text) => text,
None => {
errors.push(InvalidValue(lineno));
continue;
}
};
let mut words = text.split_whitespace();
let keyword = match words.next() {
Some(x) => x,
None => continue,
};
match keyword {
"nameserver" => {
let srv = match words.next() {
Some(srv) => srv,
None => {
errors.push(InvalidValue(lineno));
continue;
}
};
match ScopedIp::from_str(srv) {
Ok(addr) => cfg.nameservers.push(addr),
Err(e) => errors.push(InvalidIp(lineno, e)),
}
if words.next().is_some() {
errors.push(ExtraData(lineno));
}
}
"domain" => {
let domain = match words.next() {
Some(domain) => domain,
None => {
errors.push(InvalidValue(lineno));
continue;
}
};
cfg.set_domain(domain.to_owned());
if words.next().is_some() {
errors.push(ExtraData(lineno));
}
}
"search" => {
let mut search = Vec::new();
for word in words {
if !word.contains(',') {
search.push(word.to_owned());
}
}
cfg.set_search(search);
}
"sortlist" => {
cfg.sortlist.clear();
for pair in words {
match Network::from_str(pair) {
Ok(network) => cfg.sortlist.push(network),
Err(e) => errors.push(InvalidIp(lineno, e)),
}
}
}
"options" => {
for pair in words {
let mut iter = pair.splitn(2, ':');
let key = match iter.next() {
Some(key) => key,
None => {
errors.push(InvalidValue(lineno));
continue 'lines;
}
};
let value = iter.next();
if iter.next().is_some() {
errors.push(ExtraData(lineno));
continue 'lines;
}
match (key, value) {
("debug", _) => cfg.debug = true,
("ndots", Some(x)) => match u32::from_str(x) {
Ok(ndots) => cfg.ndots = ndots,
Err(_) => errors.push(InvalidOptionValue(lineno)),
},
("timeout", Some(x)) => match u32::from_str(x) {
Ok(timeout) => cfg.timeout = timeout,
Err(_) => errors.push(InvalidOptionValue(lineno)),
},
("attempts", Some(x)) => match u32::from_str(x) {
Ok(attempts) => cfg.attempts = attempts,
Err(_) => errors.push(InvalidOptionValue(lineno)),
},
("rotate", _) => cfg.rotate = true,
("no-check-names", _) => cfg.no_check_names = true,
("inet6", _) => cfg.inet6 = true,
("ip6-bytestring", _) => cfg.ip6_bytestring = true,
("ip6-dotint", _) => cfg.ip6_dotint = true,
("no-ip6-dotint", _) => cfg.ip6_dotint = false,
("edns0", _) => cfg.edns0 = true,
("single-request", _) => cfg.single_request = true,
("single-request-reopen", _) => cfg.single_request_reopen = true,
("no-reload", _) => cfg.no_reload = true,
("trust-ad", _) => cfg.trust_ad = true,
("no-tld-query", _) => cfg.no_tld_query = true,
("use-vc", _) => cfg.use_vc = true,
("no-aaaa", _) => cfg.no_aaaa = true,
_ => errors.push(InvalidOption(lineno)),
}
}
}
"lookup" => {
for word in words {
match word {
"file" => cfg.lookup.push(Lookup::File),
"bind" => cfg.lookup.push(Lookup::Bind),
extra => cfg.lookup.push(Lookup::Extra(extra.to_string())),
}
}
}
"family" => {
for word in words {
match word {
"inet4" => cfg.family.push(Family::Inet4),
"inet6" => cfg.family.push(Family::Inet6),
_ => errors.push(InvalidValue(lineno)),
}
}
}
_ => errors.push(InvalidDirective(lineno)),
}
}
(cfg, errors)
}
pub fn get_last_search_or_domain(&self) -> DomainIter<'_> {
let domain_iter = match self.last_search {
LastSearch::Search => {
DomainIterInternal::Search(self.get_search().map(|domains| domains.iter()))
}
LastSearch::Domain => DomainIterInternal::Domain(self.get_domain()),
LastSearch::None => DomainIterInternal::None,
};
DomainIter(domain_iter)
}
pub fn get_domain(&self) -> Option<&String> {
self.domain.as_ref()
}
pub fn get_search(&self) -> Option<&Vec<String>> {
self.search.as_ref()
}
pub fn set_domain(&mut self, domain: String) {
self.domain = Some(domain);
self.last_search = LastSearch::Domain;
}
pub fn set_search(&mut self, search: Vec<String>) {
self.search = Some(search);
self.last_search = LastSearch::Search;
}
pub fn glibc_normalize(&mut self) {
self.nameservers.truncate(NAMESERVER_LIMIT);
self.search = self.search.take().map(|mut s| {
s.truncate(SEARCH_LIMIT);
s
});
}
pub fn get_nameservers_or_local(&self) -> Vec<ScopedIp> {
if self.nameservers.is_empty() {
vec![
ScopedIp::from(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
ScopedIp::from(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))),
]
} else {
self.nameservers.to_vec()
}
}
pub fn get_system_domain(&self) -> Option<String> {
if self.domain.is_some() {
return self.domain.clone();
}
let mut hostname = [0u8; 1024];
#[cfg(all(target_os = "linux", target_feature = "crt-static"))]
{
use std::{fs::File, io::Read};
let mut file = File::open("/proc/sys/kernel/hostname").ok()?;
let read_bytes = file.read(&mut hostname).ok()?;
if read_bytes == hostname.len() && hostname[read_bytes - 1] != b'\n' {
return None;
}
hostname[read_bytes - 1] = 0;
}
#[cfg(not(all(target_os = "linux", target_feature = "crt-static")))]
{
extern "C" {
fn gethostname(hostname: *mut u8, size: usize) -> i32;
}
unsafe {
if gethostname(hostname.as_mut_ptr(), hostname.len()) < 0 {
return None;
}
}
}
domain_from_host(&hostname).map(|s| s.to_owned())
}
}
impl Default for Config {
fn default() -> Self {
Self {
nameservers: Vec::new(),
domain: None,
search: None,
last_search: LastSearch::None,
sortlist: Vec::new(),
debug: false,
ndots: 1,
timeout: 5,
attempts: 2,
rotate: false,
no_check_names: false,
inet6: false,
ip6_bytestring: false,
ip6_dotint: false,
edns0: false,
single_request: false,
single_request_reopen: false,
no_tld_query: false,
use_vc: false,
no_reload: false,
trust_ad: false,
lookup: Vec::new(),
family: Vec::new(),
no_aaaa: false,
}
}
}
impl fmt::Display for Config {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
let Self {
nameservers,
last_search,
domain,
search,
sortlist,
debug,
ndots,
timeout,
attempts,
rotate,
no_check_names,
inet6,
ip6_bytestring,
ip6_dotint,
edns0,
single_request,
single_request_reopen,
no_tld_query,
use_vc,
no_reload,
trust_ad,
lookup,
family,
no_aaaa,
} = self;
for nameserver in nameservers.iter() {
writeln!(fmt, "nameserver {nameserver}")?;
}
if last_search != &LastSearch::Domain {
if let Some(domain) = domain {
writeln!(fmt, "domain {domain}")?;
}
}
if let Some(search) = search {
if !search.is_empty() {
write!(fmt, "search")?;
for suffix in search.iter() {
write!(fmt, " {suffix}")?;
}
writeln!(fmt)?;
}
}
if last_search == &LastSearch::Domain {
if let Some(domain) = &self.domain {
writeln!(fmt, "domain {domain}")?;
}
}
if !sortlist.is_empty() {
write!(fmt, "sortlist")?;
for network in sortlist.iter() {
write!(fmt, " {network}")?;
}
writeln!(fmt)?;
}
if !lookup.is_empty() {
write!(fmt, "lookup")?;
for db in lookup.iter() {
match db {
Lookup::File => write!(fmt, " file")?,
Lookup::Bind => write!(fmt, " bind")?,
Lookup::Extra(extra) => write!(fmt, " {extra}")?,
}
}
writeln!(fmt)?;
}
if !family.is_empty() {
write!(fmt, "family")?;
for fam in family.iter() {
match fam {
Family::Inet4 => write!(fmt, " inet4")?,
Family::Inet6 => write!(fmt, " inet6")?,
}
}
writeln!(fmt)?;
}
if *debug {
writeln!(fmt, "options debug")?;
}
if *ndots != 1 {
writeln!(fmt, "options ndots:{}", self.ndots)?;
}
if *timeout != 5 {
writeln!(fmt, "options timeout:{}", self.timeout)?;
}
if *attempts != 2 {
writeln!(fmt, "options attempts:{}", self.attempts)?;
}
if *rotate {
writeln!(fmt, "options rotate")?;
}
if *no_check_names {
writeln!(fmt, "options no-check-names")?;
}
if *inet6 {
writeln!(fmt, "options inet6")?;
}
if *ip6_bytestring {
writeln!(fmt, "options ip6-bytestring")?;
}
if *ip6_dotint {
writeln!(fmt, "options ip6-dotint")?;
}
if *edns0 {
writeln!(fmt, "options edns0")?;
}
if *single_request {
writeln!(fmt, "options single-request")?;
}
if *single_request_reopen {
writeln!(fmt, "options single-request-reopen")?;
}
if *no_tld_query {
writeln!(fmt, "options no-tld-query")?;
}
if *use_vc {
writeln!(fmt, "options use-vc")?;
}
if *no_reload {
writeln!(fmt, "options no-reload")?;
}
if *trust_ad {
writeln!(fmt, "options trust-ad")?;
}
if *no_aaaa {
writeln!(fmt, "options no-aaaa")?;
}
Ok(())
}
}
#[derive(Debug)]
pub enum ParseError {
InvalidUtf8(usize, Utf8Error),
InvalidValue(usize),
InvalidOptionValue(usize),
InvalidOption(usize),
InvalidDirective(usize),
InvalidIp(usize, AddrParseError),
ExtraData(usize),
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::InvalidUtf8(line, err) => write!(f, "bad unicode at line {line}: {err}"),
Self::InvalidValue(line) => write!(
f,
"directive at line {line} is improperly formatted or contains invalid value",
),
Self::InvalidOptionValue(line) => write!(
f,
"directive options at line {line} contains invalid value of some option",
),
Self::InvalidOption(line) => {
write!(f, "option at line {line} is not recognized")
}
Self::InvalidDirective(line) => {
write!(f, "directive at line {line} is not recognized")
}
Self::InvalidIp(line, err) => {
write!(f, "directive at line {line} contains invalid IP: {err}")
}
Self::ExtraData(line) => write!(f, "extra data at the end of line {line}"),
}
}
}
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidUtf8(_, err) => Some(err),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct DomainIter<'a>(DomainIterInternal<'a>);
impl<'a> Iterator for DomainIter<'a> {
type Item = &'a String;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
#[derive(Debug, Clone)]
enum DomainIterInternal<'a> {
Search(Option<Iter<'a, String>>),
Domain(Option<&'a String>),
None,
}
impl<'a> Iterator for DomainIterInternal<'a> {
type Item = &'a String;
fn next(&mut self) -> Option<Self::Item> {
match self {
DomainIterInternal::Search(Some(domains)) => domains.next(),
DomainIterInternal::Domain(domain) => domain.take(),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Lookup {
File,
Bind,
Extra(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Family {
Inet4,
Inet6,
}
fn domain_from_host(hostname: &[u8]) -> Option<&str> {
let mut start = None;
for (i, b) in hostname.iter().copied().enumerate() {
if b == b'.' && start.is_none() {
start = Some(i);
continue;
} else if b > 0 {
continue;
}
return match start? {
start if i - start < 2 => None,
start => str::from_utf8(&hostname[start + 1..i]).ok(),
};
}
None
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
enum LastSearch {
None,
Domain,
Search,
}
const NAMESERVER_LIMIT: usize = 3;
const SEARCH_LIMIT: usize = 6;
#[cfg(test)]
mod test {
use super::domain_from_host;
use super::Config;
#[test]
fn parses_domain_name() {
assert!(domain_from_host(b"regular-hostname\0").is_none());
assert_eq!(domain_from_host(b"with.domain-name\0"), Some("domain-name"));
assert_eq!(
domain_from_host(b"with.multiple.dots\0"),
Some("multiple.dots")
);
assert!(domain_from_host(b"hostname.\0").is_none());
assert_eq!(domain_from_host(b"host.a\0"), Some("a"));
assert_eq!(domain_from_host(b"host.au\0"), Some("au"));
}
#[test]
fn comma_search() {
let config = Config::parse(COMMA_SEARCH).unwrap();
assert_eq!(
config.get_search().unwrap(),
&["another.example.com".to_string(),]
);
}
const COMMA_SEARCH: &str = r#"search example.com,sub.example.com another.example.com"#;
}