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, Note, Warn, Error, }
44
45pub fn classify_expert(protocol: &str, info: &str, tcp_flags: Option<u8>) -> ExpertSeverity {
46 if let Some(flags) = tcp_flags {
48 if flags & 0x04 != 0 {
49 return ExpertSeverity::Error;
50 }
51 if flags & 0x02 != 0 && flags & 0x10 == 0 {
53 return ExpertSeverity::Chat;
54 }
55 if flags & 0x01 != 0 {
57 return ExpertSeverity::Note;
58 }
59 }
60
61 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 return ExpertSeverity::Chat;
74 }
75
76 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 if protocol == "ARP" {
88 return ExpertSeverity::Chat;
89 }
90
91 if info.contains("Win=0 ") || info.contains("Win=0,") {
93 return ExpertSeverity::Warn;
94 }
95
96 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 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#[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 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 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 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 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 let addr = format!("{}:0", ip);
389 let socket_addr = addr.to_socket_addrs().ok()?.next()?;
390 dns_lookup_reverse(&socket_addr.ip())
392}
393
394fn dns_lookup_reverse(ip: &std::net::IpAddr) -> Option<String> {
395 use std::process::Command;
396 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 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 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 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
563fn 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
594type 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 let payload = if data.len() > data_offset { &data[data_offset..] } else { &[] };
713
714 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 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 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 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 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 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
858fn 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 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 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; }
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 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 if handshake.len() < 39 {
1007 return None;
1008 }
1009 let mut pos = 38;
1010 if pos >= handshake.len() { return None; }
1012 let sid_len = handshake[pos] as usize;
1013 pos += 1 + sid_len;
1014 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 if pos >= handshake.len() { return None; }
1020 let cm_len = handshake[pos] as usize;
1021 pos += 1 + cm_len;
1022 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 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
1099fn 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 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 let payload_text = extract_readable_payload(payload);
1152
1153 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 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 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 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 format!("[{} bytes binary data]", payload.len())
1266 } else {
1267 String::new()
1268 }
1269}
1270
1271fn 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
1420pub 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 let global_header: [u8; 24] = [
1430 0xd4, 0xc3, 0xb2, 0xa1, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ];
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 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 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#[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 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 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 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 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 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 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 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 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
1659fn 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
1855 fn test_dns_name_simple() {
1856 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 #[test]
1878 fn test_dns_qtype_a() {
1879 let mut data = b"\x07example\x03com\x00".to_vec();
1881 data.extend_from_slice(&[0x00, 0x01]); 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]); assert_eq!(dns_query_type(&data, 0, "example.com"), "AAAA");
1889 }
1890
1891 #[test]
1893 fn test_arp_request() {
1894 let mut data = [0u8; 28];
1895 data[6] = 0; data[7] = 1; 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; 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 #[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]; assert!(parse_ntp(&data).contains("Client"));
1933 }
1934 #[test]
1935 fn test_ntp_server() {
1936 let data = [0x24]; assert!(parse_ntp(&data).contains("Server"));
1938 }
1939 #[test]
1940 fn test_ntp_empty() { assert_eq!(parse_ntp(&[]), "NTP"); }
1941
1942 #[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 #[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 #[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 #[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 #[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 #[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}