Skip to main content

netwatch/collectors/
packets.rs

1use std::collections::HashMap;
2use std::net::ToSocketAddrs;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::mpsc as std_mpsc;
5use std::sync::{Arc, Mutex, RwLock};
6use std::thread;
7
8const MAX_PACKETS: usize = 5000;
9const DNS_CACHE_MAX: usize = 4096;
10const MAX_STREAM_SEGMENTS: usize = 10_000;
11const MAX_STREAM_BYTES: usize = 2 * 1024 * 1024;
12
13#[derive(Debug, Clone)]
14pub struct CapturedPacket {
15    pub id: u64,
16    pub timestamp: String,
17    pub src_ip: String,
18    pub dst_ip: String,
19    pub src_host: Option<String>,
20    pub dst_host: Option<String>,
21    pub protocol: String,
22    pub length: u32,
23    pub src_port: Option<u16>,
24    pub dst_port: Option<u16>,
25    pub info: String,
26    pub details: Vec<String>,
27    pub payload_text: String,
28    pub raw_hex: String,
29    pub raw_ascii: String,
30    pub raw_bytes: Vec<u8>,
31    pub stream_index: Option<u32>,
32    pub tcp_flags: Option<u8>,
33    pub expert: ExpertSeverity,
34    pub timestamp_ns: u64,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ExpertSeverity {
39    Chat,     // normal informational (SYN, DNS query)
40    Note,     // noteworthy (FIN, DNS response)
41    Warn,     // warning (zero window, ICMP unreachable)
42    Error,    // error (RST, DNS NXDOMAIN/SERVFAIL)
43}
44
45pub fn classify_expert(protocol: &str, info: &str, tcp_flags: Option<u8>) -> ExpertSeverity {
46    // TCP RST → Error
47    if let Some(flags) = tcp_flags {
48        if flags & 0x04 != 0 {
49            return ExpertSeverity::Error;
50        }
51        // SYN (without ACK) → Chat (connection initiation)
52        if flags & 0x02 != 0 && flags & 0x10 == 0 {
53            return ExpertSeverity::Chat;
54        }
55        // FIN → Note (connection teardown)
56        if flags & 0x01 != 0 {
57            return ExpertSeverity::Note;
58        }
59    }
60
61    // DNS errors
62    if protocol == "DNS" {
63        if info.contains("NXDOMAIN") || info.contains("Server Failure") || info.contains("Refused") {
64            return ExpertSeverity::Error;
65        }
66        if info.contains("Format Error") {
67            return ExpertSeverity::Warn;
68        }
69        if info.contains("Response") {
70            return ExpertSeverity::Note;
71        }
72        // DNS query
73        return ExpertSeverity::Chat;
74    }
75
76    // ICMP errors
77    if protocol == "ICMP" || protocol == "ICMPv6" {
78        if info.contains("Unreachable") || info.contains("Time Exceeded") {
79            return ExpertSeverity::Warn;
80        }
81        if info.contains("Redirect") {
82            return ExpertSeverity::Note;
83        }
84    }
85
86    // ARP
87    if protocol == "ARP" {
88        return ExpertSeverity::Chat;
89    }
90
91    // TCP zero window in info string
92    if info.contains("Win=0 ") || info.contains("Win=0,") {
93        return ExpertSeverity::Warn;
94    }
95
96    // TLS
97    if protocol == "TLS" {
98        if info.contains("Client Hello") {
99            return ExpertSeverity::Chat;
100        }
101        if info.contains("Server Hello") {
102            return ExpertSeverity::Note;
103        }
104    }
105
106    // HTTP errors  
107    if protocol == "HTTP" {
108        if info.contains("HTTP/1.1 4") || info.contains("HTTP/1.1 5") ||
109           info.contains("HTTP/1.0 4") || info.contains("HTTP/1.0 5") {
110            return ExpertSeverity::Warn;
111        }
112    }
113
114    ExpertSeverity::Chat
115}
116
117#[derive(Clone)]
118pub struct DnsCache {
119    cache: Arc<Mutex<HashMap<String, DnsEntry>>>,
120    tx: std_mpsc::Sender<String>,
121}
122
123#[derive(Clone, Debug)]
124enum DnsEntry {
125    Resolved(String),
126    Failed,
127    Pending,
128}
129
130impl DnsCache {
131    fn new() -> Self {
132        let (tx, rx) = std_mpsc::channel::<String>();
133        let cache = Arc::new(Mutex::new(HashMap::new()));
134        let resolver_cache = Arc::clone(&cache);
135        thread::spawn(move || {
136            while let Ok(ip) = rx.recv() {
137                let hostname = resolve_ip(&ip);
138                let mut c = resolver_cache.lock().unwrap();
139                match hostname {
140                    Some(name) => { c.insert(ip, DnsEntry::Resolved(name)); }
141                    None => { c.insert(ip, DnsEntry::Failed); }
142                }
143                if c.len() > DNS_CACHE_MAX {
144                    let keys: Vec<String> = c.keys().take(DNS_CACHE_MAX / 4).cloned().collect();
145                    for k in keys { c.remove(&k); }
146                }
147            }
148        });
149        Self { cache, tx }
150    }
151
152    pub fn lookup(&self, ip: &str) -> Option<String> {
153        if ip == "—" || ip.is_empty() {
154            return None;
155        }
156        let mut cache = self.cache.lock().unwrap();
157        match cache.get(ip) {
158            Some(DnsEntry::Resolved(name)) => Some(name.clone()),
159            Some(DnsEntry::Failed) | Some(DnsEntry::Pending) => None,
160            None => {
161                cache.insert(ip.to_string(), DnsEntry::Pending);
162                let _ = self.tx.send(ip.to_string());
163                None
164            }
165        }
166    }
167}
168
169// ── Stream tracking ─────────────────────────────────────────
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172pub enum StreamProtocol {
173    Tcp,
174    Udp,
175}
176
177#[derive(Debug, Clone, Hash, Eq, PartialEq)]
178pub struct StreamKey {
179    pub protocol: StreamProtocol,
180    pub addr_a: (String, u16),
181    pub addr_b: (String, u16),
182}
183
184impl StreamKey {
185    pub fn new(protocol: StreamProtocol, ip1: &str, port1: u16, ip2: &str, port2: u16) -> Self {
186        let a = (ip1.to_string(), port1);
187        let b = (ip2.to_string(), port2);
188        if a <= b {
189            Self { protocol, addr_a: a, addr_b: b }
190        } else {
191            Self { protocol, addr_a: b, addr_b: a }
192        }
193    }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum StreamDirection {
198    AtoB,
199    BtoA,
200}
201
202#[derive(Debug, Clone)]
203pub struct StreamSegment {
204    #[allow(dead_code)]
205    pub packet_id: u64,
206    #[allow(dead_code)]
207    pub timestamp: String,
208    pub direction: StreamDirection,
209    pub payload: Vec<u8>,
210}
211
212#[derive(Debug, Clone)]
213pub struct TcpHandshake {
214    pub syn_ns: u64,
215    pub syn_ack_ns: Option<u64>,
216    pub ack_ns: Option<u64>,
217}
218
219impl TcpHandshake {
220    pub fn syn_to_syn_ack_ms(&self) -> Option<f64> {
221        self.syn_ack_ns.map(|sa| (sa.saturating_sub(self.syn_ns)) as f64 / 1_000_000.0)
222    }
223
224    pub fn syn_ack_to_ack_ms(&self) -> Option<f64> {
225        match (self.syn_ack_ns, self.ack_ns) {
226            (Some(sa), Some(a)) => Some((a.saturating_sub(sa)) as f64 / 1_000_000.0),
227            _ => None,
228        }
229    }
230
231    pub fn total_ms(&self) -> Option<f64> {
232        self.ack_ns.map(|a| (a.saturating_sub(self.syn_ns)) as f64 / 1_000_000.0)
233    }
234}
235
236#[derive(Debug, Clone)]
237pub struct Stream {
238    #[allow(dead_code)]
239    pub index: u32,
240    pub key: StreamKey,
241    pub segments: Vec<StreamSegment>,
242    pub total_bytes_a_to_b: u64,
243    pub total_bytes_b_to_a: u64,
244    pub packet_count: u32,
245    pub initiator: Option<(String, u16)>,
246    total_payload_bytes: usize,
247    pub handshake: Option<TcpHandshake>,
248}
249
250pub struct StreamTracker {
251    streams: HashMap<StreamKey, u32>,
252    pub all_streams: Vec<Stream>,
253    next_index: u32,
254}
255
256impl StreamTracker {
257    pub fn new() -> Self {
258        Self {
259            streams: HashMap::new(),
260            all_streams: Vec::new(),
261            next_index: 0,
262        }
263    }
264
265    pub fn track_packet(
266        &mut self,
267        src_ip: &str,
268        src_port: u16,
269        dst_ip: &str,
270        dst_port: u16,
271        protocol: StreamProtocol,
272        payload: &[u8],
273        packet_id: u64,
274        timestamp: &str,
275        tcp_flags: Option<u8>,
276        timestamp_ns: u64,
277    ) -> u32 {
278        let key = StreamKey::new(protocol, src_ip, src_port, dst_ip, dst_port);
279
280        let stream_index = if let Some(&idx) = self.streams.get(&key) {
281            idx
282        } else {
283            let idx = self.next_index;
284            self.next_index += 1;
285            self.streams.insert(key.clone(), idx);
286            self.all_streams.push(Stream {
287                index: idx,
288                key: key.clone(),
289                segments: Vec::new(),
290                total_bytes_a_to_b: 0,
291                total_bytes_b_to_a: 0,
292                packet_count: 0,
293                initiator: None,
294                total_payload_bytes: 0,
295                handshake: None,
296            });
297            idx
298        };
299
300        let stream = &mut self.all_streams[stream_index as usize];
301        stream.packet_count += 1;
302
303        let is_a_to_b = key.addr_a == (src_ip.to_string(), src_port);
304        let direction = if is_a_to_b {
305            StreamDirection::AtoB
306        } else {
307            StreamDirection::BtoA
308        };
309
310        if stream.initiator.is_none() {
311            if let Some(flags) = tcp_flags {
312                if flags & 0x02 != 0 {
313                    stream.initiator = Some((src_ip.to_string(), src_port));
314                }
315            }
316            if stream.initiator.is_none() {
317                stream.initiator = Some((src_ip.to_string(), src_port));
318            }
319        }
320
321        // Track TCP handshake timing
322        if protocol == StreamProtocol::Tcp {
323            if let Some(flags) = tcp_flags {
324                let is_syn = flags & 0x02 != 0;
325                let is_ack = flags & 0x10 != 0;
326                if is_syn && !is_ack {
327                    // SYN — start of handshake
328                    if stream.handshake.is_none() {
329                        stream.handshake = Some(TcpHandshake {
330                            syn_ns: timestamp_ns,
331                            syn_ack_ns: None,
332                            ack_ns: None,
333                        });
334                    }
335                } else if is_syn && is_ack {
336                    // SYN-ACK
337                    if let Some(ref mut hs) = stream.handshake {
338                        if hs.syn_ack_ns.is_none() {
339                            hs.syn_ack_ns = Some(timestamp_ns);
340                        }
341                    }
342                } else if is_ack && !is_syn && stream.packet_count <= 3 {
343                    // ACK (completing handshake — only if early in connection)
344                    if let Some(ref mut hs) = stream.handshake {
345                        if hs.syn_ack_ns.is_some() && hs.ack_ns.is_none() {
346                            hs.ack_ns = Some(timestamp_ns);
347                        }
348                    }
349                }
350            }
351        }
352
353        if is_a_to_b {
354            stream.total_bytes_a_to_b += payload.len() as u64;
355        } else {
356            stream.total_bytes_b_to_a += payload.len() as u64;
357        }
358
359        if !payload.is_empty()
360            && stream.segments.len() < MAX_STREAM_SEGMENTS
361            && stream.total_payload_bytes < MAX_STREAM_BYTES
362        {
363            stream.total_payload_bytes += payload.len();
364            stream.segments.push(StreamSegment {
365                packet_id,
366                timestamp: timestamp.to_string(),
367                direction,
368                payload: payload.to_vec(),
369            });
370        }
371
372        stream_index
373    }
374
375    pub fn get_stream(&self, index: u32) -> Option<&Stream> {
376        self.all_streams.get(index as usize)
377    }
378
379    pub fn clear(&mut self) {
380        self.streams.clear();
381        self.all_streams.clear();
382        self.next_index = 0;
383    }
384}
385
386fn resolve_ip(ip: &str) -> Option<String> {
387    // Use getaddrinfo reverse lookup via the system resolver
388    let addr = format!("{}:0", ip);
389    let socket_addr = addr.to_socket_addrs().ok()?.next()?;
390    // Use DNS PTR lookup via std
391    dns_lookup_reverse(&socket_addr.ip())
392}
393
394fn dns_lookup_reverse(ip: &std::net::IpAddr) -> Option<String> {
395    use std::process::Command;
396    // Use host command for reverse DNS (available on macOS and most Linux)
397    let output = Command::new("host")
398        .arg("-W")
399        .arg("1")
400        .arg(ip.to_string())
401        .output()
402        .ok()?;
403    if !output.status.success() {
404        return None;
405    }
406    let text = String::from_utf8_lossy(&output.stdout);
407    // Parse "X.X.X.X.in-addr.arpa domain name pointer hostname."
408    let hostname = text.lines()
409        .find(|l| l.contains("domain name pointer"))?
410        .rsplit("pointer ")
411        .next()?
412        .trim_end_matches('.')
413        .to_string();
414    if hostname.is_empty() { None } else { Some(hostname) }
415}
416
417pub struct PacketCollector {
418    pub packets: Arc<RwLock<Vec<CapturedPacket>>>,
419    pub capturing: Arc<AtomicBool>,
420    pub error: Arc<Mutex<Option<String>>>,
421    pub dns_cache: DnsCache,
422    pub stream_tracker: Arc<Mutex<StreamTracker>>,
423    counter: Arc<Mutex<u64>>,
424    handle: Option<thread::JoinHandle<()>>,
425}
426
427impl PacketCollector {
428    pub fn new() -> Self {
429        Self {
430            packets: Arc::new(RwLock::new(Vec::new())),
431            capturing: Arc::new(AtomicBool::new(false)),
432            error: Arc::new(Mutex::new(None)),
433            dns_cache: DnsCache::new(),
434            stream_tracker: Arc::new(Mutex::new(StreamTracker::new())),
435            counter: Arc::new(Mutex::new(0)),
436            handle: None,
437        }
438    }
439
440    pub fn start_capture(&mut self, interface: &str, bpf_filter: Option<&str>) {
441        if self.capturing.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
442            return;
443        }
444        *self.error.lock().unwrap() = None;
445
446        let packets = Arc::clone(&self.packets);
447        let capturing = Arc::clone(&self.capturing);
448        let error = Arc::clone(&self.error);
449        let counter = Arc::clone(&self.counter);
450        let tracker = Arc::clone(&self.stream_tracker);
451        let dns = self.dns_cache.clone();
452        let iface = resolve_device_name(interface);
453        let bpf = bpf_filter.map(|s| s.to_string());
454
455        self.handle = Some(thread::spawn(move || {
456            // Try with promiscuous mode first, fall back to non-promiscuous
457            // (some interfaces like loopback don't support promisc on macOS)
458            let cap = pcap::Capture::from_device(iface.as_str())
459                .and_then(|c| c.promisc(true).snaplen(65535).timeout(100).open())
460                .or_else(|_| {
461                    pcap::Capture::from_device(iface.as_str())
462                        .and_then(|c| c.promisc(false).snaplen(65535).timeout(100).open())
463                });
464
465            let mut cap = match cap {
466                Ok(c) => c,
467                Err(e) => {
468                    let msg = if e.to_string().contains("Permission denied") {
469                        "Permission denied — run with sudo".to_string()
470                    } else {
471                        format!("Capture failed: {e}")
472                    };
473                    *error.lock().unwrap() = Some(msg);
474                    capturing.store(false, Ordering::SeqCst);
475                    return;
476                }
477            };
478
479            // Apply BPF capture filter if specified
480            if let Some(filter) = bpf.as_deref() {
481                if let Err(e) = cap.filter(filter, true) {
482                    *error.lock().unwrap() = Some(format!("BPF filter error: {e}"));
483                    capturing.store(false, Ordering::SeqCst);
484                    return;
485                }
486            }
487
488            while capturing.load(Ordering::Relaxed) {
489                match cap.next_packet() {
490                    Ok(packet) => {
491                        if let Some(mut parsed) = parse_packet(packet.data, &counter, &dns) {
492                            if let (Some(sp), Some(dp)) = (parsed.src_port, parsed.dst_port) {
493                                let proto = if parsed.tcp_flags.is_some() {
494                                    StreamProtocol::Tcp
495                                } else {
496                                    StreamProtocol::Udp
497                                };
498                                let payload = extract_app_payload(packet.data, proto);
499                                let idx = tracker.lock().unwrap().track_packet(
500                                    &parsed.src_ip, sp,
501                                    &parsed.dst_ip, dp,
502                                    proto, &payload,
503                                    parsed.id, &parsed.timestamp,
504                                    parsed.tcp_flags,
505                                    parsed.timestamp_ns,
506                                );
507                                parsed.stream_index = Some(idx);
508                            }
509                            let mut pkts = packets.write().unwrap();
510                            pkts.push(parsed);
511                            if pkts.len() > MAX_PACKETS {
512                                let excess = pkts.len() - MAX_PACKETS;
513                                pkts.drain(0..excess);
514                            }
515                        }
516                    }
517                    Err(pcap::Error::TimeoutExpired) => continue,
518                    Err(_) => break,
519                }
520            }
521        }));
522    }
523
524    pub fn stop_capture(&mut self) {
525        self.capturing.store(false, Ordering::SeqCst);
526        if let Some(h) = self.handle.take() {
527            let _ = h.join();
528        }
529    }
530
531    pub fn clear(&self) {
532        self.packets.write().unwrap().clear();
533        self.stream_tracker.lock().unwrap().clear();
534    }
535
536    pub fn is_capturing(&self) -> bool {
537        self.capturing.load(Ordering::SeqCst)
538    }
539
540    pub fn get_error(&self) -> Option<String> {
541        self.error.lock().unwrap().clone()
542    }
543
544    pub fn get_packets(&self) -> std::sync::RwLockReadGuard<'_, Vec<CapturedPacket>> {
545        self.packets.read().unwrap()
546    }
547
548    pub fn get_stream(&self, index: u32) -> Option<Stream> {
549        self.stream_tracker.lock().unwrap().get_stream(index).cloned()
550    }
551
552    pub fn get_all_streams(&self) -> Vec<Stream> {
553        self.stream_tracker.lock().unwrap().all_streams.clone()
554    }
555}
556
557impl Drop for PacketCollector {
558    fn drop(&mut self) {
559        self.stop_capture();
560    }
561}
562
563// ── Packet parsing ──────────────────────────────────────────
564
565fn parse_packet(data: &[u8], counter: &Arc<Mutex<u64>>, dns: &DnsCache) -> Option<CapturedPacket> {
566    if data.len() < 14 {
567        return None;
568    }
569
570    let mut details = vec![format!("Frame: {} bytes on wire", data.len())];
571
572    let src_mac = format_mac(&data[6..12]);
573    let dst_mac = format_mac(&data[0..6]);
574    let ethertype = u16::from_be_bytes([data[12], data[13]]);
575    let ether_name = match ethertype {
576        0x0800 => "IPv4",
577        0x0806 => "ARP",
578        0x86DD => "IPv6",
579        _ => "Unknown",
580    };
581    details.push(format!("Ethernet: {} → {}, Type: {} (0x{:04x})", src_mac, dst_mac, ether_name, ethertype));
582
583    match ethertype {
584        0x0800 => parse_ipv4_packet(data, &data[14..], &mut details, counter, dns),
585        0x0806 => {
586            let info = parse_arp(&data[14..], &mut details);
587            Some(build_packet(counter, "ARP", data.len() as u32, "—", "—", None, None, &info, details, &[], data, dns, None))
588        }
589        0x86DD => parse_ipv6_packet(data, &data[14..], &mut details, counter, dns),
590        _ => None,
591    }
592}
593
594// Transport parse result: (protocol, src_port, dst_port, info, app_payload_offset, tcp_flags)
595// app_payload_offset is relative to the transport data start
596type TransportResult = (String, Option<u16>, Option<u16>, String, usize, Option<u8>);
597
598fn parse_ipv4_packet(
599    raw: &[u8], data: &[u8], details: &mut Vec<String>, counter: &Arc<Mutex<u64>>, dns: &DnsCache,
600) -> Option<CapturedPacket> {
601    if data.len() < 20 {
602        return None;
603    }
604    let ihl = ((data[0] & 0x0F) as usize) * 4;
605    let total_len = u16::from_be_bytes([data[2], data[3]]);
606    let ttl = data[8];
607    let protocol_num = data[9];
608    let src = format!("{}.{}.{}.{}", data[12], data[13], data[14], data[15]);
609    let dst = format!("{}.{}.{}.{}", data[16], data[17], data[18], data[19]);
610
611    details.push(format!(
612        "IPv4: {} → {}, TTL: {}, Proto: {} ({}), Len: {}",
613        src, dst, ttl, ip_protocol_name(protocol_num), protocol_num, total_len
614    ));
615
616    let transport_data = if data.len() > ihl { &data[ihl..] } else { &[] };
617    let (protocol, src_port, dst_port, info, payload_off, flags) =
618        parse_transport(protocol_num, transport_data, &src, &dst, details);
619
620    let app_payload = if transport_data.len() > payload_off {
621        &transport_data[payload_off..]
622    } else {
623        &[]
624    };
625
626    Some(build_packet(
627        counter, &protocol, raw.len() as u32,
628        &src, &dst, src_port, dst_port, &info, details.clone(), app_payload, raw, dns, flags,
629    ))
630}
631
632fn parse_ipv6_packet(
633    raw: &[u8], data: &[u8], details: &mut Vec<String>, counter: &Arc<Mutex<u64>>, dns: &DnsCache,
634) -> Option<CapturedPacket> {
635    if data.len() < 40 {
636        return None;
637    }
638    let payload_len = u16::from_be_bytes([data[4], data[5]]);
639    let next_header = data[6];
640    let hop_limit = data[7];
641    let src = format_ipv6(&data[8..24]);
642    let dst = format_ipv6(&data[24..40]);
643
644    details.push(format!(
645        "IPv6: {} → {}, Hop Limit: {}, Next: {} ({}), Payload: {}",
646        src, dst, hop_limit, ip_protocol_name(next_header), next_header, payload_len
647    ));
648
649    let transport_data = if data.len() > 40 { &data[40..] } else { &[] };
650    let (protocol, src_port, dst_port, info, payload_off, flags) =
651        parse_transport(next_header, transport_data, &src, &dst, details);
652
653    let app_payload = if transport_data.len() > payload_off {
654        &transport_data[payload_off..]
655    } else {
656        &[]
657    };
658
659    Some(build_packet(
660        counter, &protocol, raw.len() as u32,
661        &src, &dst, src_port, dst_port, &info, details.clone(), app_payload, raw, dns, flags,
662    ))
663}
664
665fn parse_transport(
666    proto: u8, data: &[u8], src_ip: &str, dst_ip: &str, details: &mut Vec<String>,
667) -> TransportResult {
668    match proto {
669        6 if data.len() >= 20 => parse_tcp(data, src_ip, dst_ip, details),
670        17 if data.len() >= 8 => parse_udp(data, src_ip, dst_ip, details),
671        1 => {
672            let r = parse_icmp(data, src_ip, dst_ip, details);
673            (r.0, r.1, r.2, r.3, data.len(), None)
674        }
675        58 => {
676            let r = parse_icmpv6(data, src_ip, dst_ip, details);
677            (r.0, r.1, r.2, r.3, data.len(), None)
678        }
679        _ => {
680            let name = ip_protocol_name(proto);
681            details.push(format!("{}: {} → {}", name, src_ip, dst_ip));
682            (name.clone(), None, None, format!("{} → {} {}", src_ip, dst_ip, name), data.len(), None)
683        }
684    }
685}
686
687fn parse_tcp(
688    data: &[u8], src_ip: &str, dst_ip: &str, details: &mut Vec<String>,
689) -> TransportResult {
690    let src_port = u16::from_be_bytes([data[0], data[1]]);
691    let dst_port = u16::from_be_bytes([data[2], data[3]]);
692    let seq = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
693    let ack = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
694    let data_offset = ((data[12] >> 4) as usize) * 4;
695    let flags = data[13];
696    let window = u16::from_be_bytes([data[14], data[15]]);
697    let flag_str = tcp_flags(flags);
698
699    let src_svc = port_label(src_port);
700    let dst_svc = port_label(dst_port);
701
702    let mut detail = format!(
703        "TCP: {} ({}) → {} ({}), Seq: {}, Flags: [{}], Win: {}",
704        src_port, src_svc, dst_port, dst_svc, seq, flag_str, window
705    );
706    if flags & 0x10 != 0 {
707        detail.push_str(&format!(", Ack: {}", ack));
708    }
709    details.push(detail);
710
711    // Check for application-layer protocols in TCP payload
712    let payload = if data.len() > data_offset { &data[data_offset..] } else { &[] };
713
714    // DNS over TCP (port 53)
715    if (src_port == 53 || dst_port == 53) && payload.len() > 2 {
716        let dns_data = &payload[2..];
717        if let Some((dns_info, dns_detail)) = parse_dns(dns_data) {
718            details.push(dns_detail);
719            let info = format!("{} → {} {}", src_ip, dst_ip, dns_info);
720            return ("DNS".into(), Some(src_port), Some(dst_port), info, data_offset, Some(flags));
721        }
722    }
723
724    // TLS
725    if !payload.is_empty() {
726        if let Some((tls_info, tls_detail)) = parse_tls(payload) {
727            details.push(tls_detail);
728            let info = format!(
729                "{}:{} → {}:{} {} [{}]",
730                src_ip, src_port, dst_ip, dst_port, tls_info, flag_str
731            );
732            return ("TLS".into(), Some(src_port), Some(dst_port), info, data_offset, Some(flags));
733        }
734    }
735
736    // HTTP
737    if !payload.is_empty() {
738        if let Some((http_info, http_detail)) = parse_http(payload) {
739            details.push(http_detail);
740            let info = format!("{}:{} → {}:{} {}", src_ip, src_port, dst_ip, dst_port, http_info);
741            return ("HTTP".into(), Some(src_port), Some(dst_port), info, data_offset, Some(flags));
742        }
743    }
744
745    let payload_len = payload.len();
746    let info = format!(
747        "{}:{} → {}:{} [{}] Seq={} Win={}{} Payload={}",
748        src_ip, src_port, dst_ip, dst_port, flag_str, seq, window,
749        if flags & 0x10 != 0 { format!(" Ack={}", ack) } else { String::new() },
750        payload_len
751    );
752
753    let proto = if dst_svc != "—" {
754        dst_svc.to_string()
755    } else if src_svc != "—" {
756        src_svc.to_string()
757    } else {
758        "TCP".into()
759    };
760
761    (proto, Some(src_port), Some(dst_port), info, data_offset, Some(flags))
762}
763
764fn parse_udp(
765    data: &[u8], src_ip: &str, dst_ip: &str, details: &mut Vec<String>,
766) -> TransportResult {
767    let src_port = u16::from_be_bytes([data[0], data[1]]);
768    let dst_port = u16::from_be_bytes([data[2], data[3]]);
769    let udp_len = u16::from_be_bytes([data[4], data[5]]);
770
771    let src_svc = port_label(src_port);
772    let dst_svc = port_label(dst_port);
773
774    details.push(format!(
775        "UDP: {} ({}) → {} ({}), Len: {}",
776        src_port, src_svc, dst_port, dst_svc, udp_len
777    ));
778
779    let payload = if data.len() > 8 { &data[8..] } else { &[] };
780
781    // DNS
782    if (src_port == 53 || dst_port == 53 || src_port == 5353 || dst_port == 5353)
783        && !payload.is_empty()
784    {
785        let proto_name = if src_port == 5353 || dst_port == 5353 { "mDNS" } else { "DNS" };
786        if let Some((dns_info, dns_detail)) = parse_dns(payload) {
787            details.push(dns_detail);
788            let info = format!("{} → {} {}", src_ip, dst_ip, dns_info);
789            return (proto_name.into(), Some(src_port), Some(dst_port), info, 8, None);
790        }
791    }
792
793    // DHCP
794    if (src_port == 67 || src_port == 68) && (dst_port == 67 || dst_port == 68) {
795        let dhcp_info = parse_dhcp(payload);
796        details.push(format!("DHCP: {}", dhcp_info));
797        let info = format!("{} → {} {}", src_ip, dst_ip, dhcp_info);
798        return ("DHCP".into(), Some(src_port), Some(dst_port), info, 8, None);
799    }
800
801    // NTP
802    if src_port == 123 || dst_port == 123 {
803        let ntp_info = parse_ntp(payload);
804        details.push(format!("NTP: {}", ntp_info));
805        let info = format!("{} → {} {}", src_ip, dst_ip, ntp_info);
806        return ("NTP".into(), Some(src_port), Some(dst_port), info, 8, None);
807    }
808
809    let svc = if dst_svc != "—" { dst_svc } else { src_svc };
810    let info = format!(
811        "{}:{} → {}:{} Len={}{}",
812        src_ip, src_port, dst_ip, dst_port, udp_len,
813        if svc != "—" { format!(" ({})", svc) } else { String::new() }
814    );
815
816    let proto = if svc != "—" { svc.to_string() } else { "UDP".into() };
817    (proto, Some(src_port), Some(dst_port), info, 8, None)
818}
819
820fn parse_icmp(
821    data: &[u8], src_ip: &str, dst_ip: &str, details: &mut Vec<String>,
822) -> (String, Option<u16>, Option<u16>, String) {
823    if data.len() < 4 {
824        details.push("ICMP: (truncated)".into());
825        return ("ICMP".into(), None, None, format!("{} → {} ICMP", src_ip, dst_ip));
826    }
827    let icmp_type = data[0];
828    let icmp_code = data[1];
829    let type_name = icmp_type_name(icmp_type, icmp_code);
830
831    let extra = if (icmp_type == 0 || icmp_type == 8) && data.len() >= 8 {
832        let id = u16::from_be_bytes([data[4], data[5]]);
833        let seq = u16::from_be_bytes([data[6], data[7]]);
834        format!(", Id={}, Seq={}", id, seq)
835    } else {
836        String::new()
837    };
838
839    details.push(format!("ICMP: {}{}", type_name, extra));
840    let info = format!("{} → {} {}{}", src_ip, dst_ip, type_name, extra);
841    ("ICMP".into(), None, None, info)
842}
843
844fn parse_icmpv6(
845    data: &[u8], src_ip: &str, dst_ip: &str, details: &mut Vec<String>,
846) -> (String, Option<u16>, Option<u16>, String) {
847    if data.len() < 4 {
848        details.push("ICMPv6: (truncated)".into());
849        return ("ICMPv6".into(), None, None, format!("{} → {} ICMPv6", src_ip, dst_ip));
850    }
851    let icmp_type = data[0];
852    let type_name = icmpv6_type_name(icmp_type);
853    details.push(format!("ICMPv6: {}", type_name));
854    let info = format!("{} → {} {}", src_ip, dst_ip, type_name);
855    ("ICMPv6".into(), None, None, info)
856}
857
858// ── Application-layer parsers ───────────────────────────────
859
860fn parse_dns(data: &[u8]) -> Option<(String, String)> {
861    if data.len() < 12 {
862        return None;
863    }
864    let flags = u16::from_be_bytes([data[2], data[3]]);
865    let is_response = flags & 0x8000 != 0;
866    let qd_count = u16::from_be_bytes([data[4], data[5]]);
867    let an_count = u16::from_be_bytes([data[6], data[7]]);
868
869    if is_response {
870        let rcode = flags & 0x000F;
871        let rcode_str = match rcode {
872            0 => "No Error",
873            1 => "Format Error",
874            2 => "Server Failure",
875            3 => "Name Error (NXDOMAIN)",
876            4 => "Not Implemented",
877            5 => "Refused",
878            _ => "Unknown",
879        };
880        let info = format!("DNS Response, {} answers, {}", an_count, rcode_str);
881        let detail = format!("DNS: Response, Answers: {}, Rcode: {}", an_count, rcode_str);
882        Some((info, detail))
883    } else {
884        // Parse query name
885        let name = parse_dns_name(data, 12).unwrap_or_else(|| "?".into());
886        let qtype = dns_query_type(data, 12, &name);
887        let info = format!("DNS Query {} {}", qtype, name);
888        let detail = format!("DNS: Query, Questions: {}, Name: {}, Type: {}", qd_count, name, qtype);
889        Some((info, detail))
890    }
891}
892
893fn parse_dns_name(data: &[u8], offset: usize) -> Option<String> {
894    let mut name = String::new();
895    let mut pos = offset;
896    let mut first = true;
897    for _ in 0..128 {
898        if pos >= data.len() {
899            return None;
900        }
901        let len = data[pos] as usize;
902        if len == 0 {
903            break;
904        }
905        if len >= 0xC0 {
906            break;
907        }
908        if !first {
909            name.push('.');
910        }
911        first = false;
912        pos += 1;
913        if pos + len > data.len() {
914            return None;
915        }
916        name.push_str(&String::from_utf8_lossy(&data[pos..pos + len]));
917        pos += len;
918    }
919    if name.is_empty() { None } else { Some(name) }
920}
921
922fn dns_query_type(data: &[u8], start: usize, _name: &str) -> &'static str {
923    // Skip past the name to find the QTYPE
924    let mut pos = start;
925    for _ in 0..128 {
926        if pos >= data.len() { return "?"; }
927        let len = data[pos] as usize;
928        if len == 0 { pos += 1; break; }
929        if len >= 0xC0 { pos += 2; break; }
930        pos += 1 + len;
931    }
932    if pos + 2 > data.len() { return "?"; }
933    let qtype = u16::from_be_bytes([data[pos], data[pos + 1]]);
934    match qtype {
935        1 => "A",
936        2 => "NS",
937        5 => "CNAME",
938        6 => "SOA",
939        12 => "PTR",
940        15 => "MX",
941        16 => "TXT",
942        28 => "AAAA",
943        33 => "SRV",
944        255 => "ANY",
945        65 => "HTTPS",
946        _ => "?",
947    }
948}
949
950fn parse_tls(data: &[u8]) -> Option<(String, String)> {
951    if data.len() < 6 {
952        return None;
953    }
954    let content_type = data[0];
955    if content_type != 0x16 {
956        return None; // Not a TLS handshake
957    }
958    let tls_major = data[1];
959    let tls_minor = data[2];
960    if tls_major < 3 {
961        return None;
962    }
963    let version = match (tls_major, tls_minor) {
964        (3, 0) => "SSL 3.0",
965        (3, 1) => "TLS 1.0",
966        (3, 2) => "TLS 1.1",
967        (3, 3) => "TLS 1.2",
968        (3, 4) => "TLS 1.3",
969        _ => "TLS",
970    };
971
972    let record_len = u16::from_be_bytes([data[3], data[4]]) as usize;
973    if data.len() < 5 + 1 || record_len < 1 {
974        return None;
975    }
976    let handshake_type = data[5];
977    match handshake_type {
978        1 => {
979            // ClientHello — try to extract SNI
980            let sni = extract_sni(&data[5..]);
981            let sni_str = sni.as_deref().unwrap_or("—");
982            let info = format!("Client Hello ({}), SNI: {}", version, sni_str);
983            let detail = format!("TLS: Client Hello, Version: {}, SNI: {}", version, sni_str);
984            Some((info, detail))
985        }
986        2 => {
987            let info = format!("Server Hello ({})", version);
988            let detail = format!("TLS: Server Hello, Version: {}", version);
989            Some((info, detail))
990        }
991        11 => Some(("Certificate".into(), format!("TLS: Certificate, Version: {}", version))),
992        14 => Some(("Server Hello Done".into(), format!("TLS: Server Hello Done"))),
993        16 => Some(("Client Key Exchange".into(), format!("TLS: Client Key Exchange"))),
994        _ => {
995            let info = format!("Handshake type {}", handshake_type);
996            let detail = format!("TLS: Handshake type {}, Version: {}", handshake_type, version);
997            Some((info, detail))
998        }
999    }
1000}
1001
1002fn extract_sni(handshake: &[u8]) -> Option<String> {
1003    // ClientHello structure:
1004    // handshake_type(1) + length(3) + client_version(2) + random(32) = 38 bytes
1005    // then session_id_length(1) + session_id(var)
1006    if handshake.len() < 39 {
1007        return None;
1008    }
1009    let mut pos = 38;
1010    // Session ID
1011    if pos >= handshake.len() { return None; }
1012    let sid_len = handshake[pos] as usize;
1013    pos += 1 + sid_len;
1014    // Cipher suites
1015    if pos + 2 > handshake.len() { return None; }
1016    let cs_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
1017    pos += 2 + cs_len;
1018    // Compression methods
1019    if pos >= handshake.len() { return None; }
1020    let cm_len = handshake[pos] as usize;
1021    pos += 1 + cm_len;
1022    // Extensions length
1023    if pos + 2 > handshake.len() { return None; }
1024    let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
1025    pos += 2;
1026    let ext_end = pos + ext_len;
1027
1028    while pos + 4 <= ext_end && pos + 4 <= handshake.len() {
1029        let ext_type = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]);
1030        let ext_data_len = u16::from_be_bytes([handshake[pos + 2], handshake[pos + 3]]) as usize;
1031        pos += 4;
1032        if ext_type == 0 {
1033            // SNI extension
1034            if pos + 5 <= handshake.len() && ext_data_len >= 5 {
1035                let name_len = u16::from_be_bytes([handshake[pos + 3], handshake[pos + 4]]) as usize;
1036                let name_start = pos + 5;
1037                if name_start + name_len <= handshake.len() {
1038                    return Some(String::from_utf8_lossy(&handshake[name_start..name_start + name_len]).to_string());
1039                }
1040            }
1041            return None;
1042        }
1043        pos += ext_data_len;
1044    }
1045    None
1046}
1047
1048fn parse_http(data: &[u8]) -> Option<(String, String)> {
1049    let methods = [
1050        "GET ", "POST ", "PUT ", "DELETE ", "HEAD ", "PATCH ", "OPTIONS ", "HTTP/",
1051    ];
1052    let start = String::from_utf8_lossy(&data[..data.len().min(256)]);
1053    for method in &methods {
1054        if start.starts_with(method) {
1055            let first_line = start.lines().next().unwrap_or(&start);
1056            let truncated = if first_line.len() > 80 {
1057                format!("{}…", &first_line[..80])
1058            } else {
1059                first_line.to_string()
1060            };
1061            let detail = format!("HTTP: {}", truncated);
1062            return Some((truncated.to_string(), detail));
1063        }
1064    }
1065    None
1066}
1067
1068fn parse_dhcp(data: &[u8]) -> String {
1069    if data.is_empty() {
1070        return "DHCP".into();
1071    }
1072    let op = data[0];
1073    match op {
1074        1 => "DHCP Discover/Request".into(),
1075        2 => "DHCP Offer/ACK".into(),
1076        _ => format!("DHCP op={}", op),
1077    }
1078}
1079
1080fn parse_ntp(data: &[u8]) -> String {
1081    if data.is_empty() {
1082        return "NTP".into();
1083    }
1084    let li_vn_mode = data[0];
1085    let mode = li_vn_mode & 0x07;
1086    let version = (li_vn_mode >> 3) & 0x07;
1087    let mode_str = match mode {
1088        1 => "Symmetric Active",
1089        2 => "Symmetric Passive",
1090        3 => "Client",
1091        4 => "Server",
1092        5 => "Broadcast",
1093        6 => "Control",
1094        _ => "Unknown",
1095    };
1096    format!("NTPv{} {}", version, mode_str)
1097}
1098
1099// ── Build packet ────────────────────────────────────────────
1100
1101fn build_packet(
1102    counter: &Arc<Mutex<u64>>,
1103    protocol: &str,
1104    length: u32,
1105    src_ip: &str,
1106    dst_ip: &str,
1107    src_port: Option<u16>,
1108    dst_port: Option<u16>,
1109    info: &str,
1110    details: Vec<String>,
1111    payload: &[u8],
1112    raw: &[u8],
1113    dns: &DnsCache,
1114    tcp_flags: Option<u8>,
1115) -> CapturedPacket {
1116    let mut cnt = counter.lock().unwrap();
1117    *cnt += 1;
1118    let id = *cnt;
1119    let now = chrono::Local::now();
1120    let timestamp = now.format("%H:%M:%S%.3f").to_string();
1121    let timestamp_ns = now.timestamp_nanos_opt().unwrap_or(0) as u64;
1122
1123    // Queue reverse DNS lookups (non-blocking — results appear on next render)
1124    let src_host = dns.lookup(src_ip);
1125    let dst_host = dns.lookup(dst_ip);
1126
1127    let hex_lines = raw
1128        .chunks(16)
1129        .enumerate()
1130        .map(|(i, chunk)| {
1131            let hex: Vec<String> = chunk.iter().map(|b| format!("{b:02x}")).collect();
1132            format!("{:04x}  {}", i * 16, hex.join(" "))
1133        })
1134        .collect::<Vec<_>>()
1135        .join("\n");
1136
1137    let ascii_lines = raw
1138        .chunks(16)
1139        .enumerate()
1140        .map(|(i, chunk)| {
1141            let ascii: String = chunk
1142                .iter()
1143                .map(|&b| if b.is_ascii_graphic() || b == b' ' { b as char } else { '.' })
1144                .collect();
1145            format!("{:04x}  {}", i * 16, ascii)
1146        })
1147        .collect::<Vec<_>>()
1148        .join("\n");
1149
1150    // Extract readable text from the application payload
1151    let payload_text = extract_readable_payload(payload);
1152
1153    // Add resolved hostnames to the details
1154    let mut details = details;
1155    if src_host.is_some() || dst_host.is_some() {
1156        let src_label = src_host.as_deref().unwrap_or(src_ip);
1157        let dst_label = dst_host.as_deref().unwrap_or(dst_ip);
1158        details.push(format!("DNS: {} → {}", src_label, dst_label));
1159    }
1160
1161    let expert = classify_expert(protocol, info, tcp_flags);
1162
1163    CapturedPacket {
1164        id,
1165        timestamp,
1166        src_ip: src_ip.to_string(),
1167        dst_ip: dst_ip.to_string(),
1168        src_host,
1169        dst_host,
1170        protocol: protocol.to_string(),
1171        length,
1172        src_port,
1173        dst_port,
1174        info: info.to_string(),
1175        details,
1176        payload_text,
1177        raw_hex: hex_lines,
1178        raw_ascii: ascii_lines,
1179        raw_bytes: raw.to_vec(),
1180        stream_index: None,
1181        tcp_flags,
1182        expert,
1183        timestamp_ns,
1184    }
1185}
1186
1187fn extract_app_payload(raw: &[u8], proto: StreamProtocol) -> Vec<u8> {
1188    if raw.len() < 14 {
1189        return Vec::new();
1190    }
1191    let ethertype = u16::from_be_bytes([raw[12], raw[13]]);
1192    let ip_start = 14;
1193    let transport_start = match ethertype {
1194        0x0800 => {
1195            if raw.len() < ip_start + 20 { return Vec::new(); }
1196            let ihl = ((raw[ip_start] & 0x0F) as usize) * 4;
1197            ip_start + ihl
1198        }
1199        0x86DD => ip_start + 40,
1200        _ => return Vec::new(),
1201    };
1202    if raw.len() <= transport_start {
1203        return Vec::new();
1204    }
1205    match proto {
1206        StreamProtocol::Tcp => {
1207            if raw.len() < transport_start + 20 { return Vec::new(); }
1208            let data_offset = ((raw[transport_start + 12] >> 4) as usize) * 4;
1209            let payload_start = transport_start + data_offset;
1210            if raw.len() > payload_start { raw[payload_start..].to_vec() } else { Vec::new() }
1211        }
1212        StreamProtocol::Udp => {
1213            let payload_start = transport_start + 8;
1214            if raw.len() > payload_start { raw[payload_start..].to_vec() } else { Vec::new() }
1215        }
1216    }
1217}
1218
1219fn extract_readable_payload(payload: &[u8]) -> String {
1220    if payload.is_empty() {
1221        return String::new();
1222    }
1223
1224    // Check how much of the payload is printable text
1225    let printable = payload.iter()
1226        .take(2048)
1227        .filter(|&&b| b.is_ascii_graphic() || b == b' ' || b == b'\n' || b == b'\r' || b == b'\t')
1228        .count();
1229
1230    let sample_len = payload.len().min(2048);
1231    let ratio = printable as f64 / sample_len as f64;
1232
1233    if ratio > 0.7 {
1234        // Mostly text — show it cleaned up
1235        let text: String = payload.iter()
1236            .take(2048)
1237            .map(|&b| {
1238                if b.is_ascii_graphic() || b == b' ' || b == b'\t' { b as char }
1239                else if b == b'\n' || b == b'\r' { '\n' }
1240                else { '·' }
1241            })
1242            .collect();
1243        // Collapse multiple newlines and trim
1244        let mut result = String::new();
1245        let mut prev_newline = false;
1246        for ch in text.chars() {
1247            if ch == '\n' {
1248                if !prev_newline {
1249                    result.push('\n');
1250                }
1251                prev_newline = true;
1252            } else {
1253                prev_newline = false;
1254                result.push(ch);
1255            }
1256        }
1257        let trimmed = result.trim().to_string();
1258        if payload.len() > 2048 {
1259            format!("{}\n… ({} bytes total)", trimmed, payload.len())
1260        } else {
1261            trimmed
1262        }
1263    } else if !payload.is_empty() {
1264        // Binary data — show a summary
1265        format!("[{} bytes binary data]", payload.len())
1266    } else {
1267        String::new()
1268    }
1269}
1270
1271// ── Helpers ─────────────────────────────────────────────────
1272
1273fn format_mac(bytes: &[u8]) -> String {
1274    bytes.iter().map(|b| format!("{b:02x}")).collect::<Vec<_>>().join(":")
1275}
1276
1277fn format_ipv6(bytes: &[u8]) -> String {
1278    bytes.chunks(2)
1279        .map(|c| format!("{:x}", u16::from_be_bytes([c[0], c[1]])))
1280        .collect::<Vec<_>>()
1281        .join(":")
1282}
1283
1284fn tcp_flags(flags: u8) -> String {
1285    let mut s = Vec::new();
1286    if flags & 0x01 != 0 { s.push("FIN"); }
1287    if flags & 0x02 != 0 { s.push("SYN"); }
1288    if flags & 0x04 != 0 { s.push("RST"); }
1289    if flags & 0x08 != 0 { s.push("PSH"); }
1290    if flags & 0x10 != 0 { s.push("ACK"); }
1291    if flags & 0x20 != 0 { s.push("URG"); }
1292    if s.is_empty() { "NONE".into() } else { s.join(",") }
1293}
1294
1295fn ip_protocol_name(proto: u8) -> String {
1296    match proto {
1297        1 => "ICMP".into(),
1298        2 => "IGMP".into(),
1299        6 => "TCP".into(),
1300        17 => "UDP".into(),
1301        41 => "IPv6-encap".into(),
1302        47 => "GRE".into(),
1303        58 => "ICMPv6".into(),
1304        89 => "OSPF".into(),
1305        132 => "SCTP".into(),
1306        _ => format!("Proto({})", proto),
1307    }
1308}
1309
1310pub fn port_label(port: u16) -> &'static str {
1311    match port {
1312        20 => "FTP-Data",
1313        21 => "FTP",
1314        22 => "SSH",
1315        25 => "SMTP",
1316        53 => "DNS",
1317        67 => "DHCP-S",
1318        68 => "DHCP-C",
1319        80 => "HTTP",
1320        110 => "POP3",
1321        123 => "NTP",
1322        143 => "IMAP",
1323        443 => "HTTPS",
1324        465 => "SMTPS",
1325        587 => "Submission",
1326        993 => "IMAPS",
1327        995 => "POP3S",
1328        1883 => "MQTT",
1329        3306 => "MySQL",
1330        3389 => "RDP",
1331        5222 => "XMPP",
1332        5353 => "mDNS",
1333        5432 => "PostgreSQL",
1334        6379 => "Redis",
1335        8080 => "HTTP-Alt",
1336        8443 => "HTTPS-Alt",
1337        27017 => "MongoDB",
1338        _ => "—",
1339    }
1340}
1341
1342fn icmp_type_name(icmp_type: u8, code: u8) -> String {
1343    match icmp_type {
1344        0 => "Echo Reply".into(),
1345        3 => {
1346            let reason = match code {
1347                0 => "Network Unreachable",
1348                1 => "Host Unreachable",
1349                2 => "Protocol Unreachable",
1350                3 => "Port Unreachable",
1351                4 => "Fragmentation Needed",
1352                13 => "Administratively Prohibited",
1353                _ => "Unreachable",
1354            };
1355            format!("Dest Unreachable: {}", reason)
1356        }
1357        4 => "Source Quench".into(),
1358        5 => {
1359            let redir = match code {
1360                0 => "for Network",
1361                1 => "for Host",
1362                _ => "",
1363            };
1364            format!("Redirect {}", redir)
1365        }
1366        8 => "Echo Request".into(),
1367        9 => "Router Advertisement".into(),
1368        10 => "Router Solicitation".into(),
1369        11 => {
1370            let reason = if code == 0 { "TTL Exceeded" } else { "Fragment Reassembly Exceeded" };
1371            format!("Time Exceeded: {}", reason)
1372        }
1373        _ => format!("Type {} Code {}", icmp_type, code),
1374    }
1375}
1376
1377fn icmpv6_type_name(icmp_type: u8) -> String {
1378    match icmp_type {
1379        1 => "Dest Unreachable".into(),
1380        2 => "Packet Too Big".into(),
1381        3 => "Time Exceeded".into(),
1382        128 => "Echo Request".into(),
1383        129 => "Echo Reply".into(),
1384        133 => "Router Solicitation".into(),
1385        134 => "Router Advertisement".into(),
1386        135 => "Neighbor Solicitation".into(),
1387        136 => "Neighbor Advertisement".into(),
1388        _ => format!("Type {}", icmp_type),
1389    }
1390}
1391
1392fn parse_arp(data: &[u8], details: &mut Vec<String>) -> String {
1393    if data.len() < 28 {
1394        details.push("ARP: (truncated)".into());
1395        return "ARP (truncated)".into();
1396    }
1397    let op = u16::from_be_bytes([data[6], data[7]]);
1398    let sender_mac = format_mac(&data[8..14]);
1399    let sender_ip = format!("{}.{}.{}.{}", data[14], data[15], data[16], data[17]);
1400    let target_mac = format_mac(&data[18..24]);
1401    let target_ip = format!("{}.{}.{}.{}", data[24], data[25], data[26], data[27]);
1402
1403    let info = match op {
1404        1 => {
1405            details.push(format!("ARP: Request — Who has {}? Tell {} ({})", target_ip, sender_ip, sender_mac));
1406            format!("Who has {}? Tell {}", target_ip, sender_ip)
1407        }
1408        2 => {
1409            details.push(format!("ARP: Reply — {} is at {}", sender_ip, sender_mac));
1410            format!("{} is at {}", sender_ip, sender_mac)
1411        }
1412        _ => {
1413            details.push(format!("ARP: op={}, {} ({}) → {} ({})", op, sender_ip, sender_mac, target_ip, target_mac));
1414            format!("ARP op={}", op)
1415        }
1416    };
1417    info
1418}
1419
1420// ── PCAP export ─────────────────────────────────────────────
1421
1422pub fn export_pcap(packets: &[CapturedPacket], path: &str) -> Result<usize, String> {
1423    use std::io::Write;
1424
1425    let mut file = std::fs::File::create(path)
1426        .map_err(|e| format!("Failed to create {path}: {e}"))?;
1427
1428    // Global header: magic, version 2.4, thiszone=0, sigfigs=0, snaplen=65535, network=1 (Ethernet)
1429    let global_header: [u8; 24] = [
1430        0xd4, 0xc3, 0xb2, 0xa1, // magic (little-endian)
1431        0x02, 0x00, 0x04, 0x00, // version 2.4
1432        0x00, 0x00, 0x00, 0x00, // thiszone
1433        0x00, 0x00, 0x00, 0x00, // sigfigs
1434        0xff, 0xff, 0x00, 0x00, // snaplen 65535
1435        0x01, 0x00, 0x00, 0x00, // network: Ethernet
1436    ];
1437    file.write_all(&global_header)
1438        .map_err(|e| format!("Write error: {e}"))?;
1439
1440    let mut count = 0;
1441    for pkt in packets {
1442        if pkt.raw_bytes.is_empty() {
1443            continue;
1444        }
1445        let len = pkt.raw_bytes.len() as u32;
1446        // Use current time as a fallback; ideally we'd store capture timestamps
1447        // Parse HH:MM:SS.mmm from pkt.timestamp
1448        let (ts_sec, ts_usec) = parse_timestamp_for_pcap(&pkt.timestamp);
1449
1450        let mut rec_header = [0u8; 16];
1451        rec_header[0..4].copy_from_slice(&ts_sec.to_le_bytes());
1452        rec_header[4..8].copy_from_slice(&ts_usec.to_le_bytes());
1453        rec_header[8..12].copy_from_slice(&len.to_le_bytes());
1454        rec_header[12..16].copy_from_slice(&len.to_le_bytes());
1455
1456        file.write_all(&rec_header)
1457            .map_err(|e| format!("Write error: {e}"))?;
1458        file.write_all(&pkt.raw_bytes)
1459            .map_err(|e| format!("Write error: {e}"))?;
1460        count += 1;
1461    }
1462
1463    file.flush().map_err(|e| format!("Flush error: {e}"))?;
1464    Ok(count)
1465}
1466
1467fn parse_timestamp_for_pcap(ts: &str) -> (u32, u32) {
1468    // Format: "HH:MM:SS.mmm" → seconds since midnight, microseconds
1469    let parts: Vec<&str> = ts.split(':').collect();
1470    if parts.len() < 3 {
1471        return (0, 0);
1472    }
1473    let hours: u32 = parts[0].parse().unwrap_or(0);
1474    let minutes: u32 = parts[1].parse().unwrap_or(0);
1475    let sec_parts: Vec<&str> = parts[2].split('.').collect();
1476    let seconds: u32 = sec_parts[0].parse().unwrap_or(0);
1477    let millis: u32 = sec_parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
1478
1479    let total_sec = hours * 3600 + minutes * 60 + seconds;
1480    let usec = millis * 1000;
1481    (total_sec, usec)
1482}
1483
1484// ── Display filters ─────────────────────────────────────────
1485
1486#[derive(Debug, Clone)]
1487pub enum FilterExpr {
1488    Protocol(String),
1489    SrcIp(String),
1490    DstIp(String),
1491    Ip(String),
1492    Port(u16),
1493    Stream(u32),
1494    Contains(String),
1495    Not(Box<FilterExpr>),
1496    And(Box<FilterExpr>, Box<FilterExpr>),
1497    Or(Box<FilterExpr>, Box<FilterExpr>),
1498}
1499
1500pub fn parse_filter(input: &str) -> Option<FilterExpr> {
1501    let input = input.trim();
1502    if input.is_empty() {
1503        return None;
1504    }
1505    let tokens = tokenize(input);
1506    if tokens.is_empty() {
1507        return None;
1508    }
1509    let (expr, rest) = parse_or(&tokens)?;
1510    if rest.is_empty() { Some(expr) } else { None }
1511}
1512
1513fn tokenize(input: &str) -> Vec<String> {
1514    let mut tokens = Vec::new();
1515    let mut chars = input.chars().peekable();
1516    while let Some(&ch) = chars.peek() {
1517        if ch.is_whitespace() {
1518            chars.next();
1519            continue;
1520        }
1521        if ch == '!' {
1522            tokens.push("!".to_string());
1523            chars.next();
1524            continue;
1525        }
1526        if ch == '=' {
1527            chars.next();
1528            if chars.peek() == Some(&'=') { chars.next(); }
1529            tokens.push("==".to_string());
1530            continue;
1531        }
1532        if ch == '"' || ch == '\'' {
1533            chars.next();
1534            let mut s = String::new();
1535            while let Some(&c) = chars.peek() {
1536                if c == ch { chars.next(); break; }
1537                s.push(c);
1538                chars.next();
1539            }
1540            tokens.push(format!("\"{s}\""));
1541            continue;
1542        }
1543        let mut word = String::new();
1544        while let Some(&c) = chars.peek() {
1545            if c.is_whitespace() || c == '=' || c == '!' { break; }
1546            word.push(c);
1547            chars.next();
1548        }
1549        tokens.push(word);
1550    }
1551    tokens
1552}
1553
1554fn parse_or<'a>(tokens: &'a [String]) -> Option<(FilterExpr, &'a [String])> {
1555    let (mut left, mut rest) = parse_and(tokens)?;
1556    while !rest.is_empty() && rest[0].eq_ignore_ascii_case("or") {
1557        let (right, r) = parse_and(&rest[1..])?;
1558        left = FilterExpr::Or(Box::new(left), Box::new(right));
1559        rest = r;
1560    }
1561    Some((left, rest))
1562}
1563
1564fn parse_and<'a>(tokens: &'a [String]) -> Option<(FilterExpr, &'a [String])> {
1565    let (mut left, mut rest) = parse_not(tokens)?;
1566    while !rest.is_empty() && rest[0].eq_ignore_ascii_case("and") {
1567        let (right, r) = parse_not(&rest[1..])?;
1568        left = FilterExpr::And(Box::new(left), Box::new(right));
1569        rest = r;
1570    }
1571    Some((left, rest))
1572}
1573
1574fn parse_not<'a>(tokens: &'a [String]) -> Option<(FilterExpr, &'a [String])> {
1575    if tokens.is_empty() { return None; }
1576    if tokens[0] == "!" || tokens[0].eq_ignore_ascii_case("not") {
1577        let (expr, rest) = parse_not(&tokens[1..])?;
1578        return Some((FilterExpr::Not(Box::new(expr)), rest));
1579    }
1580    parse_atom(tokens)
1581}
1582
1583fn parse_atom<'a>(tokens: &'a [String]) -> Option<(FilterExpr, &'a [String])> {
1584    if tokens.is_empty() { return None; }
1585
1586    // ip.src == x
1587    if tokens[0].eq_ignore_ascii_case("ip.src") && tokens.len() >= 3 && tokens[1] == "==" {
1588        return Some((FilterExpr::SrcIp(tokens[2].to_lowercase()), &tokens[3..]));
1589    }
1590    // ip.dst == x
1591    if tokens[0].eq_ignore_ascii_case("ip.dst") && tokens.len() >= 3 && tokens[1] == "==" {
1592        return Some((FilterExpr::DstIp(tokens[2].to_lowercase()), &tokens[3..]));
1593    }
1594    // port [==] N
1595    if tokens[0].eq_ignore_ascii_case("port") && tokens.len() >= 2 {
1596        if tokens[1] == "==" && tokens.len() >= 3 {
1597            if let Ok(p) = tokens[2].parse::<u16>() {
1598                return Some((FilterExpr::Port(p), &tokens[3..]));
1599            }
1600        }
1601        if let Ok(p) = tokens[1].parse::<u16>() {
1602            return Some((FilterExpr::Port(p), &tokens[2..]));
1603        }
1604    }
1605    // stream N
1606    if tokens[0].eq_ignore_ascii_case("stream") && tokens.len() >= 2 {
1607        if let Ok(n) = tokens[1].parse::<u32>() {
1608            return Some((FilterExpr::Stream(n), &tokens[2..]));
1609        }
1610    }
1611    // contains "x"
1612    if tokens[0].eq_ignore_ascii_case("contains") && tokens.len() >= 2 {
1613        let val = tokens[1].trim_matches('"').to_lowercase();
1614        return Some((FilterExpr::Contains(val), &tokens[2..]));
1615    }
1616
1617    let word = &tokens[0];
1618
1619    // Bare IP address (contains a dot and digits)
1620    if word.contains('.') && word.chars().all(|c| c.is_ascii_digit() || c == '.') {
1621        return Some((FilterExpr::Ip(word.to_string()), &tokens[1..]));
1622    }
1623
1624    // Known protocol names
1625    let protocols = ["tcp", "udp", "dns", "mdns", "tls", "http", "arp", "icmp", "icmpv6",
1626                     "dhcp", "ntp", "ssh", "https", "smtp", "ftp", "imap", "pop3"];
1627    if protocols.iter().any(|p| word.eq_ignore_ascii_case(p)) {
1628        return Some((FilterExpr::Protocol(word.to_uppercase()), &tokens[1..]));
1629    }
1630
1631    // Bare word → text search
1632    let val = word.trim_matches('"').to_lowercase();
1633    Some((FilterExpr::Contains(val), &tokens[1..]))
1634}
1635
1636pub fn matches_packet(expr: &FilterExpr, pkt: &CapturedPacket) -> bool {
1637    match expr {
1638        FilterExpr::Protocol(p) => pkt.protocol.eq_ignore_ascii_case(p),
1639        FilterExpr::SrcIp(ip) => pkt.src_ip.contains(ip.as_str()),
1640        FilterExpr::DstIp(ip) => pkt.dst_ip.contains(ip.as_str()),
1641        FilterExpr::Ip(ip) => pkt.src_ip.contains(ip.as_str()) || pkt.dst_ip.contains(ip.as_str()),
1642        FilterExpr::Port(p) => pkt.src_port == Some(*p) || pkt.dst_port == Some(*p),
1643        FilterExpr::Stream(n) => pkt.stream_index == Some(*n),
1644        FilterExpr::Contains(s) => {
1645            pkt.info.to_lowercase().contains(s)
1646                || pkt.src_ip.to_lowercase().contains(s)
1647                || pkt.dst_ip.to_lowercase().contains(s)
1648                || pkt.protocol.to_lowercase().contains(s)
1649                || pkt.payload_text.to_lowercase().contains(s)
1650                || pkt.src_host.as_ref().map_or(false, |h| h.to_lowercase().contains(s))
1651                || pkt.dst_host.as_ref().map_or(false, |h| h.to_lowercase().contains(s))
1652        }
1653        FilterExpr::Not(inner) => !matches_packet(inner, pkt),
1654        FilterExpr::And(a, b) => matches_packet(a, pkt) && matches_packet(b, pkt),
1655        FilterExpr::Or(a, b) => matches_packet(a, pkt) || matches_packet(b, pkt),
1656    }
1657}
1658
1659/// On Windows, pcap needs the `\Device\NPF_{GUID}` name rather than the
1660/// friendly name (e.g. "Ethernet" or "Wi-Fi") that ipconfig reports.
1661/// This maps friendly names to the pcap device name by matching descriptions.
1662/// On non-Windows platforms this is a no-op.
1663fn resolve_device_name(friendly: &str) -> String {
1664    #[cfg(not(target_os = "windows"))]
1665    {
1666        return friendly.to_string();
1667    }
1668
1669    #[cfg(target_os = "windows")]
1670    {
1671        // If it already looks like an NPF path, use as-is.
1672        if friendly.starts_with("\\Device\\") || friendly.starts_with("\\\\") {
1673            return friendly.to_string();
1674        }
1675
1676        let devices = match pcap::Device::list() {
1677            Ok(d) => d,
1678            Err(_) => return friendly.to_string(),
1679        };
1680
1681        let friendly_lower = friendly.to_lowercase();
1682
1683        // Match against the device description (which contains the friendly name).
1684        for dev in &devices {
1685            if let Some(ref desc) = dev.desc {
1686                if desc.to_lowercase().contains(&friendly_lower) {
1687                    return dev.name.clone();
1688                }
1689            }
1690        }
1691
1692        // Fallback: return original and let pcap produce its own error.
1693        friendly.to_string()
1694    }
1695}
1696
1697#[cfg(test)]
1698mod tests {
1699    use super::*;
1700
1701    fn make_packet(proto: &str, src: &str, dst: &str, src_port: Option<u16>, dst_port: Option<u16>, info: &str) -> CapturedPacket {
1702        CapturedPacket {
1703            id: 1, timestamp: "00:00:00.000".into(),
1704            src_ip: src.into(), dst_ip: dst.into(),
1705            src_host: None, dst_host: None,
1706            protocol: proto.into(), length: 100,
1707            src_port, dst_port,
1708            info: info.into(), details: vec![],
1709            payload_text: String::new(),
1710            raw_hex: String::new(), raw_ascii: String::new(), raw_bytes: vec![],
1711            stream_index: None, tcp_flags: None,
1712            expert: ExpertSeverity::Chat, timestamp_ns: 0,
1713        }
1714    }
1715
1716    // ── format_mac ──────────────────────────────────────────
1717    #[test]
1718    fn test_format_mac_normal() {
1719        assert_eq!(format_mac(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]), "aa:bb:cc:dd:ee:ff");
1720    }
1721    #[test]
1722    fn test_format_mac_zeros() {
1723        assert_eq!(format_mac(&[0, 0, 0, 0, 0, 0]), "00:00:00:00:00:00");
1724    }
1725    #[test]
1726    fn test_format_mac_broadcast() {
1727        assert_eq!(format_mac(&[0xff; 6]), "ff:ff:ff:ff:ff:ff");
1728    }
1729
1730    // ── format_ipv6 ─────────────────────────────────────────
1731    #[test]
1732    fn test_format_ipv6_loopback() {
1733        let mut bytes = [0u8; 16];
1734        bytes[15] = 1;
1735        assert_eq!(format_ipv6(&bytes), "0:0:0:0:0:0:0:1");
1736    }
1737    #[test]
1738    fn test_format_ipv6_all_zeros() {
1739        assert_eq!(format_ipv6(&[0u8; 16]), "0:0:0:0:0:0:0:0");
1740    }
1741
1742    // ── tcp_flags ───────────────────────────────────────────
1743    #[test]
1744    fn test_tcp_flags_none() { assert_eq!(tcp_flags(0), "NONE"); }
1745    #[test]
1746    fn test_tcp_flags_syn() { assert_eq!(tcp_flags(0x02), "SYN"); }
1747    #[test]
1748    fn test_tcp_flags_syn_ack() { assert_eq!(tcp_flags(0x12), "SYN,ACK"); }
1749    #[test]
1750    fn test_tcp_flags_fin_ack() { assert_eq!(tcp_flags(0x11), "FIN,ACK"); }
1751    #[test]
1752    fn test_tcp_flags_rst() { assert_eq!(tcp_flags(0x04), "RST"); }
1753    #[test]
1754    fn test_tcp_flags_all() { assert_eq!(tcp_flags(0x3F), "FIN,SYN,RST,PSH,ACK,URG"); }
1755
1756    // ── ip_protocol_name ────────────────────────────────────
1757    #[test]
1758    fn test_ip_proto_tcp() { assert_eq!(ip_protocol_name(6), "TCP"); }
1759    #[test]
1760    fn test_ip_proto_udp() { assert_eq!(ip_protocol_name(17), "UDP"); }
1761    #[test]
1762    fn test_ip_proto_icmp() { assert_eq!(ip_protocol_name(1), "ICMP"); }
1763    #[test]
1764    fn test_ip_proto_icmpv6() { assert_eq!(ip_protocol_name(58), "ICMPv6"); }
1765    #[test]
1766    fn test_ip_proto_unknown() { assert_eq!(ip_protocol_name(255), "Proto(255)"); }
1767
1768    // ── port_label ──────────────────────────────────────────
1769    #[test]
1770    fn test_port_label_known() {
1771        assert_eq!(port_label(22), "SSH");
1772        assert_eq!(port_label(53), "DNS");
1773        assert_eq!(port_label(80), "HTTP");
1774        assert_eq!(port_label(443), "HTTPS");
1775    }
1776    #[test]
1777    fn test_port_label_unknown() { assert_eq!(port_label(12345), "—"); }
1778
1779    // ── icmp_type_name ──────────────────────────────────────
1780    #[test]
1781    fn test_icmp_echo_request() { assert_eq!(icmp_type_name(8, 0), "Echo Request"); }
1782    #[test]
1783    fn test_icmp_echo_reply() { assert_eq!(icmp_type_name(0, 0), "Echo Reply"); }
1784    #[test]
1785    fn test_icmp_dest_unreachable_port() {
1786        assert!(icmp_type_name(3, 3).contains("Port Unreachable"));
1787    }
1788    #[test]
1789    fn test_icmp_ttl_exceeded() {
1790        assert!(icmp_type_name(11, 0).contains("TTL Exceeded"));
1791    }
1792
1793    // ── icmpv6_type_name ────────────────────────────────────
1794    #[test]
1795    fn test_icmpv6_echo() {
1796        assert_eq!(icmpv6_type_name(128), "Echo Request");
1797        assert_eq!(icmpv6_type_name(129), "Echo Reply");
1798    }
1799    #[test]
1800    fn test_icmpv6_neighbor() {
1801        assert_eq!(icmpv6_type_name(135), "Neighbor Solicitation");
1802        assert_eq!(icmpv6_type_name(136), "Neighbor Advertisement");
1803    }
1804
1805    // ── classify_expert ─────────────────────────────────────
1806    #[test]
1807    fn test_expert_rst_is_error() {
1808        assert_eq!(classify_expert("TCP", "", Some(0x04)), ExpertSeverity::Error);
1809    }
1810    #[test]
1811    fn test_expert_syn_is_chat() {
1812        assert_eq!(classify_expert("TCP", "", Some(0x02)), ExpertSeverity::Chat);
1813    }
1814    #[test]
1815    fn test_expert_fin_is_note() {
1816        assert_eq!(classify_expert("TCP", "", Some(0x01)), ExpertSeverity::Note);
1817    }
1818    #[test]
1819    fn test_expert_dns_nxdomain() {
1820        assert_eq!(classify_expert("DNS", "NXDOMAIN", None), ExpertSeverity::Error);
1821    }
1822    #[test]
1823    fn test_expert_icmp_unreachable() {
1824        assert_eq!(classify_expert("ICMP", "Dest Unreachable", None), ExpertSeverity::Warn);
1825    }
1826    #[test]
1827    fn test_expert_http_error() {
1828        assert_eq!(classify_expert("HTTP", "HTTP/1.1 404 Not Found", None), ExpertSeverity::Warn);
1829    }
1830    #[test]
1831    fn test_expert_zero_window() {
1832        assert_eq!(classify_expert("TCP", "Win=0 Len=0", Some(0x10)), ExpertSeverity::Warn);
1833    }
1834
1835    // ── parse_timestamp_for_pcap ────────────────────────────
1836    #[test]
1837    fn test_pcap_timestamp_normal() {
1838        let (sec, usec) = parse_timestamp_for_pcap("12:30:45.123");
1839        assert_eq!(sec, 12 * 3600 + 30 * 60 + 45);
1840        assert_eq!(usec, 123_000);
1841    }
1842    #[test]
1843    fn test_pcap_timestamp_midnight() {
1844        let (sec, usec) = parse_timestamp_for_pcap("00:00:00.000");
1845        assert_eq!(sec, 0);
1846        assert_eq!(usec, 0);
1847    }
1848    #[test]
1849    fn test_pcap_timestamp_invalid() {
1850        assert_eq!(parse_timestamp_for_pcap("garbage"), (0, 0));
1851    }
1852
1853    // ── parse_dns_name ──────────────────────────────────────
1854    #[test]
1855    fn test_dns_name_simple() {
1856        // "\x07example\x03com\x00"
1857        let data = b"\x07example\x03com\x00";
1858        assert_eq!(parse_dns_name(data, 0), Some("example.com".into()));
1859    }
1860    #[test]
1861    fn test_dns_name_subdomain() {
1862        let data = b"\x03www\x07example\x03com\x00";
1863        assert_eq!(parse_dns_name(data, 0), Some("www.example.com".into()));
1864    }
1865    #[test]
1866    fn test_dns_name_empty() {
1867        let data = b"\x00";
1868        assert_eq!(parse_dns_name(data, 0), None);
1869    }
1870    #[test]
1871    fn test_dns_name_truncated() {
1872        let data = b"\x07exam";
1873        assert_eq!(parse_dns_name(data, 0), None);
1874    }
1875
1876    // ── dns_query_type ──────────────────────────────────────
1877    #[test]
1878    fn test_dns_qtype_a() {
1879        // name: "\x07example\x03com\x00" + qtype 1 (A)
1880        let mut data = b"\x07example\x03com\x00".to_vec();
1881        data.extend_from_slice(&[0x00, 0x01]); // A record
1882        assert_eq!(dns_query_type(&data, 0, "example.com"), "A");
1883    }
1884    #[test]
1885    fn test_dns_qtype_aaaa() {
1886        let mut data = b"\x07example\x03com\x00".to_vec();
1887        data.extend_from_slice(&[0x00, 28]); // AAAA
1888        assert_eq!(dns_query_type(&data, 0, "example.com"), "AAAA");
1889    }
1890
1891    // ── parse_arp ───────────────────────────────────────────
1892    #[test]
1893    fn test_arp_request() {
1894        let mut data = [0u8; 28];
1895        data[6] = 0; data[7] = 1; // op = request
1896        data[8..14].copy_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
1897        data[14..18].copy_from_slice(&[192, 168, 1, 1]);
1898        data[24..28].copy_from_slice(&[192, 168, 1, 2]);
1899        let mut details = vec![];
1900        let info = parse_arp(&data, &mut details);
1901        assert!(info.contains("Who has 192.168.1.2"));
1902        assert!(info.contains("Tell 192.168.1.1"));
1903    }
1904    #[test]
1905    fn test_arp_reply() {
1906        let mut data = [0u8; 28];
1907        data[6] = 0; data[7] = 2; // op = reply
1908        data[8..14].copy_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
1909        data[14..18].copy_from_slice(&[192, 168, 1, 1]);
1910        let mut details = vec![];
1911        let info = parse_arp(&data, &mut details);
1912        assert!(info.contains("192.168.1.1 is at"));
1913    }
1914    #[test]
1915    fn test_arp_truncated() {
1916        let mut details = vec![];
1917        let info = parse_arp(&[0; 10], &mut details);
1918        assert!(info.contains("truncated"));
1919    }
1920
1921    // ── parse_dhcp / parse_ntp ──────────────────────────────
1922    #[test]
1923    fn test_dhcp_discover() { assert!(parse_dhcp(&[1]).contains("Discover")); }
1924    #[test]
1925    fn test_dhcp_offer() { assert!(parse_dhcp(&[2]).contains("Offer")); }
1926    #[test]
1927    fn test_dhcp_empty() { assert_eq!(parse_dhcp(&[]), "DHCP"); }
1928
1929    #[test]
1930    fn test_ntp_client() {
1931        let data = [0x23]; // version 4, mode 3 (client)
1932        assert!(parse_ntp(&data).contains("Client"));
1933    }
1934    #[test]
1935    fn test_ntp_server() {
1936        let data = [0x24]; // version 4, mode 4 (server)
1937        assert!(parse_ntp(&data).contains("Server"));
1938    }
1939    #[test]
1940    fn test_ntp_empty() { assert_eq!(parse_ntp(&[]), "NTP"); }
1941
1942    // ── parse_http ──────────────────────────────────────────
1943    #[test]
1944    fn test_http_get() {
1945        let data = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n";
1946        let (info, _detail) = parse_http(data).unwrap();
1947        assert!(info.contains("GET /index.html"));
1948    }
1949    #[test]
1950    fn test_http_response() {
1951        let data = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n";
1952        let (info, _) = parse_http(data).unwrap();
1953        assert!(info.contains("200 OK"));
1954    }
1955    #[test]
1956    fn test_http_not_http() {
1957        assert!(parse_http(b"\x16\x03\x01binary stuff").is_none());
1958    }
1959
1960    // ── extract_readable_payload ────────────────────────────
1961    #[test]
1962    fn test_readable_text_payload() {
1963        let payload = b"Hello, World! This is readable text.";
1964        let result = extract_readable_payload(payload);
1965        assert!(result.contains("Hello, World!"));
1966    }
1967    #[test]
1968    fn test_readable_binary_payload() {
1969        let payload: Vec<u8> = vec![0u8; 100];
1970        let result = extract_readable_payload(&payload);
1971        assert!(result.contains("binary data"));
1972    }
1973    #[test]
1974    fn test_readable_empty_payload() {
1975        assert_eq!(extract_readable_payload(&[]), String::new());
1976    }
1977
1978    // ── parse_filter / matches_packet ───────────────────────
1979    #[test]
1980    fn test_filter_protocol() {
1981        let f = parse_filter("tcp").unwrap();
1982        assert!(matches!(f, FilterExpr::Protocol(ref p) if p == "TCP"));
1983    }
1984    #[test]
1985    fn test_filter_src_ip() {
1986        let f = parse_filter("ip.src == 1.2.3.4").unwrap();
1987        assert!(matches!(f, FilterExpr::SrcIp(ref ip) if ip == "1.2.3.4"));
1988    }
1989    #[test]
1990    fn test_filter_dst_ip() {
1991        let f = parse_filter("ip.dst == 10.0.0.1").unwrap();
1992        assert!(matches!(f, FilterExpr::DstIp(ref ip) if ip == "10.0.0.1"));
1993    }
1994    #[test]
1995    fn test_filter_port() {
1996        let f = parse_filter("port 80").unwrap();
1997        assert!(matches!(f, FilterExpr::Port(80)));
1998    }
1999    #[test]
2000    fn test_filter_port_eq() {
2001        let f = parse_filter("port == 443").unwrap();
2002        assert!(matches!(f, FilterExpr::Port(443)));
2003    }
2004    #[test]
2005    fn test_filter_stream() {
2006        let f = parse_filter("stream 5").unwrap();
2007        assert!(matches!(f, FilterExpr::Stream(5)));
2008    }
2009    #[test]
2010    fn test_filter_bare_ip() {
2011        let f = parse_filter("192.168.1.1").unwrap();
2012        assert!(matches!(f, FilterExpr::Ip(ref ip) if ip == "192.168.1.1"));
2013    }
2014    #[test]
2015    fn test_filter_and() {
2016        let f = parse_filter("tcp and port 80").unwrap();
2017        assert!(matches!(f, FilterExpr::And(_, _)));
2018    }
2019    #[test]
2020    fn test_filter_or() {
2021        let f = parse_filter("dns or http").unwrap();
2022        assert!(matches!(f, FilterExpr::Or(_, _)));
2023    }
2024    #[test]
2025    fn test_filter_not() {
2026        let f = parse_filter("! tcp").unwrap();
2027        assert!(matches!(f, FilterExpr::Not(_)));
2028    }
2029    #[test]
2030    fn test_filter_empty() { assert!(parse_filter("").is_none()); }
2031
2032    #[test]
2033    fn test_matches_protocol() {
2034        let pkt = make_packet("TCP", "1.1.1.1", "2.2.2.2", Some(1234), Some(80), "");
2035        let f = parse_filter("tcp").unwrap();
2036        assert!(matches_packet(&f, &pkt));
2037        let f2 = parse_filter("udp").unwrap();
2038        assert!(!matches_packet(&f2, &pkt));
2039    }
2040    #[test]
2041    fn test_matches_port() {
2042        let pkt = make_packet("TCP", "1.1.1.1", "2.2.2.2", Some(1234), Some(443), "");
2043        assert!(matches_packet(&parse_filter("port 443").unwrap(), &pkt));
2044        assert!(matches_packet(&parse_filter("port 1234").unwrap(), &pkt));
2045        assert!(!matches_packet(&parse_filter("port 80").unwrap(), &pkt));
2046    }
2047    #[test]
2048    fn test_matches_ip() {
2049        let pkt = make_packet("TCP", "10.0.0.1", "8.8.8.8", None, None, "");
2050        assert!(matches_packet(&parse_filter("10.0.0.1").unwrap(), &pkt));
2051        assert!(matches_packet(&parse_filter("8.8.8.8").unwrap(), &pkt));
2052        assert!(!matches_packet(&parse_filter("1.2.3.4").unwrap(), &pkt));
2053    }
2054    #[test]
2055    fn test_matches_and() {
2056        let pkt = make_packet("TCP", "1.1.1.1", "2.2.2.2", Some(1234), Some(80), "");
2057        assert!(matches_packet(&parse_filter("tcp and port 80").unwrap(), &pkt));
2058        assert!(!matches_packet(&parse_filter("udp and port 80").unwrap(), &pkt));
2059    }
2060    #[test]
2061    fn test_matches_not() {
2062        let pkt = make_packet("TCP", "1.1.1.1", "2.2.2.2", None, None, "");
2063        assert!(matches_packet(&parse_filter("! udp").unwrap(), &pkt));
2064        assert!(!matches_packet(&parse_filter("! tcp").unwrap(), &pkt));
2065    }
2066
2067    // ── StreamKey ───────────────────────────────────────────
2068    #[test]
2069    fn test_stream_key_normalization() {
2070        let k1 = StreamKey::new(StreamProtocol::Tcp, "1.1.1.1", 80, "2.2.2.2", 1234);
2071        let k2 = StreamKey::new(StreamProtocol::Tcp, "2.2.2.2", 1234, "1.1.1.1", 80);
2072        assert_eq!(k1, k2);
2073    }
2074    #[test]
2075    fn test_stream_key_different_proto() {
2076        let k1 = StreamKey::new(StreamProtocol::Tcp, "1.1.1.1", 80, "2.2.2.2", 1234);
2077        let k2 = StreamKey::new(StreamProtocol::Udp, "1.1.1.1", 80, "2.2.2.2", 1234);
2078        assert_ne!(k1, k2);
2079    }
2080
2081    // ── TcpHandshake ────────────────────────────────────────
2082    #[test]
2083    fn test_handshake_timing() {
2084        let hs = TcpHandshake { syn_ns: 1_000_000, syn_ack_ns: Some(2_000_000), ack_ns: Some(3_000_000) };
2085        assert!((hs.syn_to_syn_ack_ms().unwrap() - 1.0).abs() < 0.001);
2086        assert!((hs.syn_ack_to_ack_ms().unwrap() - 1.0).abs() < 0.001);
2087        assert!((hs.total_ms().unwrap() - 2.0).abs() < 0.001);
2088    }
2089    #[test]
2090    fn test_handshake_incomplete() {
2091        let hs = TcpHandshake { syn_ns: 1_000_000, syn_ack_ns: None, ack_ns: None };
2092        assert!(hs.syn_to_syn_ack_ms().is_none());
2093        assert!(hs.total_ms().is_none());
2094    }
2095
2096    // ── StreamTracker ───────────────────────────────────────
2097    #[test]
2098    fn test_stream_tracker_basic() {
2099        let mut tracker = StreamTracker::new();
2100        let idx = tracker.track_packet("1.1.1.1", 1234, "2.2.2.2", 80, StreamProtocol::Tcp, b"hello", 1, "00:00:00", Some(0x02), 1_000_000);
2101        assert_eq!(idx, 0);
2102        let stream = tracker.get_stream(0).unwrap();
2103        assert_eq!(stream.packet_count, 1);
2104        assert!(stream.handshake.is_some());
2105    }
2106    #[test]
2107    fn test_stream_tracker_same_stream() {
2108        let mut tracker = StreamTracker::new();
2109        let i1 = tracker.track_packet("1.1.1.1", 1234, "2.2.2.2", 80, StreamProtocol::Tcp, b"", 1, "t", None, 0);
2110        let i2 = tracker.track_packet("2.2.2.2", 80, "1.1.1.1", 1234, StreamProtocol::Tcp, b"", 2, "t", None, 0);
2111        assert_eq!(i1, i2);
2112        assert_eq!(tracker.get_stream(i1).unwrap().packet_count, 2);
2113    }
2114    #[test]
2115    fn test_stream_tracker_handshake() {
2116        let mut tracker = StreamTracker::new();
2117        tracker.track_packet("1.1.1.1", 1234, "2.2.2.2", 80, StreamProtocol::Tcp, b"", 1, "t", Some(0x02), 1_000_000);
2118        tracker.track_packet("2.2.2.2", 80, "1.1.1.1", 1234, StreamProtocol::Tcp, b"", 2, "t", Some(0x12), 2_000_000);
2119        tracker.track_packet("1.1.1.1", 1234, "2.2.2.2", 80, StreamProtocol::Tcp, b"", 3, "t", Some(0x10), 3_000_000);
2120        let hs = tracker.get_stream(0).unwrap().handshake.as_ref().unwrap();
2121        assert_eq!(hs.syn_ns, 1_000_000);
2122        assert_eq!(hs.syn_ack_ns, Some(2_000_000));
2123        assert_eq!(hs.ack_ns, Some(3_000_000));
2124    }
2125    #[test]
2126    fn test_stream_tracker_clear() {
2127        let mut tracker = StreamTracker::new();
2128        tracker.track_packet("1.1.1.1", 1234, "2.2.2.2", 80, StreamProtocol::Tcp, b"", 1, "t", None, 0);
2129        tracker.clear();
2130        assert!(tracker.all_streams.is_empty());
2131        assert!(tracker.get_stream(0).is_none());
2132    }
2133}