Skip to main content

pcap_toolkit/
bpf.rs

1//! Pure-Rust tcpdump/libpcap-style BPF expression filter.
2//!
3//! # Grammar (supported subset)
4//!
5//! Reference: <https://biot.com/capstats/bpf.html>
6//!
7//! ```text
8//! expr      = or_expr
9//! or_expr   = and_expr  ('or'  and_expr)*
10//! and_expr  = not_expr  ('and' not_expr)*
11//! not_expr  = 'not' not_expr | '(' expr ')' | primitive
12//! primitive = proto_kw ['host'|'net'|'port'|'portrange' …]
13//!           | dir ('host'|'net'|'port'|'portrange') …
14//!           | ('host'|'net'|'port'|'portrange') …
15//!           | 'proto' number
16//!           | 'len' cmp_op number
17//! proto_kw  = 'tcp'|'udp'|'icmp'|'icmp6'|'ip'|'ip6'|'arp'
18//! dir       = 'src' | 'dst' | 'src or dst' | 'src and dst'
19//! cmp_op    = '>' | '<' | '>=' | '<=' | '==' | '!='
20//! ```
21//!
22//! Sugar: `tcp port 443` expands to `tcp and port 443`.
23//! `arp` matches non-IP frames (best-effort: any packet with no parsed flow key).
24
25use thiserror::Error;
26
27use crate::filter::{IpNet, PacketMeta, PortRange};
28
29// ── Error ─────────────────────────────────────────────────────────────────────
30
31/// Error returned when a BPF expression cannot be parsed.
32#[derive(Debug, Error)]
33#[error("BPF parse error at column {col}: {message}")]
34pub struct BpfError {
35    pub message: String,
36    pub col: usize,
37}
38
39// ── Tokeniser ─────────────────────────────────────────────────────────────────
40
41#[derive(Debug, Clone, PartialEq)]
42enum Tok {
43    Word(String),
44    LParen,
45    RParen,
46    Gt,
47    Lt,
48    Ge,
49    Le,
50    EqEq,
51    Ne,
52}
53
54/// Scan `input` into a flat token list annotated with byte offsets.
55fn tokenize(input: &str) -> Result<Vec<(Tok, usize)>, BpfError> {
56    let mut out = Vec::new();
57    let b = input.as_bytes();
58    let mut i = 0;
59    while i < b.len() {
60        if b[i].is_ascii_whitespace() {
61            i += 1;
62            continue;
63        }
64        let col = i;
65        match b[i] {
66            b'(' => {
67                out.push((Tok::LParen, col));
68                i += 1;
69            }
70            b')' => {
71                out.push((Tok::RParen, col));
72                i += 1;
73            }
74            b'>' => {
75                if b.get(i + 1) == Some(&b'=') {
76                    out.push((Tok::Ge, col));
77                    i += 2;
78                } else {
79                    out.push((Tok::Gt, col));
80                    i += 1;
81                }
82            }
83            b'<' => {
84                if b.get(i + 1) == Some(&b'=') {
85                    out.push((Tok::Le, col));
86                    i += 2;
87                } else {
88                    out.push((Tok::Lt, col));
89                    i += 1;
90                }
91            }
92            b'=' => {
93                if b.get(i + 1) == Some(&b'=') {
94                    out.push((Tok::EqEq, col));
95                    i += 2;
96                } else {
97                    return Err(BpfError {
98                        message: "bare '=' — did you mean '=='?".into(),
99                        col,
100                    });
101                }
102            }
103            b'!' => {
104                if b.get(i + 1) == Some(&b'=') {
105                    out.push((Tok::Ne, col));
106                    i += 2;
107                } else {
108                    return Err(BpfError {
109                        message: "bare '!' — did you mean '!='?".into(),
110                        col,
111                    });
112                }
113            }
114            _ => {
115                let start = i;
116                while i < b.len()
117                    && (b[i].is_ascii_alphanumeric()
118                        || matches!(b[i], b'.' | b':' | b'/' | b'-' | b'_'))
119                {
120                    i += 1;
121                }
122                if i == start {
123                    return Err(BpfError {
124                        message: format!("unexpected character '{}'", b[col] as char),
125                        col,
126                    });
127                }
128                out.push((Tok::Word(input[start..i].to_owned()), start));
129            }
130        }
131    }
132    Ok(out)
133}
134
135// ── AST ───────────────────────────────────────────────────────────────────────
136
137/// Direction qualifier for host / net / port primitives.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum Dir {
140    /// Source endpoint only.
141    Src,
142    /// Destination endpoint only.
143    Dst,
144    /// Either endpoint (default when no direction qualifier is given, and for
145    /// both `src or dst` and `src and dst`).
146    Either,
147}
148
149/// Comparison operator used in `len` expressions.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum CmpOp {
152    Gt,
153    Lt,
154    Ge,
155    Le,
156    Eq,
157    Ne,
158}
159
160/// A compiled BPF expression tree.
161///
162/// Construct with [`parse`] and evaluate per-packet with [`BpfExpr::eval`].
163#[derive(Debug, Clone)]
164pub enum BpfExpr {
165    /// Both sub-expressions must match.
166    And(Box<Self>, Box<Self>),
167    /// Either sub-expression must match.
168    Or(Box<Self>, Box<Self>),
169    /// Sub-expression must *not* match.
170    Not(Box<Self>),
171    /// IP protocol number (`tcp`=6, `udp`=17, `icmp`=1, `icmp6`=58, `proto N`).
172    IpProto(u8),
173    /// IPv4 packet (`ip` keyword).
174    Ip,
175    /// IPv6 packet (`ip6` keyword).
176    Ip6,
177    /// ARP frame (`arp` keyword). Best-effort: matches any non-IP packet
178    /// (i.e. packets for which etherparse produces no flow key).
179    Arp,
180    /// Exact host or prefix match on one or both endpoints.
181    Host { dir: Dir, net: IpNet },
182    /// Network prefix (CIDR) match on one or both endpoints.
183    Net { dir: Dir, net: IpNet },
184    /// Port or port-range match (TCP/UDP only; silently passes other protocols).
185    Port { dir: Dir, range: PortRange },
186    /// Captured-length comparison (`len > N`, `len <= N`, …).
187    Len { op: CmpOp, val: u32 },
188}
189
190// ── Parser ────────────────────────────────────────────────────────────────────
191
192struct Parser {
193    toks: Vec<(Tok, usize)>,
194    pos: usize,
195}
196
197impl Parser {
198    fn peek(&self) -> Option<&Tok> {
199        self.toks.get(self.pos).map(|(t, _)| t)
200    }
201
202    fn peek_word(&self) -> Option<&str> {
203        match self.peek() {
204            Some(Tok::Word(w)) => Some(w.as_str()),
205            _ => None,
206        }
207    }
208
209    fn word_at(&self, offset: usize) -> Option<&str> {
210        match self.toks.get(self.pos + offset) {
211            Some((Tok::Word(w), _)) => Some(w.as_str()),
212            _ => None,
213        }
214    }
215
216    fn col(&self) -> usize {
217        self.toks.get(self.pos).map(|(_, c)| *c).unwrap_or(0)
218    }
219
220    fn advance(&mut self) {
221        self.pos += 1;
222    }
223
224    fn err(&self, msg: impl Into<String>) -> BpfError {
225        BpfError {
226            message: msg.into(),
227            col: self.col(),
228        }
229    }
230
231    fn expect_word(&mut self) -> Result<String, BpfError> {
232        match self.toks.get(self.pos) {
233            Some((Tok::Word(w), _)) => {
234                let w = w.clone();
235                self.pos += 1;
236                Ok(w)
237            }
238            _ => Err(self.err("expected identifier or value")),
239        }
240    }
241
242    // ── Grammar rules ──────────────────────────────────────────────────────
243
244    fn parse_expr(&mut self) -> Result<BpfExpr, BpfError> {
245        let expr = self.parse_or()?;
246        if self.pos < self.toks.len() {
247            return Err(self.err(format!(
248                "unexpected token after expression: {:?}",
249                self.peek_word().unwrap_or("?")
250            )));
251        }
252        Ok(expr)
253    }
254
255    fn parse_or(&mut self) -> Result<BpfExpr, BpfError> {
256        let mut left = self.parse_and()?;
257        while self.peek_word() == Some("or") {
258            self.advance();
259            let right = self.parse_and()?;
260            left = BpfExpr::Or(Box::new(left), Box::new(right));
261        }
262        Ok(left)
263    }
264
265    fn parse_and(&mut self) -> Result<BpfExpr, BpfError> {
266        let mut left = self.parse_not()?;
267        while self.peek_word() == Some("and") {
268            self.advance();
269            let right = self.parse_not()?;
270            left = BpfExpr::And(Box::new(left), Box::new(right));
271        }
272        Ok(left)
273    }
274
275    fn parse_not(&mut self) -> Result<BpfExpr, BpfError> {
276        if self.peek_word() == Some("not") {
277            self.advance();
278            let inner = self.parse_not()?; // right-associative
279            return Ok(BpfExpr::Not(Box::new(inner)));
280        }
281        if self.peek() == Some(&Tok::LParen) {
282            self.advance();
283            let inner = self.parse_or()?;
284            if self.peek() != Some(&Tok::RParen) {
285                return Err(self.err("expected ')'"));
286            }
287            self.advance();
288            return Ok(inner);
289        }
290        self.parse_primitive()
291    }
292
293    fn parse_primitive(&mut self) -> Result<BpfExpr, BpfError> {
294        // Try to consume a direction prefix (src / dst), but only when it is
295        // immediately followed by a type keyword.
296        let dir = self.parse_dir();
297
298        let col = self.col();
299        let word = match self.peek_word() {
300            Some(w) => w.to_owned(),
301            None => {
302                if dir != Dir::Either {
303                    return Err(
304                        self.err("expected 'host', 'net', 'port', or 'portrange' after direction")
305                    );
306                }
307                return Err(self.err("expected primitive"));
308            }
309        };
310
311        // If a direction was consumed, the next token must be a type keyword.
312        if dir != Dir::Either {
313            return self.parse_type_prim(dir);
314        }
315
316        match word.as_str() {
317            "tcp" => {
318                self.advance();
319                self.maybe_compound(BpfExpr::IpProto(6))
320            }
321            "udp" => {
322                self.advance();
323                self.maybe_compound(BpfExpr::IpProto(17))
324            }
325            "icmp" => {
326                self.advance();
327                self.maybe_compound(BpfExpr::IpProto(1))
328            }
329            "icmp6" | "icmpv6" => {
330                self.advance();
331                self.maybe_compound(BpfExpr::IpProto(58))
332            }
333            "ip" => {
334                self.advance();
335                self.maybe_compound(BpfExpr::Ip)
336            }
337            "ip6" => {
338                self.advance();
339                self.maybe_compound(BpfExpr::Ip6)
340            }
341            "arp" => {
342                self.advance();
343                Ok(BpfExpr::Arp)
344            }
345            "proto" => {
346                self.advance();
347                let s = self.expect_word()?;
348                let n: u8 = s.parse().map_err(|_| BpfError {
349                    message: format!("expected protocol number 0-255, got '{s}'"),
350                    col,
351                })?;
352                Ok(BpfExpr::IpProto(n))
353            }
354            "len" => {
355                self.advance();
356                let op = self.parse_cmp_op()?;
357                let s = self.expect_word()?;
358                let n: u32 = s
359                    .parse()
360                    .map_err(|_| self.err(format!("expected number after 'len', got '{s}'")))?;
361                Ok(BpfExpr::Len { op, val: n })
362            }
363            "host" | "net" | "port" | "portrange" => self.parse_type_prim(Dir::Either),
364            _ => Err(BpfError {
365                message: format!("unknown primitive '{word}'"),
366                col,
367            }),
368        }
369    }
370
371    /// After a protocol keyword, check whether a type keyword follows.
372    ///
373    /// `tcp port 443` → `And(IpProto(6), Port(Either, 443-443))`.
374    fn maybe_compound(&mut self, proto_expr: BpfExpr) -> Result<BpfExpr, BpfError> {
375        if self.is_type_keyword() {
376            let type_expr = self.parse_type_prim(Dir::Either)?;
377            Ok(BpfExpr::And(Box::new(proto_expr), Box::new(type_expr)))
378        } else {
379            Ok(proto_expr)
380        }
381    }
382
383    fn is_type_keyword(&self) -> bool {
384        matches!(
385            self.peek_word(),
386            Some("host" | "net" | "port" | "portrange")
387        )
388    }
389
390    /// Try to consume a direction qualifier (`src` / `dst`).
391    ///
392    /// Only advances `pos` when a type keyword follows, so a bare `src` that
393    /// is not a direction qualifier is left untouched (which results in a parse
394    /// error in `parse_primitive`).
395    ///
396    /// Handles `src or dst` and `src and dst` — both map to [`Dir::Either`].
397    fn parse_dir(&mut self) -> Dir {
398        let word = match self.peek_word() {
399            Some(w @ ("src" | "dst")) => w.to_owned(),
400            _ => return Dir::Either,
401        };
402
403        // Check for "src or dst <type>" or "src and dst <type>".
404        if matches!(self.word_at(1), Some("or" | "and"))
405            && matches!(self.word_at(2), Some("src" | "dst"))
406            && matches!(self.word_at(3), Some("host" | "net" | "port" | "portrange"))
407        {
408            self.pos += 3; // consume: first-dir, or/and, second-dir
409            return Dir::Either;
410        }
411
412        // Simple "src <type>" or "dst <type>".
413        if matches!(self.word_at(1), Some("host" | "net" | "port" | "portrange")) {
414            self.advance();
415            return if word == "src" { Dir::Src } else { Dir::Dst };
416        }
417
418        Dir::Either // not a direction context — do not consume
419    }
420
421    fn parse_type_prim(&mut self, dir: Dir) -> Result<BpfExpr, BpfError> {
422        let kw = self.expect_word()?;
423        match kw.as_str() {
424            "host" => {
425                let s = self.expect_word()?;
426                let net = IpNet::parse(&s)
427                    .map_err(|_| self.err(format!("invalid host address '{s}'")))?;
428                Ok(BpfExpr::Host { dir, net })
429            }
430            "net" => {
431                let s = self.expect_word()?;
432                let net = IpNet::parse(&s)
433                    .map_err(|_| self.err(format!("invalid network/CIDR '{s}'")))?;
434                Ok(BpfExpr::Net { dir, net })
435            }
436            "port" | "portrange" => {
437                let s = self.expect_word()?;
438                let range = PortRange::parse(&s)
439                    .map_err(|_| self.err(format!("invalid port or range '{s}'")))?;
440                Ok(BpfExpr::Port { dir, range })
441            }
442            other => Err(self.err(format!("expected host/net/port/portrange, got '{other}'"))),
443        }
444    }
445
446    fn parse_cmp_op(&mut self) -> Result<CmpOp, BpfError> {
447        let col = self.col();
448        let op = match self.peek() {
449            Some(Tok::Gt) => CmpOp::Gt,
450            Some(Tok::Lt) => CmpOp::Lt,
451            Some(Tok::Ge) => CmpOp::Ge,
452            Some(Tok::Le) => CmpOp::Le,
453            Some(Tok::EqEq) => CmpOp::Eq,
454            Some(Tok::Ne) => CmpOp::Ne,
455            _ => {
456                return Err(BpfError {
457                    message: "expected comparison operator (>, <, >=, <=, ==, !=)".into(),
458                    col,
459                });
460            }
461        };
462        self.advance();
463        Ok(op)
464    }
465}
466
467// ── Public API ────────────────────────────────────────────────────────────────
468
469/// Parse a tcpdump/libpcap-style BPF filter expression into a compiled tree.
470///
471/// # Errors
472/// Returns [`BpfError`] with the column offset of the first invalid token.
473///
474/// # Examples
475/// ```
476/// use pcap_toolkit::bpf::parse;
477/// let expr = parse("tcp and dst port 443").unwrap();
478/// ```
479pub fn parse(input: &str) -> Result<BpfExpr, BpfError> {
480    let toks = tokenize(input)?;
481    if toks.is_empty() {
482        return Err(BpfError {
483            message: "empty filter expression".into(),
484            col: 0,
485        });
486    }
487    let mut parser = Parser { toks, pos: 0 };
488    parser.parse_expr()
489}
490
491// ── Evaluation ────────────────────────────────────────────────────────────────
492
493impl BpfExpr {
494    /// Evaluate this expression against packet metadata.
495    ///
496    /// Returns `true` if the packet passes the filter.
497    pub fn eval(&self, meta: &PacketMeta) -> bool {
498        match self {
499            Self::And(a, b) => a.eval(meta) && b.eval(meta),
500            Self::Or(a, b) => a.eval(meta) || b.eval(meta),
501            Self::Not(inner) => !inner.eval(meta),
502
503            Self::IpProto(proto) => meta
504                .flow_key
505                .as_ref()
506                .map(|k| k.protocol == *proto)
507                .unwrap_or(false),
508
509            Self::Ip => meta
510                .flow_key
511                .as_ref()
512                .map(|k| k.src_ip.is_ipv4())
513                .unwrap_or(false),
514
515            Self::Ip6 => meta
516                .flow_key
517                .as_ref()
518                .map(|k| k.src_ip.is_ipv6())
519                .unwrap_or(false),
520
521            // ARP and other non-IP frames produce no flow key in our pipeline.
522            Self::Arp => meta.flow_key.is_none(),
523
524            Self::Host { dir, net } | Self::Net { dir, net } => {
525                let Some(k) = &meta.flow_key else {
526                    return false;
527                };
528                match dir {
529                    Dir::Src => net.contains(k.src_ip),
530                    Dir::Dst => net.contains(k.dst_ip),
531                    Dir::Either => net.contains(k.src_ip) || net.contains(k.dst_ip),
532                }
533            }
534
535            Self::Port { dir, range } => {
536                let Some(k) = &meta.flow_key else {
537                    return false;
538                };
539                // Port filters are silently ignored for non-TCP/UDP protocols.
540                if !matches!(k.protocol, 6 | 17) {
541                    return true;
542                }
543                match dir {
544                    Dir::Src => range.contains(k.src_port),
545                    Dir::Dst => range.contains(k.dst_port),
546                    Dir::Either => range.contains(k.src_port) || range.contains(k.dst_port),
547                }
548            }
549
550            Self::Len { op, val } => {
551                let l = meta.captured_len;
552                match op {
553                    CmpOp::Gt => l > *val,
554                    CmpOp::Lt => l < *val,
555                    CmpOp::Ge => l >= *val,
556                    CmpOp::Le => l <= *val,
557                    CmpOp::Eq => l == *val,
558                    CmpOp::Ne => l != *val,
559                }
560            }
561        }
562    }
563}
564
565// ── Tests ─────────────────────────────────────────────────────────────────────
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use std::net::{IpAddr, Ipv4Addr};
571
572    fn v4(a: u8, b: u8, c: u8, d: u8) -> IpAddr {
573        IpAddr::V4(Ipv4Addr::new(a, b, c, d))
574    }
575
576    fn make_meta(
577        caplen: u32,
578        src: IpAddr,
579        dst: IpAddr,
580        sport: u16,
581        dport: u16,
582        proto: u8,
583    ) -> PacketMeta {
584        use crate::flow::FlowKey;
585        PacketMeta {
586            timestamp_ns: 0,
587            captured_len: caplen,
588            flow_key: Some(FlowKey::new(src, dst, sport, dport, proto)),
589            tcp_flags: 0,
590        }
591    }
592
593    fn no_ip(caplen: u32) -> PacketMeta {
594        PacketMeta {
595            timestamp_ns: 0,
596            captured_len: caplen,
597            flow_key: None,
598            tcp_flags: 0,
599        }
600    }
601
602    // ── Parse ──────────────────────────────────────────────────────────────
603
604    #[test]
605    fn test_parse_proto_keywords() {
606        assert!(matches!(parse("tcp").unwrap(), BpfExpr::IpProto(6)));
607        assert!(matches!(parse("udp").unwrap(), BpfExpr::IpProto(17)));
608        assert!(matches!(parse("icmp").unwrap(), BpfExpr::IpProto(1)));
609        assert!(matches!(parse("icmp6").unwrap(), BpfExpr::IpProto(58)));
610        assert!(matches!(parse("icmpv6").unwrap(), BpfExpr::IpProto(58)));
611        assert!(matches!(parse("ip").unwrap(), BpfExpr::Ip));
612        assert!(matches!(parse("ip6").unwrap(), BpfExpr::Ip6));
613        assert!(matches!(parse("arp").unwrap(), BpfExpr::Arp));
614    }
615
616    #[test]
617    fn test_parse_proto_number() {
618        assert!(matches!(parse("proto 17").unwrap(), BpfExpr::IpProto(17)));
619        assert!(matches!(parse("proto 50").unwrap(), BpfExpr::IpProto(50)));
620    }
621
622    #[test]
623    fn test_parse_host_either() {
624        assert!(matches!(
625            parse("host 10.0.0.1").unwrap(),
626            BpfExpr::Host {
627                dir: Dir::Either,
628                ..
629            }
630        ));
631    }
632
633    #[test]
634    fn test_parse_src_dst_host() {
635        assert!(matches!(
636            parse("src host 10.0.0.1").unwrap(),
637            BpfExpr::Host { dir: Dir::Src, .. }
638        ));
639        assert!(matches!(
640            parse("dst host 10.0.0.1").unwrap(),
641            BpfExpr::Host { dir: Dir::Dst, .. }
642        ));
643    }
644
645    #[test]
646    fn test_parse_net_cidr() {
647        assert!(matches!(
648            parse("src net 10.0.0.0/8").unwrap(),
649            BpfExpr::Net { dir: Dir::Src, .. }
650        ));
651    }
652
653    #[test]
654    fn test_parse_port_and_portrange() {
655        assert!(matches!(
656            parse("dst port 443").unwrap(),
657            BpfExpr::Port { dir: Dir::Dst, .. }
658        ));
659        assert!(matches!(
660            parse("portrange 1024-65535").unwrap(),
661            BpfExpr::Port {
662                dir: Dir::Either,
663                ..
664            }
665        ));
666    }
667
668    #[test]
669    fn test_parse_src_or_dst_direction() {
670        let e = parse("src or dst port 80").unwrap();
671        assert!(matches!(
672            e,
673            BpfExpr::Port {
674                dir: Dir::Either,
675                ..
676            }
677        ));
678    }
679
680    #[test]
681    fn test_parse_src_and_dst_direction() {
682        let e = parse("src and dst port 80").unwrap();
683        assert!(matches!(
684            e,
685            BpfExpr::Port {
686                dir: Dir::Either,
687                ..
688            }
689        ));
690    }
691
692    #[test]
693    fn test_parse_len_all_ops() {
694        assert!(matches!(
695            parse("len > 100").unwrap(),
696            BpfExpr::Len {
697                op: CmpOp::Gt,
698                val: 100
699            }
700        ));
701        assert!(matches!(
702            parse("len < 100").unwrap(),
703            BpfExpr::Len {
704                op: CmpOp::Lt,
705                val: 100
706            }
707        ));
708        assert!(matches!(
709            parse("len >= 64").unwrap(),
710            BpfExpr::Len {
711                op: CmpOp::Ge,
712                val: 64
713            }
714        ));
715        assert!(matches!(
716            parse("len <= 1500").unwrap(),
717            BpfExpr::Len {
718                op: CmpOp::Le,
719                val: 1500
720            }
721        ));
722        assert!(matches!(
723            parse("len == 60").unwrap(),
724            BpfExpr::Len {
725                op: CmpOp::Eq,
726                val: 60
727            }
728        ));
729        assert!(matches!(
730            parse("len != 0").unwrap(),
731            BpfExpr::Len {
732                op: CmpOp::Ne,
733                val: 0
734            }
735        ));
736    }
737
738    #[test]
739    fn test_parse_not() {
740        assert!(matches!(parse("not tcp").unwrap(), BpfExpr::Not(_)));
741    }
742
743    #[test]
744    fn test_parse_and_or_precedence() {
745        // "tcp or udp and dst port 443" → tcp OR (udp AND dst port 443)
746        let e = parse("tcp or udp and dst port 443").unwrap();
747        let BpfExpr::Or(left, right) = e else {
748            panic!("expected Or")
749        };
750        assert!(matches!(*left, BpfExpr::IpProto(6)));
751        assert!(matches!(*right, BpfExpr::And(_, _)));
752    }
753
754    #[test]
755    fn test_parse_tcp_port_sugar() {
756        // "tcp port 443" → And(IpProto(6), Port(Either, 443))
757        let BpfExpr::And(l, r) = parse("tcp port 443").unwrap() else {
758            panic!()
759        };
760        assert!(matches!(*l, BpfExpr::IpProto(6)));
761        assert!(matches!(
762            *r,
763            BpfExpr::Port {
764                dir: Dir::Either,
765                ..
766            }
767        ));
768    }
769
770    #[test]
771    fn test_parse_parens() {
772        let BpfExpr::Not(inner) = parse("not (tcp or udp)").unwrap() else {
773            panic!()
774        };
775        assert!(matches!(*inner, BpfExpr::Or(_, _)));
776    }
777
778    #[test]
779    fn test_parse_ipv6_host() {
780        assert!(parse("host 2001:db8::1").is_ok());
781        assert!(parse("dst host ::1").is_ok());
782    }
783
784    #[test]
785    fn test_parse_empty_error() {
786        assert!(parse("").is_err());
787    }
788
789    #[test]
790    fn test_parse_unknown_primitive_error() {
791        assert!(parse("foobar").is_err());
792    }
793
794    #[test]
795    fn test_parse_unclosed_paren_error() {
796        assert!(parse("(tcp and udp").is_err());
797    }
798
799    #[test]
800    fn test_parse_bare_equals_error() {
801        assert!(parse("len = 100").is_err());
802    }
803
804    // ── Eval ───────────────────────────────────────────────────────────────
805
806    #[test]
807    fn test_eval_proto_match() {
808        let tcp = make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 80, 6);
809        let udp = make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 53, 17);
810        let expr = parse("tcp").unwrap();
811        assert!(expr.eval(&tcp));
812        assert!(!expr.eval(&udp));
813    }
814
815    #[test]
816    fn test_eval_ip_version() {
817        let v4pkt = make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1, 2, 6);
818        let v6src: IpAddr = "2001:db8::1".parse().unwrap();
819        let v6dst: IpAddr = "2001:db8::2".parse().unwrap();
820        let v6pkt = make_meta(60, v6src, v6dst, 1, 2, 6);
821        assert!(parse("ip").unwrap().eval(&v4pkt));
822        assert!(!parse("ip").unwrap().eval(&v6pkt));
823        assert!(parse("ip6").unwrap().eval(&v6pkt));
824        assert!(!parse("ip6").unwrap().eval(&v4pkt));
825    }
826
827    #[test]
828    fn test_eval_arp_matches_non_ip() {
829        let expr = parse("arp").unwrap();
830        assert!(expr.eval(&no_ip(60)));
831        assert!(!expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 0, 0, 17)));
832    }
833
834    #[test]
835    fn test_eval_host_either() {
836        let expr = parse("host 8.8.8.8").unwrap();
837        assert!(expr.eval(&make_meta(60, v4(8, 8, 8, 8), v4(1, 2, 3, 4), 0, 0, 17)));
838        assert!(expr.eval(&make_meta(60, v4(1, 2, 3, 4), v4(8, 8, 8, 8), 0, 0, 17)));
839        assert!(!expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 0, 0, 17)));
840    }
841
842    #[test]
843    fn test_eval_src_net() {
844        let expr = parse("src net 10.0.0.0/8").unwrap();
845        assert!(expr.eval(&make_meta(60, v4(10, 1, 2, 3), v4(8, 8, 8, 8), 0, 0, 17)));
846        assert!(!expr.eval(&make_meta(60, v4(192, 168, 1, 1), v4(8, 8, 8, 8), 0, 0, 17)));
847    }
848
849    #[test]
850    fn test_eval_dst_port() {
851        let expr = parse("dst port 443").unwrap();
852        assert!(expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 5000, 443, 6)));
853        assert!(!expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 5001, 80, 6)));
854    }
855
856    #[test]
857    fn test_eval_portrange() {
858        let expr = parse("portrange 1024-65535").unwrap();
859        // high-numbered src port matches
860        assert!(expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 8080, 80, 6)));
861        // both low — no match
862        assert!(!expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 80, 80, 6)));
863    }
864
865    #[test]
866    fn test_eval_len() {
867        assert!(parse("len > 100").unwrap().eval(&no_ip(200)));
868        assert!(!parse("len > 100").unwrap().eval(&no_ip(50)));
869        assert!(parse("len <= 40").unwrap().eval(&no_ip(40)));
870        assert!(!parse("len <= 40").unwrap().eval(&no_ip(41)));
871    }
872
873    #[test]
874    fn test_eval_tcp_port_sugar() {
875        let expr = parse("tcp port 80").unwrap();
876        assert!(expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 5000, 80, 6)));
877        assert!(!expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 5000, 80, 17))); // UDP
878        assert!(!expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 5000, 443, 6))); // wrong port
879    }
880
881    #[test]
882    fn test_eval_not() {
883        let expr = parse("not tcp").unwrap();
884        assert!(!expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 0, 0, 6)));
885        assert!(expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 0, 0, 17)));
886    }
887
888    #[test]
889    fn test_eval_and_or() {
890        let expr = parse("tcp and dst port 443").unwrap();
891        assert!(expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 5000, 443, 6)));
892        assert!(!expr.eval(&make_meta(60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 5000, 80, 6)));
893        assert!(!expr.eval(&make_meta(
894            60,
895            v4(1, 1, 1, 1),
896            v4(2, 2, 2, 2),
897            5000,
898            443,
899            17
900        )));
901    }
902
903    #[test]
904    fn test_eval_complex() {
905        // "(tcp or udp) and src net 10.0.0.0/8 and not dst port 80"
906        let expr = parse("(tcp or udp) and src net 10.0.0.0/8 and not dst port 80").unwrap();
907        // passes: TCP from 10.x, dst 443
908        assert!(expr.eval(&make_meta(
909            100,
910            v4(10, 0, 0, 1),
911            v4(8, 8, 8, 8),
912            5000,
913            443,
914            6
915        )));
916        // fails: TCP from 10.x but dst 80
917        assert!(!expr.eval(&make_meta(
918            100,
919            v4(10, 0, 0, 1),
920            v4(8, 8, 8, 8),
921            5000,
922            80,
923            6
924        )));
925        // fails: correct ports/proto but wrong src net
926        assert!(!expr.eval(&make_meta(
927            100,
928            v4(192, 168, 1, 1),
929            v4(8, 8, 8, 8),
930            5000,
931            443,
932            6
933        )));
934        // fails: ICMP from 10.x
935        assert!(!expr.eval(&make_meta(100, v4(10, 0, 0, 1), v4(8, 8, 8, 8), 0, 0, 1)));
936    }
937}