use super::types::{
AsnInfo, CnameHop, DnsProbe, DnsProbeError, MAX_CNAME_CHAIN_DEPTH, RESOLVER_TIMEOUT,
};
#[cfg(feature = "dns-cname")]
pub async fn probe_cname_chain(host: &str) -> Result<DnsProbe, DnsProbeError> {
use hickory_resolver::TokioResolver;
use hickory_resolver::config::{ResolverConfig, ResolverOpts};
use hickory_resolver::name_server::TokioConnectionProvider;
let mut opts = ResolverOpts::default();
opts.timeout = RESOLVER_TIMEOUT;
opts.attempts = 2;
let resolver = TokioResolver::builder_with_config(
ResolverConfig::cloudflare(),
TokioConnectionProvider::default(),
)
.with_options(opts)
.build();
let mut chain: Vec<CnameHop> = Vec::new();
let mut current = host.trim_end_matches('.').to_string();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for _ in 0..MAX_CNAME_CHAIN_DEPTH {
if !seen.insert(current.to_ascii_lowercase()) {
return Ok(DnsProbe {
chain,
first_a: None,
final_ptr: None,
asn: None,
});
}
let lookup_fut = resolver.lookup(¤t, hickory_resolver::proto::rr::RecordType::CNAME);
let result = match tokio::time::timeout(RESOLVER_TIMEOUT, lookup_fut).await {
Ok(Ok(r)) => r,
Ok(Err(_)) => break,
Err(_) => break, };
let mut next: Option<String> = None;
for record in result.records() {
if let hickory_resolver::proto::rr::RData::CNAME(c) = record.data() {
next = Some(c.to_string().trim_end_matches('.').to_string());
break;
}
}
match next {
Some(target) => {
chain.push(CnameHop {
query: current.clone(),
target: target.clone(),
});
current = target;
}
None => break,
}
}
if chain.len() == MAX_CNAME_CHAIN_DEPTH {
return Err(DnsProbeError::DepthExceeded);
}
let a_lookup = resolver.lookup_ip(¤t);
let first_a = match tokio::time::timeout(RESOLVER_TIMEOUT, a_lookup).await {
Ok(Ok(ips)) => {
ips.iter()
.find(|ip| ip.is_ipv4())
.or_else(|| ips.iter().next())
}
_ => None,
};
if chain.is_empty() && first_a.is_none() {
return Err(DnsProbeError::NoRecords);
}
let final_ptr = if let Some(ip) = first_a {
let ptr_fut = resolver.reverse_lookup(ip);
match tokio::time::timeout(RESOLVER_TIMEOUT, ptr_fut).await {
Ok(Ok(records)) => records
.iter()
.next()
.map(|ptr| ptr.to_string().trim_end_matches('.').to_string()),
_ => None,
}
} else {
None
};
let asn = if let Some(ip) = first_a {
lookup_asn(&resolver, ip).await
} else {
None
};
Ok(DnsProbe {
chain,
first_a,
final_ptr,
asn,
})
}
#[cfg(not(feature = "dns-cname"))]
pub async fn probe_cname_chain(_host: &str) -> Result<DnsProbe, DnsProbeError> {
Err(DnsProbeError::ResolverInitFailed)
}
#[cfg(feature = "dns-cname")]
fn first_txt_chunk(lookup: &hickory_resolver::lookup::Lookup) -> Option<&[u8]> {
lookup.records().iter().find_map(|rec| match rec.data() {
hickory_resolver::proto::rr::RData::TXT(t) => t.txt_data().first().map(|b| &**b),
_ => None,
})
}
#[cfg(feature = "dns-cname")]
async fn lookup_asn(
resolver: &hickory_resolver::TokioResolver,
ip: std::net::IpAddr,
) -> Option<AsnInfo> {
let query = match ip {
std::net::IpAddr::V4(v4) => {
let o = v4.octets();
format!("{}.{}.{}.{}.origin.asn.cymru.com", o[3], o[2], o[1], o[0])
}
std::net::IpAddr::V6(_) => {
return None;
}
};
let txt_fut = resolver.lookup(&query, hickory_resolver::proto::rr::RecordType::TXT);
let txt = match tokio::time::timeout(RESOLVER_TIMEOUT, txt_fut).await {
Ok(Ok(r)) => r,
_ => return None,
};
let first = first_txt_chunk(&txt)?;
let raw = std::str::from_utf8(first).ok()?;
let parts: Vec<&str> = raw.split('|').map(str::trim).collect();
let number: u32 = parts.first()?.parse().ok()?;
let name_query = format!("AS{number}.asn.cymru.com");
let name_fut = resolver.lookup(&name_query, hickory_resolver::proto::rr::RecordType::TXT);
let name = match tokio::time::timeout(RESOLVER_TIMEOUT, name_fut).await {
Ok(Ok(r)) => {
let bytes = first_txt_chunk(&r)?;
let raw = std::str::from_utf8(bytes).ok()?;
raw.split('|').next_back().map(|s| s.trim().to_string())?
}
_ => return None,
};
Some(AsnInfo { number, name })
}