blob: c6f4a5853ca04041d5f2ce5a366b8a60fababc57 [file] [log] [blame]
//! Hosts result from a configuration of the system hosts file
use std::collections::HashMap;
use std::io;
use std::path::Path;
#[cfg(any(unix, windows))]
use std::str::FromStr;
use std::sync::Arc;
use proto::op::Query;
use proto::rr::{Name, RecordType};
#[cfg(any(unix, windows))]
use proto::rr::{RData, Record};
use crate::dns_lru;
use crate::lookup::Lookup;
#[derive(Debug, Default)]
struct LookupType {
/// represents the A record type
a: Option<Lookup>,
/// represents the AAAA record type
aaaa: Option<Lookup>,
}
/// Configuration for the local hosts file
#[derive(Debug, Default)]
pub struct Hosts {
/// Name -> RDatas map
by_name: HashMap<Name, LookupType>,
}
impl Hosts {
/// Creates a new configuration from the system hosts file,
/// only works for Windows and Unix-like OSes,
/// will return empty configuration on others
pub fn new() -> Hosts {
read_hosts_conf(hosts_path()).unwrap_or_default()
}
/// Look up the addresses for the given host from the system hosts file.
pub fn lookup_static_host(&self, query: &Query) -> Option<Lookup> {
if !self.by_name.is_empty() {
if let Some(val) = self.by_name.get(query.name()) {
let result = match query.query_type() {
RecordType::A => val.a.clone(),
RecordType::AAAA => val.aaaa.clone(),
_ => None,
};
return result;
}
}
None
}
/// Insert a new Lookup for the associated `Name` and `RecordType`
pub fn insert(&mut self, name: Name, record_type: RecordType, lookup: Lookup) {
assert!(record_type == RecordType::A || record_type == RecordType::AAAA);
let lookup_type = self
.by_name
.entry(name.clone())
.or_insert_with(LookupType::default);
let new_lookup = {
let old_lookup = match record_type {
RecordType::A => lookup_type.a.get_or_insert_with(|| {
let query = Query::query(name.clone(), record_type);
Lookup::new_with_max_ttl(query, Arc::new(vec![]))
}),
RecordType::AAAA => lookup_type.aaaa.get_or_insert_with(|| {
let query = Query::query(name.clone(), record_type);
Lookup::new_with_max_ttl(query, Arc::new(vec![]))
}),
_ => {
warn!("unsupported IP type from Hosts file: {:#?}", record_type);
return;
}
};
old_lookup.append(lookup)
};
// replace the appended version
match record_type {
RecordType::A => lookup_type.a = Some(new_lookup),
RecordType::AAAA => lookup_type.aaaa = Some(new_lookup),
_ => warn!("unsupported IP type from Hosts file"),
}
}
}
#[cfg(unix)]
fn hosts_path() -> &'static str {
"/etc/hosts"
}
#[cfg(windows)]
fn hosts_path() -> std::path::PathBuf {
let system_root =
std::env::var_os("SystemRoot").expect("Environtment variable SystemRoot not found");
let system_root = Path::new(&system_root);
system_root.join("System32\\drivers\\etc\\hosts")
}
/// parse configuration from `path`
#[cfg(any(unix, windows))]
pub fn read_hosts_conf<P: AsRef<Path>>(path: P) -> io::Result<Hosts> {
use std::fs::File;
use std::io::{BufRead, BufReader};
use proto::rr::domain::TryParseIp;
let mut hosts = Hosts {
by_name: HashMap::new(),
};
// lines in the file should have the form `addr host1 host2 host3 ...`
// line starts with `#` will be regarded with comments and ignored,
// also empty line also will be ignored,
// if line only include `addr` without `host` will be ignored,
// file will parsed to map in the form `Name -> LookUp`.
let file = File::open(path)?;
for line in BufReader::new(file).lines() {
// Remove comments from the line
let line = line
.as_ref()
.map(|line| line.split('#').next().unwrap().trim())
.unwrap_or_default();
if line.is_empty() {
continue;
}
let fields: Vec<_> = line.split_whitespace().collect();
if fields.len() < 2 {
continue;
}
let addr = if let Some(a) = fields[0].try_parse_ip() {
a
} else {
warn!("could not parse an IP from hosts file");
continue;
};
for domain in fields.iter().skip(1).map(|domain| domain.to_lowercase()) {
if let Ok(name) = Name::from_str(&domain) {
let record = Record::from_rdata(name.clone(), dns_lru::MAX_TTL, addr.clone());
match addr {
RData::A(..) => {
let query = Query::query(name.clone(), RecordType::A);
let lookup = Lookup::new_with_max_ttl(query, Arc::new(vec![record]));
hosts.insert(name.clone(), RecordType::A, lookup);
}
RData::AAAA(..) => {
let query = Query::query(name.clone(), RecordType::AAAA);
let lookup = Lookup::new_with_max_ttl(query, Arc::new(vec![record]));
hosts.insert(name.clone(), RecordType::AAAA, lookup);
}
_ => {
warn!("unsupported IP type from Hosts file: {:#?}", addr);
continue;
}
};
// TODO: insert reverse lookup as well.
};
}
}
Ok(hosts)
}
#[cfg(not(any(unix, windows)))]
pub fn read_hosts_conf<P: AsRef<Path>>(_path: P) -> io::Result<Hosts> {
Err(io::Error::new(
io::ErrorKind::Other,
"Only Windows or Unix-like hosts file is supported".to_string(),
))
}
#[cfg(any(unix, windows))]
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::net::{Ipv4Addr, Ipv6Addr};
fn tests_dir() -> String {
let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned());
format! {"{}/crates/resolver/tests", server_path}
}
#[test]
fn test_read_hosts_conf() {
let path = format!("{}/hosts", tests_dir());
let hosts = read_hosts_conf(&path).unwrap();
let name = Name::from_str("localhost").unwrap();
let rdatas = hosts
.lookup_static_host(&Query::query(name.clone(), RecordType::A))
.unwrap()
.iter()
.map(ToOwned::to_owned)
.collect::<Vec<RData>>();
assert_eq!(rdatas, vec![RData::A(Ipv4Addr::new(127, 0, 0, 1))]);
let rdatas = hosts
.lookup_static_host(&Query::query(name, RecordType::AAAA))
.unwrap()
.iter()
.map(ToOwned::to_owned)
.collect::<Vec<RData>>();
assert_eq!(
rdatas,
vec![RData::AAAA(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))]
);
let name = Name::from_str("broadcasthost").unwrap();
let rdatas = hosts
.lookup_static_host(&Query::query(name, RecordType::A))
.unwrap()
.iter()
.map(ToOwned::to_owned)
.collect::<Vec<RData>>();
assert_eq!(rdatas, vec![RData::A(Ipv4Addr::new(255, 255, 255, 255))]);
let name = Name::from_str("example.com").unwrap();
let rdatas = hosts
.lookup_static_host(&Query::query(name, RecordType::A))
.unwrap()
.iter()
.map(ToOwned::to_owned)
.collect::<Vec<RData>>();
assert_eq!(rdatas, vec![RData::A(Ipv4Addr::new(10, 0, 1, 102))]);
let name = Name::from_str("a.example.com").unwrap();
let rdatas = hosts
.lookup_static_host(&Query::query(name, RecordType::A))
.unwrap()
.iter()
.map(ToOwned::to_owned)
.collect::<Vec<RData>>();
assert_eq!(rdatas, vec![RData::A(Ipv4Addr::new(10, 0, 1, 111))]);
let name = Name::from_str("b.example.com").unwrap();
let rdatas = hosts
.lookup_static_host(&Query::query(name, RecordType::A))
.unwrap()
.iter()
.map(ToOwned::to_owned)
.collect::<Vec<RData>>();
assert_eq!(rdatas, vec![RData::A(Ipv4Addr::new(10, 0, 1, 111))]);
}
}