allow inflight filter
This commit is contained in:
+317
@@ -0,0 +1,317 @@
|
||||
use anyhow::{Result, bail};
|
||||
use std::net::IpAddr;
|
||||
|
||||
/// The only link-layer type the IP peek understands. Anything else matches
|
||||
/// nothing -- see `matches`.
|
||||
pub const LINKTYPE_ETHERNET: u32 = 1;
|
||||
|
||||
const ETH_HEADER_LEN: usize = 14;
|
||||
const ETHERTYPE_IPV4: u16 = 0x0800;
|
||||
const ETHERTYPE_IPV6: u16 = 0x86DD;
|
||||
const ETHERTYPE_VLAN: u16 = 0x8100;
|
||||
const ETHERTYPE_QINQ: u16 = 0x88A8;
|
||||
const ETHERTYPE_QINQ_ALT: u16 = 0x9100;
|
||||
/// 802.1Q and one layer of QinQ. Deeper stacking is real but vanishingly
|
||||
/// rare in capture files, and the bound keeps a corrupt/truncated packet
|
||||
/// from walking off into the payload looking for an ethertype.
|
||||
const MAX_VLAN_TAGS: usize = 2;
|
||||
|
||||
const IPV4_HEADER_MIN: usize = 20;
|
||||
const IPV6_HEADER_LEN: usize = 40;
|
||||
|
||||
/// A set of IP addresses/prefixes to keep. A packet matches if *either* its
|
||||
/// source or destination address falls in *any* entry.
|
||||
///
|
||||
/// Sized for the expected 1-4 entries: two plain vectors, split by address
|
||||
/// family so a v4 packet never compares against a v6 entry, scanned
|
||||
/// linearly. No sorting (a prefix match isn't an equality test, so ordering
|
||||
/// buys nothing) and no hashing.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IpFilter {
|
||||
/// Set when no `--ip` was given: every packet is kept, without looking
|
||||
/// at it at all. This is the default so that the filtering code path is
|
||||
/// the *only* code path -- see the chunker.
|
||||
match_all: bool,
|
||||
/// (network, mask) pairs, network already masked at parse time.
|
||||
v4: Vec<(u32, u32)>,
|
||||
v6: Vec<(u128, u128)>,
|
||||
}
|
||||
|
||||
impl IpFilter {
|
||||
/// The no-`--ip` default: keeps everything, matches without inspecting.
|
||||
pub fn match_all() -> Self {
|
||||
Self { match_all: true, v4: Vec::new(), v6: Vec::new() }
|
||||
}
|
||||
|
||||
/// Parses `--ip` specs: a bare address (`1.1.1.1`, `2606:4700::1111`)
|
||||
/// or a CIDR prefix (`10.0.0.0/8`). An empty list yields `match_all`.
|
||||
pub fn parse(specs: &[String]) -> Result<Self> {
|
||||
if specs.is_empty() {
|
||||
return Ok(Self::match_all());
|
||||
}
|
||||
let mut f = Self { match_all: false, v4: Vec::new(), v6: Vec::new() };
|
||||
for spec in specs {
|
||||
let (addr_str, prefix_str) = match spec.split_once('/') {
|
||||
Some((a, p)) => (a, Some(p)),
|
||||
None => (spec.as_str(), None),
|
||||
};
|
||||
let addr: IpAddr = addr_str
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("--ip {spec:?}: not an IP address"))?;
|
||||
let width = if addr.is_ipv4() { 32u32 } else { 128u32 };
|
||||
let prefix = match prefix_str {
|
||||
Some(p) => p
|
||||
.parse::<u32>()
|
||||
.map_err(|_| anyhow::anyhow!("--ip {spec:?}: prefix length is not a number"))?,
|
||||
None => width,
|
||||
};
|
||||
if prefix > width {
|
||||
bail!("--ip {spec:?}: prefix length {prefix} exceeds {width} bits");
|
||||
}
|
||||
match addr {
|
||||
IpAddr::V4(v4) => {
|
||||
let mask = if prefix == 0 { 0 } else { u32::MAX << (32 - prefix) };
|
||||
f.v4.push((u32::from(v4) & mask, mask));
|
||||
}
|
||||
IpAddr::V6(v6) => {
|
||||
let mask = if prefix == 0 { 0 } else { u128::MAX << (128 - prefix) };
|
||||
f.v6.push((u128::from(v6) & mask, mask));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// Whether this filter keeps everything, i.e. no `--ip` was given.
|
||||
/// Callers use it to skip work (and warnings) that only a real filter
|
||||
/// needs.
|
||||
pub fn is_match_all(&self) -> bool {
|
||||
self.match_all
|
||||
}
|
||||
|
||||
/// Whether to keep `packet` -- the captured bytes of one pcap record,
|
||||
/// starting at the link-layer header.
|
||||
///
|
||||
/// Everything unmatched is dropped: non-IP frames (ARP, LLDP), packets
|
||||
/// truncated by snaplen before the addresses, and any link type other
|
||||
/// than Ethernet.
|
||||
pub fn matches(&self, packet: &[u8], linktype: u32) -> bool {
|
||||
if self.match_all {
|
||||
return true;
|
||||
}
|
||||
if linktype != LINKTYPE_ETHERNET || packet.len() < ETH_HEADER_LEN {
|
||||
return false;
|
||||
}
|
||||
let mut ethertype = be16(&packet[12..14]);
|
||||
let mut off = ETH_HEADER_LEN;
|
||||
let mut tags = 0;
|
||||
// A VLAN tag is TPID(2) + TCI(2); the TPID is the ethertype we just
|
||||
// read, so the *next* type field sits 2 bytes into the tag.
|
||||
while matches!(ethertype, ETHERTYPE_VLAN | ETHERTYPE_QINQ | ETHERTYPE_QINQ_ALT) {
|
||||
tags += 1;
|
||||
if tags > MAX_VLAN_TAGS || packet.len() < off + 4 {
|
||||
return false;
|
||||
}
|
||||
ethertype = be16(&packet[off + 2..off + 4]);
|
||||
off += 4;
|
||||
}
|
||||
match ethertype {
|
||||
ETHERTYPE_IPV4 => {
|
||||
if packet.len() < off + IPV4_HEADER_MIN || packet[off] >> 4 != 4 {
|
||||
return false;
|
||||
}
|
||||
let src = be32(&packet[off + 12..off + 16]);
|
||||
let dst = be32(&packet[off + 16..off + 20]);
|
||||
self.v4.iter().any(|&(net, mask)| src & mask == net || dst & mask == net)
|
||||
}
|
||||
ETHERTYPE_IPV6 => {
|
||||
if packet.len() < off + IPV6_HEADER_LEN || packet[off] >> 4 != 6 {
|
||||
return false;
|
||||
}
|
||||
let src = be128(&packet[off + 8..off + 24]);
|
||||
let dst = be128(&packet[off + 24..off + 40]);
|
||||
self.v6.iter().any(|&(net, mask)| src & mask == net || dst & mask == net)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn be16(b: &[u8]) -> u16 {
|
||||
u16::from_be_bytes(b.try_into().unwrap())
|
||||
}
|
||||
|
||||
fn be32(b: &[u8]) -> u32 {
|
||||
u32::from_be_bytes(b.try_into().unwrap())
|
||||
}
|
||||
|
||||
fn be128(b: &[u8]) -> u128 {
|
||||
u128::from_be_bytes(b.try_into().unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn specs(list: &[&str]) -> Vec<String> {
|
||||
list.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
fn filter(list: &[&str]) -> IpFilter {
|
||||
IpFilter::parse(&specs(list)).unwrap()
|
||||
}
|
||||
|
||||
/// An Ethernet frame with `vlan_tags` 802.1Q tags in front of `ethertype`.
|
||||
fn eth(vlan_tags: usize, ethertype: u16, payload: &[u8]) -> Vec<u8> {
|
||||
let mut f = vec![0u8; 12]; // dst + src MAC
|
||||
for _ in 0..vlan_tags {
|
||||
f.extend_from_slice(ÐERTYPE_VLAN.to_be_bytes());
|
||||
f.extend_from_slice(&[0x00, 0x64]); // TCI: VLAN 100
|
||||
}
|
||||
f.extend_from_slice(ðertype.to_be_bytes());
|
||||
f.extend_from_slice(payload);
|
||||
f
|
||||
}
|
||||
|
||||
fn ipv4(src: &str, dst: &str) -> Vec<u8> {
|
||||
let mut p = vec![0u8; IPV4_HEADER_MIN];
|
||||
p[0] = 0x45; // version 4, IHL 5
|
||||
let s: std::net::Ipv4Addr = src.parse().unwrap();
|
||||
let d: std::net::Ipv4Addr = dst.parse().unwrap();
|
||||
p[12..16].copy_from_slice(&s.octets());
|
||||
p[16..20].copy_from_slice(&d.octets());
|
||||
p
|
||||
}
|
||||
|
||||
fn ipv6(src: &str, dst: &str) -> Vec<u8> {
|
||||
let mut p = vec![0u8; IPV6_HEADER_LEN];
|
||||
p[0] = 0x60; // version 6
|
||||
let s: std::net::Ipv6Addr = src.parse().unwrap();
|
||||
let d: std::net::Ipv6Addr = dst.parse().unwrap();
|
||||
p[8..24].copy_from_slice(&s.octets());
|
||||
p[24..40].copy_from_slice(&d.octets());
|
||||
p
|
||||
}
|
||||
|
||||
fn v4_packet(src: &str, dst: &str) -> Vec<u8> {
|
||||
eth(0, ETHERTYPE_IPV4, &ipv4(src, dst))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_all_keeps_everything_including_non_ip_and_odd_linktypes() {
|
||||
let f = IpFilter::match_all();
|
||||
assert!(f.is_match_all());
|
||||
assert!(f.matches(&[], 113));
|
||||
assert!(f.matches(ð(0, 0x0806, &[1, 2, 3]), LINKTYPE_ETHERNET));
|
||||
// An empty --ip list is the same thing.
|
||||
assert!(IpFilter::parse(&[]).unwrap().is_match_all());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_v4_matches_either_direction_only() {
|
||||
let f = filter(&["1.1.1.1"]);
|
||||
assert!(!f.is_match_all());
|
||||
assert!(f.matches(&v4_packet("1.1.1.1", "9.9.9.9"), LINKTYPE_ETHERNET));
|
||||
assert!(f.matches(&v4_packet("9.9.9.9", "1.1.1.1"), LINKTYPE_ETHERNET));
|
||||
assert!(!f.matches(&v4_packet("9.9.9.9", "8.8.8.8"), LINKTYPE_ETHERNET));
|
||||
// Neighbouring address must not match a bare (/32) entry.
|
||||
assert!(!f.matches(&v4_packet("1.1.1.2", "9.9.9.9"), LINKTYPE_ETHERNET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_v6_matches_either_direction() {
|
||||
let f = filter(&["2606:4700::1111"]);
|
||||
let pkt = eth(0, ETHERTYPE_IPV6, &ipv6("2001:db8::1", "2606:4700::1111"));
|
||||
assert!(f.matches(&pkt, LINKTYPE_ETHERNET));
|
||||
let miss = eth(0, ETHERTYPE_IPV6, &ipv6("2001:db8::1", "2606:4700::1112"));
|
||||
assert!(!f.matches(&miss, LINKTYPE_ETHERNET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cidr_boundaries_are_exact() {
|
||||
let f = filter(&["10.0.0.0/8"]);
|
||||
assert!(f.matches(&v4_packet("10.0.0.0", "9.9.9.9"), LINKTYPE_ETHERNET));
|
||||
assert!(f.matches(&v4_packet("10.255.255.255", "9.9.9.9"), LINKTYPE_ETHERNET));
|
||||
assert!(!f.matches(&v4_packet("11.0.0.0", "9.9.9.9"), LINKTYPE_ETHERNET));
|
||||
assert!(!f.matches(&v4_packet("9.255.255.255", "9.9.9.9"), LINKTYPE_ETHERNET));
|
||||
|
||||
// /0 keeps every packet of that family, but still not non-IP.
|
||||
let all = filter(&["0.0.0.0/0"]);
|
||||
assert!(all.matches(&v4_packet("203.0.113.9", "9.9.9.9"), LINKTYPE_ETHERNET));
|
||||
assert!(!all.matches(ð(0, 0x0806, &[0u8; 28]), LINKTYPE_ETHERNET));
|
||||
|
||||
let v6 = filter(&["2606:4700::/32"]);
|
||||
assert!(v6.matches(ð(0, ETHERTYPE_IPV6, &ipv6("2606:4700:dead::1", "::1")), LINKTYPE_ETHERNET));
|
||||
assert!(!v6.matches(ð(0, ETHERTYPE_IPV6, &ipv6("2606:4701::1", "::1")), LINKTYPE_ETHERNET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn families_do_not_cross_match() {
|
||||
// A v4-only filter must never keep a v6 packet, and vice versa --
|
||||
// the split vectors mean the comparison is never even attempted.
|
||||
let v4only = filter(&["1.1.1.1"]);
|
||||
assert!(!v4only.matches(ð(0, ETHERTYPE_IPV6, &ipv6("::1", "::2")), LINKTYPE_ETHERNET));
|
||||
let v6only = filter(&["::1"]);
|
||||
assert!(!v6only.matches(&v4_packet("1.1.1.1", "9.9.9.9"), LINKTYPE_ETHERNET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vlan_tags_are_skipped_single_and_double() {
|
||||
let f = filter(&["1.1.1.1"]);
|
||||
let one = eth(1, ETHERTYPE_IPV4, &ipv4("1.1.1.1", "9.9.9.9"));
|
||||
let two = eth(2, ETHERTYPE_IPV4, &ipv4("9.9.9.9", "1.1.1.1"));
|
||||
assert!(f.matches(&one, LINKTYPE_ETHERNET));
|
||||
assert!(f.matches(&two, LINKTYPE_ETHERNET));
|
||||
// Beyond the tag bound we stop rather than hunt through the payload.
|
||||
let three = eth(3, ETHERTYPE_IPV4, &ipv4("1.1.1.1", "9.9.9.9"));
|
||||
assert!(!f.matches(&three, LINKTYPE_ETHERNET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unparseable_packets_are_dropped() {
|
||||
let f = filter(&["1.1.1.1"]);
|
||||
// ARP.
|
||||
assert!(!f.matches(ð(0, 0x0806, &[0u8; 28]), LINKTYPE_ETHERNET));
|
||||
// Truncated before the addresses (snaplen too small).
|
||||
let full = v4_packet("1.1.1.1", "9.9.9.9");
|
||||
assert!(!f.matches(&full[..ETH_HEADER_LEN + 12], LINKTYPE_ETHERNET));
|
||||
// Shorter than an Ethernet header, and empty.
|
||||
assert!(!f.matches(&full[..8], LINKTYPE_ETHERNET));
|
||||
assert!(!f.matches(&[], LINKTYPE_ETHERNET));
|
||||
// Right ethertype, wrong version nibble.
|
||||
let mut bad_version = full.clone();
|
||||
bad_version[ETH_HEADER_LEN] = 0x65;
|
||||
assert!(!f.matches(&bad_version, LINKTYPE_ETHERNET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ethernet_linktypes_match_nothing() {
|
||||
let f = filter(&["1.1.1.1"]);
|
||||
let pkt = v4_packet("1.1.1.1", "9.9.9.9");
|
||||
// 113 = LINUX_SLL, what `tcpdump -i any` produces; 101 = RAW; 0 = NULL.
|
||||
for linktype in [0u32, 101, 113, 276] {
|
||||
assert!(!f.matches(&pkt, linktype), "linktype {linktype} should match nothing");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_specs_are_rejected_at_parse() {
|
||||
for spec in ["1.1.1.1/33", "::1/129", "not-an-ip", "1.1.1.1/x", "1.1.1.256"] {
|
||||
assert!(
|
||||
IpFilter::parse(&specs(&[spec])).is_err(),
|
||||
"{spec:?} should be rejected"
|
||||
);
|
||||
}
|
||||
// /0 and full-width prefixes are fine.
|
||||
assert!(IpFilter::parse(&specs(&["1.1.1.1/32", "0.0.0.0/0", "::/0", "::1/128"])).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_entries_are_all_considered() {
|
||||
let f = filter(&["1.1.1.1", "8.8.8.8", "10.0.0.0/8", "2606:4700::/32"]);
|
||||
assert!(f.matches(&v4_packet("8.8.8.8", "9.9.9.9"), LINKTYPE_ETHERNET));
|
||||
assert!(f.matches(&v4_packet("9.9.9.9", "10.1.2.3"), LINKTYPE_ETHERNET));
|
||||
assert!(f.matches(ð(0, ETHERTYPE_IPV6, &ipv6("::1", "2606:4700::1")), LINKTYPE_ETHERNET));
|
||||
assert!(!f.matches(&v4_packet("9.9.9.9", "203.0.113.1"), LINKTYPE_ETHERNET));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user