use crate::dns::cache::RdnsCache;
use crate::dns::resolver;
use std::net::IpAddr;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, thiserror::Error)]
pub enum ReverseDnsError {
#[error("DNS resolution failed: {0}")]
ResolutionError(String),
#[error("No PTR record found")]
NotFound,
}
pub(crate) async fn reverse_dns_lookup_with_cache(
ip: IpAddr,
cache: &Arc<RwLock<RdnsCache>>,
) -> Result<String, ReverseDnsError> {
{
let cache_read = cache.read().await;
if let Some(hostname) = cache_read.get(&ip) {
return Ok(hostname);
}
}
let name = resolver::resolve_ptr(ip)
.await
.map_err(|e| ReverseDnsError::ResolutionError(e.to_string()))?;
let cache_write = cache.write().await;
cache_write.insert(ip, name.clone());
Ok(name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[tokio::test]
async fn test_reverse_dns_localhost() {
let cache = Arc::new(RwLock::new(crate::dns::cache::RdnsCache::with_default_ttl()));
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let result = reverse_dns_lookup_with_cache(ip, &cache).await;
match result {
Ok(hostname) => {
assert!(!hostname.is_empty());
}
Err(_) => {
}
}
}
#[tokio::test]
async fn test_reverse_dns_private_ip() {
let cache = Arc::new(RwLock::new(crate::dns::cache::RdnsCache::with_default_ttl()));
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
let result = reverse_dns_lookup_with_cache(ip, &cache).await;
match result {
Ok(hostname) => assert!(!hostname.is_empty()),
Err(_) => {} }
}
#[tokio::test]
async fn test_reverse_dns_caching_with_known_ips() {
let result = tokio::time::timeout(std::time::Duration::from_secs(30), async {
let cache = Arc::new(RwLock::new(crate::dns::cache::RdnsCache::with_default_ttl()));
let ip = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
let hostname1 = reverse_dns_lookup_with_cache(ip, &cache)
.await
.unwrap_or_else(|e| panic!("First lookup failed for {ip}: {e}"));
assert!(
hostname1.contains("dns.google"),
"Expected dns.google, got '{hostname1}'"
);
let hostname2 = reverse_dns_lookup_with_cache(ip, &cache)
.await
.unwrap_or_else(|e| panic!("Second lookup failed for {ip}: {e}"));
assert_eq!(hostname1, hostname2, "Cache returned different value");
let cached = {
let cache_read = cache.read().await;
cache_read.get(&ip)
};
if let Some(cached_value) = cached {
assert_eq!(cached_value, hostname1, "Cached value doesn't match");
}
})
.await;
if result.is_err() {
eprintln!(
"test_reverse_dns_caching_with_known_ips timed out (expected under coverage)"
);
}
}
#[tokio::test]
async fn test_reverse_dns_ipv6() {
let cache = Arc::new(RwLock::new(crate::dns::cache::RdnsCache::with_default_ttl()));
let ip: IpAddr = "2001:4860:4860::8888".parse().expect("valid IPv6");
let result = reverse_dns_lookup_with_cache(ip, &cache).await;
match result {
Ok(hostname) => assert!(!hostname.is_empty()),
Err(_) => {} }
}
#[tokio::test]
async fn test_error_types() {
let err = ReverseDnsError::ResolutionError("test".to_string());
assert!(err.to_string().contains("test"));
let err = ReverseDnsError::NotFound;
assert!(err.to_string().contains("No PTR"));
}
#[tokio::test]
async fn test_concurrent_reverse_lookups() {
use tokio::task::JoinSet;
let ips = vec![
IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)),
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
];
let mut tasks = JoinSet::new();
for ip in ips {
let cache = Arc::new(RwLock::new(crate::dns::cache::RdnsCache::with_default_ttl()));
tasks.spawn(async move { reverse_dns_lookup_with_cache(ip, &cache).await });
}
while let Some(result) = tasks.join_next().await {
let _ = result.expect("task should not panic");
}
}
}