use crate::Timestamp;
use crate::extractor::L4Proto;
use crate::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"))]
pub enum OverflowPolicy {
#[default]
SlidingWindow,
DropFlow,
}
#[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,
}
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()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
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,
},
}
impl AnomalyKind {
pub fn short_kind(&self) -> &'static str {
crate::obs::anomaly_label(self)
}
}
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,
}
}
}
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",
})
}
}
#[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,
ts: Timestamp,
l4: Option<L4Proto>,
},
Packet {
key: K,
side: FlowSide,
len: usize,
ts: Timestamp,
},
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,
len: 100,
ts: Timestamp::default(),
};
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));
}
}