flowscope/session.rs
1//! Pluggable L7 message parsers.
2//!
3//! Two trait families:
4//!
5//! - [`SessionParser`] — for **stream-based** protocols (HTTP/1, TLS,
6//! DNS-over-TCP). One parser per session; receives bytes via
7//! `feed_initiator` / `feed_responder`; returns a `Vec` of typed
8//! messages every call. Pair with `netring::SessionStream` to get
9//! an async stream of L7 events.
10//!
11//! - [`DatagramParser`] — for **packet-based** protocols (DNS-over-UDP,
12//! syslog, NTP, SNMP). Receives one L4 payload at a time. Pair with
13//! `netring::DatagramStream`.
14//!
15//! Both trait shapes return owned `Vec<Message>` rather than borrowed
16//! iterators or `SmallVec` to keep the public API stable across
17//! versions of `smallvec` etc. The per-call allocation is amortized
18//! across many bytes worth of work.
19//!
20//! # SessionParser vs `Reassembler`
21//!
22//! [`crate::Reassembler`] is the lower-level hook: one instance per
23//! `(flow, side)`, receives raw TCP segments, callback-driven via
24//! a user-supplied handler. `SessionParser` is the higher-level
25//! abstraction: one instance per flow, two `feed_*` methods,
26//! returns typed messages directly. Pick whichever fits your
27//! integration:
28//!
29//! | Concern | `Reassembler` | `SessionParser` |
30//! |-------------------------------|-------------------------|------------------------------|
31//! | Granularity | per (flow, side) | per flow |
32//! | Output | callback (Handler) | iterator/`Stream` of messages|
33//! | Cross-direction state | painful | natural |
34//! | UDP support | no | use [`DatagramParser`] |
35//!
36//! # Example
37//!
38//! ```
39//! use flowscope::{FlowSide, SessionParser, Timestamp};
40//!
41//! #[derive(Default, Clone)]
42//! struct LineParser {
43//! init_buf: Vec<u8>,
44//! resp_buf: Vec<u8>,
45//! }
46//!
47//! impl SessionParser for LineParser {
48//! type Message = (FlowSide, String);
49//!
50//! fn feed_initiator(&mut self, bytes: &[u8], _ts: Timestamp, out: &mut Vec<Self::Message>) {
51//! feed(&mut self.init_buf, bytes, FlowSide::Initiator, out);
52//! }
53//! fn feed_responder(&mut self, bytes: &[u8], _ts: Timestamp, out: &mut Vec<Self::Message>) {
54//! feed(&mut self.resp_buf, bytes, FlowSide::Responder, out);
55//! }
56//! }
57//!
58//! fn feed(buf: &mut Vec<u8>, bytes: &[u8], side: FlowSide, out: &mut Vec<(FlowSide, String)>) {
59//! buf.extend_from_slice(bytes);
60//! while let Some(nl) = buf.iter().position(|&b| b == b'\n') {
61//! let line = String::from_utf8_lossy(&buf[..nl]).into_owned();
62//! out.push((side, line));
63//! buf.drain(..=nl);
64//! }
65//! }
66//! ```
67
68use crate::event::{AnomalyKind, EndReason, FlowSide, FlowStats};
69use crate::timestamp::Timestamp;
70
71/// Default per-side buffer cap for [`BufferedFrameDrain`] /
72/// [`AccumulatingSessionParser`]. 64 KiB matches the TCP
73/// receive-buffer scale.
74pub const DEFAULT_FRAME_DRAIN_MAX_BUFFER: usize = 64 * 1024;
75
76/// Reasons [`BufferedFrameDrain`] / [`AccumulatingSessionParser`]
77/// poison themselves.
78#[derive(Debug, Clone, PartialEq, Eq)]
79#[non_exhaustive]
80pub enum FrameDrainError {
81 /// Buffer reached its `max_buffer` ceiling before the parser
82 /// drained a complete message. Indicates protocol desync —
83 /// the parser can't make progress.
84 BufferFull,
85 /// The `parse_one` closure returned `Some((msg, 0))` — a
86 /// zero-byte advance. Reserved for closure-author bugs; the
87 /// drain stops to avoid an infinite loop and the parser is
88 /// poisoned.
89 ZeroByteAdvance,
90}
91
92impl std::fmt::Display for FrameDrainError {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 match self {
95 FrameDrainError::BufferFull => f.write_str("parser buffer exceeded max_buffer cap"),
96 FrameDrainError::ZeroByteAdvance => {
97 f.write_str("parse_one returned Some((_, 0)) — zero-byte advance")
98 }
99 }
100 }
101}
102
103impl std::error::Error for FrameDrainError {}
104
105/// Buffered drain helper for custom parsers.
106///
107/// Encapsulates the "accumulate bytes, repeatedly call a parser
108/// closure, drain consumed prefix, retain partial" pattern that
109/// every custom [`SessionParser`] writes. Catches the off-by-one
110/// bugs around drain offsets.
111///
112/// Typical use inside a [`SessionParser`] impl:
113///
114/// ```rust
115/// # use flowscope::session::BufferedFrameDrain;
116/// # use flowscope::{FlowSide, SessionParser, Timestamp};
117/// # #[derive(Debug, Clone)] struct Msg;
118/// # fn parse_one(b: &[u8]) -> Option<(Msg, usize)> { None }
119/// #[derive(Default, Clone)]
120/// struct MyParser {
121/// init: BufferedFrameDrain<Msg>,
122/// resp: BufferedFrameDrain<Msg>,
123/// }
124///
125/// impl SessionParser for MyParser {
126/// type Message = Msg;
127/// fn feed_initiator(&mut self, b: &[u8], _: Timestamp, out: &mut Vec<Msg>) {
128/// let _ = self.init.extend(b);
129/// self.init.drain_with(parse_one);
130/// out.append(&mut self.init.take_messages());
131/// }
132/// fn feed_responder(&mut self, b: &[u8], _: Timestamp, out: &mut Vec<Msg>) {
133/// let _ = self.resp.extend(b);
134/// self.resp.drain_with(parse_one);
135/// out.append(&mut self.resp.take_messages());
136/// }
137/// }
138/// ```
139///
140/// New in 0.10.0 (plan 106).
141#[derive(Debug)]
142pub struct BufferedFrameDrain<M> {
143 buf: Vec<u8>,
144 out: Vec<M>,
145 max_buffer: usize,
146 poisoned: Option<FrameDrainError>,
147}
148
149impl<M> Default for BufferedFrameDrain<M> {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155impl<M> Clone for BufferedFrameDrain<M> {
156 /// Clones the configuration (`max_buffer`) but resets the
157 /// buffer / messages / poison state — typical use is to
158 /// rebuild the helper per session.
159 fn clone(&self) -> Self {
160 Self {
161 buf: Vec::new(),
162 out: Vec::new(),
163 max_buffer: self.max_buffer,
164 poisoned: None,
165 }
166 }
167}
168
169impl<M> BufferedFrameDrain<M> {
170 /// Construct with the [default buffer
171 /// cap](DEFAULT_FRAME_DRAIN_MAX_BUFFER).
172 pub fn new() -> Self {
173 Self::with_max_buffer(DEFAULT_FRAME_DRAIN_MAX_BUFFER)
174 }
175
176 /// Construct with a custom per-side buffer cap.
177 pub fn with_max_buffer(max_buffer: usize) -> Self {
178 Self {
179 buf: Vec::new(),
180 out: Vec::new(),
181 max_buffer,
182 poisoned: None,
183 }
184 }
185
186 /// Append `bytes` to the buffer.
187 ///
188 /// Returns `Err(FrameDrainError::BufferFull)` if the new
189 /// length would exceed `max_buffer`. The helper sets its
190 /// internal poison flag (queryable via [`Self::is_poisoned`]).
191 /// `bytes` are still appended up to the cap before poisoning,
192 /// so consumers that surface poison to the driver get the
193 /// last bit of data they can use.
194 pub fn extend(&mut self, bytes: &[u8]) -> Result<(), FrameDrainError> {
195 if self.poisoned.is_some() {
196 return Err(self.poisoned.clone().unwrap());
197 }
198 let new_len = self.buf.len().saturating_add(bytes.len());
199 if new_len > self.max_buffer {
200 let room = self.max_buffer.saturating_sub(self.buf.len());
201 self.buf.extend_from_slice(&bytes[..room.min(bytes.len())]);
202 self.poisoned = Some(FrameDrainError::BufferFull);
203 return Err(FrameDrainError::BufferFull);
204 }
205 self.buf.extend_from_slice(bytes);
206 Ok(())
207 }
208
209 /// Repeatedly call `parse_one` and drain the consumed prefix.
210 /// Pushes each parsed message into the internal `out` queue
211 /// (drain via [`Self::take_messages`]).
212 ///
213 /// `parse_one(buf) -> Option<(M, usize)>` semantics:
214 /// - `Some((msg, n))` — a complete message; advance `n` bytes.
215 /// - `None` — need more bytes.
216 /// - `Some((_, 0))` is treated as poison (zero-byte advance
217 /// would loop forever).
218 pub fn drain_with<F>(&mut self, mut parse_one: F)
219 where
220 F: FnMut(&[u8]) -> Option<(M, usize)>,
221 {
222 while self.poisoned.is_none() {
223 match parse_one(&self.buf) {
224 Some((msg, 0)) => {
225 self.out.push(msg);
226 self.poisoned = Some(FrameDrainError::ZeroByteAdvance);
227 return;
228 }
229 Some((msg, n)) => {
230 self.out.push(msg);
231 if n >= self.buf.len() {
232 self.buf.clear();
233 } else {
234 self.buf.drain(..n);
235 }
236 }
237 None => return,
238 }
239 }
240 }
241
242 /// Take the accumulated messages, clearing the queue.
243 pub fn take_messages(&mut self) -> Vec<M> {
244 std::mem::take(&mut self.out)
245 }
246
247 /// Bytes currently held in the buffer.
248 pub fn buffered_len(&self) -> usize {
249 self.buf.len()
250 }
251
252 /// `true` if the helper has poisoned itself (`BufferFull` or
253 /// `ZeroByteAdvance`). Once set, [`Self::extend`] is a no-op.
254 pub fn is_poisoned(&self) -> bool {
255 self.poisoned.is_some()
256 }
257
258 /// Poison reason, if any.
259 pub fn poison_reason(&self) -> Option<&FrameDrainError> {
260 self.poisoned.as_ref()
261 }
262}
263
264/// Convenience [`SessionParser`] impl over a `parse_one` closure
265/// of shape `Fn(&[u8]) -> Option<(M, usize)>`.
266///
267/// Wraps two [`BufferedFrameDrain`]s (one per side) and exposes
268/// the canonical "init + resp + drain-loop" pattern through one
269/// constructor call. Reduces ~25 LoC of boilerplate per custom
270/// parser to:
271///
272/// ```rust
273/// # use flowscope::session::AccumulatingSessionParser;
274/// # #[derive(Debug, Clone)] struct Msg;
275/// # fn parse_one(b: &[u8]) -> Option<(Msg, usize)> { None }
276/// let parser = AccumulatingSessionParser::new("my-protocol", parse_one);
277/// ```
278///
279/// The closure must be `Clone + Send + 'static` so the parser
280/// can be cloned per session (via the [`SessionParserFactory`]
281/// blanket impl). Capturing closures usually satisfy `Clone` if
282/// the captured values do.
283///
284/// New in 0.10.0 (plan 106).
285pub struct AccumulatingSessionParser<F, M>
286where
287 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
288 M: Send + std::fmt::Debug + 'static,
289{
290 parser_kind: &'static str,
291 parse_one: F,
292 max_buffer: usize,
293 init: BufferedFrameDrain<M>,
294 resp: BufferedFrameDrain<M>,
295}
296
297impl<F, M> std::fmt::Debug for AccumulatingSessionParser<F, M>
298where
299 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
300 M: Send + std::fmt::Debug + 'static,
301{
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 f.debug_struct("AccumulatingSessionParser")
304 .field("parser_kind", &self.parser_kind)
305 .field("init_buffered", &self.init.buffered_len())
306 .field("resp_buffered", &self.resp.buffered_len())
307 .field(
308 "poisoned",
309 &(self.init.is_poisoned() || self.resp.is_poisoned()),
310 )
311 .finish()
312 }
313}
314
315impl<F, M> Clone for AccumulatingSessionParser<F, M>
316where
317 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
318 M: Send + std::fmt::Debug + 'static,
319{
320 /// Clones the configuration + parse closure; resets per-session
321 /// buffer state. Use for per-session reuse via the
322 /// [`SessionParserFactory`] blanket impl.
323 fn clone(&self) -> Self {
324 Self {
325 parser_kind: self.parser_kind,
326 parse_one: self.parse_one.clone(),
327 max_buffer: self.max_buffer,
328 init: BufferedFrameDrain::with_max_buffer(self.max_buffer),
329 resp: BufferedFrameDrain::with_max_buffer(self.max_buffer),
330 }
331 }
332}
333
334impl<F, M> AccumulatingSessionParser<F, M>
335where
336 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
337 M: Send + std::fmt::Debug + 'static,
338{
339 /// Construct with the [default per-side buffer
340 /// cap](DEFAULT_FRAME_DRAIN_MAX_BUFFER).
341 pub fn new(parser_kind: &'static str, parse_one: F) -> Self {
342 Self::with_max_buffer(parser_kind, parse_one, DEFAULT_FRAME_DRAIN_MAX_BUFFER)
343 }
344
345 /// Construct with a custom per-side buffer cap.
346 pub fn with_max_buffer(parser_kind: &'static str, parse_one: F, max_buffer: usize) -> Self {
347 Self {
348 parser_kind,
349 parse_one,
350 max_buffer,
351 init: BufferedFrameDrain::with_max_buffer(max_buffer),
352 resp: BufferedFrameDrain::with_max_buffer(max_buffer),
353 }
354 }
355}
356
357impl<F, M> SessionParser for AccumulatingSessionParser<F, M>
358where
359 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
360 M: Send + std::fmt::Debug + 'static,
361{
362 type Message = M;
363
364 fn feed_initiator(&mut self, bytes: &[u8], _ts: Timestamp, out: &mut Vec<M>) {
365 if self.init.extend(bytes).is_err() {
366 out.append(&mut self.init.take_messages());
367 return;
368 }
369 let parse_one = self.parse_one.clone();
370 self.init.drain_with(parse_one);
371 out.append(&mut self.init.take_messages());
372 }
373
374 fn feed_responder(&mut self, bytes: &[u8], _ts: Timestamp, out: &mut Vec<M>) {
375 if self.resp.extend(bytes).is_err() {
376 out.append(&mut self.resp.take_messages());
377 return;
378 }
379 let parse_one = self.parse_one.clone();
380 self.resp.drain_with(parse_one);
381 out.append(&mut self.resp.take_messages());
382 }
383
384 fn parser_kind(&self) -> &'static str {
385 self.parser_kind
386 }
387
388 fn is_poisoned(&self) -> bool {
389 self.init.is_poisoned() || self.resp.is_poisoned()
390 }
391
392 fn poison_reason(&self) -> Option<&str> {
393 // First poisoned side wins.
394 let err = self.init.poison_reason().or(self.resp.poison_reason())?;
395 Some(match err {
396 FrameDrainError::BufferFull => "buffer cap exceeded",
397 FrameDrainError::ZeroByteAdvance => "parse_one returned zero-byte advance",
398 })
399 }
400}
401
402/// Convenience [`DatagramParser`] impl over a `parse_one` closure
403/// of shape `Fn(&[u8]) -> Option<M>`.
404///
405/// One UDP packet, one optional message. The closure receives the
406/// raw payload; return `Some(message)` to emit one event,
407/// `None` to drop the packet silently.
408///
409/// New in 0.10.0 (plan 106).
410pub struct PerDatagramParser<F, M>
411where
412 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
413 M: Send + std::fmt::Debug + 'static,
414{
415 parser_kind: &'static str,
416 parse_one: F,
417}
418
419impl<F, M> std::fmt::Debug for PerDatagramParser<F, M>
420where
421 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
422 M: Send + std::fmt::Debug + 'static,
423{
424 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
425 f.debug_struct("PerDatagramParser")
426 .field("parser_kind", &self.parser_kind)
427 .finish()
428 }
429}
430
431impl<F, M> Clone for PerDatagramParser<F, M>
432where
433 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
434 M: Send + std::fmt::Debug + 'static,
435{
436 fn clone(&self) -> Self {
437 Self {
438 parser_kind: self.parser_kind,
439 parse_one: self.parse_one.clone(),
440 }
441 }
442}
443
444impl<F, M> PerDatagramParser<F, M>
445where
446 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
447 M: Send + std::fmt::Debug + 'static,
448{
449 pub fn new(parser_kind: &'static str, parse_one: F) -> Self {
450 Self {
451 parser_kind,
452 parse_one,
453 }
454 }
455}
456
457impl<F, M> DatagramParser for PerDatagramParser<F, M>
458where
459 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
460 M: Send + std::fmt::Debug + 'static,
461{
462 type Message = M;
463
464 fn parse(&mut self, payload: &[u8], _side: FlowSide, _ts: Timestamp, out: &mut Vec<M>) {
465 if let Some(msg) = (self.parse_one)(payload) {
466 out.push(msg);
467 }
468 }
469
470 fn parser_kind(&self) -> &'static str {
471 self.parser_kind
472 }
473}
474
475/// Parses a stream-oriented L7 protocol session. One instance per
476/// flow; both directions feed through the same parser, allowing
477/// state to interleave.
478///
479/// Implementors are owned by the per-flow slot; sync (no `await`).
480/// Backpressure flows from the consuming `Stream` back to the
481/// kernel ring once the per-flow message buffer fills up — see
482/// the `netring::SessionStream` adapter.
483///
484/// # Per-flow rich state
485///
486/// For consumers that maintain per-flow user state updated by BOTH
487/// the reassembler and the parser — TCP rich stats, application-
488/// level counters, middleware state machines — keep that state on
489/// [`crate::FlowEntry::user`] (typed via the `S` parameter on
490/// [`crate::FlowSessionDriver`]) and update it from your event
491/// loop after `track()`. The pattern is documented in
492/// `docs/recipes.md` → "Per-flow user state via the consumer
493/// loop". Avoid piping `&mut S` through `feed_*` — it would
494/// ripple a generic parameter through every shipped parser.
495pub trait SessionParser: Send + 'static {
496 /// L7 message produced by this parser.
497 ///
498 /// - `Send + 'static` so messages can cross task boundaries.
499 /// - `Debug` is required for the per-message `tracing::trace!`
500 /// event under the `tracing` feature (filter via
501 /// `EnvFilter::new("flowscope.message=warn")` to suppress).
502 /// Almost every Rust type derives `Debug` anyway, and the
503 /// bound is trivial to add for those that don't.
504 type Message: Send + std::fmt::Debug + 'static;
505
506 /// Feed the next chunk of bytes from the **initiator** side.
507 /// `ts` is the observed time of the packet carrying these bytes.
508 /// Push any complete messages parsed during this call into
509 /// `out`.
510 ///
511 /// **0.11 break (plan 119):** signature changed from
512 /// `-> Vec<Self::Message>` to taking `out: &mut Vec<…>`.
513 /// Same idiom as `httparse::Request::parse` etc.
514 fn feed_initiator(&mut self, bytes: &[u8], ts: Timestamp, out: &mut Vec<Self::Message>);
515
516 /// Feed the next chunk of bytes from the **responder** side.
517 fn feed_responder(&mut self, bytes: &[u8], ts: Timestamp, out: &mut Vec<Self::Message>);
518
519 /// Initiator side has FIN'd. Default: no-op.
520 fn fin_initiator(&mut self, _out: &mut Vec<Self::Message>) {}
521
522 /// Responder side has FIN'd.
523 fn fin_responder(&mut self, _out: &mut Vec<Self::Message>) {}
524
525 /// Initiator side observed a RST. Default: no-op.
526 fn rst_initiator(&mut self) {}
527
528 /// Responder side observed a RST.
529 fn rst_responder(&mut self) {}
530
531 /// Periodic time hook. The driver calls this on every `sweep` /
532 /// `finish` with the sweep's `now`, for every still-live parser.
533 /// Lets stateful parsers emit time-driven messages (timeouts,
534 /// unanswered requests). Emitted messages are attributed to
535 /// [`FlowSide::Initiator`]. Default: no-op.
536 fn on_tick(&mut self, _now: Timestamp, _out: &mut Vec<Self::Message>) {}
537
538 /// True after the parser has hit an unrecoverable error and
539 /// can no longer make progress. The driver checks this after
540 /// every `feed_*` / `fin_*` call and tears the flow down on
541 /// `true`. Default: `false` (parser never poisons).
542 ///
543 /// Parsers that want to drop a malformed message and keep
544 /// going should NOT use this — just don't push the message
545 /// into the returned `Vec`. Reserve poison for cases where
546 /// internal state is corrupted past recovery (desynced framing,
547 /// invalid magic bytes that won't appear later, etc.).
548 ///
549 /// Mirrors [`crate::Reassembler::is_poisoned`] — same wiring
550 /// shape, same operator mental model.
551 fn is_poisoned(&self) -> bool {
552 false
553 }
554
555 /// Optional human-readable description of why the parser
556 /// poisoned. Consulted only when [`is_poisoned`](Self::is_poisoned)
557 /// returns `true`. Default: `None`.
558 ///
559 /// The driver truncates to ~256 bytes when forwarding via
560 /// [`crate::SessionEvent::FlowAnomaly`].
561 fn poison_reason(&self) -> Option<&str> {
562 None
563 }
564
565 /// Symmetric "I'm done — close this flow cleanly" signal.
566 /// Default: `false` (parser never self-terminates).
567 ///
568 /// Returning `true` tells the driver this parser has no more
569 /// useful work to extract — the flow can close ahead of FIN
570 /// / idle-timeout. The driver responds by synthesising
571 /// [`crate::SessionEvent::Closed`] with
572 /// [`crate::EndReason::ParserDone`] on the next check, after
573 /// flushing any pending messages from the same `feed_*` /
574 /// `on_tick` call.
575 ///
576 /// Reserve for protocols with intrinsic completion semantics:
577 /// HTTP/1.0 `Connection: close` after body fully received;
578 /// DNS-over-TCP after a query/response pair; framed protocols
579 /// with a session-end sentinel. Do **not** use this to give
580 /// up on bad input — that's [`is_poisoned`](Self::is_poisoned),
581 /// which routes through [`crate::EndReason::ParseError`].
582 ///
583 /// Should be idempotent: once `is_done()` returns `true`, it
584 /// should keep returning `true` for the lifetime of the parser.
585 /// [`is_poisoned`](Self::is_poisoned) takes precedence — a
586 /// parser that's both `is_done` and `is_poisoned` surfaces as
587 /// `ParseError`, not `ParserDone`.
588 fn is_done(&self) -> bool {
589 false
590 }
591
592 /// Identifier for this parser, threaded into
593 /// [`crate::SessionEvent::Application::parser_kind`]. New in
594 /// 0.5.0.
595 ///
596 /// Use a stable, label-safe identifier — operators route
597 /// metrics on this string. Convention:
598 ///
599 /// - Lowercase, ASCII, snake-case or slash-separated
600 /// (`http/1`, `dns-udp`, `rtp`, `length-prefixed`).
601 /// - Stable for the lifetime of the parser instance.
602 /// - Default: `""` (no kind set).
603 ///
604 /// `&'static str` rather than `Cow` so the value can flow into
605 /// `metrics::counter!` labels without allocation. Parsers
606 /// needing a dynamic kind should bake it into
607 /// [`Self::Message`].
608 fn parser_kind(&self) -> &'static str {
609 ""
610 }
611}
612
613/// Builds a fresh [`SessionParser`] per session. Modeled on
614/// [`crate::ReassemblerFactory`].
615///
616/// Most parsers can skip implementing this manually: any parser
617/// that's `SessionParser + Default + Clone` automatically becomes
618/// a factory via the blanket impl below.
619pub trait SessionParserFactory<K>: Send + 'static {
620 type Parser: SessionParser;
621 fn new_parser(&mut self, key: &K) -> Self::Parser;
622}
623
624impl<K, P> SessionParserFactory<K> for P
625where
626 P: SessionParser + Default + Clone,
627{
628 type Parser = P;
629 fn new_parser(&mut self, _key: &K) -> P {
630 self.clone()
631 }
632}
633
634/// Parses a packet-oriented L7 protocol. One instance per flow;
635/// receives one L4 payload at a time along with which side sent it.
636pub trait DatagramParser: Send + 'static {
637 /// L7 message produced by this parser. Same `Debug` bound as
638 /// [`SessionParser::Message`].
639 type Message: Send + std::fmt::Debug + 'static;
640
641 /// Parse one L4 payload. `side` is the direction relative to
642 /// the flow's initiator; `ts` is the observed time of the
643 /// datagram. Push any complete messages decoded into `out`.
644 ///
645 /// **0.11 break (plan 119):** signature changed from
646 /// `-> Vec<Self::Message>` to taking `out: &mut Vec<…>`.
647 fn parse(
648 &mut self,
649 payload: &[u8],
650 side: FlowSide,
651 ts: Timestamp,
652 out: &mut Vec<Self::Message>,
653 );
654
655 /// Periodic time hook — see [`SessionParser::on_tick`]. The
656 /// driver calls this on every `sweep` / `finish`. Default: no-op.
657 fn on_tick(&mut self, _now: Timestamp, _out: &mut Vec<Self::Message>) {}
658
659 /// True after the parser has hit an unrecoverable error. See
660 /// [`SessionParser::is_poisoned`] for the contract.
661 fn is_poisoned(&self) -> bool {
662 false
663 }
664
665 /// Optional reason for poison. See
666 /// [`SessionParser::poison_reason`].
667 fn poison_reason(&self) -> Option<&str> {
668 None
669 }
670
671 /// Symmetric "I'm done — close this flow cleanly" signal,
672 /// mirroring [`SessionParser::is_done`]. Default: `false`.
673 fn is_done(&self) -> bool {
674 false
675 }
676
677 /// See [`SessionParser::parser_kind`]. Default `""`.
678 fn parser_kind(&self) -> &'static str {
679 ""
680 }
681}
682
683/// Builds a fresh [`DatagramParser`] per session.
684pub trait DatagramParserFactory<K>: Send + 'static {
685 type Parser: DatagramParser;
686 fn new_parser(&mut self, key: &K) -> Self::Parser;
687}
688
689impl<K, P> DatagramParserFactory<K> for P
690where
691 P: DatagramParser + Default + Clone,
692{
693 type Parser = P;
694 fn new_parser(&mut self, _key: &K) -> P {
695 self.clone()
696 }
697}
698
699/// Output of a [`SessionParser`] or [`DatagramParser`]-backed stream.
700///
701/// `K` is the flow key, `M` is the parser's message type.
702///
703/// `#[non_exhaustive]` to keep future variants additive without
704/// breaking exhaustive external `match` blocks. Match with a
705/// trailing `_ => {}` arm for forward-compatibility.
706#[derive(Debug, Clone)]
707#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
708#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
709#[cfg_attr(
710 feature = "serde",
711 serde(bound(
712 serialize = "K: serde::Serialize, M: serde::Serialize",
713 deserialize = "K: serde::de::DeserializeOwned, M: serde::de::DeserializeOwned"
714 ))
715)]
716#[non_exhaustive]
717pub enum SessionEvent<K, M> {
718 /// First packet of a new session.
719 Started { key: K, ts: Timestamp },
720 /// Parser emitted a complete L7 message.
721 Application {
722 key: K,
723 side: FlowSide,
724 message: M,
725 ts: Timestamp,
726 /// Identifier of the parser that produced this message —
727 /// the value returned by [`SessionParser::parser_kind`] (or
728 /// [`DatagramParser::parser_kind`] for UDP). New in 0.5.0.
729 /// `""` when the parser doesn't override the default.
730 parser_kind: &'static str,
731 },
732 /// Session ended (FIN/RST/idle/eviction). Any messages the
733 /// parser flushed on close arrive as `Application` events
734 /// before the corresponding `Closed`.
735 Closed {
736 key: K,
737 reason: EndReason,
738 stats: FlowStats,
739 /// L4 protocol of the flow this session was tracked over.
740 /// New in 0.7.0; mirrors [`crate::FlowEvent::Ended::l4`].
741 l4: Option<crate::extractor::L4Proto>,
742 },
743 /// Live, in-flight per-flow anomaly forwarded from
744 /// [`crate::FlowEvent::FlowAnomaly`]. Emitted only when the
745 /// owning driver has `with_emit_anomalies(true)` set.
746 FlowAnomaly {
747 key: K,
748 kind: AnomalyKind,
749 ts: Timestamp,
750 },
751
752 /// Live, in-flight tracker-global anomaly forwarded from
753 /// [`crate::FlowEvent::TrackerAnomaly`] (e.g.
754 /// [`AnomalyKind::FlowTableEvictionPressure`]). Opt-in like
755 /// [`Self::FlowAnomaly`].
756 TrackerAnomaly { kind: AnomalyKind, ts: Timestamp },
757
758 /// Periodic [`FlowStats`] snapshot forwarded from
759 /// [`crate::FlowEvent::Tick`]. Emitted when the underlying
760 /// [`crate::FlowTrackerConfig::flow_tick_interval`] is `Some`.
761 /// New in 0.5.0.
762 FlowTick {
763 key: K,
764 stats: FlowStats,
765 ts: Timestamp,
766 },
767}
768
769impl<K, M> SessionEvent<K, M> {
770 /// Borrow the anomaly kind if this event is an anomaly (either
771 /// per-flow or tracker-global). Returns `None` for the
772 /// non-anomaly variants.
773 pub fn anomaly_kind(&self) -> Option<&AnomalyKind> {
774 match self {
775 SessionEvent::FlowAnomaly { kind, .. } | SessionEvent::TrackerAnomaly { kind, .. } => {
776 Some(kind)
777 }
778 _ => None,
779 }
780 }
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 #[derive(Default, Clone)]
788 struct CountParser {
789 init_bytes: usize,
790 resp_bytes: usize,
791 }
792
793 impl SessionParser for CountParser {
794 type Message = (FlowSide, usize);
795 fn feed_initiator(&mut self, b: &[u8], _ts: Timestamp, out: &mut Vec<Self::Message>) {
796 self.init_bytes += b.len();
797 out.push((FlowSide::Initiator, self.init_bytes));
798 }
799 fn feed_responder(&mut self, b: &[u8], _ts: Timestamp, out: &mut Vec<Self::Message>) {
800 self.resp_bytes += b.len();
801 out.push((FlowSide::Responder, self.resp_bytes));
802 }
803 }
804
805 #[test]
806 fn auto_impl_session_parser_factory() {
807 // CountParser is Default + Clone + SessionParser → automatic factory.
808 let mut f: CountParser = CountParser::default();
809 let mut p: CountParser = SessionParserFactory::<u32>::new_parser(&mut f, &7);
810 let mut m = Vec::new();
811 p.feed_initiator(b"abc", Timestamp::default(), &mut m);
812 assert_eq!(m, vec![(FlowSide::Initiator, 3)]);
813 }
814
815 #[derive(Default, Clone)]
816 struct EchoDgram;
817 impl DatagramParser for EchoDgram {
818 type Message = (FlowSide, Vec<u8>);
819 fn parse(
820 &mut self,
821 payload: &[u8],
822 side: FlowSide,
823 _ts: Timestamp,
824 out: &mut Vec<Self::Message>,
825 ) {
826 out.push((side, payload.to_vec()));
827 }
828 }
829
830 #[test]
831 fn auto_impl_datagram_parser_factory() {
832 let mut f = EchoDgram;
833 let mut p: EchoDgram = DatagramParserFactory::<()>::new_parser(&mut f, &());
834 let mut m = Vec::new();
835 p.parse(b"hello", FlowSide::Responder, Timestamp::default(), &mut m);
836 assert_eq!(m, vec![(FlowSide::Responder, b"hello".to_vec())]);
837 }
838}