use bitflags::bitflags;
use crate::{
Timestamp,
extractor::{L4Proto, Orientation},
history::HistoryString,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum FlowSide {
Initiator,
Responder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum EndReason {
Fin,
Rst,
IdleTimeout,
Evicted,
BufferOverflow,
ParseError,
ParserDone,
ForceClosed,
}
impl EndReason {
pub fn as_str(&self) -> &'static str {
crate::obs::reason_label(*self)
}
pub fn as_zeek_state(&self) -> &'static str {
match self {
EndReason::Fin | EndReason::ParserDone => "SF",
EndReason::Rst => "RSTO",
EndReason::BufferOverflow => "S0",
EndReason::ParseError => "REJ",
EndReason::IdleTimeout | EndReason::Evicted | EndReason::ForceClosed => "OTH",
}
}
}
impl std::fmt::Display for EndReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum OverflowPolicy {
#[default]
SlidingWindow,
DropFlow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum TcpOverlapPolicy {
#[default]
First,
Last,
LowerSeq,
HigherSeq,
}
impl TcpOverlapPolicy {
pub fn as_str(&self) -> &'static str {
match self {
TcpOverlapPolicy::First => "first",
TcpOverlapPolicy::Last => "last",
TcpOverlapPolicy::LowerSeq => "lower_seq",
TcpOverlapPolicy::HigherSeq => "higher_seq",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum MemcapPolicy {
#[default]
Ignore,
DropFlow,
DropPacket,
PassThrough,
}
impl MemcapPolicy {
pub fn as_str(&self) -> &'static str {
match self {
MemcapPolicy::Ignore => "ignore",
MemcapPolicy::DropFlow => "drop_flow",
MemcapPolicy::DropPacket => "drop_packet",
MemcapPolicy::PassThrough => "pass_through",
}
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct FlowStats {
pub packets_initiator: u64,
pub packets_responder: u64,
pub bytes_initiator: u64,
pub bytes_responder: u64,
pub started: Timestamp,
pub last_seen: Timestamp,
pub reassembly_dropped_ooo_initiator: u64,
pub reassembly_dropped_ooo_responder: u64,
pub reassembly_bytes_dropped_oversize_initiator: u64,
pub reassembly_bytes_dropped_oversize_responder: u64,
pub reassembler_high_watermark_initiator: u64,
pub reassembler_high_watermark_responder: u64,
pub retransmits_initiator: u64,
pub retransmits_responder: u64,
pub last_seen_initiator: Timestamp,
pub last_seen_responder: Timestamp,
pub iat_flow: crate::correlate::WelfordStats,
pub iat_initiator: crate::correlate::WelfordStats,
pub iat_responder: crate::correlate::WelfordStats,
pub active_periods: crate::correlate::WelfordStats,
pub idle_periods: crate::correlate::WelfordStats,
pub active_period_start: Option<Timestamp>,
pub initiator_orientation: Orientation,
pub source_idx_forward: Option<u32>,
pub source_idx_reverse: Option<u32>,
pub capture_leg_inconsistent: bool,
pub direction_flipped: bool,
}
impl FlowStats {
#[inline]
pub fn total_bytes(&self) -> u64 {
self.bytes_initiator + self.bytes_responder
}
#[inline]
pub fn total_packets(&self) -> u64 {
self.packets_initiator + self.packets_responder
}
#[inline]
pub fn total_retransmits(&self) -> u64 {
self.retransmits_initiator + self.retransmits_responder
}
pub fn retransmit_rate(&self) -> f64 {
let total = self.total_packets();
if total == 0 {
0.0
} else {
self.total_retransmits() as f64 / total as f64
}
}
pub fn duration(&self) -> std::time::Duration {
self.last_seen.saturating_sub(self.started)
}
pub fn duration_secs(&self) -> f64 {
self.duration().as_secs_f64()
}
#[inline]
pub fn side_for(&self, orientation: Orientation) -> FlowSide {
if orientation == self.initiator_orientation {
FlowSide::Initiator
} else {
FlowSide::Responder
}
}
#[inline]
pub fn orientation_for(&self, side: FlowSide) -> Orientation {
match side {
FlowSide::Initiator => self.initiator_orientation,
FlowSide::Responder => self.initiator_orientation.flipped(),
}
}
#[inline]
pub fn source_idx_for(&self, orientation: Orientation) -> Option<u32> {
match orientation {
Orientation::Forward => self.source_idx_forward,
Orientation::Reverse => self.source_idx_reverse,
}
}
#[inline]
pub fn bytes_for(&self, side: FlowSide) -> u64 {
match side {
FlowSide::Initiator => self.bytes_initiator,
FlowSide::Responder => self.bytes_responder,
}
}
#[inline]
pub fn pkts_for(&self, side: FlowSide) -> u64 {
match side {
FlowSide::Initiator => self.packets_initiator,
FlowSide::Responder => self.packets_responder,
}
}
pub fn mean_pkt_size_for(&self, side: FlowSide) -> f64 {
let pkts = self.pkts_for(side);
if pkts == 0 {
return 0.0;
}
self.bytes_for(side) as f64 / pkts as f64
}
pub fn direction_skew(&self) -> f64 {
let total = self.total_bytes();
if total == 0 {
return 0.0;
}
let init = self.bytes_initiator as f64;
let resp = self.bytes_responder as f64;
(init - resp) / total as f64
}
pub fn throughput_bps(&self) -> f64 {
safe_div_u64(self.total_bytes(), self.duration_secs())
}
pub fn throughput_pps(&self) -> f64 {
safe_div_u64(self.total_packets(), self.duration_secs())
}
pub fn throughput_bps_for(&self, side: FlowSide) -> f64 {
safe_div_u64(self.bytes_for(side), self.duration_secs())
}
pub fn throughput_pps_for(&self, side: FlowSide) -> f64 {
safe_div_u64(self.pkts_for(side), self.duration_secs())
}
}
#[inline]
fn safe_div_u64(num: u64, den: f64) -> f64 {
if den > 0.0 { num as f64 / den } else { 0.0 }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum FlowState {
SynSent,
SynReceived,
Established,
FinWait,
ClosingTcp,
Active,
Closed,
Reset,
Aborted,
}
impl FlowState {
pub fn is_terminal(self) -> bool {
matches!(
self,
FlowState::Closed | FlowState::Reset | FlowState::Aborted
)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
#[non_exhaustive]
pub enum AnomalyKind {
BufferOverflow {
side: FlowSide,
bytes: u64,
policy: OverflowPolicy,
},
OutOfOrderSegment { side: FlowSide, count: u64 },
FlowTableEvictionPressure {
evicted_in_tick: u64,
evicted_total: u64,
},
SessionParseError {
side: FlowSide,
reason: Option<String>,
},
RetransmittedSegment { side: FlowSide, count: u64 },
ReassemblerHighWatermark {
side: FlowSide,
bytes: u64,
cap: u64,
threshold_pct: u8,
},
TcpRexmitInconsistency { side: FlowSide, count: u64 },
GlobalMemcapHit {
bytes_in_flight: u64,
cap: u64,
policy: MemcapPolicy,
},
}
impl AnomalyKind {
pub fn short_kind(&self) -> &'static str {
crate::obs::anomaly_label(self)
}
}
impl crate::AnomalyFields for AnomalyKind {
fn anomaly_type(&self) -> Option<&'static str> {
Some(match self {
AnomalyKind::BufferOverflow { .. }
| AnomalyKind::OutOfOrderSegment { .. }
| AnomalyKind::RetransmittedSegment { .. }
| AnomalyKind::TcpRexmitInconsistency { .. }
| AnomalyKind::ReassemblerHighWatermark { .. } => "stream",
AnomalyKind::SessionParseError { .. } => "applayer",
AnomalyKind::FlowTableEvictionPressure { .. } => "stream",
AnomalyKind::GlobalMemcapHit { .. } => "stream",
})
}
fn anomaly_event(&self) -> Option<&'static str> {
Some(self.short_kind())
}
}
impl std::fmt::Display for AnomalyKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(crate::obs::anomaly_label(self))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum Severity {
Info,
Warning,
Error,
Critical,
}
impl AnomalyKind {
pub fn severity(&self) -> Severity {
match self {
AnomalyKind::OutOfOrderSegment { .. } | AnomalyKind::RetransmittedSegment { .. } => {
Severity::Info
}
AnomalyKind::ReassemblerHighWatermark { .. }
| AnomalyKind::BufferOverflow { .. }
| AnomalyKind::FlowTableEvictionPressure { .. } => Severity::Warning,
AnomalyKind::SessionParseError { .. } => Severity::Error,
AnomalyKind::TcpRexmitInconsistency { .. } => Severity::Error,
AnomalyKind::GlobalMemcapHit { .. } => Severity::Critical,
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Severity::Info => "info",
Severity::Warning => "warning",
Severity::Error => "error",
Severity::Critical => "critical",
})
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct EventMask: u8 {
const STARTED = 1 << 0;
const PACKET = 1 << 1;
const ESTABLISHED = 1 << 2;
const STATE_CHANGE = 1 << 3;
const ENDED = 1 << 4;
const FLOW_ANOMALY = 1 << 5;
const TRACKER_ANOMALY = 1 << 6;
const TICK = 1 << 7;
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
#[cfg_attr(
feature = "serde",
serde(bound(
serialize = "K: serde::Serialize",
deserialize = "K: serde::de::DeserializeOwned"
))
)]
#[non_exhaustive]
pub enum FlowEvent<K> {
Started {
key: K,
side: FlowSide,
orientation: Orientation,
ts: Timestamp,
l4: Option<L4Proto>,
},
#[non_exhaustive]
Packet {
key: K,
side: FlowSide,
orientation: Orientation,
len: usize,
ts: Timestamp,
#[cfg_attr(feature = "serde", serde(default))]
source_idx: Option<u32>,
},
Established {
key: K,
ts: Timestamp,
l4: Option<L4Proto>,
},
StateChange {
key: K,
from: FlowState,
to: FlowState,
ts: Timestamp,
},
Ended {
key: K,
reason: EndReason,
stats: FlowStats,
history: HistoryString,
l4: Option<L4Proto>,
},
FlowAnomaly {
key: K,
kind: AnomalyKind,
ts: Timestamp,
},
TrackerAnomaly { kind: AnomalyKind, ts: Timestamp },
Tick {
key: K,
stats: FlowStats,
ts: Timestamp,
},
}
impl<K> FlowEvent<K> {
pub fn key(&self) -> Option<&K> {
match self {
FlowEvent::Started { key, .. }
| FlowEvent::Packet { key, .. }
| FlowEvent::Established { key, .. }
| FlowEvent::StateChange { key, .. }
| FlowEvent::Ended { key, .. }
| FlowEvent::FlowAnomaly { key, .. }
| FlowEvent::Tick { key, .. } => Some(key),
FlowEvent::TrackerAnomaly { .. } => None,
}
}
pub fn anomaly_kind(&self) -> Option<&AnomalyKind> {
match self {
FlowEvent::FlowAnomaly { kind, .. } | FlowEvent::TrackerAnomaly { kind, .. } => {
Some(kind)
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flow_state_terminal() {
assert!(FlowState::Closed.is_terminal());
assert!(FlowState::Reset.is_terminal());
assert!(FlowState::Aborted.is_terminal());
assert!(!FlowState::Active.is_terminal());
assert!(!FlowState::Established.is_terminal());
assert!(!FlowState::SynSent.is_terminal());
}
#[test]
fn flow_event_key_borrow() {
let evt: FlowEvent<u32> = FlowEvent::Packet {
key: 7,
side: FlowSide::Initiator,
orientation: Orientation::Forward,
len: 100,
ts: Timestamp::default(),
source_idx: None,
};
assert_eq!(evt.key().copied(), Some(7));
}
#[test]
fn flow_event_key_returns_none_for_global_anomaly() {
let evt: FlowEvent<u32> = FlowEvent::TrackerAnomaly {
kind: AnomalyKind::FlowTableEvictionPressure {
evicted_in_tick: 1,
evicted_total: 42,
},
ts: Timestamp::default(),
};
assert!(evt.key().is_none());
assert!(evt.anomaly_kind().is_some());
}
#[test]
fn flow_event_key_returns_some_for_per_flow_anomaly() {
let evt: FlowEvent<u32> = FlowEvent::FlowAnomaly {
key: 7,
kind: AnomalyKind::OutOfOrderSegment {
side: FlowSide::Initiator,
count: 3,
},
ts: Timestamp::default(),
};
assert_eq!(evt.key().copied(), Some(7));
}
fn skewed_stats(init_bytes: u64, init_pkts: u64, resp_bytes: u64, resp_pkts: u64) -> FlowStats {
FlowStats {
packets_initiator: init_pkts,
packets_responder: resp_pkts,
bytes_initiator: init_bytes,
bytes_responder: resp_bytes,
..FlowStats::default()
}
}
#[test]
fn bytes_for_returns_per_side_count() {
let s = skewed_stats(100, 5, 200, 8);
assert_eq!(s.bytes_for(FlowSide::Initiator), 100);
assert_eq!(s.bytes_for(FlowSide::Responder), 200);
}
#[test]
fn pkts_for_returns_per_side_count() {
let s = skewed_stats(100, 5, 200, 8);
assert_eq!(s.pkts_for(FlowSide::Initiator), 5);
assert_eq!(s.pkts_for(FlowSide::Responder), 8);
}
#[test]
fn mean_pkt_size_for_zero_packets_returns_zero() {
let s = skewed_stats(0, 0, 200, 8);
assert_eq!(s.mean_pkt_size_for(FlowSide::Initiator), 0.0);
assert_eq!(s.mean_pkt_size_for(FlowSide::Responder), 25.0);
}
#[test]
fn mean_pkt_size_for_balanced_flow() {
let s = skewed_stats(100, 5, 200, 10);
assert_eq!(s.mean_pkt_size_for(FlowSide::Initiator), 20.0);
assert_eq!(s.mean_pkt_size_for(FlowSide::Responder), 20.0);
}
#[test]
fn direction_skew_empty_flow_returns_zero() {
let s = FlowStats::default();
assert_eq!(s.direction_skew(), 0.0);
}
#[test]
fn direction_skew_initiator_only_returns_one() {
let s = skewed_stats(100, 5, 0, 0);
assert_eq!(s.direction_skew(), 1.0);
}
#[test]
fn direction_skew_responder_only_returns_negative_one() {
let s = skewed_stats(0, 0, 200, 8);
assert_eq!(s.direction_skew(), -1.0);
}
#[test]
fn direction_skew_balanced_returns_zero() {
let s = skewed_stats(100, 5, 100, 5);
assert_eq!(s.direction_skew(), 0.0);
}
#[test]
fn direction_skew_two_to_one_initiator_heavy() {
let s = skewed_stats(200, 5, 100, 5);
assert!((s.direction_skew() - 1.0 / 3.0).abs() < 1e-12);
}
#[test]
fn direction_skew_within_unit_range() {
for (init, resp) in [(1u64, 0u64), (0, 1), (1, 1), (1_000_000, 1), (1, 1_000_000)] {
let s = skewed_stats(init, 1, resp, 1);
let skew = s.direction_skew();
assert!(
(-1.0..=1.0).contains(&skew),
"skew {skew} out of [-1, 1] for ({init}, {resp})"
);
}
}
fn stats_with_duration(
init_bytes: u64,
init_pkts: u64,
resp_bytes: u64,
resp_pkts: u64,
secs: u32,
) -> FlowStats {
FlowStats {
packets_initiator: init_pkts,
packets_responder: resp_pkts,
bytes_initiator: init_bytes,
bytes_responder: resp_bytes,
started: crate::Timestamp::new(0, 0),
last_seen: crate::Timestamp::new(secs, 0),
..FlowStats::default()
}
}
#[test]
fn throughput_bps_lifetime_avg() {
let s = stats_with_duration(1000, 5, 500, 5, 10);
let bps = s.throughput_bps();
assert!((bps - 150.0).abs() < 1e-9, "expected 150.0 B/s, got {bps}");
}
#[test]
fn throughput_pps_lifetime_avg() {
let s = stats_with_duration(1000, 5, 500, 5, 10);
assert!((s.throughput_pps() - 1.0).abs() < 1e-9);
}
#[test]
fn throughput_bps_for_split_by_side() {
let s = stats_with_duration(1000, 5, 500, 5, 10);
assert!((s.throughput_bps_for(FlowSide::Initiator) - 100.0).abs() < 1e-9);
assert!((s.throughput_bps_for(FlowSide::Responder) - 50.0).abs() < 1e-9);
}
#[test]
fn throughput_pps_for_split_by_side() {
let s = stats_with_duration(1000, 5, 500, 8, 10);
assert!((s.throughput_pps_for(FlowSide::Initiator) - 0.5).abs() < 1e-9);
assert!((s.throughput_pps_for(FlowSide::Responder) - 0.8).abs() < 1e-9);
}
#[test]
fn throughput_zero_duration_flow_returns_zero_not_nan() {
let s = FlowStats {
packets_initiator: 1,
bytes_initiator: 1500,
started: crate::Timestamp::new(0, 0),
last_seen: crate::Timestamp::new(0, 0),
..FlowStats::default()
};
for v in [
s.throughput_bps(),
s.throughput_pps(),
s.throughput_bps_for(FlowSide::Initiator),
s.throughput_pps_for(FlowSide::Responder),
] {
assert!(v.is_finite(), "value {v} is not finite");
assert_eq!(v, 0.0);
}
}
#[test]
fn throughput_sides_sum_to_total() {
let s = stats_with_duration(1000, 5, 500, 5, 10);
let total = s.throughput_bps();
let by_side =
s.throughput_bps_for(FlowSide::Initiator) + s.throughput_bps_for(FlowSide::Responder);
assert!((total - by_side).abs() < 1e-9);
}
}