Skip to main content

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}