#![cfg(feature = "dns")]
use std::net::{IpAddr, Ipv4Addr};
use std::time::Duration;
use flowscope::Timestamp;
use flowscope::dns::{
DnsFlags, DnsQuestion, DnsRcode, DnsRdata, DnsRecord, DnsResponse, NameClaim, NameMap,
NameMapConfig, Provenance,
};
fn ts(secs: u32) -> Timestamp {
Timestamp::new(secs, 0)
}
fn response(qname: &str, answers: Vec<DnsRecord>) -> DnsResponse {
DnsResponse::new(
1,
DnsFlags(0x8180),
vec![DnsQuestion::new(qname, 1, 1)],
answers,
vec![],
vec![],
DnsRcode::NoError,
ts(0),
None,
)
}
#[test]
fn full_acceptance_scenario() {
let mut m = NameMap::with_config(NameMapConfig::default());
let client = IpAddr::from(Ipv4Addr::new(10, 0, 0, 1));
let target = Ipv4Addr::new(93, 184, 216, 34);
let ip = IpAddr::V4(target);
m.observe_response(
client,
&response(
"example.com",
vec![DnsRecord::new(
"example.com",
1,
1,
300,
DnsRdata::A(target),
)],
),
ts(0),
);
m.observe_claim(
ip,
NameClaim::new("cdn.example.net", Provenance::Sni, ts(1)).with_client(client),
);
let names = m.names(ip, ts(2));
assert_eq!(names.len(), 2, "plural provenance coexists");
let drained = m.drain_new();
assert_eq!(drained.len(), 2);
assert!(m.drain_new().is_empty(), "exactly once");
let for_client: Vec<_> = m
.names_for_client(client, ip, ts(2))
.map(|c| c.name.clone())
.collect();
assert!(for_client.contains(&"example.com".to_string()));
assert_eq!(
m.names(ip, ts(200)).len(),
2,
"claims live within TTL+grace"
);
let removed = m.sweep(ts(100_000));
assert!(removed >= 1);
assert_eq!(m.names(ip, ts(100_000)).len(), 0);
}
#[test]
fn ptr_reverse_claim() {
let mut m = NameMap::new();
m.observe_response(
IpAddr::from(Ipv4Addr::new(10, 0, 0, 1)),
&response(
"4.3.2.1.in-addr.arpa",
vec![DnsRecord::new(
"4.3.2.1.in-addr.arpa",
12,
1,
300,
DnsRdata::PTR("host.example".into()),
)],
),
ts(0),
);
let ip = IpAddr::from(Ipv4Addr::new(1, 2, 3, 4));
let names = m.names(ip, ts(1));
assert_eq!(names.len(), 1);
assert_eq!(names[0].name, "host.example");
assert_eq!(names[0].provenance, Provenance::DnsPtr);
}
#[test]
fn cdn_heavy_bounded_memory() {
let mut cfg = NameMapConfig::default();
cfg.max_ips = 128;
cfg.max_claims_per_ip = 4;
cfg.max_pending = 256;
cfg.default_ttl = Duration::from_secs(60);
cfg.grace = Duration::from_secs(30);
let mut m = NameMap::with_config(cfg);
for i in 0..2000u32 {
let ip = IpAddr::from(Ipv4Addr::from(i.to_be_bytes()));
for j in 0..8 {
m.observe_claim(
ip,
NameClaim::new(format!("n{j}.cdn.example"), Provenance::Other, ts(i)),
);
}
}
assert!(m.len() <= 128, "IP cap holds under CDN churn");
}