use std::{net::IpAddr, num::NonZeroUsize, time::Duration};
use lru::LruCache;
use crate::{
Timestamp,
dns::{DnsRdata, DnsResponse},
};
pub struct DnsResolutionCache {
inner: LruCache<(IpAddr, IpAddr), Resolution>,
ttl: Duration,
}
#[derive(Debug, Clone)]
struct Resolution {
name: String,
observed: Timestamp,
}
impl DnsResolutionCache {
pub fn new(ttl: Duration) -> Self {
Self::with_capacity(ttl, 16_384)
}
pub fn with_capacity(ttl: Duration, capacity: usize) -> Self {
let capacity = NonZeroUsize::new(capacity.max(1)).unwrap();
Self {
inner: LruCache::new(capacity),
ttl,
}
}
pub fn observe_response(&mut self, client_ip: IpAddr, response: &DnsResponse, now: Timestamp) {
let name = canonical_name(response).to_ascii_lowercase();
for answer in &response.answers {
let target_ip = match answer.data {
DnsRdata::A(ip) => IpAddr::V4(ip),
DnsRdata::AAAA(ip) => IpAddr::V6(ip),
_ => continue,
};
self.inner.put(
(client_ip, target_ip),
Resolution {
name: name.clone(),
observed: now,
},
);
}
}
pub fn was_resolved(&mut self, client_ip: IpAddr, target_ip: IpAddr, now: Timestamp) -> bool {
self.lookup_name(client_ip, target_ip, now).is_some()
}
pub fn peek_resolved(&self, client_ip: IpAddr, target_ip: IpAddr, now: Timestamp) -> bool {
self.peek_name(client_ip, target_ip, now).is_some()
}
pub fn lookup_name(
&mut self,
client_ip: IpAddr,
target_ip: IpAddr,
now: Timestamp,
) -> Option<&str> {
let ttl = self.ttl;
let entry = self.inner.get(&(client_ip, target_ip))?;
if is_expired(entry, now, ttl) {
return None;
}
Some(entry.name.as_str())
}
pub fn peek_name(&self, client_ip: IpAddr, target_ip: IpAddr, now: Timestamp) -> Option<&str> {
let entry = self.inner.peek(&(client_ip, target_ip))?;
if is_expired(entry, now, self.ttl) {
return None;
}
Some(entry.name.as_str())
}
pub fn sweep(&mut self, now: Timestamp) -> usize {
let ttl = self.ttl;
let expired: Vec<(IpAddr, IpAddr)> = self
.inner
.iter()
.filter(|(_, res)| is_expired(res, now, ttl))
.map(|(k, _)| *k)
.collect();
let n = expired.len();
for key in expired {
self.inner.pop(&key);
}
n
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
fn is_expired(entry: &Resolution, now: Timestamp, ttl: Duration) -> bool {
let elapsed = now
.to_duration()
.saturating_sub(entry.observed.to_duration());
elapsed > ttl
}
fn canonical_name(response: &DnsResponse) -> &str {
response
.questions
.first()
.map(|q| q.name.as_str())
.unwrap_or("")
}