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