Skip to main content

pcap_toolkit/
filter.rs

1//! Structured packet filter.
2//!
3//! Filters are evaluated per-packet. Rules within the same category are
4//! OR-ed; rules of different categories are AND-ed. An empty [`Filter`]
5//! (the default) matches every packet.
6//!
7//! # Evaluation order
8//!
9//! 1. Time range (cheapest — no parsing required)
10//! 2. Packet length
11//! 3. Protocol, IP, port, flow ID, TCP flags (requires etherparse)
12
13use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
14
15use etherparse::{NetSlice, SlicedPacket, TransportSlice};
16use thiserror::Error;
17
18use rustc_hash::FxHashSet;
19
20use crate::flow::{FlowKey, normalize_ip};
21
22// ── Error ────────────────────────────────────────────────────────────────────
23
24/// Error type for filter construction.
25#[derive(Debug, Error)]
26pub enum FilterError {
27    #[error("invalid IP address or CIDR: '{0}'")]
28    InvalidIp(String),
29
30    #[error("invalid port or range (expected e.g. '443' or '1024-65535'): '{0}'")]
31    InvalidPort(String),
32
33    #[error("invalid protocol (expected 'tcp', 'udp', 'icmp', 'icmp6', or a number 0-255): '{0}'")]
34    InvalidProto(String),
35
36    #[error("invalid flow ID (expected hex, optionally prefixed with '0x'): '{0}'")]
37    InvalidFlowId(String),
38
39    #[error("invalid datetime (expected RFC 3339 or Unix epoch integer in seconds): '{0}'")]
40    InvalidDateTime(String),
41
42    #[error(
43        "invalid TCP flags (e.g. 'SYN', 'SYN+ACK', 'FIN:exact'); \
44         known flags: FIN SYN RST PSH ACK URG ECE CWR: '{0}'"
45    )]
46    InvalidTcpFlags(String),
47
48    #[error("invalid filter operator (expected 'and', 'or', or 'not'): '{0}'")]
49    InvalidOp(String),
50}
51
52// ── IpNet ────────────────────────────────────────────────────────────────────
53
54/// An IP network in CIDR notation, or a single host address (`/32` or `/128`).
55///
56/// IPv4-mapped IPv6 addresses are normalised to IPv4 at parse time so that
57/// `::ffff:10.0.0.0/8` and `10.0.0.0/8` behave identically.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum IpNet {
60    V4 { addr: Ipv4Addr, prefix_len: u8 },
61    V6 { addr: Ipv6Addr, prefix_len: u8 },
62}
63
64impl IpNet {
65    /// Parse an address or CIDR prefix string.
66    ///
67    /// # Errors
68    /// Returns [`FilterError::InvalidIp`] if the string cannot be parsed.
69    pub fn parse(s: &str) -> Result<Self, FilterError> {
70        let err = || FilterError::InvalidIp(s.to_owned());
71
72        if let Some((ip_str, prefix_str)) = s.split_once('/') {
73            let prefix_len: u8 = prefix_str.parse().map_err(|_| err())?;
74            let ip = normalize_ip(ip_str.parse::<IpAddr>().map_err(|_| err())?);
75            match ip {
76                IpAddr::V4(v4) => {
77                    if prefix_len > 32 {
78                        return Err(err());
79                    }
80                    Ok(Self::V4 {
81                        addr: v4,
82                        prefix_len,
83                    })
84                }
85                IpAddr::V6(v6) => {
86                    if prefix_len > 128 {
87                        return Err(err());
88                    }
89                    Ok(Self::V6 {
90                        addr: v6,
91                        prefix_len,
92                    })
93                }
94            }
95        } else {
96            match normalize_ip(s.parse::<IpAddr>().map_err(|_| err())?) {
97                IpAddr::V4(v4) => Ok(Self::V4 {
98                    addr: v4,
99                    prefix_len: 32,
100                }),
101                IpAddr::V6(v6) => Ok(Self::V6 {
102                    addr: v6,
103                    prefix_len: 128,
104                }),
105            }
106        }
107    }
108
109    /// Return `true` if `ip` falls within this network.
110    ///
111    /// An IPv4 network and an IPv6 network never overlap. The `ip` argument
112    /// is normalised before comparison so that IPv4-mapped addresses match
113    /// their IPv4 equivalents.
114    pub fn contains(&self, ip: IpAddr) -> bool {
115        let ip = normalize_ip(ip);
116        match (self, ip) {
117            (Self::V4 { addr, prefix_len }, IpAddr::V4(v4)) => {
118                if *prefix_len == 0 {
119                    return true;
120                }
121                let shift = 32 - u32::from(*prefix_len);
122                (u32::from(*addr) >> shift) == (u32::from(v4) >> shift)
123            }
124            (Self::V6 { addr, prefix_len }, IpAddr::V6(v6)) => {
125                if *prefix_len == 0 {
126                    return true;
127                }
128                let shift = 128 - u128::from(*prefix_len);
129                (u128::from(*addr) >> shift) == (u128::from(v6) >> shift)
130            }
131            _ => false,
132        }
133    }
134}
135
136// ── PortRange ────────────────────────────────────────────────────────────────
137
138/// A single port or an inclusive port range.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct PortRange {
141    pub start: u16,
142    pub end: u16,
143}
144
145impl PortRange {
146    /// Parse `"443"` or `"1024-65535"`.
147    ///
148    /// # Errors
149    /// Returns [`FilterError::InvalidPort`] if the string cannot be parsed or
150    /// `start > end`.
151    pub fn parse(s: &str) -> Result<Self, FilterError> {
152        let err = || FilterError::InvalidPort(s.to_owned());
153        if let Some((lo, hi)) = s.split_once('-') {
154            let start: u16 = lo.trim().parse().map_err(|_| err())?;
155            let end: u16 = hi.trim().parse().map_err(|_| err())?;
156            if start > end {
157                return Err(err());
158            }
159            Ok(Self { start, end })
160        } else {
161            let p: u16 = s.trim().parse().map_err(|_| err())?;
162            Ok(Self { start: p, end: p })
163        }
164    }
165
166    /// Return `true` if `port` is within `[start, end]`.
167    pub fn contains(self, port: u16) -> bool {
168        port >= self.start && port <= self.end
169    }
170}
171
172// ── TcpFlagsFilter ───────────────────────────────────────────────────────────
173
174/// TCP control-flags filter.
175///
176/// The `mask` field selects which bits are tested; `value` is the expected
177/// result of `flags & mask`.
178///
179/// - **any mode** (default): at least one bit in `mask` must be set —
180///   `(flags & mask) != 0`.
181/// - **exact mode** (append `:exact` to the flag string): all bits in `mask`
182///   must equal `value` — `(flags & mask) == value`.
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct TcpFlagsFilter {
185    /// Bitmask of flags to test.
186    pub mask: u8,
187    /// Expected value under `mask` (used in exact mode).
188    pub value: u8,
189    /// When `true`, require `(flags & mask) == value`; otherwise `!= 0`.
190    pub exact: bool,
191}
192
193impl TcpFlagsFilter {
194    /// Parse a flag specification string.
195    ///
196    /// Flags are joined with `+`: `"SYN"`, `"SYN+ACK"`, `"RST+FIN"`.
197    /// Append `:exact` for exact-match mode (default: any).
198    ///
199    /// Known flag names (case-insensitive): `FIN`, `SYN`, `RST`, `PSH`,
200    /// `ACK`, `URG`, `ECE`, `CWR`.
201    ///
202    /// # Errors
203    /// Returns [`FilterError::InvalidTcpFlags`] for unknown names or an
204    /// empty specification.
205    pub fn parse(s: &str) -> Result<Self, FilterError> {
206        let err = || FilterError::InvalidTcpFlags(s.to_owned());
207
208        let (flags_part, exact) = if let Some(f) = s.strip_suffix(":exact") {
209            (f, true)
210        } else if let Some(f) = s.strip_suffix(":any") {
211            (f, false)
212        } else {
213            (s, false)
214        };
215
216        let mut mask = 0u8;
217        for token in flags_part.split('+') {
218            let bit = match token.trim().to_ascii_uppercase().as_str() {
219                "FIN" => 0x01,
220                "SYN" => 0x02,
221                "RST" => 0x04,
222                "PSH" => 0x08,
223                "ACK" => 0x10,
224                "URG" => 0x20,
225                "ECE" => 0x40,
226                "CWR" => 0x80,
227                _ => return Err(err()),
228            };
229            mask |= bit;
230        }
231        if mask == 0 {
232            return Err(err());
233        }
234        Ok(Self {
235            mask,
236            value: mask,
237            exact,
238        })
239    }
240
241    /// Return `true` if `flags` satisfies this filter.
242    pub fn matches(self, flags: u8) -> bool {
243        if self.exact {
244            // All specified flags must be set and no other flags may be set.
245            flags == self.value
246        } else {
247            (flags & self.mask) != 0
248        }
249    }
250}
251
252// ── Parse helpers ─────────────────────────────────────────────────────────────
253
254/// Parse a comma-separated protocol list into IP protocol numbers.
255///
256/// Accepts well-known names (`tcp`, `udp`, `icmp`, `icmp6`) or decimal
257/// numbers `0–255`.
258///
259/// # Errors
260/// Returns [`FilterError::InvalidProto`] for any unrecognised token.
261pub fn parse_proto_list(s: &str) -> Result<Vec<u8>, FilterError> {
262    s.split(',').map(|p| parse_proto(p.trim())).collect()
263}
264
265fn parse_proto(s: &str) -> Result<u8, FilterError> {
266    match s.to_ascii_lowercase().as_str() {
267        "tcp" => Ok(6),
268        "udp" => Ok(17),
269        "icmp" => Ok(1),
270        "icmp6" | "icmpv6" => Ok(58),
271        "sctp" => Ok(132),
272        "esp" => Ok(50),
273        "ah" => Ok(51),
274        other => other
275            .parse::<u8>()
276            .map_err(|_| FilterError::InvalidProto(s.to_owned())),
277    }
278}
279
280/// Parse a comma-separated list of hex flow IDs.
281///
282/// Each entry may be prefixed with `0x`.
283///
284/// # Errors
285/// Returns [`FilterError::InvalidFlowId`] for any non-hex token.
286pub fn parse_flow_ids(s: &str) -> Result<Vec<u64>, FilterError> {
287    s.split(',')
288        .map(|id| {
289            let id = id.trim().trim_start_matches("0x");
290            u64::from_str_radix(id, 16).map_err(|_| FilterError::InvalidFlowId(id.to_owned()))
291        })
292        .collect()
293}
294
295/// Parse an RFC 3339 datetime string or a Unix epoch integer (in **seconds**)
296/// to nanoseconds since the Unix epoch.
297///
298/// Bare integers are treated as whole seconds, which is the dominant convention
299/// (`date +%s`, Python `time.time()`, etc.). Sub-second precision requires an
300/// RFC 3339 string with a fractional seconds component.
301///
302/// # Errors
303/// Returns [`FilterError::InvalidDateTime`] if the string is neither a valid
304/// RFC 3339 string nor a decimal integer.
305pub fn parse_datetime_ns(s: &str) -> Result<u64, FilterError> {
306    // Bare integers are Unix epoch seconds (the dominant convention).
307    if let Ok(secs) = s.parse::<u64>() {
308        return Ok(secs.saturating_mul(1_000_000_000));
309    }
310    chrono::DateTime::parse_from_rfc3339(s)
311        .map_err(|_| FilterError::InvalidDateTime(s.to_owned()))
312        .and_then(|dt| {
313            let ns = dt
314                .timestamp_nanos_opt()
315                .ok_or_else(|| FilterError::InvalidDateTime(s.to_owned()))?;
316            Ok(ns.max(0) as u64)
317        })
318}
319
320// ── PacketMeta ───────────────────────────────────────────────────────────────
321
322/// Packet metadata extracted for filter evaluation.
323///
324/// `flow_key` is `None` for non-IP frames (e.g. ARP). `tcp_flags` is `0`
325/// for non-TCP traffic.
326#[derive(Debug, Clone)]
327pub struct PacketMeta {
328    /// Packet timestamp in nanoseconds since the Unix epoch.
329    pub timestamp_ns: u64,
330    /// Number of captured bytes (without the record header).
331    pub captured_len: u32,
332    /// Parsed 5-tuple; `None` for non-IP or unrecognised frames.
333    pub flow_key: Option<FlowKey>,
334    /// TCP control flags; `0` when the transport is not TCP.
335    pub tcp_flags: u8,
336}
337
338impl PacketMeta {
339    /// Extract filter-relevant metadata from a raw Ethernet frame.
340    pub fn from_packet(timestamp_ns: u64, captured_len: u32, data: &[u8]) -> Self {
341        let (flow_key, tcp_flags) = parse_ethernet(data);
342        Self {
343            timestamp_ns,
344            captured_len,
345            flow_key,
346            tcp_flags,
347        }
348    }
349}
350
351fn parse_ethernet(data: &[u8]) -> (Option<FlowKey>, u8) {
352    let sliced = match SlicedPacket::from_ethernet(data) {
353        Ok(s) => s,
354        Err(_) => return (None, 0),
355    };
356
357    let (src_ip, dst_ip, protocol) = match &sliced.net {
358        Some(NetSlice::Ipv4(v4)) => {
359            let h = v4.header();
360            (
361                IpAddr::V4(h.source_addr()),
362                IpAddr::V4(h.destination_addr()),
363                h.protocol().0,
364            )
365        }
366        Some(NetSlice::Ipv6(v6)) => {
367            let h = v6.header();
368            (
369                IpAddr::V6(h.source_addr()),
370                IpAddr::V6(h.destination_addr()),
371                h.next_header().0,
372            )
373        }
374        _ => return (None, 0),
375    };
376
377    let (src_port, dst_port, tcp_flags) = match &sliced.transport {
378        Some(TransportSlice::Tcp(tcp)) => {
379            let mut f = 0u8;
380            if tcp.fin() {
381                f |= 0x01;
382            }
383            if tcp.syn() {
384                f |= 0x02;
385            }
386            if tcp.rst() {
387                f |= 0x04;
388            }
389            if tcp.psh() {
390                f |= 0x08;
391            }
392            if tcp.ack() {
393                f |= 0x10;
394            }
395            if tcp.urg() {
396                f |= 0x20;
397            }
398            if tcp.ece() {
399                f |= 0x40;
400            }
401            if tcp.cwr() {
402                f |= 0x80;
403            }
404            (tcp.source_port(), tcp.destination_port(), f)
405        }
406        Some(TransportSlice::Udp(udp)) => (udp.source_port(), udp.destination_port(), 0),
407        _ => (0, 0, 0),
408    };
409
410    let key = FlowKey::new(src_ip, dst_ip, src_port, dst_port, protocol);
411    (Some(key), tcp_flags)
412}
413
414// ── Logical composition ───────────────────────────────────────────────────────
415
416/// Logical operator used when chaining [`FilterRule`]s in a [`Filter`].
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
418pub enum Op {
419    /// `accumulated = accumulated AND rule.matches(packet)` (default).
420    #[default]
421    And,
422    /// `accumulated = accumulated OR rule.matches(packet)`.
423    Or,
424    /// `accumulated = accumulated AND NOT rule.matches(packet)`.
425    Not,
426}
427
428impl Op {
429    /// Parse `"and"`, `"or"`, or `"not"` (case-insensitive).
430    ///
431    /// # Errors
432    /// Returns [`FilterError::InvalidOp`] for any other value.
433    pub fn parse(s: &str) -> Result<Self, FilterError> {
434        match s.to_ascii_lowercase().as_str() {
435            "and" => Ok(Self::And),
436            "or" => Ok(Self::Or),
437            "not" => Ok(Self::Not),
438            _ => Err(FilterError::InvalidOp(s.to_owned())),
439        }
440    }
441}
442
443// ── Shared flat-body matching ─────────────────────────────────────────────────
444
445/// Private trait — provides getters so `eval_body` can work over both
446/// [`Filter`] (base body) and [`FilterRule`] without code duplication.
447trait FilterBody {
448    fn protocols(&self) -> &[u8];
449    fn src_ips(&self) -> &[IpNet];
450    fn dst_ips(&self) -> &[IpNet];
451    fn ips(&self) -> &[IpNet];
452    fn src_ports(&self) -> &[PortRange];
453    fn dst_ports(&self) -> &[PortRange];
454    fn ports(&self) -> &[PortRange];
455    fn flow_ids(&self) -> &FxHashSet<u64>;
456    fn start_ns(&self) -> Option<u64>;
457    fn end_ns(&self) -> Option<u64>;
458    fn tcp_flags(&self) -> Option<TcpFlagsFilter>;
459    fn min_len(&self) -> Option<u32>;
460    fn max_len(&self) -> Option<u32>;
461    fn unidirectional(&self) -> bool;
462}
463
464/// Generate a `FilterBody` impl for a struct whose fields have the same names.
465macro_rules! impl_filter_body {
466    ($T:ty) => {
467        impl FilterBody for $T {
468            fn protocols(&self) -> &[u8] {
469                &self.protocols
470            }
471            fn src_ips(&self) -> &[IpNet] {
472                &self.src_ips
473            }
474            fn dst_ips(&self) -> &[IpNet] {
475                &self.dst_ips
476            }
477            fn ips(&self) -> &[IpNet] {
478                &self.ips
479            }
480            fn src_ports(&self) -> &[PortRange] {
481                &self.src_ports
482            }
483            fn dst_ports(&self) -> &[PortRange] {
484                &self.dst_ports
485            }
486            fn ports(&self) -> &[PortRange] {
487                &self.ports
488            }
489            fn flow_ids(&self) -> &FxHashSet<u64> {
490                &self.flow_ids
491            }
492            fn start_ns(&self) -> Option<u64> {
493                self.from_ns
494            }
495            fn end_ns(&self) -> Option<u64> {
496                self.to_ns
497            }
498            fn tcp_flags(&self) -> Option<TcpFlagsFilter> {
499                self.tcp_flags
500            }
501            fn min_len(&self) -> Option<u32> {
502                self.min_len
503            }
504            fn max_len(&self) -> Option<u32> {
505                self.max_len
506            }
507            fn unidirectional(&self) -> bool {
508                self.unidirectional
509            }
510        }
511    };
512}
513
514/// Core flat-filter evaluation shared by [`Filter`] and [`FilterRule`].
515///
516/// - Categories are **AND**-ed: all active categories must pass.
517/// - Values within a category are **OR**-ed: any one match is sufficient.
518/// - Port filters are silently skipped for protocols other than TCP (6) / UDP (17).
519fn eval_body(b: &impl FilterBody, meta: &PacketMeta) -> bool {
520    // ── Time range ────────────────────────────────────────────────────────
521    if let Some(from) = b.start_ns()
522        && meta.timestamp_ns < from
523    {
524        return false;
525    }
526    if let Some(to) = b.end_ns()
527        && meta.timestamp_ns > to
528    {
529        return false;
530    }
531
532    // ── Packet length ─────────────────────────────────────────────────────
533    if let Some(min) = b.min_len()
534        && meta.captured_len < min
535    {
536        return false;
537    }
538    if let Some(max) = b.max_len()
539        && meta.captured_len > max
540    {
541        return false;
542    }
543
544    // Early exit: remaining checks all require a parsed flow key.
545    let need_flow = !b.protocols().is_empty()
546        || !b.src_ips().is_empty()
547        || !b.dst_ips().is_empty()
548        || !b.ips().is_empty()
549        || !b.src_ports().is_empty()
550        || !b.dst_ports().is_empty()
551        || !b.ports().is_empty()
552        || !b.flow_ids().is_empty()
553        || b.tcp_flags().is_some();
554
555    if !need_flow {
556        return true;
557    }
558
559    // Non-IP packets fail any flow-level filter.
560    let key = match &meta.flow_key {
561        Some(k) => k,
562        None => return false,
563    };
564
565    // ── Protocol ──────────────────────────────────────────────────────────
566    if !b.protocols().is_empty() && !b.protocols().contains(&key.protocol) {
567        return false;
568    }
569
570    // ── IP address / CIDR ─────────────────────────────────────────────────
571    if !b.src_ips().is_empty() && !b.src_ips().iter().any(|n| n.contains(key.src_ip)) {
572        return false;
573    }
574    if !b.dst_ips().is_empty() && !b.dst_ips().iter().any(|n| n.contains(key.dst_ip)) {
575        return false;
576    }
577    if !b.ips().is_empty()
578        && !b
579            .ips()
580            .iter()
581            .any(|n| n.contains(key.src_ip) || n.contains(key.dst_ip))
582    {
583        return false;
584    }
585
586    // ── Port (TCP / UDP only; silently ignored for other protocols) ────────
587    if matches!(key.protocol, 6 | 17) {
588        if !b.src_ports().is_empty() && !b.src_ports().iter().any(|r| r.contains(key.src_port)) {
589            return false;
590        }
591        if !b.dst_ports().is_empty() && !b.dst_ports().iter().any(|r| r.contains(key.dst_port)) {
592            return false;
593        }
594        if !b.ports().is_empty()
595            && !b
596                .ports()
597                .iter()
598                .any(|r| r.contains(key.src_port) || r.contains(key.dst_port))
599        {
600            return false;
601        }
602    }
603
604    // ── Flow ID ───────────────────────────────────────────────────────────
605    if !b.flow_ids().is_empty() {
606        let id = key.flow_id(b.unidirectional());
607        if !b.flow_ids().contains(&id) {
608            return false;
609        }
610    }
611
612    // ── TCP flags (protocol 6 only) ────────────────────────────────────────
613    if let Some(ff) = b.tcp_flags()
614        && key.protocol == 6
615        && !ff.matches(meta.tcp_flags)
616    {
617        return false;
618    }
619
620    true
621}
622
623// ── Filter ───────────────────────────────────────────────────────────────────
624
625/// Compiled structured packet filter.
626///
627/// ## Flat-body composition (within the base filter)
628///
629/// - Multiple values within the same category are **OR**-ed.
630/// - Values across different categories are **AND**-ed.
631/// - An empty category places no constraint.
632///
633/// ## Global negation
634///
635/// When `negate` is `true` the entire result of the flat-body evaluation
636/// (and the rule chain, if any) is inverted.  Use `--not` on the CLI.
637///
638/// ## Rule chain
639///
640/// `rules` is an ordered list of [`FilterRule`]s evaluated left-to-right
641/// after the base body. Each rule carries an [`Op`] that controls how it
642/// combines with the accumulated result:
643///
644/// - `Op::And` — `acc = acc AND rule.matches(pkt)` (default)
645/// - `Op::Or`  — `acc = acc OR rule.matches(pkt)`
646/// - `Op::Not` — `acc = acc AND NOT rule.matches(pkt)`
647///
648/// An empty `rules` list (the default) has no effect.
649#[derive(Debug, Default, Clone)]
650pub struct Filter {
651    /// When `true`, the entire filter result is inverted.
652    pub negate: bool,
653
654    /// Additional rules chained after the base body.
655    pub rules: Vec<FilterRule>,
656
657    /// IP protocol numbers to match. Empty = any protocol.
658    pub protocols: Vec<u8>,
659
660    /// Source IP/CIDR rules (OR-ed within this set).
661    pub src_ips: Vec<IpNet>,
662    /// Destination IP/CIDR rules (OR-ed).
663    pub dst_ips: Vec<IpNet>,
664    /// Either-endpoint IP/CIDR rules: matches if src **or** dst is in range.
665    pub ips: Vec<IpNet>,
666
667    /// Source port ranges (OR-ed). Only applied to TCP/UDP.
668    pub src_ports: Vec<PortRange>,
669    /// Destination port ranges (OR-ed). Only applied to TCP/UDP.
670    pub dst_ports: Vec<PortRange>,
671    /// Either-endpoint port ranges: matches if src port **or** dst port is in range.
672    pub ports: Vec<PortRange>,
673
674    /// Flow IDs (pre-computed) to retain. Empty = any flow.
675    pub flow_ids: FxHashSet<u64>,
676
677    /// Inclusive lower timestamp bound (nanoseconds). `None` = no lower bound.
678    pub from_ns: Option<u64>,
679    /// Inclusive upper timestamp bound (nanoseconds). `None` = no upper bound.
680    pub to_ns: Option<u64>,
681
682    /// TCP control-flags filter.
683    pub tcp_flags: Option<TcpFlagsFilter>,
684
685    /// Minimum captured length in bytes. `None` = no minimum.
686    pub min_len: Option<u32>,
687    /// Maximum captured length in bytes. `None` = no maximum.
688    pub max_len: Option<u32>,
689
690    /// When `true`, flow IDs are computed unidirectionally.
691    pub unidirectional: bool,
692}
693
694impl_filter_body!(Filter);
695
696impl Filter {
697    /// Return `true` if the filter places no constraints (matches every packet).
698    ///
699    /// A filter with `negate = true` or a non-empty `rules` list is never empty.
700    pub fn is_empty(&self) -> bool {
701        !self.negate
702            && self.rules.is_empty()
703            && self.protocols.is_empty()
704            && self.src_ips.is_empty()
705            && self.dst_ips.is_empty()
706            && self.ips.is_empty()
707            && self.src_ports.is_empty()
708            && self.dst_ports.is_empty()
709            && self.ports.is_empty()
710            && self.flow_ids.is_empty()
711            && self.from_ns.is_none()
712            && self.to_ns.is_none()
713            && self.tcp_flags.is_none()
714            && self.min_len.is_none()
715            && self.max_len.is_none()
716    }
717
718    /// Evaluate the filter against `meta`.
719    ///
720    /// 1. Evaluate the base flat body.
721    /// 2. Apply `negate` to that result.
722    /// 3. Fold over `rules` using each rule's [`Op`].
723    ///
724    /// Returns `true` if the packet passes.
725    pub fn matches(&self, meta: &PacketMeta) -> bool {
726        let base = eval_body(self, meta);
727        let acc = if self.negate { !base } else { base };
728        self.rules.iter().fold(acc, |acc, rule| {
729            let rule_result = eval_body(rule, meta);
730            match rule.op {
731                Op::And => acc && rule_result,
732                Op::Or => acc || rule_result,
733                Op::Not => acc && !rule_result,
734            }
735        })
736    }
737}
738
739// ── FilterRule ────────────────────────────────────────────────────────────────
740
741/// A single rule in a [`Filter`] rule chain.
742///
743/// All flat-body semantics are identical to the base [`Filter`]: categories
744/// are OR-ed within, AND-ed across.  The [`Op`] field controls how this rule
745/// is combined with the accumulated result.
746#[derive(Debug, Default, Clone)]
747pub struct FilterRule {
748    /// How this rule combines with the accumulated filter result.
749    pub op: Op,
750
751    pub protocols: Vec<u8>,
752    pub src_ips: Vec<IpNet>,
753    pub dst_ips: Vec<IpNet>,
754    pub ips: Vec<IpNet>,
755    pub src_ports: Vec<PortRange>,
756    pub dst_ports: Vec<PortRange>,
757    pub ports: Vec<PortRange>,
758    pub flow_ids: FxHashSet<u64>,
759    pub from_ns: Option<u64>,
760    pub to_ns: Option<u64>,
761    pub tcp_flags: Option<TcpFlagsFilter>,
762    pub min_len: Option<u32>,
763    pub max_len: Option<u32>,
764    pub unidirectional: bool,
765}
766
767impl_filter_body!(FilterRule);
768
769// ── Tests ─────────────────────────────────────────────────────────────────────
770
771#[cfg(test)]
772mod tests {
773    use super::*;
774    use std::net::Ipv4Addr;
775
776    fn v4(a: u8, b: u8, c: u8, d: u8) -> IpAddr {
777        IpAddr::V4(Ipv4Addr::new(a, b, c, d))
778    }
779
780    fn meta(
781        ts_ns: u64,
782        caplen: u32,
783        src: IpAddr,
784        dst: IpAddr,
785        sport: u16,
786        dport: u16,
787        proto: u8,
788        tcp_flags: u8,
789    ) -> PacketMeta {
790        PacketMeta {
791            timestamp_ns: ts_ns,
792            captured_len: caplen,
793            flow_key: Some(FlowKey::new(src, dst, sport, dport, proto)),
794            tcp_flags,
795        }
796    }
797
798    fn no_flow_meta(ts_ns: u64, caplen: u32) -> PacketMeta {
799        PacketMeta {
800            timestamp_ns: ts_ns,
801            captured_len: caplen,
802            flow_key: None,
803            tcp_flags: 0,
804        }
805    }
806
807    // ── IpNet ────────────────────────────────────────────────────────────────
808
809    #[test]
810    fn test_ipnet_parse_host_v4() {
811        let net = IpNet::parse("10.0.0.1").unwrap();
812        assert!(net.contains(v4(10, 0, 0, 1)));
813        assert!(!net.contains(v4(10, 0, 0, 2)));
814    }
815
816    #[test]
817    fn test_ipnet_parse_cidr_v4() {
818        let net = IpNet::parse("10.0.0.0/8").unwrap();
819        assert!(net.contains(v4(10, 0, 0, 1)));
820        assert!(net.contains(v4(10, 255, 255, 255)));
821        assert!(!net.contains(v4(11, 0, 0, 1)));
822    }
823
824    #[test]
825    fn test_ipnet_parse_cidr_v4_slash_32() {
826        let net = IpNet::parse("192.168.1.1/32").unwrap();
827        assert!(net.contains(v4(192, 168, 1, 1)));
828        assert!(!net.contains(v4(192, 168, 1, 2)));
829    }
830
831    #[test]
832    fn test_ipnet_parse_cidr_v6() {
833        let net = IpNet::parse("2001:db8::/32").unwrap();
834        assert!(net.contains("2001:db8::1".parse().unwrap()));
835        assert!(!net.contains("2001:db9::1".parse().unwrap()));
836    }
837
838    #[test]
839    fn test_ipnet_v4_mapped_normalised() {
840        // normalize_ip() converts ::ffff:a.b.c.d → plain IPv4 before the CIDR
841        // check, so an IPv4 network matches IPv4-mapped IPv6 addresses.
842        let net = IpNet::parse("10.0.0.0/8").unwrap();
843        let mapped: IpAddr = "::ffff:10.1.2.3".parse().unwrap();
844        assert!(net.contains(mapped));
845        let other_mapped: IpAddr = "::ffff:11.0.0.1".parse().unwrap();
846        assert!(!net.contains(other_mapped));
847    }
848
849    #[test]
850    fn test_ipnet_parse_invalid() {
851        assert!(IpNet::parse("not-an-ip").is_err());
852        assert!(IpNet::parse("10.0.0.0/33").is_err());
853        assert!(IpNet::parse("2001:db8::/129").is_err());
854    }
855
856    // ── PortRange ────────────────────────────────────────────────────────────
857
858    #[test]
859    fn test_port_range_single() {
860        let r = PortRange::parse("443").unwrap();
861        assert!(r.contains(443));
862        assert!(!r.contains(444));
863    }
864
865    #[test]
866    fn test_port_range_range() {
867        let r = PortRange::parse("1024-65535").unwrap();
868        assert!(r.contains(1024));
869        assert!(r.contains(65535));
870        assert!(!r.contains(1023));
871    }
872
873    #[test]
874    fn test_port_range_invalid() {
875        assert!(PortRange::parse("abc").is_err());
876        assert!(PortRange::parse("100-50").is_err()); // start > end
877    }
878
879    // ── TcpFlagsFilter ───────────────────────────────────────────────────────
880
881    #[test]
882    fn test_tcp_flags_syn_any() {
883        let f = TcpFlagsFilter::parse("SYN").unwrap();
884        assert!(f.matches(0x02)); // SYN set
885        assert!(f.matches(0x12)); // SYN+ACK
886        assert!(!f.matches(0x10)); // ACK only
887    }
888
889    #[test]
890    fn test_tcp_flags_syn_ack_exact() {
891        let f = TcpFlagsFilter::parse("SYN+ACK:exact").unwrap();
892        assert!(f.matches(0x12)); // SYN+ACK exactly
893        assert!(!f.matches(0x02)); // SYN only
894        assert!(!f.matches(0x13)); // SYN+ACK+FIN
895    }
896
897    #[test]
898    fn test_tcp_flags_invalid() {
899        assert!(TcpFlagsFilter::parse("").is_err());
900        assert!(TcpFlagsFilter::parse("INVALID").is_err());
901    }
902
903    // ── parse_proto_list ─────────────────────────────────────────────────────
904
905    #[test]
906    fn test_parse_proto_list() {
907        assert_eq!(parse_proto_list("tcp,udp").unwrap(), [6, 17]);
908        assert_eq!(parse_proto_list("icmp").unwrap(), [1]);
909        assert_eq!(parse_proto_list("6,17").unwrap(), [6, 17]);
910        assert!(parse_proto_list("foobar").is_err());
911    }
912
913    // ── parse_flow_ids ───────────────────────────────────────────────────────
914
915    #[test]
916    fn test_parse_flow_ids() {
917        assert_eq!(parse_flow_ids("deadbeef").unwrap(), [0xdeadbeefu64]);
918        assert_eq!(
919            parse_flow_ids("0xdeadbeef,0xcafe1234").unwrap(),
920            [0xdeadbeefu64, 0xcafe1234u64]
921        );
922        assert!(parse_flow_ids("xyz").is_err());
923    }
924
925    // ── parse_datetime_ns ────────────────────────────────────────────────────
926
927    #[test]
928    fn test_parse_datetime_secs_epoch() {
929        // "1" second = 1_000_000_000 ns.
930        assert_eq!(parse_datetime_ns("1").unwrap(), 1_000_000_000);
931    }
932
933    #[test]
934    fn test_parse_datetime_secs_epoch_common_value() {
935        // A realistic Unix timestamp (2001-09-09 01:46:40 UTC).
936        let secs: u64 = 1_000_000_000;
937        let ns = parse_datetime_ns(&secs.to_string()).unwrap();
938        assert_eq!(ns, secs * 1_000_000_000);
939    }
940
941    #[test]
942    fn test_parse_datetime_secs_matches_rfc3339() {
943        // "1" second since epoch via integer must equal the RFC 3339 equivalent.
944        let via_int = parse_datetime_ns("1").unwrap();
945        let via_rfc = parse_datetime_ns("1970-01-01T00:00:01Z").unwrap();
946        assert_eq!(via_int, via_rfc);
947    }
948
949    #[test]
950    fn test_parse_datetime_zero() {
951        assert_eq!(parse_datetime_ns("0").unwrap(), 0);
952    }
953
954    #[test]
955    fn test_parse_datetime_rfc3339() {
956        let ns = parse_datetime_ns("1970-01-01T00:00:01Z").unwrap();
957        assert_eq!(ns, 1_000_000_000);
958    }
959
960    #[test]
961    fn test_parse_datetime_rfc3339_subsecond() {
962        // Sub-second precision is only available via RFC 3339.
963        let ns = parse_datetime_ns("1970-01-01T00:00:01.5Z").unwrap();
964        assert_eq!(ns, 1_500_000_000);
965    }
966
967    #[test]
968    fn test_parse_datetime_invalid() {
969        assert!(parse_datetime_ns("not-a-date").is_err());
970    }
971
972    // ── Filter::matches ──────────────────────────────────────────────────────
973
974    #[test]
975    fn test_empty_filter_matches_everything() {
976        let f = Filter::default();
977        assert!(f.is_empty());
978        assert!(f.matches(&meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 80, 443, 6, 0)));
979        assert!(f.matches(&no_flow_meta(0, 100)));
980    }
981
982    #[test]
983    fn test_filter_time_range() {
984        let mut f = Filter::default();
985        f.from_ns = Some(1_000);
986        f.to_ns = Some(2_000);
987        assert!(f.matches(&no_flow_meta(1_000, 10)));
988        assert!(f.matches(&no_flow_meta(2_000, 10)));
989        assert!(!f.matches(&no_flow_meta(999, 10)));
990        assert!(!f.matches(&no_flow_meta(2_001, 10)));
991    }
992
993    #[test]
994    fn test_filter_packet_length() {
995        let mut f = Filter::default();
996        f.min_len = Some(50);
997        f.max_len = Some(100);
998        assert!(f.matches(&no_flow_meta(0, 50)));
999        assert!(f.matches(&no_flow_meta(0, 100)));
1000        assert!(!f.matches(&no_flow_meta(0, 49)));
1001        assert!(!f.matches(&no_flow_meta(0, 101)));
1002    }
1003
1004    #[test]
1005    fn test_filter_protocol_tcp_only() {
1006        let mut f = Filter::default();
1007        f.protocols = vec![6]; // TCP
1008        let tcp = meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 80, 6, 0);
1009        let udp = meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 53, 17, 0);
1010        assert!(f.matches(&tcp));
1011        assert!(!f.matches(&udp));
1012    }
1013
1014    #[test]
1015    fn test_filter_protocol_multiple_or() {
1016        let mut f = Filter::default();
1017        f.protocols = vec![6, 17]; // TCP or UDP
1018        assert!(f.matches(&meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1, 2, 6, 0)));
1019        assert!(f.matches(&meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1, 2, 17, 0)));
1020        assert!(!f.matches(&meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 0, 0, 1, 0))); // ICMP
1021    }
1022
1023    #[test]
1024    fn test_filter_src_ip_cidr() {
1025        let mut f = Filter::default();
1026        f.src_ips = vec![IpNet::parse("10.0.0.0/8").unwrap()];
1027        assert!(f.matches(&meta(0, 60, v4(10, 1, 2, 3), v4(8, 8, 8, 8), 0, 0, 17, 0)));
1028        assert!(!f.matches(&meta(0, 60, v4(11, 0, 0, 1), v4(8, 8, 8, 8), 0, 0, 17, 0)));
1029    }
1030
1031    #[test]
1032    fn test_filter_dst_ip() {
1033        let mut f = Filter::default();
1034        f.dst_ips = vec![IpNet::parse("8.8.8.8").unwrap()];
1035        assert!(f.matches(&meta(0, 60, v4(1, 2, 3, 4), v4(8, 8, 8, 8), 0, 0, 17, 0)));
1036        assert!(!f.matches(&meta(0, 60, v4(1, 2, 3, 4), v4(1, 1, 1, 1), 0, 0, 17, 0)));
1037    }
1038
1039    #[test]
1040    fn test_filter_ip_either_endpoint() {
1041        let mut f = Filter::default();
1042        f.ips = vec![IpNet::parse("10.0.0.1").unwrap()];
1043        // Matches if src OR dst is 10.0.0.1
1044        assert!(f.matches(&meta(0, 60, v4(10, 0, 0, 1), v4(8, 8, 8, 8), 0, 0, 17, 0)));
1045        assert!(f.matches(&meta(0, 60, v4(8, 8, 8, 8), v4(10, 0, 0, 1), 0, 0, 17, 0)));
1046        assert!(!f.matches(&meta(0, 60, v4(1, 2, 3, 4), v4(5, 6, 7, 8), 0, 0, 17, 0)));
1047    }
1048
1049    #[test]
1050    fn test_filter_src_ip_multiple_or() {
1051        let mut f = Filter::default();
1052        f.src_ips = vec![
1053            IpNet::parse("10.0.0.1").unwrap(),
1054            IpNet::parse("192.168.0.0/16").unwrap(),
1055        ];
1056        assert!(f.matches(&meta(0, 60, v4(10, 0, 0, 1), v4(8, 8, 8, 8), 0, 0, 6, 0)));
1057        assert!(f.matches(&meta(0, 60, v4(192, 168, 1, 2), v4(8, 8, 8, 8), 0, 0, 6, 0)));
1058        assert!(!f.matches(&meta(0, 60, v4(172, 16, 0, 1), v4(8, 8, 8, 8), 0, 0, 6, 0)));
1059    }
1060
1061    #[test]
1062    fn test_filter_dst_port() {
1063        let mut f = Filter::default();
1064        f.dst_ports = vec![PortRange {
1065            start: 443,
1066            end: 443,
1067        }];
1068        assert!(f.matches(&meta(
1069            0,
1070            60,
1071            v4(1, 1, 1, 1),
1072            v4(2, 2, 2, 2),
1073            1234,
1074            443,
1075            6,
1076            0
1077        )));
1078        assert!(!f.matches(&meta(0, 60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 80, 6, 0)));
1079    }
1080
1081    #[test]
1082    fn test_filter_port_range_either() {
1083        let mut f = Filter::default();
1084        f.ports = vec![PortRange {
1085            start: 8000,
1086            end: 9000,
1087        }];
1088        assert!(f.matches(&meta(
1089            0,
1090            60,
1091            v4(1, 1, 1, 1),
1092            v4(2, 2, 2, 2),
1093            8080,
1094            1234,
1095            6,
1096            0
1097        )));
1098        assert!(f.matches(&meta(
1099            0,
1100            60,
1101            v4(1, 1, 1, 1),
1102            v4(2, 2, 2, 2),
1103            1234,
1104            8080,
1105            6,
1106            0
1107        )));
1108        assert!(!f.matches(&meta(0, 60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 80, 443, 6, 0)));
1109    }
1110
1111    #[test]
1112    fn test_filter_port_ignored_for_icmp() {
1113        // Port filters are silently ignored for non-TCP/UDP protocols.
1114        let mut f = Filter::default();
1115        f.dst_ports = vec![PortRange {
1116            start: 443,
1117            end: 443,
1118        }];
1119        // ICMP packet with dst_port=0: port filter is ignored, but protocol
1120        // filter is not set, so the only active filter is dst_port which
1121        // doesn't apply to ICMP → packet should pass.
1122        assert!(f.matches(&meta(0, 60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 0, 0, 1, 0)));
1123    }
1124
1125    #[test]
1126    fn test_filter_flow_id() {
1127        let key = FlowKey::new(v4(10, 0, 0, 1), v4(10, 0, 0, 2), 1234, 443, 6);
1128        let id = key.flow_id(false);
1129        let mut f = Filter::default();
1130        f.flow_ids = [id].into_iter().collect();
1131
1132        let matching = PacketMeta {
1133            timestamp_ns: 0,
1134            captured_len: 60,
1135            flow_key: Some(key),
1136            tcp_flags: 0,
1137        };
1138        assert!(f.matches(&matching));
1139
1140        let other = PacketMeta {
1141            timestamp_ns: 0,
1142            captured_len: 60,
1143            flow_key: Some(FlowKey::new(v4(10, 0, 0, 3), v4(10, 0, 0, 4), 5678, 80, 6)),
1144            tcp_flags: 0,
1145        };
1146        assert!(!f.matches(&other));
1147    }
1148
1149    #[test]
1150    fn test_filter_flow_id_multiple() {
1151        let keys: Vec<FlowKey> = (0u8..4)
1152            .map(|i| {
1153                FlowKey::new(
1154                    v4(10, 0, 0, i + 1),
1155                    v4(10, 0, 0, 100),
1156                    1000 + i as u16,
1157                    80,
1158                    6,
1159                )
1160            })
1161            .collect();
1162        let ids: Vec<u64> = keys.iter().map(|k| k.flow_id(false)).collect();
1163
1164        let mut f = Filter::default();
1165        f.flow_ids = ids[..3].iter().copied().collect(); // allow first three
1166
1167        for (i, key) in keys.iter().enumerate() {
1168            let pkt = PacketMeta {
1169                timestamp_ns: 0,
1170                captured_len: 60,
1171                flow_key: Some(key.clone()),
1172                tcp_flags: 0,
1173            };
1174            if i < 3 {
1175                assert!(f.matches(&pkt), "flow {i} should be allowed");
1176            } else {
1177                assert!(!f.matches(&pkt), "flow {i} should be blocked");
1178            }
1179        }
1180    }
1181
1182    #[test]
1183    fn test_filter_flow_id_empty_allows_all() {
1184        // An empty flow_ids set must not filter anything.
1185        let f = Filter::default();
1186        assert!(f.flow_ids.is_empty());
1187        let pkt = meta(0, 60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 80, 6, 0);
1188        assert!(f.matches(&pkt));
1189    }
1190
1191    #[test]
1192    fn test_filter_flow_id_non_ip_blocked() {
1193        // A non-IP packet has no flow key; if flow_ids is non-empty it should
1194        // not match (there is no ID to look up).
1195        let key = FlowKey::new(v4(10, 0, 0, 1), v4(10, 0, 0, 2), 1234, 443, 6);
1196        let mut f = Filter::default();
1197        f.flow_ids = [key.flow_id(false)].into_iter().collect();
1198
1199        let no_key = PacketMeta {
1200            timestamp_ns: 0,
1201            captured_len: 60,
1202            flow_key: None,
1203            tcp_flags: 0,
1204        };
1205        assert!(!f.matches(&no_key));
1206    }
1207
1208    #[test]
1209    fn test_filter_tcp_flags() {
1210        let mut f = Filter::default();
1211        f.tcp_flags = Some(TcpFlagsFilter::parse("SYN").unwrap());
1212
1213        // SYN packet
1214        assert!(f.matches(&meta(
1215            0,
1216            60,
1217            v4(1, 1, 1, 1),
1218            v4(2, 2, 2, 2),
1219            1234,
1220            80,
1221            6,
1222            0x02
1223        )));
1224        // ACK-only packet
1225        assert!(!f.matches(&meta(
1226            0,
1227            60,
1228            v4(1, 1, 1, 1),
1229            v4(2, 2, 2, 2),
1230            1234,
1231            80,
1232            6,
1233            0x10
1234        )));
1235    }
1236
1237    #[test]
1238    fn test_filter_and_composition() {
1239        // Protocol=TCP AND dst_port=443 AND src_ip=10.0.0.0/8 — all must hold.
1240        let mut f = Filter::default();
1241        f.protocols = vec![6];
1242        f.dst_ports = vec![PortRange {
1243            start: 443,
1244            end: 443,
1245        }];
1246        f.src_ips = vec![IpNet::parse("10.0.0.0/8").unwrap()];
1247
1248        // All match
1249        assert!(f.matches(&meta(
1250            0,
1251            60,
1252            v4(10, 1, 2, 3),
1253            v4(8, 8, 8, 8),
1254            5000,
1255            443,
1256            6,
1257            0
1258        )));
1259        // Wrong protocol
1260        assert!(!f.matches(&meta(
1261            0,
1262            60,
1263            v4(10, 1, 2, 3),
1264            v4(8, 8, 8, 8),
1265            5000,
1266            443,
1267            17,
1268            0
1269        )));
1270        // Wrong dst port
1271        assert!(!f.matches(&meta(
1272            0,
1273            60,
1274            v4(10, 1, 2, 3),
1275            v4(8, 8, 8, 8),
1276            5000,
1277            80,
1278            6,
1279            0
1280        )));
1281        // Wrong src IP
1282        assert!(!f.matches(&meta(
1283            0,
1284            60,
1285            v4(11, 0, 0, 1),
1286            v4(8, 8, 8, 8),
1287            5000,
1288            443,
1289            6,
1290            0
1291        )));
1292    }
1293
1294    #[test]
1295    fn test_filter_non_ip_fails_when_flow_filter_active() {
1296        let mut f = Filter::default();
1297        f.protocols = vec![6];
1298        assert!(!f.matches(&no_flow_meta(0, 100)));
1299    }
1300
1301    #[test]
1302    fn test_filter_non_ip_passes_when_only_length_filter() {
1303        let mut f = Filter::default();
1304        f.min_len = Some(50);
1305        assert!(f.matches(&no_flow_meta(0, 60)));
1306        assert!(!f.matches(&no_flow_meta(0, 40)));
1307    }
1308}