use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use etherparse::{NetSlice, SlicedPacket, TransportSlice};
use thiserror::Error;
use rustc_hash::FxHashSet;
use crate::flow::{FlowKey, normalize_ip};
#[derive(Debug, Error)]
pub enum FilterError {
#[error("invalid IP address or CIDR: '{0}'")]
InvalidIp(String),
#[error("invalid port or range (expected e.g. '443' or '1024-65535'): '{0}'")]
InvalidPort(String),
#[error("invalid protocol (expected 'tcp', 'udp', 'icmp', 'icmp6', or a number 0-255): '{0}'")]
InvalidProto(String),
#[error("invalid flow ID (expected hex, optionally prefixed with '0x'): '{0}'")]
InvalidFlowId(String),
#[error("invalid datetime (expected RFC 3339 or Unix epoch integer in seconds): '{0}'")]
InvalidDateTime(String),
#[error(
"invalid TCP flags (e.g. 'SYN', 'SYN+ACK', 'FIN:exact'); \
known flags: FIN SYN RST PSH ACK URG ECE CWR: '{0}'"
)]
InvalidTcpFlags(String),
#[error("invalid filter operator (expected 'and', 'or', or 'not'): '{0}'")]
InvalidOp(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IpNet {
V4 { addr: Ipv4Addr, prefix_len: u8 },
V6 { addr: Ipv6Addr, prefix_len: u8 },
}
impl IpNet {
pub fn parse(s: &str) -> Result<Self, FilterError> {
let err = || FilterError::InvalidIp(s.to_owned());
if let Some((ip_str, prefix_str)) = s.split_once('/') {
let prefix_len: u8 = prefix_str.parse().map_err(|_| err())?;
let ip = normalize_ip(ip_str.parse::<IpAddr>().map_err(|_| err())?);
match ip {
IpAddr::V4(v4) => {
if prefix_len > 32 {
return Err(err());
}
Ok(Self::V4 {
addr: v4,
prefix_len,
})
}
IpAddr::V6(v6) => {
if prefix_len > 128 {
return Err(err());
}
Ok(Self::V6 {
addr: v6,
prefix_len,
})
}
}
} else {
match normalize_ip(s.parse::<IpAddr>().map_err(|_| err())?) {
IpAddr::V4(v4) => Ok(Self::V4 {
addr: v4,
prefix_len: 32,
}),
IpAddr::V6(v6) => Ok(Self::V6 {
addr: v6,
prefix_len: 128,
}),
}
}
}
pub fn contains(&self, ip: IpAddr) -> bool {
let ip = normalize_ip(ip);
match (self, ip) {
(Self::V4 { addr, prefix_len }, IpAddr::V4(v4)) => {
if *prefix_len == 0 {
return true;
}
let shift = 32 - u32::from(*prefix_len);
(u32::from(*addr) >> shift) == (u32::from(v4) >> shift)
}
(Self::V6 { addr, prefix_len }, IpAddr::V6(v6)) => {
if *prefix_len == 0 {
return true;
}
let shift = 128 - u128::from(*prefix_len);
(u128::from(*addr) >> shift) == (u128::from(v6) >> shift)
}
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PortRange {
pub start: u16,
pub end: u16,
}
impl PortRange {
pub fn parse(s: &str) -> Result<Self, FilterError> {
let err = || FilterError::InvalidPort(s.to_owned());
if let Some((lo, hi)) = s.split_once('-') {
let start: u16 = lo.trim().parse().map_err(|_| err())?;
let end: u16 = hi.trim().parse().map_err(|_| err())?;
if start > end {
return Err(err());
}
Ok(Self { start, end })
} else {
let p: u16 = s.trim().parse().map_err(|_| err())?;
Ok(Self { start: p, end: p })
}
}
pub fn contains(self, port: u16) -> bool {
port >= self.start && port <= self.end
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TcpFlagsFilter {
pub mask: u8,
pub value: u8,
pub exact: bool,
}
impl TcpFlagsFilter {
pub fn parse(s: &str) -> Result<Self, FilterError> {
let err = || FilterError::InvalidTcpFlags(s.to_owned());
let (flags_part, exact) = if let Some(f) = s.strip_suffix(":exact") {
(f, true)
} else if let Some(f) = s.strip_suffix(":any") {
(f, false)
} else {
(s, false)
};
let mut mask = 0u8;
for token in flags_part.split('+') {
let bit = match token.trim().to_ascii_uppercase().as_str() {
"FIN" => 0x01,
"SYN" => 0x02,
"RST" => 0x04,
"PSH" => 0x08,
"ACK" => 0x10,
"URG" => 0x20,
"ECE" => 0x40,
"CWR" => 0x80,
_ => return Err(err()),
};
mask |= bit;
}
if mask == 0 {
return Err(err());
}
Ok(Self {
mask,
value: mask,
exact,
})
}
pub fn matches(self, flags: u8) -> bool {
if self.exact {
flags == self.value
} else {
(flags & self.mask) != 0
}
}
}
pub fn parse_proto_list(s: &str) -> Result<Vec<u8>, FilterError> {
s.split(',').map(|p| parse_proto(p.trim())).collect()
}
fn parse_proto(s: &str) -> Result<u8, FilterError> {
match s.to_ascii_lowercase().as_str() {
"tcp" => Ok(6),
"udp" => Ok(17),
"icmp" => Ok(1),
"icmp6" | "icmpv6" => Ok(58),
"sctp" => Ok(132),
"esp" => Ok(50),
"ah" => Ok(51),
other => other
.parse::<u8>()
.map_err(|_| FilterError::InvalidProto(s.to_owned())),
}
}
pub fn parse_flow_ids(s: &str) -> Result<Vec<u64>, FilterError> {
s.split(',')
.map(|id| {
let id = id.trim().trim_start_matches("0x");
u64::from_str_radix(id, 16).map_err(|_| FilterError::InvalidFlowId(id.to_owned()))
})
.collect()
}
pub fn parse_datetime_ns(s: &str) -> Result<u64, FilterError> {
if let Ok(secs) = s.parse::<u64>() {
return Ok(secs.saturating_mul(1_000_000_000));
}
chrono::DateTime::parse_from_rfc3339(s)
.map_err(|_| FilterError::InvalidDateTime(s.to_owned()))
.and_then(|dt| {
let ns = dt
.timestamp_nanos_opt()
.ok_or_else(|| FilterError::InvalidDateTime(s.to_owned()))?;
Ok(ns.max(0) as u64)
})
}
#[derive(Debug, Clone)]
pub struct PacketMeta {
pub timestamp_ns: u64,
pub captured_len: u32,
pub flow_key: Option<FlowKey>,
pub tcp_flags: u8,
}
impl PacketMeta {
pub fn from_packet(timestamp_ns: u64, captured_len: u32, data: &[u8]) -> Self {
let (flow_key, tcp_flags) = parse_ethernet(data);
Self {
timestamp_ns,
captured_len,
flow_key,
tcp_flags,
}
}
}
fn parse_ethernet(data: &[u8]) -> (Option<FlowKey>, u8) {
let sliced = match SlicedPacket::from_ethernet(data) {
Ok(s) => s,
Err(_) => return (None, 0),
};
let (src_ip, dst_ip, protocol) = match &sliced.net {
Some(NetSlice::Ipv4(v4)) => {
let h = v4.header();
(
IpAddr::V4(h.source_addr()),
IpAddr::V4(h.destination_addr()),
h.protocol().0,
)
}
Some(NetSlice::Ipv6(v6)) => {
let h = v6.header();
(
IpAddr::V6(h.source_addr()),
IpAddr::V6(h.destination_addr()),
h.next_header().0,
)
}
_ => return (None, 0),
};
let (src_port, dst_port, tcp_flags) = match &sliced.transport {
Some(TransportSlice::Tcp(tcp)) => {
let mut f = 0u8;
if tcp.fin() {
f |= 0x01;
}
if tcp.syn() {
f |= 0x02;
}
if tcp.rst() {
f |= 0x04;
}
if tcp.psh() {
f |= 0x08;
}
if tcp.ack() {
f |= 0x10;
}
if tcp.urg() {
f |= 0x20;
}
if tcp.ece() {
f |= 0x40;
}
if tcp.cwr() {
f |= 0x80;
}
(tcp.source_port(), tcp.destination_port(), f)
}
Some(TransportSlice::Udp(udp)) => (udp.source_port(), udp.destination_port(), 0),
_ => (0, 0, 0),
};
let key = FlowKey::new(src_ip, dst_ip, src_port, dst_port, protocol);
(Some(key), tcp_flags)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Op {
#[default]
And,
Or,
Not,
}
impl Op {
pub fn parse(s: &str) -> Result<Self, FilterError> {
match s.to_ascii_lowercase().as_str() {
"and" => Ok(Self::And),
"or" => Ok(Self::Or),
"not" => Ok(Self::Not),
_ => Err(FilterError::InvalidOp(s.to_owned())),
}
}
}
trait FilterBody {
fn protocols(&self) -> &[u8];
fn src_ips(&self) -> &[IpNet];
fn dst_ips(&self) -> &[IpNet];
fn ips(&self) -> &[IpNet];
fn src_ports(&self) -> &[PortRange];
fn dst_ports(&self) -> &[PortRange];
fn ports(&self) -> &[PortRange];
fn flow_ids(&self) -> &FxHashSet<u64>;
fn start_ns(&self) -> Option<u64>;
fn end_ns(&self) -> Option<u64>;
fn tcp_flags(&self) -> Option<TcpFlagsFilter>;
fn min_len(&self) -> Option<u32>;
fn max_len(&self) -> Option<u32>;
fn unidirectional(&self) -> bool;
}
macro_rules! impl_filter_body {
($T:ty) => {
impl FilterBody for $T {
fn protocols(&self) -> &[u8] {
&self.protocols
}
fn src_ips(&self) -> &[IpNet] {
&self.src_ips
}
fn dst_ips(&self) -> &[IpNet] {
&self.dst_ips
}
fn ips(&self) -> &[IpNet] {
&self.ips
}
fn src_ports(&self) -> &[PortRange] {
&self.src_ports
}
fn dst_ports(&self) -> &[PortRange] {
&self.dst_ports
}
fn ports(&self) -> &[PortRange] {
&self.ports
}
fn flow_ids(&self) -> &FxHashSet<u64> {
&self.flow_ids
}
fn start_ns(&self) -> Option<u64> {
self.from_ns
}
fn end_ns(&self) -> Option<u64> {
self.to_ns
}
fn tcp_flags(&self) -> Option<TcpFlagsFilter> {
self.tcp_flags
}
fn min_len(&self) -> Option<u32> {
self.min_len
}
fn max_len(&self) -> Option<u32> {
self.max_len
}
fn unidirectional(&self) -> bool {
self.unidirectional
}
}
};
}
fn eval_body(b: &impl FilterBody, meta: &PacketMeta) -> bool {
if let Some(from) = b.start_ns()
&& meta.timestamp_ns < from
{
return false;
}
if let Some(to) = b.end_ns()
&& meta.timestamp_ns > to
{
return false;
}
if let Some(min) = b.min_len()
&& meta.captured_len < min
{
return false;
}
if let Some(max) = b.max_len()
&& meta.captured_len > max
{
return false;
}
let need_flow = !b.protocols().is_empty()
|| !b.src_ips().is_empty()
|| !b.dst_ips().is_empty()
|| !b.ips().is_empty()
|| !b.src_ports().is_empty()
|| !b.dst_ports().is_empty()
|| !b.ports().is_empty()
|| !b.flow_ids().is_empty()
|| b.tcp_flags().is_some();
if !need_flow {
return true;
}
let key = match &meta.flow_key {
Some(k) => k,
None => return false,
};
if !b.protocols().is_empty() && !b.protocols().contains(&key.protocol) {
return false;
}
if !b.src_ips().is_empty() && !b.src_ips().iter().any(|n| n.contains(key.src_ip)) {
return false;
}
if !b.dst_ips().is_empty() && !b.dst_ips().iter().any(|n| n.contains(key.dst_ip)) {
return false;
}
if !b.ips().is_empty()
&& !b
.ips()
.iter()
.any(|n| n.contains(key.src_ip) || n.contains(key.dst_ip))
{
return false;
}
if matches!(key.protocol, 6 | 17) {
if !b.src_ports().is_empty() && !b.src_ports().iter().any(|r| r.contains(key.src_port)) {
return false;
}
if !b.dst_ports().is_empty() && !b.dst_ports().iter().any(|r| r.contains(key.dst_port)) {
return false;
}
if !b.ports().is_empty()
&& !b
.ports()
.iter()
.any(|r| r.contains(key.src_port) || r.contains(key.dst_port))
{
return false;
}
}
if !b.flow_ids().is_empty() {
let id = key.flow_id(b.unidirectional());
if !b.flow_ids().contains(&id) {
return false;
}
}
if let Some(ff) = b.tcp_flags()
&& key.protocol == 6
&& !ff.matches(meta.tcp_flags)
{
return false;
}
true
}
#[derive(Debug, Default, Clone)]
pub struct Filter {
pub negate: bool,
pub rules: Vec<FilterRule>,
pub protocols: Vec<u8>,
pub src_ips: Vec<IpNet>,
pub dst_ips: Vec<IpNet>,
pub ips: Vec<IpNet>,
pub src_ports: Vec<PortRange>,
pub dst_ports: Vec<PortRange>,
pub ports: Vec<PortRange>,
pub flow_ids: FxHashSet<u64>,
pub from_ns: Option<u64>,
pub to_ns: Option<u64>,
pub tcp_flags: Option<TcpFlagsFilter>,
pub min_len: Option<u32>,
pub max_len: Option<u32>,
pub unidirectional: bool,
}
impl_filter_body!(Filter);
impl Filter {
pub fn is_empty(&self) -> bool {
!self.negate
&& self.rules.is_empty()
&& self.protocols.is_empty()
&& self.src_ips.is_empty()
&& self.dst_ips.is_empty()
&& self.ips.is_empty()
&& self.src_ports.is_empty()
&& self.dst_ports.is_empty()
&& self.ports.is_empty()
&& self.flow_ids.is_empty()
&& self.from_ns.is_none()
&& self.to_ns.is_none()
&& self.tcp_flags.is_none()
&& self.min_len.is_none()
&& self.max_len.is_none()
}
pub fn matches(&self, meta: &PacketMeta) -> bool {
let base = eval_body(self, meta);
let acc = if self.negate { !base } else { base };
self.rules.iter().fold(acc, |acc, rule| {
let rule_result = eval_body(rule, meta);
match rule.op {
Op::And => acc && rule_result,
Op::Or => acc || rule_result,
Op::Not => acc && !rule_result,
}
})
}
}
#[derive(Debug, Default, Clone)]
pub struct FilterRule {
pub op: Op,
pub protocols: Vec<u8>,
pub src_ips: Vec<IpNet>,
pub dst_ips: Vec<IpNet>,
pub ips: Vec<IpNet>,
pub src_ports: Vec<PortRange>,
pub dst_ports: Vec<PortRange>,
pub ports: Vec<PortRange>,
pub flow_ids: FxHashSet<u64>,
pub from_ns: Option<u64>,
pub to_ns: Option<u64>,
pub tcp_flags: Option<TcpFlagsFilter>,
pub min_len: Option<u32>,
pub max_len: Option<u32>,
pub unidirectional: bool,
}
impl_filter_body!(FilterRule);
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
fn v4(a: u8, b: u8, c: u8, d: u8) -> IpAddr {
IpAddr::V4(Ipv4Addr::new(a, b, c, d))
}
fn meta(
ts_ns: u64,
caplen: u32,
src: IpAddr,
dst: IpAddr,
sport: u16,
dport: u16,
proto: u8,
tcp_flags: u8,
) -> PacketMeta {
PacketMeta {
timestamp_ns: ts_ns,
captured_len: caplen,
flow_key: Some(FlowKey::new(src, dst, sport, dport, proto)),
tcp_flags,
}
}
fn no_flow_meta(ts_ns: u64, caplen: u32) -> PacketMeta {
PacketMeta {
timestamp_ns: ts_ns,
captured_len: caplen,
flow_key: None,
tcp_flags: 0,
}
}
#[test]
fn test_ipnet_parse_host_v4() {
let net = IpNet::parse("10.0.0.1").unwrap();
assert!(net.contains(v4(10, 0, 0, 1)));
assert!(!net.contains(v4(10, 0, 0, 2)));
}
#[test]
fn test_ipnet_parse_cidr_v4() {
let net = IpNet::parse("10.0.0.0/8").unwrap();
assert!(net.contains(v4(10, 0, 0, 1)));
assert!(net.contains(v4(10, 255, 255, 255)));
assert!(!net.contains(v4(11, 0, 0, 1)));
}
#[test]
fn test_ipnet_parse_cidr_v4_slash_32() {
let net = IpNet::parse("192.168.1.1/32").unwrap();
assert!(net.contains(v4(192, 168, 1, 1)));
assert!(!net.contains(v4(192, 168, 1, 2)));
}
#[test]
fn test_ipnet_parse_cidr_v6() {
let net = IpNet::parse("2001:db8::/32").unwrap();
assert!(net.contains("2001:db8::1".parse().unwrap()));
assert!(!net.contains("2001:db9::1".parse().unwrap()));
}
#[test]
fn test_ipnet_v4_mapped_normalised() {
let net = IpNet::parse("10.0.0.0/8").unwrap();
let mapped: IpAddr = "::ffff:10.1.2.3".parse().unwrap();
assert!(net.contains(mapped));
let other_mapped: IpAddr = "::ffff:11.0.0.1".parse().unwrap();
assert!(!net.contains(other_mapped));
}
#[test]
fn test_ipnet_parse_invalid() {
assert!(IpNet::parse("not-an-ip").is_err());
assert!(IpNet::parse("10.0.0.0/33").is_err());
assert!(IpNet::parse("2001:db8::/129").is_err());
}
#[test]
fn test_port_range_single() {
let r = PortRange::parse("443").unwrap();
assert!(r.contains(443));
assert!(!r.contains(444));
}
#[test]
fn test_port_range_range() {
let r = PortRange::parse("1024-65535").unwrap();
assert!(r.contains(1024));
assert!(r.contains(65535));
assert!(!r.contains(1023));
}
#[test]
fn test_port_range_invalid() {
assert!(PortRange::parse("abc").is_err());
assert!(PortRange::parse("100-50").is_err()); }
#[test]
fn test_tcp_flags_syn_any() {
let f = TcpFlagsFilter::parse("SYN").unwrap();
assert!(f.matches(0x02)); assert!(f.matches(0x12)); assert!(!f.matches(0x10)); }
#[test]
fn test_tcp_flags_syn_ack_exact() {
let f = TcpFlagsFilter::parse("SYN+ACK:exact").unwrap();
assert!(f.matches(0x12)); assert!(!f.matches(0x02)); assert!(!f.matches(0x13)); }
#[test]
fn test_tcp_flags_invalid() {
assert!(TcpFlagsFilter::parse("").is_err());
assert!(TcpFlagsFilter::parse("INVALID").is_err());
}
#[test]
fn test_parse_proto_list() {
assert_eq!(parse_proto_list("tcp,udp").unwrap(), [6, 17]);
assert_eq!(parse_proto_list("icmp").unwrap(), [1]);
assert_eq!(parse_proto_list("6,17").unwrap(), [6, 17]);
assert!(parse_proto_list("foobar").is_err());
}
#[test]
fn test_parse_flow_ids() {
assert_eq!(parse_flow_ids("deadbeef").unwrap(), [0xdeadbeefu64]);
assert_eq!(
parse_flow_ids("0xdeadbeef,0xcafe1234").unwrap(),
[0xdeadbeefu64, 0xcafe1234u64]
);
assert!(parse_flow_ids("xyz").is_err());
}
#[test]
fn test_parse_datetime_secs_epoch() {
assert_eq!(parse_datetime_ns("1").unwrap(), 1_000_000_000);
}
#[test]
fn test_parse_datetime_secs_epoch_common_value() {
let secs: u64 = 1_000_000_000;
let ns = parse_datetime_ns(&secs.to_string()).unwrap();
assert_eq!(ns, secs * 1_000_000_000);
}
#[test]
fn test_parse_datetime_secs_matches_rfc3339() {
let via_int = parse_datetime_ns("1").unwrap();
let via_rfc = parse_datetime_ns("1970-01-01T00:00:01Z").unwrap();
assert_eq!(via_int, via_rfc);
}
#[test]
fn test_parse_datetime_zero() {
assert_eq!(parse_datetime_ns("0").unwrap(), 0);
}
#[test]
fn test_parse_datetime_rfc3339() {
let ns = parse_datetime_ns("1970-01-01T00:00:01Z").unwrap();
assert_eq!(ns, 1_000_000_000);
}
#[test]
fn test_parse_datetime_rfc3339_subsecond() {
let ns = parse_datetime_ns("1970-01-01T00:00:01.5Z").unwrap();
assert_eq!(ns, 1_500_000_000);
}
#[test]
fn test_parse_datetime_invalid() {
assert!(parse_datetime_ns("not-a-date").is_err());
}
#[test]
fn test_empty_filter_matches_everything() {
let f = Filter::default();
assert!(f.is_empty());
assert!(f.matches(&meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 80, 443, 6, 0)));
assert!(f.matches(&no_flow_meta(0, 100)));
}
#[test]
fn test_filter_time_range() {
let mut f = Filter::default();
f.from_ns = Some(1_000);
f.to_ns = Some(2_000);
assert!(f.matches(&no_flow_meta(1_000, 10)));
assert!(f.matches(&no_flow_meta(2_000, 10)));
assert!(!f.matches(&no_flow_meta(999, 10)));
assert!(!f.matches(&no_flow_meta(2_001, 10)));
}
#[test]
fn test_filter_packet_length() {
let mut f = Filter::default();
f.min_len = Some(50);
f.max_len = Some(100);
assert!(f.matches(&no_flow_meta(0, 50)));
assert!(f.matches(&no_flow_meta(0, 100)));
assert!(!f.matches(&no_flow_meta(0, 49)));
assert!(!f.matches(&no_flow_meta(0, 101)));
}
#[test]
fn test_filter_protocol_tcp_only() {
let mut f = Filter::default();
f.protocols = vec![6]; let tcp = meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 80, 6, 0);
let udp = meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 53, 17, 0);
assert!(f.matches(&tcp));
assert!(!f.matches(&udp));
}
#[test]
fn test_filter_protocol_multiple_or() {
let mut f = Filter::default();
f.protocols = vec![6, 17]; assert!(f.matches(&meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1, 2, 6, 0)));
assert!(f.matches(&meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1, 2, 17, 0)));
assert!(!f.matches(&meta(0, 100, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 0, 0, 1, 0))); }
#[test]
fn test_filter_src_ip_cidr() {
let mut f = Filter::default();
f.src_ips = vec![IpNet::parse("10.0.0.0/8").unwrap()];
assert!(f.matches(&meta(0, 60, v4(10, 1, 2, 3), v4(8, 8, 8, 8), 0, 0, 17, 0)));
assert!(!f.matches(&meta(0, 60, v4(11, 0, 0, 1), v4(8, 8, 8, 8), 0, 0, 17, 0)));
}
#[test]
fn test_filter_dst_ip() {
let mut f = Filter::default();
f.dst_ips = vec![IpNet::parse("8.8.8.8").unwrap()];
assert!(f.matches(&meta(0, 60, v4(1, 2, 3, 4), v4(8, 8, 8, 8), 0, 0, 17, 0)));
assert!(!f.matches(&meta(0, 60, v4(1, 2, 3, 4), v4(1, 1, 1, 1), 0, 0, 17, 0)));
}
#[test]
fn test_filter_ip_either_endpoint() {
let mut f = Filter::default();
f.ips = vec![IpNet::parse("10.0.0.1").unwrap()];
assert!(f.matches(&meta(0, 60, v4(10, 0, 0, 1), v4(8, 8, 8, 8), 0, 0, 17, 0)));
assert!(f.matches(&meta(0, 60, v4(8, 8, 8, 8), v4(10, 0, 0, 1), 0, 0, 17, 0)));
assert!(!f.matches(&meta(0, 60, v4(1, 2, 3, 4), v4(5, 6, 7, 8), 0, 0, 17, 0)));
}
#[test]
fn test_filter_src_ip_multiple_or() {
let mut f = Filter::default();
f.src_ips = vec![
IpNet::parse("10.0.0.1").unwrap(),
IpNet::parse("192.168.0.0/16").unwrap(),
];
assert!(f.matches(&meta(0, 60, v4(10, 0, 0, 1), v4(8, 8, 8, 8), 0, 0, 6, 0)));
assert!(f.matches(&meta(0, 60, v4(192, 168, 1, 2), v4(8, 8, 8, 8), 0, 0, 6, 0)));
assert!(!f.matches(&meta(0, 60, v4(172, 16, 0, 1), v4(8, 8, 8, 8), 0, 0, 6, 0)));
}
#[test]
fn test_filter_dst_port() {
let mut f = Filter::default();
f.dst_ports = vec![PortRange {
start: 443,
end: 443,
}];
assert!(f.matches(&meta(
0,
60,
v4(1, 1, 1, 1),
v4(2, 2, 2, 2),
1234,
443,
6,
0
)));
assert!(!f.matches(&meta(0, 60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 80, 6, 0)));
}
#[test]
fn test_filter_port_range_either() {
let mut f = Filter::default();
f.ports = vec![PortRange {
start: 8000,
end: 9000,
}];
assert!(f.matches(&meta(
0,
60,
v4(1, 1, 1, 1),
v4(2, 2, 2, 2),
8080,
1234,
6,
0
)));
assert!(f.matches(&meta(
0,
60,
v4(1, 1, 1, 1),
v4(2, 2, 2, 2),
1234,
8080,
6,
0
)));
assert!(!f.matches(&meta(0, 60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 80, 443, 6, 0)));
}
#[test]
fn test_filter_port_ignored_for_icmp() {
let mut f = Filter::default();
f.dst_ports = vec![PortRange {
start: 443,
end: 443,
}];
assert!(f.matches(&meta(0, 60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 0, 0, 1, 0)));
}
#[test]
fn test_filter_flow_id() {
let key = FlowKey::new(v4(10, 0, 0, 1), v4(10, 0, 0, 2), 1234, 443, 6);
let id = key.flow_id(false);
let mut f = Filter::default();
f.flow_ids = [id].into_iter().collect();
let matching = PacketMeta {
timestamp_ns: 0,
captured_len: 60,
flow_key: Some(key),
tcp_flags: 0,
};
assert!(f.matches(&matching));
let other = PacketMeta {
timestamp_ns: 0,
captured_len: 60,
flow_key: Some(FlowKey::new(v4(10, 0, 0, 3), v4(10, 0, 0, 4), 5678, 80, 6)),
tcp_flags: 0,
};
assert!(!f.matches(&other));
}
#[test]
fn test_filter_flow_id_multiple() {
let keys: Vec<FlowKey> = (0u8..4)
.map(|i| {
FlowKey::new(
v4(10, 0, 0, i + 1),
v4(10, 0, 0, 100),
1000 + i as u16,
80,
6,
)
})
.collect();
let ids: Vec<u64> = keys.iter().map(|k| k.flow_id(false)).collect();
let mut f = Filter::default();
f.flow_ids = ids[..3].iter().copied().collect();
for (i, key) in keys.iter().enumerate() {
let pkt = PacketMeta {
timestamp_ns: 0,
captured_len: 60,
flow_key: Some(key.clone()),
tcp_flags: 0,
};
if i < 3 {
assert!(f.matches(&pkt), "flow {i} should be allowed");
} else {
assert!(!f.matches(&pkt), "flow {i} should be blocked");
}
}
}
#[test]
fn test_filter_flow_id_empty_allows_all() {
let f = Filter::default();
assert!(f.flow_ids.is_empty());
let pkt = meta(0, 60, v4(1, 1, 1, 1), v4(2, 2, 2, 2), 1234, 80, 6, 0);
assert!(f.matches(&pkt));
}
#[test]
fn test_filter_flow_id_non_ip_blocked() {
let key = FlowKey::new(v4(10, 0, 0, 1), v4(10, 0, 0, 2), 1234, 443, 6);
let mut f = Filter::default();
f.flow_ids = [key.flow_id(false)].into_iter().collect();
let no_key = PacketMeta {
timestamp_ns: 0,
captured_len: 60,
flow_key: None,
tcp_flags: 0,
};
assert!(!f.matches(&no_key));
}
#[test]
fn test_filter_tcp_flags() {
let mut f = Filter::default();
f.tcp_flags = Some(TcpFlagsFilter::parse("SYN").unwrap());
assert!(f.matches(&meta(
0,
60,
v4(1, 1, 1, 1),
v4(2, 2, 2, 2),
1234,
80,
6,
0x02
)));
assert!(!f.matches(&meta(
0,
60,
v4(1, 1, 1, 1),
v4(2, 2, 2, 2),
1234,
80,
6,
0x10
)));
}
#[test]
fn test_filter_and_composition() {
let mut f = Filter::default();
f.protocols = vec![6];
f.dst_ports = vec![PortRange {
start: 443,
end: 443,
}];
f.src_ips = vec![IpNet::parse("10.0.0.0/8").unwrap()];
assert!(f.matches(&meta(
0,
60,
v4(10, 1, 2, 3),
v4(8, 8, 8, 8),
5000,
443,
6,
0
)));
assert!(!f.matches(&meta(
0,
60,
v4(10, 1, 2, 3),
v4(8, 8, 8, 8),
5000,
443,
17,
0
)));
assert!(!f.matches(&meta(
0,
60,
v4(10, 1, 2, 3),
v4(8, 8, 8, 8),
5000,
80,
6,
0
)));
assert!(!f.matches(&meta(
0,
60,
v4(11, 0, 0, 1),
v4(8, 8, 8, 8),
5000,
443,
6,
0
)));
}
#[test]
fn test_filter_non_ip_fails_when_flow_filter_active() {
let mut f = Filter::default();
f.protocols = vec![6];
assert!(!f.matches(&no_flow_meta(0, 100)));
}
#[test]
fn test_filter_non_ip_passes_when_only_length_filter() {
let mut f = Filter::default();
f.min_len = Some(50);
assert!(f.matches(&no_flow_meta(0, 60)));
assert!(!f.matches(&no_flow_meta(0, 40)));
}
}