use std::net::{IpAddr, SocketAddr};
use hickory_resolver::TokioResolver;
use hickory_resolver::config::{NameServerConfig, ResolverConfig};
use hickory_resolver::net::NetError;
use hickory_resolver::net::runtime::TokioRuntimeProvider;
use hickory_resolver::proto::rr::RData;
use crate::contact::{Contact, Scope, Source};
use crate::dns;
use crate::error::Error;
use crate::query::DomainName;
#[derive(Clone, Debug)]
pub struct Resolver {
inner: TokioResolver,
}
impl Resolver {
pub fn new() -> Result<Self, Error> {
let inner = TokioResolver::builder_tokio()
.and_then(|builder| builder.build())
.map_err(|source| dns_error("the system resolver configuration", source))?;
Ok(Self { inner })
}
pub fn with_nameservers(nameservers: &[SocketAddr]) -> Result<Self, Error> {
let servers = nameservers
.iter()
.map(|address| {
let mut server = NameServerConfig::udp_and_tcp(address.ip());
for connection in &mut server.connections {
connection.port = address.port();
}
server
})
.collect();
let config = ResolverConfig::from_parts(None, Vec::new(), servers);
let inner = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
.build()
.map_err(|source| dns_error("the nameservers given", source))?;
Ok(Self { inner })
}
pub async fn abusix(&self, ip: IpAddr) -> Result<Vec<Contact>, Error> {
let ip = crate::query::unmap(ip);
if !crate::is_public(ip) {
return Err(Error::NotPublic {
target: ip.to_string(),
});
}
let records = self.txt(&dns::abusix_name(ip)).await?;
Ok(dns::contacts_from_txt(
&records,
Scope::Network,
Source::Abusix,
))
}
pub async fn abuse_net(&self, domain: &DomainName) -> Result<Vec<Contact>, Error> {
let name = dns::abuse_net_name(domain);
if name.len() > DomainName::MAX_BYTES {
return Ok(Vec::new());
}
let records = self.txt(&name).await?;
Ok(dns::contacts_from_txt(
&records,
Scope::Domain,
Source::AbuseNet,
))
}
pub async fn rfc2142(&self, domain: &DomainName) -> Result<Option<Contact>, Error> {
let name = absolute(domain.as_str());
let exchanges: Vec<String> = match self.inner.mx_lookup(name.as_str()).await {
Ok(lookup) => lookup
.answers()
.iter()
.filter_map(|record| match &record.data {
RData::MX(mx) => Some(mx.exchange.to_string()),
_ => None,
})
.collect(),
Err(error) if error.is_nx_domain() => return Ok(None),
Err(error) if error.is_no_records_found() => Vec::new(),
Err(source) => return Err(dns_error(&name, source)),
};
let takes_mail = if exchanges.is_empty() {
self.has_address(&name).await?
} else {
dns::names_a_mail_host(exchanges.iter().map(String::as_str))
};
if !takes_mail {
return Ok(None);
}
dns::rfc2142_contact(domain).map(Some).map_err(Error::from)
}
async fn txt(&self, name: &str) -> Result<Vec<String>, Error> {
let name = absolute(name);
match self.inner.txt_lookup(name.as_str()).await {
Ok(lookup) => Ok(lookup
.answers()
.iter()
.filter_map(|record| match &record.data {
RData::TXT(txt) => txt_value(&txt.txt_data),
_ => None,
})
.collect()),
Err(error) if error.is_no_records_found() => Ok(Vec::new()),
Err(source) => Err(dns_error(&name, source)),
}
}
async fn has_address(&self, name: &str) -> Result<bool, Error> {
match self.inner.lookup_ip(name).await {
Ok(lookup) => Ok(lookup.iter().next().is_some()),
Err(error) if error.is_no_records_found() => Ok(false),
Err(source) => Err(dns_error(name, source)),
}
}
}
fn absolute(name: &str) -> String {
format!("{}.", name.trim_end_matches('.'))
}
fn txt_value(strings: &[Box<[u8]>]) -> Option<String> {
String::from_utf8(strings.concat()).ok()
}
fn dns_error(name: &str, source: NetError) -> Error {
Error::Dns {
name: name.to_owned(),
source: Box::new(source),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_name_gets_the_trailing_dot_that_keeps_it_from_a_search_domain() {
assert_eq!(
absolute("229.132.16.104.abuse-contacts.abusix.zone"),
"229.132.16.104.abuse-contacts.abusix.zone."
);
assert_eq!(absolute("example.com."), "example.com.");
}
#[test]
fn the_strings_of_a_record_join_into_its_value() {
let strings: Vec<Box<[u8]>> = vec![
b"arin-contact@google.com,".to_vec().into_boxed_slice(),
b"network-abuse@google.com".to_vec().into_boxed_slice(),
];
assert_eq!(
txt_value(&strings),
Some("arin-contact@google.com,network-abuse@google.com".to_owned())
);
}
#[test]
fn a_character_split_across_two_strings_is_kept_whole() {
let strings: Vec<Box<[u8]>> = vec![
b"abuse@ex\xc3".to_vec().into_boxed_slice(),
b"\xa6mple.com".to_vec().into_boxed_slice(),
];
assert_eq!(txt_value(&strings), Some("abuse@exæmple.com".to_owned()));
}
#[test]
fn a_record_that_is_not_utf8_has_no_value() {
let strings: Vec<Box<[u8]>> = vec![b"abuse@\xffexample.com".to_vec().into_boxed_slice()];
assert_eq!(txt_value(&strings), None);
}
}