use super::{DnsEntry, QueryError, QueryResult};
use crate::{Error, Instant, MessageAuthenticator, authenticator::DEFAULT_MAX_NEGATIVE_TTL};
use hickory_resolver::{
TokioResolver,
config::{CLOUDFLARE, GOOGLE, QUAD9, ResolverConfig, ResolverOpts},
net::{DnsError, NetError, runtime::TokioRuntimeProvider},
proto::{
ProtoError,
rr::{Name, RData},
},
system_conf::read_system_conf,
};
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::Arc,
};
impl MessageAuthenticator {
#[cfg(feature = "aws-lc-rs")]
pub fn new_cloudflare_tls() -> Result<Self, NetError> {
Self::new(ResolverConfig::tls(&CLOUDFLARE), ResolverOpts::default())
}
pub fn new_cloudflare() -> Result<Self, NetError> {
Self::new(
ResolverConfig::udp_and_tcp(&CLOUDFLARE),
ResolverOpts::default(),
)
}
pub fn new_google() -> Result<Self, NetError> {
Self::new(
ResolverConfig::udp_and_tcp(&GOOGLE),
ResolverOpts::default(),
)
}
pub fn new_quad9() -> Result<Self, NetError> {
Self::new(ResolverConfig::udp_and_tcp(&QUAD9), ResolverOpts::default())
}
#[cfg(feature = "aws-lc-rs")]
pub fn new_quad9_tls() -> Result<Self, NetError> {
Self::new(ResolverConfig::tls(&QUAD9), ResolverOpts::default())
}
pub fn new_system_conf() -> Result<Self, NetError> {
let (config, options) = read_system_conf()?;
Self::new(config, options)
}
pub fn new(config: ResolverConfig, options: ResolverOpts) -> Result<Self, NetError> {
Ok(MessageAuthenticator {
resolver: TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
.with_options(options)
.build()?,
max_negative_ttl: DEFAULT_MAX_NEGATIVE_TTL,
})
}
pub(crate) async fn query_txt(&self, key: &str) -> QueryResult<Vec<Vec<u8>>> {
let lookup = self
.resolver
.txt_lookup(Name::from_str_relaxed::<&str>(key)?)
.await?;
let expires = lookup.valid_until();
let mut entry: Vec<Vec<u8>> = Vec::new();
for record in lookup.answers() {
let RData::TXT(txt) = &record.data else {
continue;
};
match txt.txt_data.len() {
0 => {}
1 => entry.push(txt.txt_data[0].to_vec()),
_ => {
let mut data = Vec::with_capacity(255 * txt.txt_data.len());
for chunk in txt.txt_data.iter() {
data.extend_from_slice(chunk);
}
entry.push(data);
}
}
}
Ok(DnsEntry { entry, expires })
}
pub(crate) async fn query_mx(&self, key: &str) -> QueryResult<Vec<(u16, Box<str>)>> {
let lookup = self
.resolver
.mx_lookup(Name::from_str_relaxed::<&str>(key)?)
.await?;
let expires = lookup.valid_until();
let entry = lookup
.answers()
.iter()
.filter_map(|r| {
let RData::MX(mx) = &r.data else {
return None;
};
Some((
mx.preference,
mx.exchange.to_lowercase().to_ascii().into_boxed_str(),
))
})
.collect();
Ok(DnsEntry { entry, expires })
}
pub(crate) async fn query_ipv4(&self, key: &str) -> QueryResult<Arc<[Ipv4Addr]>> {
let lookup = self
.resolver
.ipv4_lookup(Name::from_str_relaxed::<&str>(key)?)
.await?;
let expires = lookup.valid_until();
let entry: Arc<[Ipv4Addr]> = lookup
.answers()
.iter()
.filter_map(|r| {
if let RData::A(a) = &r.data {
Some(a.0)
} else {
None
}
})
.collect::<Vec<Ipv4Addr>>()
.into();
Ok(DnsEntry { entry, expires })
}
pub(crate) async fn query_ipv6(&self, key: &str) -> QueryResult<Arc<[Ipv6Addr]>> {
let lookup = self
.resolver
.ipv6_lookup(Name::from_str_relaxed::<&str>(key)?)
.await?;
let expires = lookup.valid_until();
let entry: Arc<[Ipv6Addr]> = lookup
.answers()
.iter()
.filter_map(|r| {
if let RData::AAAA(aaaa) = &r.data {
Some(aaaa.0)
} else {
None
}
})
.collect::<Vec<Ipv6Addr>>()
.into();
Ok(DnsEntry { entry, expires })
}
pub(crate) async fn query_ptr(&self, addr: IpAddr) -> QueryResult<Arc<[Box<str>]>> {
let lookup = self.resolver.reverse_lookup(addr).await?;
let expires: Instant = lookup.valid_until();
let entry = lookup
.answers()
.iter()
.filter_map(|r| {
let RData::PTR(ptr) = &r.data else {
return None;
};
if !ptr.is_empty() {
Some(ptr.to_lowercase().to_ascii().into_boxed_str())
} else {
None
}
})
.collect::<Arc<[Box<str>]>>();
Ok(DnsEntry { entry, expires })
}
pub(crate) async fn query_exists(&self, key: &str) -> crate::Result<bool> {
match self
.resolver
.lookup_ip(Name::from_str_relaxed::<&str>(key)?)
.await
{
Ok(result) => Ok(result.as_lookup().answers().iter().any(|r| {
matches!(
&r.data.record_type(),
hickory_resolver::proto::rr::RecordType::A
| hickory_resolver::proto::rr::RecordType::AAAA
)
})),
Err(err) if err.is_no_records_found() => Ok(false),
Err(err) => Err(err.into()),
}
}
}
impl From<ProtoError> for Error {
fn from(_: ProtoError) -> Self {
Error::Parse
}
}
impl From<ProtoError> for QueryError {
fn from(_: ProtoError) -> Self {
QueryError::Other(Error::Parse)
}
}
impl From<NetError> for QueryError {
fn from(err: NetError) -> Self {
match &err {
NetError::Dns(DnsError::NoRecordsFound(no_records)) => QueryError::NotFound {
code: no_records.response_code,
negative_ttl: no_records.negative_ttl,
},
_ => QueryError::Other(Error::Dns(crate::DnsError::Resolver(err.to_string()))),
}
}
}
impl From<NetError> for Error {
fn from(err: NetError) -> Self {
QueryError::from(err).into()
}
}
#[cfg(test)]
mod test {
use super::Name;
use crate::Error;
#[test]
fn invalid_name_is_permanent_error() {
let err = Name::from_str_relaxed(format!("{}.example.org", "a".repeat(64))).unwrap_err();
assert_eq!(Error::from(err), Error::Parse);
}
}