use arrayvec::ArrayVec;
pub const MAX_SAMPLES: usize = 32;
#[derive(Debug, Default, Clone)]
pub struct FingerprintBuilder {
len_seq: ArrayVec<u16, MAX_SAMPLES>,
iat_seq: ArrayVec<u32, MAX_SAMPLES>,
last_ts_micros: Option<u64>,
bytes_init: u64,
bytes_resp: u64,
pkts_init: u32,
pkts_resp: u32,
}
impl FingerprintBuilder {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn observe(&mut self, payload_len: usize, is_initiator: bool, ts_micros: u64) {
let len_u16 = payload_len.min(u16::MAX as usize) as u16;
if is_initiator {
self.bytes_init = self.bytes_init.saturating_add(payload_len as u64);
self.pkts_init = self.pkts_init.saturating_add(1);
} else {
self.bytes_resp = self.bytes_resp.saturating_add(payload_len as u64);
self.pkts_resp = self.pkts_resp.saturating_add(1);
}
let _ = self.len_seq.try_push(len_u16);
let iat = match self.last_ts_micros {
Some(prev) => ts_micros.saturating_sub(prev).min(u32::MAX as u64) as u32,
None => 0,
};
let _ = self.iat_seq.try_push(iat);
self.last_ts_micros = Some(ts_micros);
}
#[inline]
pub fn samples(&self) -> usize {
self.len_seq.len()
}
#[inline]
pub fn is_saturated(&self) -> bool {
self.len_seq.is_full()
}
pub fn finish(self) -> FlowFingerprint {
FlowFingerprint {
len_seq: self.len_seq,
iat_seq: self.iat_seq,
bytes_init: self.bytes_init,
bytes_resp: self.bytes_resp,
pkts_init: self.pkts_init,
pkts_resp: self.pkts_resp,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct FlowFingerprint {
pub len_seq: ArrayVec<u16, MAX_SAMPLES>,
pub iat_seq: ArrayVec<u32, MAX_SAMPLES>,
pub bytes_init: u64,
pub bytes_resp: u64,
pub pkts_init: u32,
pub pkts_resp: u32,
}
impl FlowFingerprint {
pub fn fnv1a(&self) -> u64 {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut h = FNV_OFFSET;
for v in &self.len_seq {
for b in v.to_be_bytes() {
h ^= b as u64;
h = h.wrapping_mul(FNV_PRIME);
}
}
for v in &self.iat_seq {
for b in v.to_be_bytes() {
h ^= b as u64;
h = h.wrapping_mul(FNV_PRIME);
}
}
h
}
pub fn as_features(&self) -> [f64; 8] {
let samples = self.len_seq.len() as f64;
let mean_len = if self.len_seq.is_empty() {
0.0
} else {
self.len_seq.iter().map(|&v| v as u64).sum::<u64>() as f64 / samples
};
let mean_iat = if self.iat_seq.is_empty() {
0.0
} else {
self.iat_seq.iter().map(|&v| v as u64).sum::<u64>() as f64 / (self.iat_seq.len() as f64)
};
let total = self.bytes_init + self.bytes_resp;
let skew = if total == 0 {
0.0
} else {
(self.bytes_init as f64 - self.bytes_resp as f64) / total as f64
};
[
samples,
mean_len,
mean_iat,
self.bytes_init as f64,
self.bytes_resp as f64,
self.pkts_init as f64,
self.pkts_resp as f64,
skew,
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_builder_finishes_to_empty_fingerprint() {
let fp = FingerprintBuilder::new().finish();
assert_eq!(fp.len_seq.len(), 0);
assert_eq!(fp.iat_seq.len(), 0);
assert_eq!(fp.bytes_init, 0);
assert_eq!(fp.bytes_resp, 0);
}
#[test]
fn single_observation_first_iat_is_zero() {
let mut b = FingerprintBuilder::new();
b.observe(100, true, 1_000_000);
let fp = b.finish();
assert_eq!(fp.len_seq.as_slice(), &[100]);
assert_eq!(fp.iat_seq.as_slice(), &[0]);
assert_eq!(fp.bytes_init, 100);
assert_eq!(fp.pkts_init, 1);
}
#[test]
fn iat_computed_from_consecutive_observations() {
let mut b = FingerprintBuilder::new();
b.observe(50, true, 1_000_000);
b.observe(60, false, 1_000_500); b.observe(70, true, 1_002_500); let fp = b.finish();
assert_eq!(fp.iat_seq.as_slice(), &[0, 500, 2000]);
}
#[test]
fn saturates_at_max_samples() {
let mut b = FingerprintBuilder::new();
for i in 0..(MAX_SAMPLES + 10) {
b.observe(i, true, i as u64);
}
assert!(b.is_saturated());
assert_eq!(b.samples(), MAX_SAMPLES);
let fp = b.finish();
assert_eq!(fp.pkts_init as usize, MAX_SAMPLES + 10);
}
#[test]
fn payload_len_clamps_at_u16_max() {
let mut b = FingerprintBuilder::new();
b.observe(usize::MAX, true, 0);
let fp = b.finish();
assert_eq!(fp.len_seq[0], u16::MAX);
assert_eq!(fp.bytes_init, usize::MAX as u64);
}
#[test]
fn direction_split_initiator_vs_responder() {
let mut b = FingerprintBuilder::new();
b.observe(100, true, 0);
b.observe(200, false, 100);
b.observe(50, true, 200);
let fp = b.finish();
assert_eq!(fp.bytes_init, 150);
assert_eq!(fp.bytes_resp, 200);
assert_eq!(fp.pkts_init, 2);
assert_eq!(fp.pkts_resp, 1);
}
#[test]
fn fnv1a_stable_across_identical_fingerprints() {
let mut a = FingerprintBuilder::new();
let mut b = FingerprintBuilder::new();
for i in 0..5 {
a.observe(100 + i, true, i as u64);
b.observe(100 + i, true, i as u64);
}
assert_eq!(a.finish().fnv1a(), b.finish().fnv1a());
}
#[test]
fn fnv1a_differs_for_distinct_sequences() {
let mut a = FingerprintBuilder::new();
let mut b = FingerprintBuilder::new();
a.observe(100, true, 0);
b.observe(101, true, 0);
assert_ne!(a.finish().fnv1a(), b.finish().fnv1a());
}
#[test]
fn fnv1a_differs_when_only_iat_changes() {
let mut a = FingerprintBuilder::new();
let mut b = FingerprintBuilder::new();
a.observe(100, true, 0);
a.observe(100, true, 1000);
b.observe(100, true, 0);
b.observe(100, true, 2000); assert_ne!(a.finish().fnv1a(), b.finish().fnv1a());
}
#[test]
fn as_features_layout() {
let mut b = FingerprintBuilder::new();
b.observe(100, true, 0);
b.observe(200, false, 1000);
let f = b.finish().as_features();
assert_eq!(f[0], 2.0); assert_eq!(f[1], 150.0); assert_eq!(f[2], 500.0); assert_eq!(f[3], 100.0); assert_eq!(f[4], 200.0); assert_eq!(f[5], 1.0); assert_eq!(f[6], 1.0); assert!((f[7] - (-1.0 / 3.0)).abs() < 1e-12);
}
#[test]
fn as_features_empty_returns_all_zero() {
let fp = FingerprintBuilder::new().finish();
let f = fp.as_features();
assert!(f.iter().all(|&v| v == 0.0));
}
#[test]
fn observe_does_not_allocate_after_first_few_packets() {
let mut b = FingerprintBuilder::new();
for i in 0..MAX_SAMPLES {
b.observe(i, true, i as u64);
}
assert!(b.is_saturated());
b.observe(9999, true, 9999);
assert!(b.is_saturated());
assert_eq!(b.samples(), MAX_SAMPLES);
}
}