use std::hash::Hash;
use std::sync::Arc;
use std::time::Duration;
use crossbeam_queue::SegQueue;
use crate::PacketView;
use crate::Timestamp;
use crate::dedup::Dedup;
use crate::detect::signatures::SignatureFn;
use crate::event::{AnomalyKind, EndReason, FlowEvent, FlowSide, FlowStats};
use crate::extractor::{FlowExtractor, L4Proto, TcpInfo};
use crate::flow_driver::FlowDriver;
use crate::history::HistoryString;
use crate::reassembler::NoopReassemblerFactory;
use crate::session::{DatagramParser, SessionParser};
use crate::tracker::{FlowTracker, FlowTrackerConfig};
use super::BroadcastSlotHandle;
use super::slot::{SlotHandle, SlotMessage};
use super::typed_slot::{ErasedSlot, TypedConcreteDatagramSlot, TypedConcreteSlot};
use super::typed_slot_heuristic::{TypedHeuristicDatagramSlot, TypedHeuristicSessionSlot};
type IdleTimeoutFn<K> =
Box<dyn Fn(&K, Option<L4Proto>) -> Option<Duration> + Send + Sync + 'static>;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Event<K> {
FlowStarted {
key: K,
ts: Timestamp,
l4: Option<L4Proto>,
},
FlowEstablished {
key: K,
ts: Timestamp,
l4: Option<L4Proto>,
},
FlowPacket {
key: K,
side: FlowSide,
len: usize,
ts: Timestamp,
tcp: Option<TcpInfo>,
},
FlowEnded {
key: K,
reason: EndReason,
stats: FlowStats,
history: HistoryString,
l4: Option<L4Proto>,
ts: Timestamp,
},
FlowTick {
key: K,
stats: FlowStats,
ts: Timestamp,
},
ParserClosed {
key: K,
parser_kind: &'static str,
reason: EndReason,
ts: Timestamp,
},
FlowAnomaly {
key: K,
kind: AnomalyKind,
ts: Timestamp,
},
TrackerAnomaly { kind: AnomalyKind, ts: Timestamp },
}
impl<K> Event<K> {
pub fn key(&self) -> Option<&K> {
match self {
Event::FlowStarted { key, .. }
| Event::FlowEstablished { key, .. }
| Event::FlowPacket { key, .. }
| Event::FlowEnded { key, .. }
| Event::FlowTick { key, .. }
| Event::ParserClosed { key, .. }
| Event::FlowAnomaly { key, .. } => Some(key),
Event::TrackerAnomaly { .. } => None,
}
}
pub fn tcp(&self) -> Option<&TcpInfo> {
match self {
Event::FlowPacket { tcp, .. } => tcp.as_ref(),
_ => None,
}
}
pub fn timestamp(&self) -> Timestamp {
match self {
Event::FlowStarted { ts, .. }
| Event::FlowEstablished { ts, .. }
| Event::FlowPacket { ts, .. }
| Event::FlowEnded { ts, .. }
| Event::FlowTick { ts, .. }
| Event::ParserClosed { ts, .. }
| Event::FlowAnomaly { ts, .. }
| Event::TrackerAnomaly { ts, .. } => *ts,
}
}
}
pub struct Driver<E>
where
E: FlowExtractor,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
{
central: FlowDriver<E, NoopReassemblerFactory, ()>,
extractor: E,
emit_packet_details: bool,
slots: Vec<Box<dyn ErasedSlot<E::Key> + Send + Sync>>,
}
impl<E> Driver<E>
where
E: FlowExtractor + Clone + Send + 'static,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
{
pub fn builder(extractor: E) -> DriverBuilder<E> {
DriverBuilder {
extractor,
config: FlowTrackerConfig::default(),
monotonic_timestamps: false,
emit_anomalies: false,
emit_packet_details: false,
dedup: None,
idle_timeout_fn: None,
slots: Vec::new(),
}
}
pub fn deferred() -> DeferredDriverBuilder<E> {
DeferredDriverBuilder {
config: FlowTrackerConfig::default(),
monotonic_timestamps: false,
emit_anomalies: false,
emit_packet_details: false,
dedup: None,
idle_timeout_fn: None,
pending: Vec::new(),
}
}
pub fn track<'v>(&mut self, view: impl Into<PacketView<'v>>) -> Vec<Event<E::Key>> {
let mut out = Vec::new();
self.track_into(view, &mut out);
out
}
pub fn track_into<'v>(
&mut self,
view: impl Into<PacketView<'v>>,
out: &mut Vec<Event<E::Key>>,
) {
let view: PacketView<'v> = view.into();
let ts = view.timestamp;
let tcp_for_packet: Option<TcpInfo> = if self.emit_packet_details {
self.extractor.extract(view).and_then(|e| e.tcp)
} else {
None
};
let mut tcp_slot = tcp_for_packet;
for flow_ev in self.central.track(view).into_iter() {
let this_tcp = if matches!(flow_ev, FlowEvent::Packet { .. }) {
let t = tcp_slot;
tcp_slot = None;
t
} else {
None
};
if let Some(ev) = map_flow_event(flow_ev, this_tcp) {
out.push(ev);
}
}
for slot in &mut self.slots {
slot.track_into(view, ts, out);
}
}
pub fn sweep(&mut self, now: Timestamp) -> Vec<Event<E::Key>> {
let mut out = Vec::new();
self.sweep_into(now, &mut out);
out
}
pub fn sweep_into(&mut self, now: Timestamp, out: &mut Vec<Event<E::Key>>) {
for flow_ev in self.central.sweep(now) {
if let Some(ev) = map_flow_event(flow_ev, None) {
out.push(ev);
}
}
for slot in &mut self.slots {
slot.sweep_into(now, out);
}
}
pub fn finish(&mut self) -> Vec<Event<E::Key>> {
let mut out = Vec::new();
self.finish_into(&mut out);
out
}
pub fn finish_into(&mut self, out: &mut Vec<Event<E::Key>>) {
for flow_ev in self.central.finish() {
if let Some(ev) = map_flow_event(flow_ev, None) {
out.push(ev);
}
}
for slot in &mut self.slots {
slot.finish_into(out);
}
}
pub fn force_close(&mut self, key: &E::Key, now: Timestamp) -> Vec<Event<E::Key>> {
let mut out = Vec::new();
self.force_close_into(key, now, &mut out);
out
}
pub fn force_close_into(&mut self, key: &E::Key, now: Timestamp, out: &mut Vec<Event<E::Key>>) {
for slot in &mut self.slots {
slot.force_close_into(key, now, out);
}
for flow_ev in self.central.force_close(key, now) {
if let Some(ev) = map_flow_event(flow_ev, None) {
out.push(ev);
}
}
}
pub fn tracker(&self) -> &FlowTracker<E, ()> {
self.central.tracker()
}
pub fn tracker_mut(&mut self) -> &mut FlowTracker<E, ()> {
self.central.tracker_mut()
}
}
pub struct DriverBuilder<E>
where
E: FlowExtractor,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
{
extractor: E,
config: FlowTrackerConfig,
monotonic_timestamps: bool,
emit_anomalies: bool,
emit_packet_details: bool,
dedup: Option<Dedup>,
idle_timeout_fn: Option<IdleTimeoutFn<E::Key>>,
slots: Vec<Box<dyn ErasedSlot<E::Key> + Send + Sync>>,
}
impl<E> DriverBuilder<E>
where
E: FlowExtractor + Clone + Send + 'static,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
{
pub fn config(&mut self, c: FlowTrackerConfig) -> &mut Self {
self.config = c;
self
}
pub fn monotonic_timestamps(&mut self, on: bool) -> &mut Self {
self.monotonic_timestamps = on;
self
}
pub fn emit_packet_details(&mut self, on: bool) -> &mut Self {
self.emit_packet_details = on;
self
}
pub fn emit_anomalies(&mut self, on: bool) -> &mut Self {
self.emit_anomalies = on;
self
}
pub fn dedup(&mut self, dedup: Dedup) -> &mut Self {
self.dedup = Some(dedup);
self
}
pub fn idle_timeout_fn<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&E::Key, Option<L4Proto>) -> Option<Duration> + Send + Sync + 'static,
{
self.idle_timeout_fn = Some(Box::new(f));
self
}
pub fn session_on_ports<P, I>(&mut self, parser: P, ports: I) -> SlotHandle<P::Message, E::Key>
where
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
I: IntoIterator<Item = u16>,
{
let port_set: smallvec::SmallVec<[u16; 4]> = ports.into_iter().collect();
let (slot, handle) = TypedConcreteSlot::new(
self.extractor.clone(),
parser,
self.config.clone(),
Some(port_set),
self.monotonic_timestamps,
);
self.slots.push(Box::new(slot));
handle
}
pub fn session_on_ports_broadcast_each<P, I>(
&mut self,
parser: P,
ports: I,
) -> BroadcastSlotHandle<P::Message, E::Key>
where
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + Clone + 'static,
E::Key: Send + Sync + Clone + 'static,
I: IntoIterator<Item = u16>,
{
let port_set: smallvec::SmallVec<[u16; 4]> = ports.into_iter().collect();
let (slot, handle) = super::typed_slot::TypedBroadcastSlot::new(
self.extractor.clone(),
parser,
self.config.clone(),
Some(port_set),
self.monotonic_timestamps,
);
self.slots.push(Box::new(slot));
handle
}
pub fn session_broadcast<P>(&mut self, parser: P) -> SlotHandle<P::Message, E::Key>
where
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
{
let (slot, handle) = TypedConcreteSlot::new(
self.extractor.clone(),
parser,
self.config.clone(),
None,
self.monotonic_timestamps,
);
self.slots.push(Box::new(slot));
handle
}
pub fn session_heuristic<P>(
&mut self,
parser: P,
signature: SignatureFn,
) -> SlotHandle<P::Message, E::Key>
where
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
{
self.session_heuristic_with_budget(
parser,
signature,
super::typed_slot_heuristic::DEFAULT_PROBE_PACKETS,
)
}
pub fn session_heuristic_with_budget<P>(
&mut self,
parser: P,
signature: SignatureFn,
max_probe_packets: u8,
) -> SlotHandle<P::Message, E::Key>
where
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
{
let (slot, handle) = TypedHeuristicSessionSlot::new(
self.extractor.clone(),
parser,
self.config.clone(),
signature,
max_probe_packets,
self.monotonic_timestamps,
);
self.slots.push(Box::new(slot));
handle
}
pub fn datagram_on_ports<D, I>(&mut self, parser: D, ports: I) -> SlotHandle<D::Message, E::Key>
where
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
I: IntoIterator<Item = u16>,
{
let port_set: smallvec::SmallVec<[u16; 4]> = ports.into_iter().collect();
let (slot, handle) = TypedConcreteDatagramSlot::new(
self.extractor.clone(),
parser,
self.config.clone(),
Some(port_set),
self.monotonic_timestamps,
);
self.slots.push(Box::new(slot));
handle
}
pub fn datagram_broadcast<D>(&mut self, parser: D) -> SlotHandle<D::Message, E::Key>
where
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
{
let (slot, handle) = TypedConcreteDatagramSlot::new(
self.extractor.clone(),
parser,
self.config.clone(),
None,
self.monotonic_timestamps,
);
self.slots.push(Box::new(slot));
handle
}
pub fn datagram_heuristic<D>(
&mut self,
parser: D,
signature: SignatureFn,
) -> SlotHandle<D::Message, E::Key>
where
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
{
self.datagram_heuristic_with_budget(
parser,
signature,
super::typed_slot_heuristic::DEFAULT_PROBE_PACKETS,
)
}
pub fn datagram_heuristic_with_budget<D>(
&mut self,
parser: D,
signature: SignatureFn,
max_probe_packets: u8,
) -> SlotHandle<D::Message, E::Key>
where
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
{
let (slot, handle) = TypedHeuristicDatagramSlot::new(
self.extractor.clone(),
parser,
self.config.clone(),
signature,
max_probe_packets,
self.monotonic_timestamps,
);
self.slots.push(Box::new(slot));
handle
}
pub fn build(self) -> Driver<E> {
let mut central =
FlowDriver::with_config(self.extractor.clone(), NoopReassemblerFactory, self.config)
.with_emit_anomalies(self.emit_anomalies)
.with_monotonic_timestamps(self.monotonic_timestamps);
if let Some(d) = self.dedup {
central = central.with_dedup(d);
}
if let Some(f) = self.idle_timeout_fn {
central
.tracker_mut()
.set_idle_timeout_fn(move |k, l4| f(k, l4));
}
Driver {
central,
extractor: self.extractor,
emit_packet_details: self.emit_packet_details,
slots: self.slots,
}
}
}
type DeferredMaterialiser<E> = Box<
dyn FnOnce(
&E,
&FlowTrackerConfig,
bool,
) -> Box<dyn ErasedSlot<<E as FlowExtractor>::Key> + Send + Sync>
+ Send
+ Sync,
>;
pub struct DeferredDriverBuilder<E>
where
E: FlowExtractor,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
{
config: FlowTrackerConfig,
monotonic_timestamps: bool,
emit_anomalies: bool,
emit_packet_details: bool,
dedup: Option<Dedup>,
idle_timeout_fn: Option<IdleTimeoutFn<E::Key>>,
pending: Vec<DeferredMaterialiser<E>>,
}
impl<E> DeferredDriverBuilder<E>
where
E: FlowExtractor + Clone + Send + 'static,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
{
pub fn config(&mut self, c: FlowTrackerConfig) -> &mut Self {
self.config = c;
self
}
pub fn monotonic_timestamps(&mut self, on: bool) -> &mut Self {
self.monotonic_timestamps = on;
self
}
pub fn emit_packet_details(&mut self, on: bool) -> &mut Self {
self.emit_packet_details = on;
self
}
pub fn emit_anomalies(&mut self, on: bool) -> &mut Self {
self.emit_anomalies = on;
self
}
pub fn dedup(&mut self, dedup: Dedup) -> &mut Self {
self.dedup = Some(dedup);
self
}
pub fn idle_timeout_fn<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&E::Key, Option<L4Proto>) -> Option<Duration> + Send + Sync + 'static,
{
self.idle_timeout_fn = Some(Box::new(f));
self
}
pub fn session_on_ports<P, I>(&mut self, parser: P, ports: I) -> SlotHandle<P::Message, E::Key>
where
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
I: IntoIterator<Item = u16>,
{
let port_set: smallvec::SmallVec<[u16; 4]> = ports.into_iter().collect();
let (handle, mat) = make_session_materialiser(parser, Some(port_set));
self.pending.push(mat);
handle
}
pub fn session_broadcast<P>(&mut self, parser: P) -> SlotHandle<P::Message, E::Key>
where
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
{
let (handle, mat) = make_session_materialiser::<E, P>(parser, None);
self.pending.push(mat);
handle
}
pub fn session_heuristic<P>(
&mut self,
parser: P,
signature: SignatureFn,
) -> SlotHandle<P::Message, E::Key>
where
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
{
self.session_heuristic_with_budget(
parser,
signature,
super::typed_slot_heuristic::DEFAULT_PROBE_PACKETS,
)
}
pub fn session_heuristic_with_budget<P>(
&mut self,
parser: P,
signature: SignatureFn,
max_probe_packets: u8,
) -> SlotHandle<P::Message, E::Key>
where
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
{
let (handle, mat) =
make_session_heuristic_materialiser::<E, P>(parser, signature, max_probe_packets);
self.pending.push(mat);
handle
}
pub fn datagram_on_ports<D, I>(&mut self, parser: D, ports: I) -> SlotHandle<D::Message, E::Key>
where
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
I: IntoIterator<Item = u16>,
{
let port_set: smallvec::SmallVec<[u16; 4]> = ports.into_iter().collect();
let (handle, mat) = make_datagram_materialiser::<E, D>(parser, Some(port_set));
self.pending.push(mat);
handle
}
pub fn datagram_broadcast<D>(&mut self, parser: D) -> SlotHandle<D::Message, E::Key>
where
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
{
let (handle, mat) = make_datagram_materialiser::<E, D>(parser, None);
self.pending.push(mat);
handle
}
pub fn datagram_heuristic<D>(
&mut self,
parser: D,
signature: SignatureFn,
) -> SlotHandle<D::Message, E::Key>
where
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
{
self.datagram_heuristic_with_budget(
parser,
signature,
super::typed_slot_heuristic::DEFAULT_PROBE_PACKETS,
)
}
pub fn datagram_heuristic_with_budget<D>(
&mut self,
parser: D,
signature: SignatureFn,
max_probe_packets: u8,
) -> SlotHandle<D::Message, E::Key>
where
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
{
let (handle, mat) =
make_datagram_heuristic_materialiser::<E, D>(parser, signature, max_probe_packets);
self.pending.push(mat);
handle
}
pub fn build_with(self, extractor: E) -> Driver<E> {
let mut central = FlowDriver::with_config(
extractor.clone(),
NoopReassemblerFactory,
self.config.clone(),
)
.with_emit_anomalies(self.emit_anomalies)
.with_monotonic_timestamps(self.monotonic_timestamps);
if let Some(d) = self.dedup {
central = central.with_dedup(d);
}
if let Some(f) = self.idle_timeout_fn {
central
.tracker_mut()
.set_idle_timeout_fn(move |k, l4| f(k, l4));
}
let mut slots: Vec<Box<dyn ErasedSlot<E::Key> + Send + Sync>> =
Vec::with_capacity(self.pending.len());
for materialiser in self.pending {
slots.push(materialiser(
&extractor,
&self.config,
self.monotonic_timestamps,
));
}
Driver {
central,
extractor,
emit_packet_details: self.emit_packet_details,
slots,
}
}
}
fn make_session_materialiser<E, P>(
parser: P,
ports: Option<smallvec::SmallVec<[u16; 4]>>,
) -> (SlotHandle<P::Message, E::Key>, DeferredMaterialiser<E>)
where
E: FlowExtractor + Clone + Send + 'static,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
{
let parser_kind = parser.parser_kind();
let msg_buf: Arc<SegQueue<SlotMessage<P::Message, E::Key>>> = Arc::new(SegQueue::new());
let handle = SlotHandle {
inner: Arc::clone(&msg_buf),
parser_kind,
};
let materialiser: DeferredMaterialiser<E> = Box::new(move |ext, cfg, mono| {
let slot =
TypedConcreteSlot::with_queue(ext.clone(), parser, cfg.clone(), ports, mono, msg_buf);
Box::new(slot) as Box<dyn ErasedSlot<E::Key> + Send + Sync>
});
(handle, materialiser)
}
fn make_session_heuristic_materialiser<E, P>(
parser: P,
signature: SignatureFn,
max_probe_packets: u8,
) -> (SlotHandle<P::Message, E::Key>, DeferredMaterialiser<E>)
where
E: FlowExtractor + Clone + Send + 'static,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
P: SessionParser + Clone + Send + Sync + 'static,
P::Message: Send + Sync + 'static,
{
let parser_kind = parser.parser_kind();
let msg_buf: Arc<SegQueue<SlotMessage<P::Message, E::Key>>> = Arc::new(SegQueue::new());
let handle = SlotHandle {
inner: Arc::clone(&msg_buf),
parser_kind,
};
let materialiser: DeferredMaterialiser<E> = Box::new(move |ext, cfg, mono| {
let slot = TypedHeuristicSessionSlot::with_queue(
ext.clone(),
parser,
cfg.clone(),
signature,
max_probe_packets,
mono,
msg_buf,
);
Box::new(slot) as Box<dyn ErasedSlot<E::Key> + Send + Sync>
});
(handle, materialiser)
}
fn make_datagram_materialiser<E, D>(
parser: D,
ports: Option<smallvec::SmallVec<[u16; 4]>>,
) -> (SlotHandle<D::Message, E::Key>, DeferredMaterialiser<E>)
where
E: FlowExtractor + Clone + Send + 'static,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
{
let parser_kind = parser.parser_kind();
let msg_buf: Arc<SegQueue<SlotMessage<D::Message, E::Key>>> = Arc::new(SegQueue::new());
let handle = SlotHandle {
inner: Arc::clone(&msg_buf),
parser_kind,
};
let materialiser: DeferredMaterialiser<E> = Box::new(move |ext, cfg, mono| {
let slot = TypedConcreteDatagramSlot::with_queue(
ext.clone(),
parser,
cfg.clone(),
ports,
mono,
msg_buf,
);
Box::new(slot) as Box<dyn ErasedSlot<E::Key> + Send + Sync>
});
(handle, materialiser)
}
fn make_datagram_heuristic_materialiser<E, D>(
parser: D,
signature: SignatureFn,
max_probe_packets: u8,
) -> (SlotHandle<D::Message, E::Key>, DeferredMaterialiser<E>)
where
E: FlowExtractor + Clone + Send + 'static,
E::Key: Hash + Eq + Clone + Send + Sync + 'static,
D: DatagramParser + Clone + Send + Sync + 'static,
D::Message: Send + Sync + 'static,
{
let parser_kind = parser.parser_kind();
let msg_buf: Arc<SegQueue<SlotMessage<D::Message, E::Key>>> = Arc::new(SegQueue::new());
let handle = SlotHandle {
inner: Arc::clone(&msg_buf),
parser_kind,
};
let materialiser: DeferredMaterialiser<E> = Box::new(move |ext, cfg, mono| {
let slot = TypedHeuristicDatagramSlot::with_queue(
ext.clone(),
parser,
cfg.clone(),
signature,
max_probe_packets,
mono,
msg_buf,
);
Box::new(slot) as Box<dyn ErasedSlot<E::Key> + Send + Sync>
});
(handle, materialiser)
}
fn map_flow_event<K>(ev: FlowEvent<K>, tcp: Option<TcpInfo>) -> Option<Event<K>> {
match ev {
FlowEvent::Started { key, ts, l4, .. } => Some(Event::FlowStarted { key, ts, l4 }),
FlowEvent::Established { key, ts, l4 } => Some(Event::FlowEstablished { key, ts, l4 }),
FlowEvent::Packet { key, side, len, ts } => Some(Event::FlowPacket {
key,
side,
len,
ts,
tcp,
}),
FlowEvent::Ended {
key,
reason,
stats,
history,
l4,
} => {
let ts = stats.last_seen;
Some(Event::FlowEnded {
key,
reason,
stats,
history,
l4,
ts,
})
}
FlowEvent::Tick { key, stats, ts } => Some(Event::FlowTick { key, stats, ts }),
FlowEvent::FlowAnomaly { key, kind, ts } => Some(Event::FlowAnomaly { key, kind, ts }),
FlowEvent::TrackerAnomaly { kind, ts } => Some(Event::TrackerAnomaly { kind, ts }),
FlowEvent::StateChange { .. } => None,
}
}