use super::expr::{Comparison, FilterExpr};
const INET_DIAG_BC_S_GE: u8 = 2;
const INET_DIAG_BC_S_LE: u8 = 3;
const INET_DIAG_BC_D_GE: u8 = 4;
const INET_DIAG_BC_D_LE: u8 = 5;
pub const INET_DIAG_REQ_BYTECODE: u16 = 1;
struct PortPrimitive {
code: u8,
port: u16,
}
fn lower_port(is_source: bool, cmp: Comparison, port: u16) -> Option<Vec<PortPrimitive>> {
let (ge, le) = if is_source {
(INET_DIAG_BC_S_GE, INET_DIAG_BC_S_LE)
} else {
(INET_DIAG_BC_D_GE, INET_DIAG_BC_D_LE)
};
Some(match cmp {
Comparison::Eq => vec![
PortPrimitive { code: ge, port },
PortPrimitive { code: le, port },
],
Comparison::Ge => vec![PortPrimitive { code: ge, port }],
Comparison::Le => vec![PortPrimitive { code: le, port }],
Comparison::Gt => vec![PortPrimitive {
code: ge,
port: port.checked_add(1)?,
}],
Comparison::Lt => vec![PortPrimitive {
code: le,
port: port.checked_sub(1)?,
}],
Comparison::Ne => return None,
})
}
fn emit(prims: &[PortPrimitive]) -> Vec<u8> {
let total = prims.len() * 8;
let mut bc = Vec::with_capacity(total);
for (i, p) in prims.iter().enumerate() {
let pos = i * 8;
let remaining = (total - pos) as u16;
let no = remaining + 4;
bc.push(p.code);
bc.push(8);
bc.extend_from_slice(&no.to_ne_bytes());
bc.push(0);
bc.push(0);
bc.extend_from_slice(&p.port.to_ne_bytes());
}
bc
}
pub fn for_ports(sport: Option<u16>, dport: Option<u16>) -> Option<Vec<u8>> {
let mut prims = Vec::new();
if let Some(s) = sport {
prims.extend(lower_port(true, Comparison::Eq, s)?);
}
if let Some(d) = dport {
prims.extend(lower_port(false, Comparison::Eq, d)?);
}
if prims.is_empty() {
return None;
}
Some(emit(&prims))
}
fn collect(expr: &FilterExpr, out: &mut Vec<PortPrimitive>) -> Option<()> {
match expr {
FilterExpr::And(a, b) => {
collect(a, out)?;
collect(b, out)?;
Some(())
}
FilterExpr::Sport(cmp, port) => {
out.extend(lower_port(true, *cmp, *port)?);
Some(())
}
FilterExpr::Dport(cmp, port) => {
out.extend(lower_port(false, *cmp, *port)?);
Some(())
}
_ => None,
}
}
pub fn compile(expr: &FilterExpr) -> Option<Vec<u8>> {
let mut prims = Vec::new();
collect(expr, &mut prims)?;
if prims.is_empty() {
return None;
}
Some(emit(&prims))
}
#[cfg(test)]
mod tests {
use super::*;
fn op(bytes: &[u8], i: usize) -> (u8, u8, u16) {
let b = &bytes[i * 4..i * 4 + 4];
(b[0], b[1], u16::from_ne_bytes([b[2], b[3]]))
}
#[test]
fn sport_eq_compiles_to_ge_le_range() {
let expr = FilterExpr::parse("sport = :22").unwrap();
let bc = compile(&expr).unwrap();
assert_eq!(bc.len(), 16);
assert_eq!(op(&bc, 0), (INET_DIAG_BC_S_GE, 8, 20));
assert_eq!(op(&bc, 1), (0, 0, 22));
assert_eq!(op(&bc, 2), (INET_DIAG_BC_S_LE, 8, 12));
assert_eq!(op(&bc, 3), (0, 0, 22));
}
#[test]
fn dport_gt_uses_ge_plus_one() {
let bc = compile(&FilterExpr::parse("dport > :1024").unwrap()).unwrap();
assert_eq!(bc.len(), 8);
assert_eq!(op(&bc, 0), (INET_DIAG_BC_D_GE, 8, 12));
assert_eq!(op(&bc, 1), (0, 0, 1025));
}
#[test]
fn sport_lt_uses_le_minus_one() {
let bc = compile(&FilterExpr::parse("sport < :1024").unwrap()).unwrap();
assert_eq!(op(&bc, 0), (INET_DIAG_BC_S_LE, 8, 12));
assert_eq!(op(&bc, 1), (0, 0, 1023));
}
#[test]
fn and_chain_offsets_decrease() {
let bc = compile(&FilterExpr::parse("sport = :22 and dport >= :80").unwrap()).unwrap();
assert_eq!(bc.len(), 24);
assert_eq!(op(&bc, 0), (INET_DIAG_BC_S_GE, 8, 28));
assert_eq!(op(&bc, 2), (INET_DIAG_BC_S_LE, 8, 20));
assert_eq!(op(&bc, 4), (INET_DIAG_BC_D_GE, 8, 12));
for i in [0usize, 2, 4] {
assert_eq!(op(&bc, i).2 % 4, 0);
}
}
#[test]
fn for_ports_builds_exact_match_program() {
let bc = for_ports(Some(22), None).unwrap();
assert_eq!(bc.len(), 16);
assert_eq!(op(&bc, 0), (INET_DIAG_BC_S_GE, 8, 20));
assert_eq!(op(&bc, 2), (INET_DIAG_BC_S_LE, 8, 12));
let bc = for_ports(Some(22), Some(443)).unwrap();
assert_eq!(bc.len(), 32);
assert_eq!(op(&bc, 0).0, INET_DIAG_BC_S_GE);
assert_eq!(op(&bc, 4).0, INET_DIAG_BC_D_GE);
assert_eq!(op(&bc, 6), (INET_DIAG_BC_D_LE, 8, 12));
assert!(for_ports(None, None).is_none());
}
#[test]
fn unsupported_exprs_return_none() {
assert!(compile(&FilterExpr::parse("dst 10.0.0.0/8").unwrap()).is_none());
assert!(compile(&FilterExpr::parse("state established").unwrap()).is_none());
assert!(compile(&FilterExpr::parse("sport = :22 or sport = :80").unwrap()).is_none());
assert!(compile(&FilterExpr::parse("not sport = :22").unwrap()).is_none());
assert!(compile(&FilterExpr::parse("sport != :22").unwrap()).is_none());
}
#[test]
fn boundary_ports_that_cannot_match_return_none() {
assert!(compile(&FilterExpr::parse("dport > :65535").unwrap()).is_none());
assert!(compile(&FilterExpr::parse("sport < :0").unwrap()).is_none());
}
}