use crate::base::name::{self, Dname};
use smallvec::SmallVec;
use std::cmp::Ordering;
use std::default::Default;
use std::io::Read;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::Path;
use std::str::{self, FromStr, SplitWhitespace};
use std::time::Duration;
use std::vec::Vec;
use std::{convert, error, fmt, fs, io, ops};
#[derive(Clone, Debug)]
pub struct ResolvOptions {
pub search: SearchList,
pub ndots: usize,
pub timeout: Duration,
pub attempts: usize,
pub aa_only: bool,
pub use_vc: bool,
pub primary: bool,
pub ign_tc: bool,
pub recurse: bool,
pub default_names: bool,
pub stay_open: bool,
pub dn_search: bool,
pub use_inet6: bool,
pub rotate: bool,
pub no_check_name: bool,
pub keep_tsig: bool,
pub blast: bool,
pub use_bstring: bool,
pub use_ip6dotint: bool,
pub use_edns0: bool,
pub single_request: bool,
pub single_request_reopen: bool,
pub no_tld_query: bool,
}
impl Default for ResolvOptions {
fn default() -> Self {
ResolvOptions {
search: SearchList::new(),
ndots: 1,
timeout: Duration::new(5, 0),
attempts: 2,
recurse: true,
default_names: true,
dn_search: true,
aa_only: false,
use_vc: false,
primary: false,
ign_tc: false,
stay_open: false,
use_inet6: false,
rotate: false,
no_check_name: false,
keep_tsig: false,
blast: false,
use_bstring: false,
use_ip6dotint: false,
use_edns0: false,
single_request: false,
single_request_reopen: false,
no_tld_query: false,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Transport {
Udp,
Tcp,
}
impl Transport {
pub fn is_preferred(self) -> bool {
match self {
Transport::Udp => true,
Transport::Tcp => false,
}
}
pub fn is_stream(self) -> bool {
match self {
Transport::Udp => false,
Transport::Tcp => true,
}
}
}
#[derive(Clone, Debug)]
pub struct ServerConf {
pub addr: SocketAddr,
pub transport: Transport,
pub request_timeout: Duration,
pub recv_size: usize,
pub udp_payload_size: u16,
}
impl ServerConf {
pub fn new(addr: SocketAddr, transport: Transport) -> Self {
ServerConf {
addr,
transport,
request_timeout: Duration::from_secs(2),
recv_size: 1232,
udp_payload_size: 1232,
}
}
}
#[derive(Clone, Debug)]
pub struct ResolvConf {
pub servers: Vec<ServerConf>,
pub options: ResolvOptions,
}
impl ResolvConf {
pub fn new() -> Self {
ResolvConf {
servers: Vec::new(),
options: ResolvOptions::default(),
}
}
pub fn finalize(&mut self) {
if self.servers.is_empty() {
let addr =
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 53);
self.servers.push(ServerConf::new(addr, Transport::Udp));
self.servers.push(ServerConf::new(addr, Transport::Tcp));
}
if self.options.search.is_empty() {
self.options.search.push(Dname::root())
}
for server in &mut self.servers {
server.request_timeout = self.options.timeout
}
}
pub fn default() -> Self {
let mut res = ResolvConf::new();
let _ = res.parse_file("/etc/resolv.conf");
res.finalize();
res
}
}
impl ResolvConf {
pub fn parse_file<P: AsRef<Path>>(
&mut self,
path: P,
) -> Result<(), Error> {
let mut file = fs::File::open(path)?;
self.parse(&mut file)
}
pub fn parse<R: Read>(&mut self, reader: &mut R) -> Result<(), Error> {
use std::io::BufRead;
for line in io::BufReader::new(reader).lines() {
let line = line?;
let line = line.trim_end();
if line.is_empty()
|| line.starts_with(';')
|| line.starts_with('#')
{
continue;
}
let mut words = line.split_whitespace();
let keyword = words.next();
match keyword {
Some("nameserver") => self.parse_nameserver(words)?,
Some("domain") => self.parse_domain(words)?,
Some("search") => self.parse_search(words)?,
Some("sortlist") => {
}
Some("options") => self.parse_options(words)?,
_ => return Err(Error::ParseError),
}
}
Ok(())
}
fn parse_nameserver(
&mut self,
mut words: SplitWhitespace,
) -> Result<(), Error> {
use std::net::ToSocketAddrs;
for addr in (next_word(&mut words)?, 53).to_socket_addrs()? {
self.servers.push(ServerConf::new(addr, Transport::Udp));
self.servers.push(ServerConf::new(addr, Transport::Tcp));
}
no_more_words(words)
}
fn parse_domain(
&mut self,
mut words: SplitWhitespace,
) -> Result<(), Error> {
let domain = SearchSuffix::from_str(next_word(&mut words)?)?;
self.options.search = domain.into();
no_more_words(words)
}
fn parse_search(&mut self, words: SplitWhitespace) -> Result<(), Error> {
let mut search = SearchList::new();
let mut root = false;
for word in words {
let name = SearchSuffix::from_str(word)?;
if name.is_root() {
root = true
}
search.push(name);
}
if !root {
search.push(SearchSuffix::root());
}
self.options.search = search;
Ok(())
}
#[allow(clippy::match_same_arms)]
fn parse_options(&mut self, words: SplitWhitespace) -> Result<(), Error> {
for word in words {
match split_arg(word)? {
("debug", None) => {}
("ndots", Some(n)) => self.options.ndots = n,
("timeout", Some(n)) => {
self.options.timeout = Duration::new(n as u64, 0)
}
("attempts", Some(n)) => self.options.attempts = n,
("rotate", None) => self.options.rotate = true,
("no-check-names", None) => self.options.no_check_name = true,
("inet6", None) => self.options.use_inet6 = true,
("ip6-bytestring", None) => self.options.use_bstring = true,
("ip6-dotint", None) => self.options.use_ip6dotint = true,
("no-ip6-dotint", None) => self.options.use_ip6dotint = false,
("edns0", None) => self.options.use_edns0 = true,
("single-request", None) => {
self.options.single_request = true
}
("single-request-reopen", None) => {
self.options.single_request_reopen = true
}
("no-tld-query", None) => self.options.no_tld_query = true,
("use-vc", None) => self.options.use_vc = true,
_ => {}
}
}
Ok(())
}
}
impl Default for ResolvConf {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ResolvConf {
#[allow(clippy::cognitive_complexity)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for server in &self.servers {
let server = server.addr;
f.write_str("nameserver ")?;
if server.port() == 53 {
server.ip().fmt(f)?;
} else {
server.fmt(f)?;
}
"\n".fmt(f)?;
}
match self.options.search.len().cmp(&1) {
Ordering::Equal => {
writeln!(f, "domain {}", self.options.search[0])?;
}
Ordering::Greater => {
"search".fmt(f)?;
for name in self.options.search.as_slice() {
write!(f, " {}", name)?;
}
"\n".fmt(f)?;
}
Ordering::Less => {}
}
let mut options = Vec::new();
if self.options.ndots != 1 {
options.push(format!("ndots:{}", self.options.ndots));
}
if self.options.timeout != Duration::new(5, 0) {
options
.push(format!("timeout:{}", self.options.timeout.as_secs()));
}
if self.options.attempts != 2 {
options.push(format!("attempts:{}", self.options.attempts));
}
if self.options.aa_only {
options.push("aa-only".into())
}
if self.options.use_vc {
options.push("use-vc".into())
}
if self.options.primary {
options.push("primary".into())
}
if self.options.ign_tc {
options.push("ign-tc".into())
}
if !self.options.recurse {
options.push("no-recurse".into())
}
if !self.options.default_names {
options.push("no-default-names".into())
}
if self.options.stay_open {
options.push("stay-open".into())
}
if !self.options.dn_search {
options.push("no-dn-search".into())
}
if self.options.use_inet6 {
options.push("use-inet6".into())
}
if self.options.rotate {
options.push("rotate".into())
}
if self.options.no_check_name {
options.push("no-check-name".into())
}
if self.options.keep_tsig {
options.push("keep-tsig".into())
}
if self.options.blast {
options.push("blast".into())
}
if self.options.use_bstring {
options.push("use-bstring".into())
}
if self.options.use_ip6dotint {
options.push("ip6dotint".into())
}
if self.options.use_edns0 {
options.push("use-edns0".into())
}
if self.options.single_request {
options.push("single-request".into())
}
if self.options.single_request_reopen {
options.push("single-request-reopen".into())
}
if self.options.no_tld_query {
options.push("no-tld-query".into())
}
if !options.is_empty() {
"options".fmt(f)?;
for option in options {
write!(f, " {}", option)?;
}
"\n".fmt(f)?;
}
Ok(())
}
}
pub type SearchSuffix = Dname<SmallVec<[u8; 24]>>;
#[derive(Clone, Debug, Default)]
pub struct SearchList {
search: Vec<SearchSuffix>,
}
impl SearchList {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, name: SearchSuffix) {
self.search.push(name)
}
pub fn push_root(&mut self) {
self.search.push(Dname::root())
}
pub fn get(&self, pos: usize) -> Option<&SearchSuffix> {
self.search.get(pos)
}
pub fn as_slice(&self) -> &[SearchSuffix] {
self.as_ref()
}
}
impl From<SearchSuffix> for SearchList {
fn from(name: SearchSuffix) -> Self {
let mut res = Self::new();
res.push(name);
res
}
}
impl AsRef<[SearchSuffix]> for SearchList {
fn as_ref(&self) -> &[SearchSuffix] {
self.search.as_ref()
}
}
impl ops::Deref for SearchList {
type Target = [SearchSuffix];
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
fn next_word<'a>(
words: &'a mut str::SplitWhitespace,
) -> Result<&'a str, Error> {
match words.next() {
Some(word) => Ok(word),
None => Err(Error::ParseError),
}
}
fn no_more_words(mut words: str::SplitWhitespace) -> Result<(), Error> {
match words.next() {
Some(..) => Err(Error::ParseError),
None => Ok(()),
}
}
fn split_arg(s: &str) -> Result<(&str, Option<usize>), Error> {
match s.find(':') {
Some(idx) => {
let (left, right) = s.split_at(idx);
Ok((left, Some(right[1..].parse()?)))
}
None => Ok((s, None)),
}
}
#[derive(Debug)]
pub enum Error {
ParseError,
Io(io::Error),
}
impl error::Error for Error {}
impl convert::From<io::Error> for Error {
fn from(error: io::Error) -> Error {
Error::Io(error)
}
}
impl convert::From<name::FromStrError> for Error {
fn from(_: name::FromStrError) -> Error {
Error::ParseError
}
}
impl convert::From<::std::num::ParseIntError> for Error {
fn from(_: ::std::num::ParseIntError) -> Error {
Error::ParseError
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Error::ParseError => write!(f, "error parsing configuration"),
Error::Io(ref e) => e.fmt(f),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use std::io;
use std::string::ToString;
#[test]
fn parse_resolv_conf() {
let mut conf = ResolvConf::new();
let data = "nameserver 192.0.2.0\n\
nameserver 192.0.2.1\n\
options use-vc ndots:122\n"
.to_string();
assert!(conf.parse(&mut io::Cursor::new(data)).is_ok());
assert!(conf.options.use_vc);
assert_eq!(conf.options.ndots, 122);
}
}