use std::env;
use flowscope::dns::{DnsHandler, DnsQuery, DnsRdata, DnsResponse, DnsUdpObserver};
use flowscope::extract::FiveTuple;
use flowscope::pcap::PcapFlowSource;
use flowscope::{FlowTracker, Timestamp};
struct Logger;
impl DnsHandler for Logger {
fn on_query(&self, q: &DnsQuery) {
let names: Vec<&str> = q.questions.iter().map(|q| q.name.as_str()).collect();
println!("→ Q id=0x{:04x} {}", q.transaction_id, names.join(","));
}
fn on_response(&self, r: &DnsResponse) {
let n = r.questions.first().map(|q| q.name.as_str()).unwrap_or("?");
let ms = r
.elapsed
.map(|d| format!(" rtt={:.2}ms", d.as_secs_f64() * 1000.0))
.unwrap_or_default();
let preview = r
.answers
.iter()
.take(2)
.map(|a| match &a.data {
DnsRdata::A(ip) => ip.to_string(),
DnsRdata::AAAA(ip) => ip.to_string(),
DnsRdata::CNAME(s) | DnsRdata::NS(s) | DnsRdata::PTR(s) => s.clone(),
DnsRdata::MX { exchange, .. } => exchange.clone(),
_ => "<…>".to_string(),
})
.collect::<Vec<_>>()
.join(",");
println!(
"← R id=0x{:04x} {} rcode={:?} answers={}{}{}",
r.transaction_id,
n,
r.rcode,
r.answers.len(),
if preview.is_empty() {
String::new()
} else {
format!(" [{preview}]")
},
ms
);
}
fn on_unanswered(&self, q: &DnsQuery) {
let n = q.questions.first().map(|q| q.name.as_str()).unwrap_or("?");
println!("⏱ unanswered id=0x{:04x} {n}", q.transaction_id);
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = env::args().nth(1).ok_or("usage: dns_log <trace.pcap>")?;
let observer = DnsUdpObserver::new(FiveTuple::bidirectional(), Logger);
let mut tracker: FlowTracker<_, ()> = FlowTracker::new(observer);
let mut last_sweep_sec: u32 = 0;
for view in PcapFlowSource::open(&path)?.views() {
let view = view?;
let now = view.timestamp;
for _ev in tracker.track(view.as_view()) {}
let now_sec = now.to_duration().as_secs() as u32;
if now_sec > last_sweep_sec {
tracker.extractor().sweep_unanswered(now);
last_sweep_sec = now_sec;
}
}
tracker
.extractor()
.sweep_unanswered(Timestamp::new(u32::MAX, 0));
Ok(())
}