use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use flowscope::L4Proto;
use flowscope::PacketView;
use flowscope::driver::Event as FsEvent;
use flowscope::extract::FiveTuple;
use crate::AsyncCapture;
use crate::anomaly::sink::AnomalySink;
use crate::ctx::{CounterRegistry, Ctx, SourceIdx, StateMap};
use crate::error::Result;
use crate::monitor::backend::AnyBackend;
use crate::monitor::dispatcher::Dispatcher;
use crate::monitor::subscription::PacketSubscription;
use crate::monitor::subscription::packet::{PacketFields, packet_field_extractor};
use crate::monitor::{BackendErrorPolicy, HandlerErrorPolicy, Monitor};
use crate::protocol::FlowKey;
#[cfg(feature = "icmp")]
use crate::protocol::builtin::Icmp;
use crate::protocol::builtin::{Tcp, Udp};
use crate::protocol::event_typed::{
AnyFlowAnomaly, FlowEnded, FlowEstablished, FlowPacket, FlowStarted, FlowTick, ParserClosed,
TcpRst, Tick,
};
use std::time::SystemTime;
pub(crate) enum StopCondition {
Deadline(Instant),
Signal,
Idle(Duration),
}
enum BackendSpec {
AfPacket(String),
#[cfg(feature = "af-xdp")]
Xdp(crate::monitor::XdpIfaceSpec),
}
fn open_backend(
spec: &BackendSpec,
fanout: Option<(crate::config::FanoutMode, u16)>,
kernel_prefilter: &Option<crate::config::BpfFilter>,
) -> Result<AnyBackend> {
match spec {
BackendSpec::AfPacket(iface) => {
let cap = match fanout {
Some((mode, group_id)) => {
let rx = crate::Capture::builder()
.interface(iface)
.fanout(mode, group_id)
.build()?;
AsyncCapture::new(rx)?
}
None => AsyncCapture::open(iface)?,
};
if let Some(filter) = kernel_prefilter {
cap.set_filter(filter)?;
}
Ok(AnyBackend::AfPacket(cap))
}
#[cfg(feature = "af-xdp")]
BackendSpec::Xdp(xspec) => Ok(AnyBackend::Xdp(open_xdp_backend(xspec)?)),
}
}
#[cfg(feature = "af-xdp")]
fn open_xdp_backend(spec: &crate::monitor::XdpIfaceSpec) -> Result<crate::AsyncXdpSocket> {
#[cfg(feature = "xdp-loader")]
if spec.self_load {
let socket = crate::XdpSocketBuilder::default()
.interface(&spec.iface)
.mode(crate::XdpMode::Rx)
.with_default_program()
.build()?;
return crate::AsyncXdpSocket::new(socket);
}
crate::AsyncXdpSocket::open(&spec.iface)
}
pub(crate) async fn run_loop(monitor: Monitor, stop: StopCondition) -> Result<()> {
let Monitor {
interfaces,
#[cfg(feature = "af-xdp")]
xdp_interfaces,
mut driver,
mut dispatcher,
mut protocol_slots,
mut state_map,
mut counters,
mut sink,
mut tick_handlers,
detector_names: _,
monitor_name,
drain_timeout,
broadcast_handles: _,
#[cfg(all(feature = "pcap", feature = "tokio"))]
pcap_source_path: _,
#[cfg(all(feature = "pcap", feature = "tokio"))]
pcap_speed_factor: _,
mut flow_states,
fanout,
label_table,
mut merge_rx,
handler_error_policy,
backend_error_policy,
mut capture_stats,
health,
mut flow_exporters,
flow_active_timeout,
packet_subs,
kernel_prefilter,
} = monitor;
let monitor_name_borrow: Option<&str> = monitor_name.as_deref();
let pkt_extractor = packet_field_extractor();
let mut specs: Vec<BackendSpec> = Vec::new();
for iface in &interfaces {
specs.push(BackendSpec::AfPacket(iface.clone()));
}
#[cfg(feature = "af-xdp")]
for spec in &xdp_interfaces {
specs.push(BackendSpec::Xdp(spec.clone()));
}
let mut caps: Vec<AnyBackend> = Vec::with_capacity(specs.len());
for spec in &specs {
caps.push(open_backend(spec, fanout, &kernel_prefilter)?);
}
health.mark_started();
health.mark_sockets_open();
let mut events: Vec<FsEvent<FlowKey>> = Vec::with_capacity(64);
let mut shutdown = ShutdownSignal::new(stop);
let mut rr_anchor: usize = 0;
let mut backend_errors: u64 = 0;
let mut last_event_at = Instant::now();
let mut tick_intervals: Vec<tokio::time::Interval> = tick_handlers
.iter()
.map(|t| {
let mut int =
tokio::time::interval_at(tokio::time::Instant::now() + t.period, t.period);
int.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
int
})
.collect();
let mut telemetry_interval = capture_stats.as_ref().map(|reg| {
let mut int =
tokio::time::interval_at(tokio::time::Instant::now() + reg.period, reg.period);
int.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
int
});
let mut telemetry_sampler =
crate::monitor::telemetry::TelemetrySampler::new(if capture_stats.is_some() {
caps.len()
} else {
0
});
let mut active_export = flow_active_timeout
.filter(|_| !flow_exporters.is_empty())
.map(|period| {
let mut int = tokio::time::interval_at(tokio::time::Instant::now() + period, period);
int.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
(int, period)
});
let mut last_active_export: std::collections::HashMap<FlowKey, flowscope::Timestamp> =
std::collections::HashMap::new();
loop {
let ready = tokio::select! {
biased;
_ = shutdown.recv(last_event_at) => break,
idx = ready_capture(&mut caps, &mut rr_anchor) => idx,
tick_idx = next_tick(&mut tick_intervals), if !tick_intervals.is_empty() => {
last_event_at = Instant::now();
fire_tick(
tick_idx,
&mut tick_handlers,
&mut dispatcher,
sink.as_mut(),
&mut state_map,
&mut counters,
monitor_name_borrow,
&mut flow_states,
&label_table,
)
.await?;
health.record_event(driver.tracker().flow_count());
continue;
}
req = recv_merge(&mut merge_rx), if merge_rx.is_some() => {
if let Some(req) = req {
let taken = state_map.take_dyn(req.type_id);
let _ = req.reply.send(taken);
}
continue;
}
_ = next_telemetry_sample(&mut telemetry_interval),
if telemetry_interval.is_some() =>
{
if let Some(reg) = capture_stats.as_mut() {
sample_and_fire_capture_stats(
&caps,
&mut telemetry_sampler,
reg,
sink.as_mut(),
&mut state_map,
&mut counters,
monitor_name_borrow,
&mut flow_states,
&label_table,
&health,
)?;
}
continue;
}
_ = next_active_export(&mut active_export), if active_export.is_some() => {
if let Some((_, period)) = active_export.as_ref() {
emit_active_flow_records(
&driver,
&mut flow_exporters,
&mut last_active_export,
*period,
);
}
continue;
}
};
let i = match ready {
Some((i, Ok(()))) => i,
Some((i, Err(e))) => match backend_error_policy {
BackendErrorPolicy::FailFast => return Err(e),
BackendErrorPolicy::SkipSource => {
backend_errors += 1;
health.record_backend_error();
tracing::warn!(error = %e, count = backend_errors, "capture backend error (SkipSource)");
if backend_errors > 64 {
return Err(e);
}
tokio::time::sleep(Duration::from_millis(50)).await;
continue;
}
BackendErrorPolicy::Reopen => {
backend_errors += 1;
health.record_backend_error();
match open_backend(&specs[i], fanout, &kernel_prefilter) {
Ok(b) => {
caps[i] = b;
tracing::warn!(error = %e, idx = i, count = backend_errors, "capture backend error (Reopen) — source reopened");
}
Err(e2) => {
tracing::warn!(error = %e, reopen_error = %e2, idx = i, count = backend_errors, "capture backend error (Reopen) — reopen failed, will retry");
}
}
if backend_errors > 64 {
return Err(e);
}
tokio::time::sleep(Duration::from_millis(50)).await;
continue;
}
},
None => break, };
backend_errors = 0; let source = SourceIdx(i as u8);
last_event_at = Instant::now();
events.clear();
let mut packet_err: Option<crate::error::Error> = None;
let last_ts = caps[i]
.drain_batch(|view| {
if !packet_subs.is_empty()
&& packet_err.is_none()
&& let Err(e) = dispatch_packet_subs(
&packet_subs,
view,
&pkt_extractor,
sink.as_mut(),
&mut state_map,
&mut counters,
&mut flow_states,
&label_table,
source,
monitor_name_borrow,
handler_error_policy,
&health,
)
{
packet_err = Some(e);
}
driver.track_into(view, &mut events)
})
.await?;
if let Some(e) = packet_err {
return Err(e);
}
let Some(ts) = last_ts else { continue };
dispatch_tracked_events(
&mut dispatcher,
sink.as_mut(),
&mut state_map,
&mut counters,
&mut events,
source,
monitor_name_borrow,
&mut flow_states,
&label_table,
handler_error_policy,
&mut flow_exporters,
&health,
)
.await?;
drain_protocol_slots(
&mut dispatcher,
&mut protocol_slots,
&driver,
sink.as_mut(),
&mut state_map,
&mut counters,
&mut flow_states,
ts,
source,
monitor_name_borrow,
&label_table,
handler_error_policy,
&health,
)?;
health.record_event(driver.tracker().flow_count());
}
if !drain_timeout.is_zero() {
let deadline = Instant::now() + drain_timeout;
drain_phase(
&mut driver,
&mut dispatcher,
sink.as_mut(),
&mut state_map,
&mut counters,
&mut protocol_slots,
monitor_name_borrow,
deadline,
&mut flow_states,
&label_table,
handler_error_policy,
&mut flow_exporters,
&health,
)
.await?;
}
for exporter in flow_exporters.iter_mut() {
let _ = exporter.flush();
}
Ok(())
}
#[cfg(all(feature = "pcap", feature = "tokio"))]
pub(crate) async fn replay_loop(
monitor: Monitor,
path: std::path::PathBuf,
config: crate::pcap_source::AsyncPcapConfig,
) -> Result<()> {
use std::pin::Pin;
use futures_core::Stream;
let Monitor {
interfaces: _,
#[cfg(feature = "af-xdp")]
xdp_interfaces: _,
mut driver,
mut dispatcher,
mut protocol_slots,
mut state_map,
mut counters,
mut sink,
tick_handlers: _,
detector_names: _,
monitor_name,
drain_timeout,
broadcast_handles: _,
pcap_source_path: _,
pcap_speed_factor: _,
mut flow_states,
fanout: _,
label_table,
merge_rx: _, handler_error_policy,
backend_error_policy: _, capture_stats: _, health,
mut flow_exporters,
flow_active_timeout: _, packet_subs,
kernel_prefilter: _,
} = monitor;
let monitor_name_borrow: Option<&str> = monitor_name.as_deref();
let mut source = crate::pcap_source::AsyncPcapSource::open_with_config(&path, config).await?;
let mut events: Vec<FsEvent<FlowKey>> = Vec::with_capacity(64);
let pkt_extractor = packet_field_extractor();
health.mark_started();
health.mark_sockets_open();
loop {
let next = std::future::poll_fn(|cx| Pin::new(&mut source).poll_next(cx)).await;
let pkt = match next {
Some(Ok(p)) => p,
Some(Err(e)) => return Err(e),
None => break,
};
let view = flowscope::PacketView::new(&pkt.data, pkt.timestamp);
if !packet_subs.is_empty() {
dispatch_packet_subs(
&packet_subs,
view,
&pkt_extractor,
sink.as_mut(),
&mut state_map,
&mut counters,
&mut flow_states,
&label_table,
SourceIdx(0),
monitor_name_borrow,
handler_error_policy,
&health,
)?;
}
events.clear();
driver.track_into(view, &mut events);
dispatch_tracked_events(
&mut dispatcher,
sink.as_mut(),
&mut state_map,
&mut counters,
&mut events,
SourceIdx(0),
monitor_name_borrow,
&mut flow_states,
&label_table,
handler_error_policy,
&mut flow_exporters,
&health,
)
.await?;
drain_protocol_slots(
&mut dispatcher,
&mut protocol_slots,
&driver,
sink.as_mut(),
&mut state_map,
&mut counters,
&mut flow_states,
pkt.timestamp,
SourceIdx(0),
monitor_name_borrow,
&label_table,
handler_error_policy,
&health,
)?;
health.record_event(driver.tracker().flow_count());
}
if !drain_timeout.is_zero() {
let deadline = Instant::now() + drain_timeout;
drain_phase(
&mut driver,
&mut dispatcher,
sink.as_mut(),
&mut state_map,
&mut counters,
&mut protocol_slots,
monitor_name_borrow,
deadline,
&mut flow_states,
&label_table,
handler_error_policy,
&mut flow_exporters,
&health,
)
.await?;
}
for exporter in flow_exporters.iter_mut() {
let _ = exporter.flush();
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn drain_phase(
driver: &mut flowscope::driver::Driver<FiveTuple>,
dispatcher: &mut Dispatcher,
sink: &mut dyn AnomalySink,
state_map: &mut StateMap,
counters: &mut CounterRegistry,
protocol_slots: &mut [Box<dyn crate::monitor::ProtocolSlot>],
monitor_name: Option<&str>,
deadline: Instant,
flow_states: &mut crate::ctx::FlowStateRegistry,
label_table: &flowscope::well_known::LabelTable,
policy: HandlerErrorPolicy,
flow_exporters: &mut [Box<dyn crate::export::FlowExporter>],
health: &crate::monitor::health::HealthState,
) -> Result<()> {
let mut leftover: Vec<FsEvent<FlowKey>> = Vec::new();
driver.finish_into(&mut leftover);
for evt in leftover.drain(..) {
if Instant::now() >= deadline {
return Ok(());
}
if !flow_exporters.is_empty()
&& let FsEvent::FlowEnded {
key, stats, reason, ..
} = &evt
{
let record = crate::export::FlowRecord::from_ended(key, stats, *reason);
for exporter in flow_exporters.iter_mut() {
exporter.export(&record);
}
}
let res = match dispatch_lifecycle(
dispatcher,
sink,
state_map,
counters,
evt.clone(),
SourceIdx(0),
monitor_name,
flow_states,
label_table,
) {
Ok(()) => match dispatch_lifecycle_async(dispatcher, evt.clone()).await {
Ok(()) if dispatcher.effect_handler_count() > 0 => {
dispatch_lifecycle_effects(
dispatcher,
sink,
state_map,
counters,
evt,
SourceIdx(0),
monitor_name,
flow_states,
label_table,
)
.await
}
other => other,
},
Err(e) => Err(e),
};
if let Err(e) = res {
match policy {
HandlerErrorPolicy::Propagate => return Err(e),
HandlerErrorPolicy::Isolate => {
health.record_handler_error();
tracing::warn!(error = %e, "handler error isolated (drain)")
}
}
}
}
if Instant::now() >= deadline {
return Ok(());
}
let ts = flowscope::Timestamp::from_system_time(SystemTime::now());
for slot in protocol_slots.iter_mut() {
if Instant::now() >= deadline {
return Ok(());
}
let mut ctx = Ctx::new(
None,
ts,
SourceIdx(0),
state_map,
sink,
counters,
flow_states,
);
ctx.monitor_name = monitor_name;
ctx.label_table = label_table;
ctx.tracker = Some(driver.tracker());
if let Err(e) = slot.drain_and_dispatch(dispatcher, &mut ctx) {
match policy {
HandlerErrorPolicy::Propagate => return Err(e),
HandlerErrorPolicy::Isolate => {
health.record_handler_error();
tracing::warn!(error = %e, "handler error isolated (drain slot)")
}
}
}
}
if Instant::now() >= deadline {
return Ok(());
}
sink.flush().map_err(|e| {
crate::error::Error::Io(std::io::Error::new(e.kind(), format!("sink flush: {e}")))
})?;
Ok(())
}
async fn ready_capture(caps: &mut [AnyBackend], anchor: &mut usize) -> Option<(usize, Result<()>)> {
std::future::poll_fn(
|cx: &mut Context<'_>| -> Poll<Option<(usize, Result<()>)>> {
let n = caps.len();
if n == 0 {
return Poll::Ready(None);
}
let start = *anchor % n;
for offset in 0..n {
let i = (start + offset) % n;
match caps[i].poll_read_ready(cx) {
Poll::Ready(Ok(())) => {
*anchor = (i + 1) % n;
return Poll::Ready(Some((i, Ok(()))));
}
Poll::Ready(Err(e)) => {
*anchor = (i + 1) % n;
return Poll::Ready(Some((i, Err(e))));
}
Poll::Pending => {}
}
}
Poll::Pending
},
)
.await
}
async fn next_tick(intervals: &mut [tokio::time::Interval]) -> usize {
std::future::poll_fn(|cx: &mut Context<'_>| -> Poll<usize> {
for (i, interval) in intervals.iter_mut().enumerate() {
if interval.poll_tick(cx).is_ready() {
return Poll::Ready(i);
}
}
Poll::Pending
})
.await
}
async fn recv_merge(
rx: &mut Option<tokio::sync::mpsc::UnboundedReceiver<crate::monitor::merge::MergeRequest>>,
) -> Option<crate::monitor::merge::MergeRequest> {
match rx {
Some(r) => r.recv().await,
None => std::future::pending().await,
}
}
async fn next_telemetry_sample(interval: &mut Option<tokio::time::Interval>) {
match interval {
Some(int) => {
int.tick().await;
}
None => std::future::pending().await,
}
}
async fn next_active_export(slot: &mut Option<(tokio::time::Interval, Duration)>) {
match slot {
Some((int, _)) => {
int.tick().await;
}
None => std::future::pending().await,
}
}
fn emit_active_flow_records(
driver: &flowscope::driver::Driver<FiveTuple>,
exporters: &mut [Box<dyn crate::export::FlowExporter>],
last_export: &mut std::collections::HashMap<FlowKey, flowscope::Timestamp>,
period: Duration,
) {
use crate::export::FlowRecord;
let now = flowscope::Timestamp::from_system_time(std::time::SystemTime::now());
let snapshot: Vec<(FlowKey, flowscope::FlowStats)> = driver
.tracker()
.iter_active()
.map(|af| (*af.key, af.stats.clone()))
.collect();
let mut live: std::collections::HashSet<FlowKey> =
std::collections::HashSet::with_capacity(snapshot.len());
for (key, stats) in &snapshot {
live.insert(*key);
let last = last_export.get(key).copied().unwrap_or(stats.started);
if now.saturating_sub(last) >= period {
let rec = FlowRecord::from_active(key, stats);
for ex in exporters.iter_mut() {
ex.export(&rec);
}
last_export.insert(*key, now);
}
}
last_export.retain(|k, _| live.contains(k));
}
#[allow(clippy::too_many_arguments)]
fn sample_and_fire_capture_stats(
caps: &[AnyBackend],
sampler: &mut crate::monitor::telemetry::TelemetrySampler,
reg: &mut crate::monitor::telemetry::CaptureStatsRegistration,
sink: &mut dyn AnomalySink,
state_map: &mut StateMap,
counters: &mut CounterRegistry,
monitor_name: Option<&str>,
flow_states: &mut crate::ctx::FlowStateRegistry,
label_table: &flowscope::well_known::LabelTable,
health: &crate::monitor::health::HealthState,
) -> Result<()> {
let now = flowscope::Timestamp::from_system_time(SystemTime::now());
let mut total_packets: u64 = 0;
let mut total_drops: u64 = 0;
for (i, cap) in caps.iter().enumerate() {
let cum = match cap.cumulative_stats() {
Ok(s) => s,
Err(e) => {
tracing::warn!(
source = i,
error = %e,
"capture stats read failed; skipping telemetry sample for this source"
);
continue;
}
};
let telemetry = sampler.sample(i, cum);
total_packets += telemetry.packets;
total_drops += telemetry.drops;
let mut ctx = Ctx::new(
None,
now,
SourceIdx(i as u8),
state_map,
sink,
counters,
flow_states,
);
ctx.monitor_name = monitor_name;
ctx.label_table = label_table;
(reg.handler)(&telemetry, &mut ctx)?;
}
health.record_totals(total_packets, total_drops);
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn dispatch_tracked_events(
dispatcher: &mut Dispatcher,
sink: &mut dyn AnomalySink,
state_map: &mut StateMap,
counters: &mut CounterRegistry,
events: &mut Vec<FsEvent<FlowKey>>,
source: SourceIdx,
monitor_name: Option<&str>,
flow_states: &mut crate::ctx::FlowStateRegistry,
label_table: &flowscope::well_known::LabelTable,
policy: HandlerErrorPolicy,
flow_exporters: &mut [Box<dyn crate::export::FlowExporter>],
health: &crate::monitor::health::HealthState,
) -> Result<()> {
for evt in events.drain(..) {
if !flow_exporters.is_empty()
&& let FsEvent::FlowEnded {
key, stats, reason, ..
} = &evt
{
let record = crate::export::FlowRecord::from_ended(key, stats, *reason);
for exporter in flow_exporters.iter_mut() {
exporter.export(&record);
}
}
let res = match dispatch_lifecycle(
dispatcher,
sink,
state_map,
counters,
evt.clone(),
source,
monitor_name,
flow_states,
label_table,
) {
Ok(()) => match dispatch_lifecycle_async(dispatcher, evt.clone()).await {
Ok(()) if dispatcher.effect_handler_count() > 0 => {
dispatch_lifecycle_effects(
dispatcher,
sink,
state_map,
counters,
evt,
source,
monitor_name,
flow_states,
label_table,
)
.await
}
other => other,
},
Err(e) => Err(e),
};
if let Err(e) = res {
match policy {
HandlerErrorPolicy::Propagate => return Err(e),
HandlerErrorPolicy::Isolate => {
health.record_handler_error();
tracing::warn!(error = %e, "handler error isolated (per-event)")
}
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn drain_protocol_slots(
dispatcher: &mut Dispatcher,
protocol_slots: &mut [Box<dyn crate::monitor::ProtocolSlot>],
driver: &flowscope::driver::Driver<FiveTuple>,
sink: &mut dyn AnomalySink,
state_map: &mut StateMap,
counters: &mut CounterRegistry,
flow_states: &mut crate::ctx::FlowStateRegistry,
ts: flowscope::Timestamp,
source: SourceIdx,
monitor_name: Option<&str>,
label_table: &flowscope::well_known::LabelTable,
policy: HandlerErrorPolicy,
health: &crate::monitor::health::HealthState,
) -> Result<()> {
for slot in protocol_slots.iter_mut() {
let mut ctx = Ctx::new(None, ts, source, state_map, sink, counters, flow_states);
ctx.monitor_name = monitor_name;
ctx.label_table = label_table;
ctx.tracker = Some(driver.tracker());
if let Err(e) = slot.drain_and_dispatch(dispatcher, &mut ctx) {
match policy {
HandlerErrorPolicy::Propagate => return Err(e),
HandlerErrorPolicy::Isolate => {
health.record_handler_error();
tracing::warn!(error = %e, "handler error isolated (per-slot)")
}
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn fire_tick(
tick_idx: usize,
tick_handlers: &mut [crate::monitor::tick::TickRegistration],
dispatcher: &mut Dispatcher,
sink: &mut dyn AnomalySink,
state_map: &mut StateMap,
counters: &mut CounterRegistry,
monitor_name: Option<&str>,
flow_states: &mut crate::ctx::FlowStateRegistry,
label_table: &flowscope::well_known::LabelTable,
) -> Result<()> {
let reg = &mut tick_handlers[tick_idx];
let tick = Tick {
now: flowscope::Timestamp::from_system_time(SystemTime::now()),
period: reg.period,
};
{
let mut ctx = Ctx::new(
None,
tick.now,
SourceIdx(0),
state_map,
sink,
counters,
flow_states,
);
ctx.monitor_name = monitor_name;
ctx.label_table = label_table;
(reg.handler)(&tick, &mut ctx)?;
}
{
let mut ctx = Ctx::new(
None,
tick.now,
SourceIdx(0),
state_map,
sink,
counters,
flow_states,
);
ctx.monitor_name = monitor_name;
ctx.label_table = label_table;
dispatcher.dispatch::<Tick>(&tick, &mut ctx)?;
}
dispatcher.dispatch_async::<Tick>(&tick).await?;
Ok(())
}
struct ShutdownSignal {
stop: StopCondition,
sig_int: Option<tokio::signal::unix::Signal>,
sig_term: Option<tokio::signal::unix::Signal>,
}
impl ShutdownSignal {
fn new(stop: StopCondition) -> Self {
let (sig_int, sig_term) = match &stop {
StopCondition::Signal => {
let sigint =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()).ok();
let sigterm =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).ok();
(sigint, sigterm)
}
StopCondition::Deadline(_) | StopCondition::Idle(_) => (None, None),
};
Self {
stop,
sig_int,
sig_term,
}
}
async fn recv(&mut self, last_event_at: Instant) {
match &mut self.stop {
StopCondition::Deadline(t) => {
tokio::time::sleep_until((*t).into()).await;
}
StopCondition::Idle(window) => {
tokio::time::sleep_until((last_event_at + *window).into()).await;
}
StopCondition::Signal => match (self.sig_int.as_mut(), self.sig_term.as_mut()) {
(Some(i), Some(t)) => {
tokio::select! {
_ = i.recv() => {},
_ = t.recv() => {},
}
}
(Some(i), None) => {
let _ = i.recv().await;
}
(None, Some(t)) => {
let _ = t.recv().await;
}
(None, None) => std::future::pending::<()>().await,
},
}
}
}
async fn dispatch_lifecycle_async(
dispatcher: &mut Dispatcher,
evt: FsEvent<FlowKey>,
) -> Result<()> {
match evt {
FsEvent::FlowStarted { key, ts, l4 } => match l4 {
Some(L4Proto::Tcp) => {
dispatcher
.dispatch_async(&FlowStarted::<Tcp>::new(key, l4, ts))
.await?;
}
Some(L4Proto::Udp) => {
dispatcher
.dispatch_async(&FlowStarted::<Udp>::new(key, l4, ts))
.await?;
}
#[cfg(feature = "icmp")]
Some(L4Proto::Icmp) | Some(L4Proto::IcmpV6) => {
dispatcher
.dispatch_async(&FlowStarted::<Icmp>::new(key, l4, ts))
.await?;
}
_ => {}
},
FsEvent::FlowEnded {
key,
reason,
stats,
ts,
l4,
..
} => match l4 {
Some(L4Proto::Tcp) => {
let is_rst = reason == flowscope::EndReason::Rst;
dispatcher
.dispatch_async(&FlowEnded::<Tcp>::new(key, reason, stats.clone(), l4, ts))
.await?;
if is_rst {
dispatcher
.dispatch_async(&TcpRst::new(key, stats, ts))
.await?;
}
}
Some(L4Proto::Udp) => {
dispatcher
.dispatch_async(&FlowEnded::<Udp>::new(key, reason, stats, l4, ts))
.await?;
}
#[cfg(feature = "icmp")]
Some(L4Proto::Icmp) | Some(L4Proto::IcmpV6) => {
dispatcher
.dispatch_async(&FlowEnded::<Icmp>::new(key, reason, stats, l4, ts))
.await?;
}
_ => {}
},
FsEvent::FlowEstablished { key, ts, l4 } => {
if matches!(l4, Some(L4Proto::Tcp)) {
dispatcher
.dispatch_async(&FlowEstablished::<Tcp>::new(key, ts))
.await?;
}
}
FsEvent::FlowAnomaly { key, kind, ts } => {
dispatcher
.dispatch_async(&AnyFlowAnomaly {
key: Some(key),
kind,
ts,
})
.await?;
}
FsEvent::TrackerAnomaly { kind, ts } => {
dispatcher
.dispatch_async(&AnyFlowAnomaly {
key: None,
kind,
ts,
})
.await?;
}
FsEvent::FlowPacket {
key,
side,
len,
ts,
tcp,
} => {
dispatcher
.dispatch_async(&FlowPacket::new(key.proto, key, side, len, tcp, ts))
.await?;
}
FsEvent::FlowTick { key, stats, ts } => match key.proto {
L4Proto::Tcp => {
dispatcher
.dispatch_async(&FlowTick::<Tcp>::new(key, stats, ts))
.await?;
}
L4Proto::Udp => {
dispatcher
.dispatch_async(&FlowTick::<Udp>::new(key, stats, ts))
.await?;
}
#[cfg(feature = "icmp")]
L4Proto::Icmp | L4Proto::IcmpV6 => {
dispatcher
.dispatch_async(&FlowTick::<Icmp>::new(key, stats, ts))
.await?;
}
_ => {}
},
FsEvent::ParserClosed {
key,
parser_kind,
reason,
ts,
} => match key.proto {
L4Proto::Tcp => {
dispatcher
.dispatch_async(&ParserClosed::<Tcp>::new(key, parser_kind, reason, ts))
.await?;
}
L4Proto::Udp => {
dispatcher
.dispatch_async(&ParserClosed::<Udp>::new(key, parser_kind, reason, ts))
.await?;
}
#[cfg(feature = "icmp")]
L4Proto::Icmp | L4Proto::IcmpV6 => {
dispatcher
.dispatch_async(&ParserClosed::<Icmp>::new(key, parser_kind, reason, ts))
.await?;
}
_ => {}
},
_ => {}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn dispatch_packet_subs(
subs: &[PacketSubscription],
view: PacketView<'_>,
extractor: &FiveTuple,
sink: &mut dyn AnomalySink,
state_map: &mut StateMap,
counters: &mut CounterRegistry,
flow_states: &mut crate::ctx::FlowStateRegistry,
label_table: &flowscope::well_known::LabelTable,
source: SourceIdx,
monitor_name: Option<&str>,
policy: HandlerErrorPolicy,
health: &crate::monitor::health::HealthState,
) -> Result<()> {
let Some((key, fields)) = PacketFields::extract(view, extractor) else {
return Ok(());
};
let mut ctx = Ctx {
flow: Some(key),
ts: view.timestamp,
source,
monitor_name,
state_map,
sink,
counters,
flow_states,
label_table,
tracker: None,
};
for sub in subs {
if sub.predicate.eval(&fields)
&& let Err(e) = (sub.handler)(&view, &mut ctx)
{
match policy {
HandlerErrorPolicy::Propagate => return Err(e),
HandlerErrorPolicy::Isolate => {
health.record_handler_error();
tracing::warn!(error = %e, "packet-sub handler error isolated");
}
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn dispatch_lifecycle(
dispatcher: &mut Dispatcher,
sink: &mut dyn AnomalySink,
state_map: &mut StateMap,
counters: &mut CounterRegistry,
evt: FsEvent<FlowKey>,
source: SourceIdx,
monitor_name: Option<&str>,
flow_states: &mut crate::ctx::FlowStateRegistry,
label_table: &flowscope::well_known::LabelTable,
) -> Result<()> {
macro_rules! dispatch_one {
($ty:ty, $payload:expr, $flow:expr, $ts:expr) => {{
let mut ctx = Ctx {
flow: $flow,
ts: $ts,
source,
monitor_name,
state_map: &mut *state_map,
sink: &mut *sink,
counters: &mut *counters,
flow_states: &mut *flow_states,
label_table,
tracker: None,
};
dispatcher.dispatch::<$ty>(&$payload, &mut ctx)?;
}};
}
match evt {
FsEvent::FlowStarted { key, ts, l4 } => match l4 {
Some(L4Proto::Tcp) => {
dispatch_one!(
FlowStarted<Tcp>,
FlowStarted::<Tcp>::new(key, l4, ts),
Some(key),
ts
);
}
Some(L4Proto::Udp) => {
dispatch_one!(
FlowStarted<Udp>,
FlowStarted::<Udp>::new(key, l4, ts),
Some(key),
ts
);
}
#[cfg(feature = "icmp")]
Some(L4Proto::Icmp) | Some(L4Proto::IcmpV6) => {
dispatch_one!(
FlowStarted<Icmp>,
FlowStarted::<Icmp>::new(key, l4, ts),
Some(key),
ts
);
}
_ => {}
},
FsEvent::FlowEnded {
key,
reason,
stats,
ts,
l4,
..
} => match l4 {
Some(L4Proto::Tcp) => {
let is_rst = reason == flowscope::EndReason::Rst;
dispatch_one!(
FlowEnded<Tcp>,
FlowEnded::<Tcp>::new(key, reason, stats.clone(), l4, ts),
Some(key),
ts
);
if is_rst {
dispatch_one!(TcpRst, TcpRst::new(key, stats, ts), Some(key), ts);
}
}
Some(L4Proto::Udp) => {
dispatch_one!(
FlowEnded<Udp>,
FlowEnded::<Udp>::new(key, reason, stats, l4, ts),
Some(key),
ts
);
}
#[cfg(feature = "icmp")]
Some(L4Proto::Icmp) | Some(L4Proto::IcmpV6) => {
dispatch_one!(
FlowEnded<Icmp>,
FlowEnded::<Icmp>::new(key, reason, stats, l4, ts),
Some(key),
ts
);
}
_ => {}
},
FsEvent::FlowEstablished { key, ts, l4 } => {
if matches!(l4, Some(L4Proto::Tcp)) {
dispatch_one!(
FlowEstablished<Tcp>,
FlowEstablished::<Tcp>::new(key, ts),
Some(key),
ts
);
}
}
FsEvent::FlowAnomaly { key, kind, ts } => {
dispatch_one!(
AnyFlowAnomaly,
AnyFlowAnomaly {
key: Some(key),
kind,
ts,
},
Some(key),
ts
);
}
FsEvent::TrackerAnomaly { kind, ts } => {
dispatch_one!(
AnyFlowAnomaly,
AnyFlowAnomaly {
key: None,
kind,
ts,
},
None,
ts
);
}
FsEvent::FlowPacket {
key,
side,
len,
ts,
tcp,
} => {
dispatch_one!(
FlowPacket,
FlowPacket::new(key.proto, key, side, len, tcp, ts),
Some(key),
ts
);
}
FsEvent::FlowTick { key, stats, ts } => match key.proto {
L4Proto::Tcp => {
dispatch_one!(
FlowTick<Tcp>,
FlowTick::<Tcp>::new(key, stats, ts),
Some(key),
ts
);
}
L4Proto::Udp => {
dispatch_one!(
FlowTick<Udp>,
FlowTick::<Udp>::new(key, stats, ts),
Some(key),
ts
);
}
#[cfg(feature = "icmp")]
L4Proto::Icmp | L4Proto::IcmpV6 => {
dispatch_one!(
FlowTick<Icmp>,
FlowTick::<Icmp>::new(key, stats, ts),
Some(key),
ts
);
}
_ => {}
},
FsEvent::ParserClosed {
key,
parser_kind,
reason,
ts,
} => match key.proto {
L4Proto::Tcp => {
dispatch_one!(
ParserClosed<Tcp>,
ParserClosed::<Tcp>::new(key, parser_kind, reason, ts),
Some(key),
ts
);
}
L4Proto::Udp => {
dispatch_one!(
ParserClosed<Udp>,
ParserClosed::<Udp>::new(key, parser_kind, reason, ts),
Some(key),
ts
);
}
#[cfg(feature = "icmp")]
L4Proto::Icmp | L4Proto::IcmpV6 => {
dispatch_one!(
ParserClosed<Icmp>,
ParserClosed::<Icmp>::new(key, parser_kind, reason, ts),
Some(key),
ts
);
}
_ => {}
},
_ => {}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn dispatch_lifecycle_effects(
dispatcher: &mut Dispatcher,
sink: &mut dyn AnomalySink,
state_map: &mut StateMap,
counters: &mut CounterRegistry,
evt: FsEvent<FlowKey>,
source: SourceIdx,
monitor_name: Option<&str>,
flow_states: &mut crate::ctx::FlowStateRegistry,
label_table: &flowscope::well_known::LabelTable,
) -> Result<()> {
macro_rules! dispatch_one {
($ty:ty, $payload:expr, $flow:expr, $ts:expr) => {{
let mut ctx = Ctx {
flow: $flow,
ts: $ts,
source,
monitor_name,
state_map: &mut *state_map,
sink: &mut *sink,
counters: &mut *counters,
flow_states: &mut *flow_states,
label_table,
tracker: None,
};
dispatcher
.dispatch_effects::<$ty>(&$payload, &mut ctx)
.await?;
}};
}
match evt {
FsEvent::FlowStarted { key, ts, l4 } => match l4 {
Some(L4Proto::Tcp) => {
dispatch_one!(
FlowStarted<Tcp>,
FlowStarted::<Tcp>::new(key, l4, ts),
Some(key),
ts
);
}
Some(L4Proto::Udp) => {
dispatch_one!(
FlowStarted<Udp>,
FlowStarted::<Udp>::new(key, l4, ts),
Some(key),
ts
);
}
#[cfg(feature = "icmp")]
Some(L4Proto::Icmp) | Some(L4Proto::IcmpV6) => {
dispatch_one!(
FlowStarted<Icmp>,
FlowStarted::<Icmp>::new(key, l4, ts),
Some(key),
ts
);
}
_ => {}
},
FsEvent::FlowEnded {
key,
reason,
stats,
ts,
l4,
..
} => match l4 {
Some(L4Proto::Tcp) => {
let is_rst = reason == flowscope::EndReason::Rst;
dispatch_one!(
FlowEnded<Tcp>,
FlowEnded::<Tcp>::new(key, reason, stats.clone(), l4, ts),
Some(key),
ts
);
if is_rst {
dispatch_one!(TcpRst, TcpRst::new(key, stats, ts), Some(key), ts);
}
}
Some(L4Proto::Udp) => {
dispatch_one!(
FlowEnded<Udp>,
FlowEnded::<Udp>::new(key, reason, stats, l4, ts),
Some(key),
ts
);
}
#[cfg(feature = "icmp")]
Some(L4Proto::Icmp) | Some(L4Proto::IcmpV6) => {
dispatch_one!(
FlowEnded<Icmp>,
FlowEnded::<Icmp>::new(key, reason, stats, l4, ts),
Some(key),
ts
);
}
_ => {}
},
FsEvent::FlowEstablished { key, ts, l4 } => {
if matches!(l4, Some(L4Proto::Tcp)) {
dispatch_one!(
FlowEstablished<Tcp>,
FlowEstablished::<Tcp>::new(key, ts),
Some(key),
ts
);
}
}
FsEvent::FlowAnomaly { key, kind, ts } => {
dispatch_one!(
AnyFlowAnomaly,
AnyFlowAnomaly {
key: Some(key),
kind,
ts,
},
Some(key),
ts
);
}
FsEvent::TrackerAnomaly { kind, ts } => {
dispatch_one!(
AnyFlowAnomaly,
AnyFlowAnomaly {
key: None,
kind,
ts,
},
None,
ts
);
}
FsEvent::FlowPacket {
key,
side,
len,
ts,
tcp,
} => {
dispatch_one!(
FlowPacket,
FlowPacket::new(key.proto, key, side, len, tcp, ts),
Some(key),
ts
);
}
FsEvent::FlowTick { key, stats, ts } => match key.proto {
L4Proto::Tcp => {
dispatch_one!(
FlowTick<Tcp>,
FlowTick::<Tcp>::new(key, stats, ts),
Some(key),
ts
);
}
L4Proto::Udp => {
dispatch_one!(
FlowTick<Udp>,
FlowTick::<Udp>::new(key, stats, ts),
Some(key),
ts
);
}
#[cfg(feature = "icmp")]
L4Proto::Icmp | L4Proto::IcmpV6 => {
dispatch_one!(
FlowTick<Icmp>,
FlowTick::<Icmp>::new(key, stats, ts),
Some(key),
ts
);
}
_ => {}
},
FsEvent::ParserClosed {
key,
parser_kind,
reason,
ts,
} => match key.proto {
L4Proto::Tcp => {
dispatch_one!(
ParserClosed<Tcp>,
ParserClosed::<Tcp>::new(key, parser_kind, reason, ts),
Some(key),
ts
);
}
L4Proto::Udp => {
dispatch_one!(
ParserClosed<Udp>,
ParserClosed::<Udp>::new(key, parser_kind, reason, ts),
Some(key),
ts
);
}
#[cfg(feature = "icmp")]
L4Proto::Icmp | L4Proto::IcmpV6 => {
dispatch_one!(
ParserClosed<Icmp>,
ParserClosed::<Icmp>::new(key, parser_kind, reason, ts),
Some(key),
ts
);
}
_ => {}
},
_ => {}
}
Ok(())
}
#[cfg(test)]
mod active_export_tests {
use std::sync::{Arc, Mutex};
use std::time::Duration;
use flowscope::driver::Driver;
use flowscope::extract::FiveTuple;
use super::*;
use crate::export::{FlowExporter, FlowRecord};
struct Collect(Arc<Mutex<Vec<FlowRecord>>>);
impl FlowExporter for Collect {
fn export(&mut self, r: &FlowRecord) {
self.0.lock().unwrap().push(*r);
}
}
fn tcp_frame() -> Vec<u8> {
use etherparse::PacketBuilder;
let b = PacketBuilder::ethernet2([1, 2, 3, 4, 5, 6], [6, 5, 4, 3, 2, 1])
.ipv4([10, 0, 0, 1], [10, 0, 0, 2], 64)
.tcp(1234, 80, 0, 1024);
let mut frame = Vec::new();
b.write(&mut frame, &[]).unwrap();
frame
}
#[test]
fn emit_active_records_one_per_window_with_dedup() {
let mut driver = Driver::builder(FiveTuple::bidirectional()).build();
let frame = tcp_frame();
let ts = flowscope::Timestamp::from_unix_f64(1000.0);
let mut events = Vec::new();
driver.track_into(flowscope::PacketView::new(&frame, ts), &mut events);
assert!(driver.tracker().flow_count() >= 1, "flow should be tracked");
let sink = Arc::new(Mutex::new(Vec::new()));
let mut exporters: Vec<Box<dyn FlowExporter>> = vec![Box::new(Collect(sink.clone()))];
let mut last_export = std::collections::HashMap::new();
emit_active_flow_records(
&driver,
&mut exporters,
&mut last_export,
Duration::from_secs(1),
);
{
let recs = sink.lock().unwrap();
assert_eq!(recs.len(), 1, "one interim record for the live flow");
assert!(recs[0].is_ongoing(), "interim record has reason == None");
}
emit_active_flow_records(
&driver,
&mut exporters,
&mut last_export,
Duration::from_secs(1),
);
assert_eq!(
sink.lock().unwrap().len(),
1,
"dedup: no second record within the active window"
);
}
}