flowscope/session/mod.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 and yields the ordered
24//! byte stream as [`crate::StreamChunks`] (data interleaved with gap
25//! marks). `SessionParser` is the higher-level
26//! abstraction: one instance per flow, two `feed_*` methods,
27//! returns typed messages directly. Pick whichever fits your
28//! integration:
29//!
30//! | Concern | `Reassembler` | `SessionParser` |
31//! |-------------------------------|-------------------------|------------------------------|
32//! | Granularity | per (flow, side) | per flow |
33//! | Output | `StreamChunks` (bytes + gaps) | typed messages |
34//! | Cross-direction state | painful | natural |
35//! | UDP support | no | use [`DatagramParser`] |
36//!
37//! # Running parsers
38//!
39//! - [`SessionDriver`] / [`DatagramDriver`] — one parser type over a
40//! whole capture, yielding an ordered stream of [`SessionEvent`]s
41//! (lifecycle, messages, parser closes, anomalies). What
42//! `netring`'s `SessionStream` / `DatagramStream` wrap.
43//! - [`crate::driver::Driver`] — several parsers (by port, by
44//! signature, or on every flow) sharing one flow table, with typed
45//! per-parser drain handles.
46//!
47//! Both are built on the same engine, so they behave identically:
48//! one flow table, one reassembler per flow side (shared by every
49//! parser), and these per-parser rules:
50//!
51//! - A parser that reports [`SessionParser::is_poisoned`] or
52//! [`SessionParser::is_done`] after a call is **closed** (a parser
53//! close event with [`EndReason::ParseError`] /
54//! [`EndReason::ParserDone`], plus an
55//! [`AnomalyKind::SessionParseError`] for poison when anomalies are
56//! on). It is never fed again for that flow — and never re-created:
57//! the flow itself stays tracked until its transport end. A parser
58//! close never ends the flow; `Ended` / `Closed` always carries a
59//! transport reason (`Fin`, `Rst`, `IdleTimeout`, `Evicted`,
60//! `ForceClosed`).
61//! - The one reporting difference: a parser still open when its flow
62//! ends. `SessionDriver` / `DatagramDriver` say so with `Closed`
63//! alone; the typed `Driver` also emits one `Event::ParserClosed`
64//! per slot carrying the transport reason
65//! ([`EndReason::is_transport`]) right before `Ended`.
66//! - Missing bytes are reported through [`SessionParser::on_gap`].
67//! The default answer, [`GapResponse::StopSide`], stops feeding
68//! **that side** only (a parser side-stopped event with
69//! [`EndReason::StreamGap`]); the other side keeps being parsed.
70//! [`GapResponse::Continue`] resynchronises, [`GapResponse::Stop`]
71//! closes the whole parser.
72//! - A reassembly stop on one side (per-side cap under
73//! [`crate::OverflowPolicy::DropFlow`], memcap) stops that side
74//! with [`EndReason::BufferOverflow`].
75//! - When both sides are stopped the parser closes once (detail
76//! `"both sides stopped"`).
77//! - At flow end each side still being read gets
78//! [`SessionParser::fin_initiator`] / [`SessionParser::fin_responder`]
79//! (graceful end, or that side sent a FIN) or `rst_*`; a stopped
80//! side gets neither.
81//! - Datagram parsers only see the transports they declare
82//! ([`DatagramParser::transports`], default [`Transports::UDP`]):
83//! a UDP parser never gets ICMP messages and vice versa.
84//!
85//! # Example
86//!
87//! ```
88//! use flowscope::{FlowSide, SessionParser, Timestamp};
89//!
90//! #[derive(Default, Clone)]
91//! struct LineParser {
92//! init_buf: Vec<u8>,
93//! resp_buf: Vec<u8>,
94//! }
95//!
96//! impl SessionParser for LineParser {
97//! type Message = (FlowSide, String);
98//!
99//! fn feed_initiator(&mut self, bytes: &[u8], _ts: Timestamp, out: &mut Vec<Self::Message>) {
100//! feed(&mut self.init_buf, bytes, FlowSide::Initiator, out);
101//! }
102//! fn feed_responder(&mut self, bytes: &[u8], _ts: Timestamp, out: &mut Vec<Self::Message>) {
103//! feed(&mut self.resp_buf, bytes, FlowSide::Responder, out);
104//! }
105//! }
106//!
107//! fn feed(buf: &mut Vec<u8>, bytes: &[u8], side: FlowSide, out: &mut Vec<(FlowSide, String)>) {
108//! buf.extend_from_slice(bytes);
109//! while let Some(nl) = buf.iter().position(|&b| b == b'\n') {
110//! let line = String::from_utf8_lossy(&buf[..nl]).into_owned();
111//! out.push((side, line));
112//! buf.drain(..=nl);
113//! }
114//! }
115//! ```
116
117use crate::{
118 event::{AnomalyKind, EndReason, FlowSide, FlowStats},
119 extractor::{L4Proto, Orientation},
120 parser_kind::ParserKind,
121 timestamp::Timestamp,
122};
123
124#[cfg(all(feature = "extractors", feature = "reassembler"))]
125pub(crate) mod core;
126#[cfg(all(feature = "extractors", feature = "reassembler"))]
127mod driver;
128#[cfg(all(feature = "extractors", feature = "reassembler"))]
129pub(crate) mod engine;
130
131#[cfg(all(feature = "extractors", feature = "reassembler"))]
132pub use driver::{DatagramDriver, SessionDriver};
133
134/// Default per-side buffer cap for [`BufferedFrameDrain`] /
135/// [`AccumulatingSessionParser`]. 64 KiB matches the TCP
136/// receive-buffer scale.
137pub const DEFAULT_FRAME_DRAIN_MAX_BUFFER: usize = 64 * 1024;
138
139/// Reasons [`BufferedFrameDrain`] / [`AccumulatingSessionParser`]
140/// poison themselves.
141#[derive(Debug, Clone, PartialEq, Eq)]
142#[non_exhaustive]
143pub enum FrameDrainError {
144 /// Buffer reached its `max_buffer` ceiling before the parser
145 /// drained a complete message. Indicates protocol desync —
146 /// the parser can't make progress.
147 BufferFull,
148 /// The `parse_one` closure returned `Some((msg, 0))` — a
149 /// zero-byte advance. Reserved for closure-author bugs; the
150 /// drain stops to avoid an infinite loop and the parser is
151 /// poisoned.
152 ZeroByteAdvance,
153}
154
155impl std::fmt::Display for FrameDrainError {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 FrameDrainError::BufferFull => f.write_str("parser buffer exceeded max_buffer cap"),
159 FrameDrainError::ZeroByteAdvance => {
160 f.write_str("parse_one returned Some((_, 0)) — zero-byte advance")
161 }
162 }
163 }
164}
165
166impl std::error::Error for FrameDrainError {}
167
168/// Buffered drain helper for custom parsers.
169///
170/// Encapsulates the "accumulate bytes, repeatedly call a parser
171/// closure, drain consumed prefix, retain partial" pattern that
172/// every custom [`SessionParser`] writes. Catches the off-by-one
173/// bugs around drain offsets.
174///
175/// Typical use inside a [`SessionParser`] impl:
176///
177/// ```rust
178/// # use flowscope::session::BufferedFrameDrain;
179/// # use flowscope::{FlowSide, SessionParser, Timestamp};
180/// # #[derive(Debug, Clone)] struct Msg;
181/// # fn parse_one(b: &[u8]) -> Option<(Msg, usize)> { None }
182/// #[derive(Default, Clone)]
183/// struct MyParser {
184/// init: BufferedFrameDrain<Msg>,
185/// resp: BufferedFrameDrain<Msg>,
186/// }
187///
188/// impl SessionParser for MyParser {
189/// type Message = Msg;
190/// fn feed_initiator(&mut self, b: &[u8], _: Timestamp, out: &mut Vec<Msg>) {
191/// let _ = self.init.extend(b);
192/// self.init.drain_with(parse_one);
193/// out.append(&mut self.init.take_messages());
194/// }
195/// fn feed_responder(&mut self, b: &[u8], _: Timestamp, out: &mut Vec<Msg>) {
196/// let _ = self.resp.extend(b);
197/// self.resp.drain_with(parse_one);
198/// out.append(&mut self.resp.take_messages());
199/// }
200/// }
201/// ```
202///
203/// New in 0.10.0 (plan 106).
204#[derive(Debug)]
205pub struct BufferedFrameDrain<M> {
206 buf: Vec<u8>,
207 out: Vec<M>,
208 max_buffer: usize,
209 poisoned: Option<FrameDrainError>,
210}
211
212impl<M> Default for BufferedFrameDrain<M> {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218impl<M> Clone for BufferedFrameDrain<M> {
219 /// Clones the configuration (`max_buffer`) but resets the
220 /// buffer / messages / poison state — typical use is to
221 /// rebuild the helper per session.
222 fn clone(&self) -> Self {
223 Self {
224 buf: Vec::new(),
225 out: Vec::new(),
226 max_buffer: self.max_buffer,
227 poisoned: None,
228 }
229 }
230}
231
232impl<M> BufferedFrameDrain<M> {
233 /// Construct with the [default buffer
234 /// cap](DEFAULT_FRAME_DRAIN_MAX_BUFFER).
235 pub fn new() -> Self {
236 Self::with_max_buffer(DEFAULT_FRAME_DRAIN_MAX_BUFFER)
237 }
238
239 /// Construct with a custom per-side buffer cap.
240 pub fn with_max_buffer(max_buffer: usize) -> Self {
241 Self {
242 buf: Vec::new(),
243 out: Vec::new(),
244 max_buffer,
245 poisoned: None,
246 }
247 }
248
249 /// Append `bytes` to the buffer.
250 ///
251 /// Returns `Err(FrameDrainError::BufferFull)` if the new
252 /// length would exceed `max_buffer`. The helper sets its
253 /// internal poison flag (queryable via [`Self::is_poisoned`]).
254 /// `bytes` are still appended up to the cap before poisoning,
255 /// so consumers that surface poison to the driver get the
256 /// last bit of data they can use.
257 pub fn extend(&mut self, bytes: &[u8]) -> Result<(), FrameDrainError> {
258 if self.poisoned.is_some() {
259 return Err(self.poisoned.clone().unwrap());
260 }
261 let new_len = self.buf.len().saturating_add(bytes.len());
262 if new_len > self.max_buffer {
263 let room = self.max_buffer.saturating_sub(self.buf.len());
264 self.buf.extend_from_slice(&bytes[..room.min(bytes.len())]);
265 self.poisoned = Some(FrameDrainError::BufferFull);
266 return Err(FrameDrainError::BufferFull);
267 }
268 self.buf.extend_from_slice(bytes);
269 Ok(())
270 }
271
272 /// Repeatedly call `parse_one` and drain the consumed prefix.
273 /// Pushes each parsed message into the internal `out` queue
274 /// (drain via [`Self::take_messages`]).
275 ///
276 /// `parse_one(buf) -> Option<(M, usize)>` semantics:
277 /// - `Some((msg, n))` — a complete message; advance `n` bytes.
278 /// - `None` — need more bytes.
279 /// - `Some((_, 0))` is treated as poison (zero-byte advance
280 /// would loop forever).
281 pub fn drain_with<F>(&mut self, mut parse_one: F)
282 where
283 F: FnMut(&[u8]) -> Option<(M, usize)>,
284 {
285 while self.poisoned.is_none() {
286 match parse_one(&self.buf) {
287 Some((msg, 0)) => {
288 self.out.push(msg);
289 self.poisoned = Some(FrameDrainError::ZeroByteAdvance);
290 return;
291 }
292 Some((msg, n)) => {
293 self.out.push(msg);
294 if n >= self.buf.len() {
295 self.buf.clear();
296 } else {
297 self.buf.drain(..n);
298 }
299 }
300 None => return,
301 }
302 }
303 }
304
305 /// Take the accumulated messages, clearing the queue.
306 pub fn take_messages(&mut self) -> Vec<M> {
307 std::mem::take(&mut self.out)
308 }
309
310 /// Bytes currently held in the buffer.
311 pub fn buffered_len(&self) -> usize {
312 self.buf.len()
313 }
314
315 /// `true` if the helper has poisoned itself (`BufferFull` or
316 /// `ZeroByteAdvance`). Once set, [`Self::extend`] is a no-op.
317 pub fn is_poisoned(&self) -> bool {
318 self.poisoned.is_some()
319 }
320
321 /// Poison reason, if any.
322 pub fn poison_reason(&self) -> Option<&FrameDrainError> {
323 self.poisoned.as_ref()
324 }
325}
326
327/// Convenience [`SessionParser`] impl over a `parse_one` closure
328/// of shape `Fn(&[u8]) -> Option<(M, usize)>`.
329///
330/// Wraps two [`BufferedFrameDrain`]s (one per side) and exposes
331/// the canonical "init + resp + drain-loop" pattern through one
332/// constructor call. Reduces ~25 LoC of boilerplate per custom
333/// parser to:
334///
335/// ```rust
336/// # use flowscope::session::AccumulatingSessionParser;
337/// # use flowscope::ParserKind;
338/// # #[derive(Debug, Clone)] struct Msg;
339/// # fn parse_one(b: &[u8]) -> Option<(Msg, usize)> { None }
340/// let parser = AccumulatingSessionParser::new(ParserKind::Other("my-protocol"), parse_one);
341/// ```
342///
343/// The closure must be `Clone + Send + 'static` so the parser
344/// can be cloned per session (via the [`SessionParserFactory`]
345/// blanket impl). Capturing closures usually satisfy `Clone` if
346/// the captured values do.
347///
348/// New in 0.10.0 (plan 106).
349pub struct AccumulatingSessionParser<F, M>
350where
351 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
352 M: Send + std::fmt::Debug + 'static,
353{
354 parser_kind: ParserKind,
355 parse_one: F,
356 max_buffer: usize,
357 init: BufferedFrameDrain<M>,
358 resp: BufferedFrameDrain<M>,
359}
360
361impl<F, M> std::fmt::Debug for AccumulatingSessionParser<F, M>
362where
363 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
364 M: Send + std::fmt::Debug + 'static,
365{
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 f.debug_struct("AccumulatingSessionParser")
368 .field("parser_kind", &self.parser_kind)
369 .field("init_buffered", &self.init.buffered_len())
370 .field("resp_buffered", &self.resp.buffered_len())
371 .field(
372 "poisoned",
373 &(self.init.is_poisoned() || self.resp.is_poisoned()),
374 )
375 .finish()
376 }
377}
378
379impl<F, M> Clone for AccumulatingSessionParser<F, M>
380where
381 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
382 M: Send + std::fmt::Debug + 'static,
383{
384 /// Clones the configuration + parse closure; resets per-session
385 /// buffer state. Use for per-session reuse via the
386 /// [`SessionParserFactory`] blanket impl.
387 fn clone(&self) -> Self {
388 Self {
389 parser_kind: self.parser_kind,
390 parse_one: self.parse_one.clone(),
391 max_buffer: self.max_buffer,
392 init: BufferedFrameDrain::with_max_buffer(self.max_buffer),
393 resp: BufferedFrameDrain::with_max_buffer(self.max_buffer),
394 }
395 }
396}
397
398impl<F, M> AccumulatingSessionParser<F, M>
399where
400 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
401 M: Send + std::fmt::Debug + 'static,
402{
403 /// Construct with the [default per-side buffer
404 /// cap](DEFAULT_FRAME_DRAIN_MAX_BUFFER).
405 pub fn new(parser_kind: ParserKind, parse_one: F) -> Self {
406 Self::with_max_buffer(parser_kind, parse_one, DEFAULT_FRAME_DRAIN_MAX_BUFFER)
407 }
408
409 /// Construct with a custom per-side buffer cap.
410 pub fn with_max_buffer(parser_kind: ParserKind, parse_one: F, max_buffer: usize) -> Self {
411 Self {
412 parser_kind,
413 parse_one,
414 max_buffer,
415 init: BufferedFrameDrain::with_max_buffer(max_buffer),
416 resp: BufferedFrameDrain::with_max_buffer(max_buffer),
417 }
418 }
419}
420
421impl<F, M> SessionParser for AccumulatingSessionParser<F, M>
422where
423 F: Fn(&[u8]) -> Option<(M, usize)> + Clone + Send + 'static,
424 M: Send + std::fmt::Debug + 'static,
425{
426 type Message = M;
427
428 fn feed_initiator(&mut self, bytes: &[u8], _ts: Timestamp, out: &mut Vec<M>) {
429 if self.init.extend(bytes).is_err() {
430 out.append(&mut self.init.take_messages());
431 return;
432 }
433 let parse_one = self.parse_one.clone();
434 self.init.drain_with(parse_one);
435 out.append(&mut self.init.take_messages());
436 }
437
438 fn feed_responder(&mut self, bytes: &[u8], _ts: Timestamp, out: &mut Vec<M>) {
439 if self.resp.extend(bytes).is_err() {
440 out.append(&mut self.resp.take_messages());
441 return;
442 }
443 let parse_one = self.parse_one.clone();
444 self.resp.drain_with(parse_one);
445 out.append(&mut self.resp.take_messages());
446 }
447
448 fn parser_kind(&self) -> ParserKind {
449 self.parser_kind
450 }
451
452 fn is_poisoned(&self) -> bool {
453 self.init.is_poisoned() || self.resp.is_poisoned()
454 }
455
456 fn poison_reason(&self) -> Option<&str> {
457 // First poisoned side wins.
458 let err = self.init.poison_reason().or(self.resp.poison_reason())?;
459 Some(match err {
460 FrameDrainError::BufferFull => "buffer cap exceeded",
461 FrameDrainError::ZeroByteAdvance => "parse_one returned zero-byte advance",
462 })
463 }
464}
465
466/// Convenience [`DatagramParser`] impl over a `parse_one` closure
467/// of shape `Fn(&[u8]) -> Option<M>`.
468///
469/// One UDP packet, one optional message. The closure receives the
470/// raw payload; return `Some(message)` to emit one event,
471/// `None` to drop the packet silently.
472///
473/// New in 0.10.0 (plan 106).
474pub struct PerDatagramParser<F, M>
475where
476 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
477 M: Send + std::fmt::Debug + 'static,
478{
479 parser_kind: ParserKind,
480 parse_one: F,
481}
482
483impl<F, M> std::fmt::Debug for PerDatagramParser<F, M>
484where
485 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
486 M: Send + std::fmt::Debug + 'static,
487{
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 f.debug_struct("PerDatagramParser")
490 .field("parser_kind", &self.parser_kind)
491 .finish()
492 }
493}
494
495impl<F, M> Clone for PerDatagramParser<F, M>
496where
497 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
498 M: Send + std::fmt::Debug + 'static,
499{
500 fn clone(&self) -> Self {
501 Self {
502 parser_kind: self.parser_kind,
503 parse_one: self.parse_one.clone(),
504 }
505 }
506}
507
508impl<F, M> PerDatagramParser<F, M>
509where
510 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
511 M: Send + std::fmt::Debug + 'static,
512{
513 pub fn new(parser_kind: ParserKind, parse_one: F) -> Self {
514 Self {
515 parser_kind,
516 parse_one,
517 }
518 }
519}
520
521impl<F, M> DatagramParser for PerDatagramParser<F, M>
522where
523 F: Fn(&[u8]) -> Option<M> + Clone + Send + 'static,
524 M: Send + std::fmt::Debug + 'static,
525{
526 type Message = M;
527
528 fn parse(&mut self, payload: &[u8], _side: FlowSide, _ts: Timestamp, out: &mut Vec<M>) {
529 if let Some(msg) = (self.parse_one)(payload) {
530 out.push(msg);
531 }
532 }
533
534 fn parser_kind(&self) -> ParserKind {
535 self.parser_kind
536 }
537}
538
539/// Parses a stream-oriented L7 protocol session. One instance per
540/// flow; both directions feed through the same parser, allowing
541/// state to interleave.
542///
543/// Implementors are owned by the per-flow slot; sync (no `await`).
544/// Backpressure flows from the consuming `Stream` back to the
545/// kernel ring once the per-flow message buffer fills up — see
546/// the `netring::SessionStream` adapter.
547///
548/// # Per-flow rich state
549///
550/// For consumers that maintain per-flow user state updated by BOTH
551/// the reassembler and the parser — TCP rich stats, application-
552/// level counters, middleware state machines — keep that state on
553/// [`crate::FlowEntry::user`] (typed via the `S` parameter on
554/// the internal session engine) and update it from your event
555/// loop after `track()`. The pattern is documented in
556/// `docs/recipes.md` → "Per-flow user state via the consumer
557/// loop". Avoid piping `&mut S` through `feed_*` — it would
558/// ripple a generic parameter through every shipped parser.
559pub trait SessionParser: Send + 'static {
560 /// L7 message produced by this parser.
561 ///
562 /// - `Send + 'static` so messages can cross task boundaries.
563 /// - `Debug` is required for the per-message `tracing::trace!`
564 /// event under the `tracing` feature (filter via
565 /// `EnvFilter::new("flowscope.message=warn")` to suppress).
566 /// Almost every Rust type derives `Debug` anyway, and the
567 /// bound is trivial to add for those that don't.
568 type Message: Send + std::fmt::Debug + 'static;
569
570 /// Feed the next chunk of bytes from the **initiator** side.
571 /// `ts` is the observed time of the packet carrying these bytes.
572 /// Push any complete messages parsed during this call into
573 /// `out`.
574 ///
575 /// **0.11 break (plan 119):** signature changed from
576 /// `-> Vec<Self::Message>` to taking `out: &mut Vec<…>`.
577 /// Same idiom as `httparse::Request::parse` etc.
578 fn feed_initiator(&mut self, bytes: &[u8], ts: Timestamp, out: &mut Vec<Self::Message>);
579
580 /// Feed the next chunk of bytes from the **responder** side.
581 fn feed_responder(&mut self, bytes: &[u8], ts: Timestamp, out: &mut Vec<Self::Message>);
582
583 /// The initiator side ended cleanly: called once at flow end when
584 /// the flow ended gracefully ([`EndReason::is_graceful`]: FIN,
585 /// idle timeout, force close) or the initiator sent a FIN — after
586 /// its final bytes were fed. Not called for a side the parser
587 /// stopped reading ([`GapResponse::StopSide`], reassembly stop).
588 /// Default: no-op.
589 fn fin_initiator(&mut self, _out: &mut Vec<Self::Message>) {}
590
591 /// The responder side ended cleanly; see
592 /// [`fin_initiator`](Self::fin_initiator).
593 fn fin_responder(&mut self, _out: &mut Vec<Self::Message>) {}
594
595 /// The initiator side ended abruptly: called once at flow end
596 /// instead of [`fin_initiator`](Self::fin_initiator) when the flow
597 /// was not ended gracefully (RST, eviction) and this side sent no
598 /// FIN. Not called for a stopped side. Default: no-op.
599 fn rst_initiator(&mut self) {}
600
601 /// The responder side ended abruptly; see
602 /// [`rst_initiator`](Self::rst_initiator).
603 fn rst_responder(&mut self) {}
604
605 /// Periodic time hook. The driver calls this on every `sweep` /
606 /// `finish` with the sweep's `now`, for every still-live parser.
607 /// Lets stateful parsers emit time-driven messages (timeouts,
608 /// unanswered requests). Emitted messages are attributed to
609 /// [`FlowSide::Initiator`]. Default: no-op.
610 fn on_tick(&mut self, _now: Timestamp, _out: &mut Vec<Self::Message>) {}
611
612 /// Bytes are missing from `side`'s stream: `missing` bytes the
613 /// reassembler never saw (capture loss, asymmetric routing, an
614 /// out-of-order hole that expired, or bytes dropped by the
615 /// sliding-window buffer cap). The next `feed_*` call for that
616 /// side resumes **after** the hole.
617 ///
618 /// Return [`GapResponse::Continue`] if the parser can cope (it
619 /// resynchronises on message boundaries, or only counts bytes).
620 /// The default, [`GapResponse::StopSide`], stops feeding **this
621 /// side** (a framed parser fed a spliced stream would otherwise
622 /// mis-parse silently, as in Suricata, where app-layer parsers
623 /// that don't declare gap support stop on a gap) while the other
624 /// side keeps being parsed and still gets its `fin_*` / `rst_*`.
625 /// [`GapResponse::Stop`] closes the whole parser
626 /// ([`EndReason::StreamGap`]) — for protocols whose state spans
627 /// both directions (HTTP/2's HPACK).
628 ///
629 /// A stopped side gets no further `feed_*`, `on_gap` or
630 /// `fin_*` / `rst_*` call: flush anything worth keeping here.
631 /// Messages pushed into `out` are emitted before the stop.
632 fn on_gap(
633 &mut self,
634 _side: FlowSide,
635 _missing: u64,
636 _ts: Timestamp,
637 _out: &mut Vec<Self::Message>,
638 ) -> GapResponse {
639 GapResponse::StopSide
640 }
641
642 /// True after the parser has hit an unrecoverable error and
643 /// can no longer make progress. The engines check this after
644 /// every `feed_*` / `on_gap` / `on_tick` call and close the parser
645 /// on `true` — it is not fed again for that flow, and the flow
646 /// itself stays tracked. Default: `false` (parser never poisons).
647 ///
648 /// Parsers that want to drop a malformed message and keep
649 /// going should NOT use this — just don't push the message
650 /// into the returned `Vec`. Reserve poison for cases where
651 /// internal state is corrupted past recovery (desynced framing,
652 /// invalid magic bytes that won't appear later, etc.).
653 ///
654 /// The reassembly-side counterpart is
655 /// [`crate::Reassembler::stop_reason`].
656 fn is_poisoned(&self) -> bool {
657 false
658 }
659
660 /// Optional human-readable description of why the parser
661 /// poisoned. Consulted only when [`is_poisoned`](Self::is_poisoned)
662 /// returns `true`. Default: `None`.
663 ///
664 /// The driver truncates to ~256 bytes when forwarding the
665 /// resulting `FlowAnomaly` ([`AnomalyKind::SessionParseError`]).
666 fn poison_reason(&self) -> Option<&str> {
667 None
668 }
669
670 /// Symmetric "I'm done — close this flow cleanly" signal.
671 /// Default: `false` (parser never self-terminates).
672 ///
673 /// Returning `true` tells the engine this parser has no more
674 /// useful work to extract. The engine closes the parser with
675 /// [`crate::EndReason::ParserDone`] right after emitting the
676 /// messages of the same `feed_*` / `on_tick` call and stops
677 /// feeding it; the flow itself stays tracked until its transport
678 /// end.
679 ///
680 /// Reserve for protocols where **nothing** further on the
681 /// connection can matter: a session-end sentinel after which the
682 /// peers only close (e.g. an SMTP `QUIT` / `221` exchange), or a
683 /// protocol switch to something this parser cannot read. Not for
684 /// "one exchange complete" — DNS-over-TCP and HTTP/1.1 reuse the
685 /// connection, and a parser done after the first pair would lose
686 /// every later one. Do **not** use this to give up on bad input —
687 /// that's [`is_poisoned`](Self::is_poisoned), which routes through
688 /// [`crate::EndReason::ParseError`].
689 ///
690 /// Should be idempotent: once `is_done()` returns `true`, it
691 /// should keep returning `true` for the lifetime of the parser.
692 /// [`is_poisoned`](Self::is_poisoned) takes precedence — a
693 /// parser that's both `is_done` and `is_poisoned` surfaces as
694 /// `ParseError`, not `ParserDone`.
695 fn is_done(&self) -> bool {
696 false
697 }
698
699 /// Typed identity of this parser, surfaced on
700 /// [`crate::driver::Event::ParserClosed::parser_kind`] and
701 /// [`crate::driver::SlotHandle::parser_kind`]. Lifted from
702 /// `&'static str` to the [`ParserKind`] enum in 0.20 (#109);
703 /// originally added in 0.5.0.
704 ///
705 /// Built-in parsers return their dedicated variant
706 /// ([`ParserKind::Http1`], [`ParserKind::DnsUdp`], …).
707 /// Downstream parsers either add a [`ParserKind::Other`] with a
708 /// stable, label-safe slug (`"crate-name/protocol"`) or leave
709 /// the default.
710 ///
711 /// - Default: [`ParserKind::Unspecified`] (no kind set).
712 /// - `as_str()` yields the metric-label slug (allocation-free).
713 fn parser_kind(&self) -> ParserKind {
714 ParserKind::Unspecified
715 }
716}
717
718/// Builds a fresh [`SessionParser`] per session. Modeled on
719/// [`crate::ReassemblerFactory`].
720///
721/// Most parsers can skip implementing this manually: any parser
722/// that's `SessionParser + Default + Clone` automatically becomes
723/// a factory via the blanket impl below.
724pub trait SessionParserFactory<K>: Send + 'static {
725 type Parser: SessionParser;
726 fn new_parser(&mut self, key: &K) -> Self::Parser;
727}
728
729impl<K, P> SessionParserFactory<K> for P
730where
731 P: SessionParser + Default + Clone,
732{
733 type Parser = P;
734 fn new_parser(&mut self, _key: &K) -> P {
735 self.clone()
736 }
737}
738
739/// Parses a packet-oriented L7 protocol. One instance per flow;
740/// receives one L4 payload at a time along with which side sent it.
741pub trait DatagramParser: Send + 'static {
742 /// L7 message produced by this parser. Same `Debug` bound as
743 /// [`SessionParser::Message`].
744 type Message: Send + std::fmt::Debug + 'static;
745
746 /// Parse one L4 payload. `side` is the direction relative to
747 /// the flow's initiator; `ts` is the observed time of the
748 /// datagram. Push any complete messages decoded into `out`.
749 ///
750 /// **0.11 break (plan 119):** signature changed from
751 /// `-> Vec<Self::Message>` to taking `out: &mut Vec<…>`.
752 fn parse(
753 &mut self,
754 payload: &[u8],
755 side: FlowSide,
756 ts: Timestamp,
757 out: &mut Vec<Self::Message>,
758 );
759
760 /// Periodic time hook — see [`SessionParser::on_tick`]. The
761 /// driver calls this on every `sweep` / `finish`. Default: no-op.
762 fn on_tick(&mut self, _now: Timestamp, _out: &mut Vec<Self::Message>) {}
763
764 /// True after the parser has hit an unrecoverable error. See
765 /// [`SessionParser::is_poisoned`] for the contract.
766 fn is_poisoned(&self) -> bool {
767 false
768 }
769
770 /// Optional reason for poison. See
771 /// [`SessionParser::poison_reason`].
772 fn poison_reason(&self) -> Option<&str> {
773 None
774 }
775
776 /// Symmetric "I'm done — close this flow cleanly" signal,
777 /// mirroring [`SessionParser::is_done`]. Default: `false`.
778 fn is_done(&self) -> bool {
779 false
780 }
781
782 /// See [`SessionParser::parser_kind`]. Default
783 /// [`ParserKind::Unspecified`].
784 fn parser_kind(&self) -> ParserKind {
785 ParserKind::Unspecified
786 }
787
788 /// Which transports this parser reads. The engines hand it only
789 /// datagrams of these transports: a UDP parser never sees an
790 /// ICMP message and an ICMP parser never sees a UDP payload.
791 /// Default [`Transports::UDP`]. New in 0.25.0.
792 fn transports(&self) -> Transports {
793 Transports::UDP
794 }
795}
796
797bitflags::bitflags! {
798 /// Transports a [`DatagramParser`] reads (see
799 /// [`DatagramParser::transports`]). New in 0.25.0.
800 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
801 pub struct Transports: u8 {
802 /// UDP payloads.
803 const UDP = 1;
804 /// Whole ICMPv4 messages.
805 const ICMP = 1 << 1;
806 /// Whole ICMPv6 messages.
807 const ICMPV6 = 1 << 2;
808 /// SCTP packets (the whole SCTP packet: common header and chunks).
809 const SCTP = 1 << 3;
810 /// Any other non-TCP IP protocol (the whole L4 payload).
811 const OTHER = 1 << 4;
812 }
813}
814
815impl Transports {
816 /// ICMPv4 and ICMPv6.
817 pub const ICMP_ANY: Transports = Transports::ICMP.union(Transports::ICMPV6);
818
819 /// Whether a datagram of this L4 protocol is admitted. Unknown
820 /// L4 (`None`) counts as [`Transports::OTHER`].
821 pub fn admits(self, l4: Option<crate::L4Proto>) -> bool {
822 use crate::L4Proto;
823 let bit = match l4 {
824 Some(L4Proto::Udp) => Transports::UDP,
825 Some(L4Proto::Icmp) => Transports::ICMP,
826 Some(L4Proto::IcmpV6) => Transports::ICMPV6,
827 Some(L4Proto::Sctp) => Transports::SCTP,
828 Some(L4Proto::Tcp) => return false,
829 _ => Transports::OTHER,
830 };
831 self.contains(bit)
832 }
833}
834
835/// Builds a fresh [`DatagramParser`] per session.
836pub trait DatagramParserFactory<K>: Send + 'static {
837 type Parser: DatagramParser;
838 fn new_parser(&mut self, key: &K) -> Self::Parser;
839
840 /// Transports the parsers read (see
841 /// [`DatagramParser::transports`]). Default [`Transports::UDP`];
842 /// factories that clone a template delegate to it.
843 fn transports(&self) -> Transports {
844 Transports::UDP
845 }
846}
847
848impl<K, P> DatagramParserFactory<K> for P
849where
850 P: DatagramParser + Default + Clone,
851{
852 type Parser = P;
853 fn new_parser(&mut self, _key: &K) -> P {
854 self.clone()
855 }
856 fn transports(&self) -> Transports {
857 DatagramParser::transports(self)
858 }
859}
860
861/// Answer to [`SessionParser::on_gap`].
862#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
863#[non_exhaustive]
864pub enum GapResponse {
865 /// Keep feeding this parser; bytes after the gap follow.
866 Continue,
867 /// **Default.** Stop feeding the side with the gap; the other
868 /// side keeps being parsed. Reported as a parser side stop
869 /// ([`SessionEvent::ParserSideStopped`] /
870 /// [`crate::driver::Event::ParserSideStopped`]). When both sides
871 /// are stopped the parser is closed. New in 0.25.0.
872 #[default]
873 StopSide,
874 /// Close the parser for this flow ([`EndReason::StreamGap`]).
875 Stop,
876}
877
878/// Output of [`SessionDriver`] / [`DatagramDriver`]: flow lifecycle,
879/// parser messages, parser closes and anomalies, in the order they
880/// happened. `K` is the flow key, `M` the parser's message type.
881///
882/// For one flow the order is: `Started`, then any number of
883/// `Application` / `FlowAnomaly` / `Tick` / `ParserSideStopped`,
884/// possibly one `ParserClosed` (the parser gave up or finished early
885/// — the flow goes on), then `Closed`. Messages a parser flushes at
886/// flow end (`fin_*`) come before `Closed`.
887#[derive(Debug, Clone)]
888#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
889#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
890#[cfg_attr(
891 feature = "serde",
892 serde(bound(
893 serialize = "K: serde::Serialize, M: serde::Serialize",
894 deserialize = "K: serde::de::DeserializeOwned, M: serde::de::DeserializeOwned"
895 ))
896)]
897#[non_exhaustive]
898pub enum SessionEvent<K, M> {
899 /// First packet of a new flow.
900 Started {
901 key: K,
902 /// Logical role of the side that sent the first packet
903 /// (arrival-order relative; see `orientation` for the
904 /// race-stable axis).
905 side: FlowSide,
906 /// Canonical, address-sorted direction of the first packet.
907 orientation: Orientation,
908 ts: Timestamp,
909 l4: Option<L4Proto>,
910 },
911 /// The parser emitted a message.
912 Application {
913 key: K,
914 /// Side whose bytes produced the message (`on_tick` output is
915 /// attributed to the initiator).
916 side: FlowSide,
917 /// Canonical direction matching `side`.
918 orientation: Orientation,
919 message: M,
920 /// Timestamp of the packet (or sweep) that produced it.
921 ts: Timestamp,
922 /// [`SessionParser::parser_kind`] of the producing parser.
923 parser_kind: ParserKind,
924 },
925 /// The parser was closed before its flow ended — poisoned
926 /// ([`EndReason::ParseError`]), done ([`EndReason::ParserDone`]),
927 /// a gap answered with [`GapResponse::Stop`]
928 /// ([`EndReason::StreamGap`]), or both sides stopped (detail
929 /// `"both sides stopped"`, reason of the last side stop). The flow
930 /// stays tracked and ends later with `Closed`; the parser is not
931 /// fed again for it. (A parser still open when its flow ends gets
932 /// no `ParserClosed`: `Closed` says so — so `reason.is_parser()`
933 /// always holds here. The typed [`crate::driver::Driver`] differs:
934 /// it emits an `Event::ParserClosed` with the transport reason per
935 /// slot right before `Ended`; [`EndReason::is_transport`] /
936 /// [`EndReason::is_parser`] tell the two apart.) Not emitted while
937 /// events are paused (0.26, #204).
938 ParserClosed {
939 key: K,
940 parser_kind: ParserKind,
941 reason: EndReason,
942 /// Human-readable detail: the parser's `poison_reason()`
943 /// (truncated to 256 bytes), the gap size, or the stop reason.
944 detail: Option<String>,
945 ts: Timestamp,
946 },
947 /// The parser stopped reading one side of the flow — a gap it
948 /// cannot bridge ([`EndReason::StreamGap`],
949 /// [`GapResponse::StopSide`]) or a reassembly limit on that side
950 /// ([`EndReason::BufferOverflow`]). The other side keeps being
951 /// parsed; when both sides are stopped a `ParserClosed` follows.
952 /// New in 0.25.0.
953 ParserSideStopped {
954 key: K,
955 parser_kind: ParserKind,
956 side: FlowSide,
957 reason: EndReason,
958 /// The gap size or the reassembly stop reason.
959 detail: Option<String>,
960 ts: Timestamp,
961 },
962 /// The flow ended (transport reason: FIN / RST / idle / eviction /
963 /// force-close). `stats` includes the reassembly diagnostics.
964 Closed {
965 key: K,
966 reason: EndReason,
967 stats: FlowStats,
968 l4: Option<L4Proto>,
969 ts: Timestamp,
970 },
971 /// Per-flow anomaly (reassembly, parser poison). Only when the
972 /// driver was built with anomalies enabled.
973 FlowAnomaly {
974 key: K,
975 kind: AnomalyKind,
976 ts: Timestamp,
977 },
978 /// Tracker-global anomaly (eviction pressure, memcap). Only when
979 /// anomalies are enabled.
980 TrackerAnomaly { kind: AnomalyKind, ts: Timestamp },
981 /// Periodic stats snapshot, when
982 /// [`crate::FlowTrackerConfig::flow_tick_interval`] is set.
983 Tick {
984 key: K,
985 stats: FlowStats,
986 ts: Timestamp,
987 },
988}
989
990impl<K, M> SessionEvent<K, M> {
991 /// The session form of a flow-tracker event, as the session
992 /// engines emit it: `Started` and anomalies / ticks map 1:1,
993 /// `Ended` becomes `Closed` (stamped with the flow's last packet).
994 /// Per-packet `Packet` / `Established` / `StateChange` have no
995 /// session form (`None`). Lets a caller moving from a flow stream to
996 /// a session stream keep the flow events it already queued.
997 pub fn from_flow_event(ev: crate::event::FlowEvent<K>) -> Option<Self> {
998 use crate::event::FlowEvent;
999 Some(match ev {
1000 FlowEvent::Started {
1001 key,
1002 side,
1003 orientation,
1004 ts,
1005 l4,
1006 } => SessionEvent::Started {
1007 key,
1008 side,
1009 orientation,
1010 ts,
1011 l4,
1012 },
1013 FlowEvent::Ended {
1014 key,
1015 reason,
1016 stats,
1017 l4,
1018 ..
1019 } => {
1020 let ts = stats.last_seen;
1021 SessionEvent::Closed {
1022 key,
1023 reason,
1024 stats,
1025 l4,
1026 ts,
1027 }
1028 }
1029 FlowEvent::FlowAnomaly { key, kind, ts } => SessionEvent::FlowAnomaly { key, kind, ts },
1030 FlowEvent::TrackerAnomaly { kind, ts } => SessionEvent::TrackerAnomaly { kind, ts },
1031 FlowEvent::Tick { key, stats, ts } => SessionEvent::Tick { key, stats, ts },
1032 FlowEvent::Packet { .. }
1033 | FlowEvent::Established { .. }
1034 | FlowEvent::StateChange { .. } => return None,
1035 })
1036 }
1037
1038 /// The flow key, when the variant has one.
1039 pub fn key(&self) -> Option<&K> {
1040 match self {
1041 SessionEvent::Started { key, .. }
1042 | SessionEvent::Application { key, .. }
1043 | SessionEvent::ParserClosed { key, .. }
1044 | SessionEvent::ParserSideStopped { key, .. }
1045 | SessionEvent::Closed { key, .. }
1046 | SessionEvent::FlowAnomaly { key, .. }
1047 | SessionEvent::Tick { key, .. } => Some(key),
1048 SessionEvent::TrackerAnomaly { .. } => None,
1049 }
1050 }
1051
1052 /// The event's timestamp.
1053 pub fn timestamp(&self) -> Timestamp {
1054 match self {
1055 SessionEvent::Started { ts, .. }
1056 | SessionEvent::Application { ts, .. }
1057 | SessionEvent::ParserClosed { ts, .. }
1058 | SessionEvent::ParserSideStopped { ts, .. }
1059 | SessionEvent::Closed { ts, .. }
1060 | SessionEvent::FlowAnomaly { ts, .. }
1061 | SessionEvent::TrackerAnomaly { ts, .. }
1062 | SessionEvent::Tick { ts, .. } => *ts,
1063 }
1064 }
1065
1066 /// The anomaly kind, for the two anomaly variants.
1067 pub fn anomaly_kind(&self) -> Option<&AnomalyKind> {
1068 match self {
1069 SessionEvent::FlowAnomaly { kind, .. } | SessionEvent::TrackerAnomaly { kind, .. } => {
1070 Some(kind)
1071 }
1072 _ => None,
1073 }
1074 }
1075}
1076
1077/// A [`SessionParserFactory`] / [`DatagramParserFactory`] that clones
1078/// a template parser for every flow. The blanket factory impls need
1079/// `Default + Clone`; this one only needs `Clone`, for parsers built
1080/// from configuration.
1081#[derive(Debug, Clone)]
1082pub struct TemplateFactory<P>(pub P);
1083
1084impl<K, P> SessionParserFactory<K> for TemplateFactory<P>
1085where
1086 P: SessionParser + Clone,
1087{
1088 type Parser = P;
1089 fn new_parser(&mut self, _key: &K) -> P {
1090 self.0.clone()
1091 }
1092}
1093
1094impl<K, P> DatagramParserFactory<K> for TemplateFactory<P>
1095where
1096 P: DatagramParser + Clone,
1097{
1098 type Parser = P;
1099 fn new_parser(&mut self, _key: &K) -> P {
1100 self.0.clone()
1101 }
1102 fn transports(&self) -> Transports {
1103 self.0.transports()
1104 }
1105}
1106
1107#[cfg(any(feature = "ftp", feature = "smtp"))]
1108/// Gap recovery for line-oriented protocols: the bytes right after a
1109/// gap start mid-line, so they are discarded up to and including the
1110/// next `\n`. Feed each new chunk through this while `pending` is
1111/// set; returns the part of `bytes` to parse.
1112pub(crate) fn skip_partial_line<'a>(pending: &mut bool, bytes: &'a [u8]) -> &'a [u8] {
1113 if !*pending {
1114 return bytes;
1115 }
1116 match bytes.iter().position(|&b| b == b'\n') {
1117 Some(p) => {
1118 *pending = false;
1119 &bytes[p + 1..]
1120 }
1121 None => &[],
1122 }
1123}
1124
1125#[cfg(any(feature = "smb", feature = "modbus"))]
1126/// Gap recovery for framed binary protocols: when `pending`, drop
1127/// buffered bytes up to the first offset `find` accepts as a frame
1128/// start (keeping at most `keep` trailing bytes while none is found).
1129pub(crate) fn resync_frames(
1130 pending: &mut bool,
1131 buf: &mut bytes::BytesMut,
1132 keep: usize,
1133 find: impl Fn(&[u8]) -> Option<usize>,
1134) {
1135 if !*pending {
1136 return;
1137 }
1138 match find(buf) {
1139 Some(at) => {
1140 let _ = buf.split_to(at);
1141 *pending = false;
1142 }
1143 None => {
1144 let drop = buf.len().saturating_sub(keep);
1145 let _ = buf.split_to(drop);
1146 }
1147 }
1148}
1149
1150#[cfg(test)]
1151mod tests {
1152 use super::*;
1153
1154 #[test]
1155 fn flow_events_map_to_their_session_form() {
1156 use crate::event::FlowEvent;
1157 let stats = FlowStats {
1158 last_seen: Timestamp::new(7, 0),
1159 ..Default::default()
1160 };
1161 let closed: Option<SessionEvent<u8, ()>> =
1162 SessionEvent::from_flow_event(FlowEvent::Ended {
1163 key: 1u8,
1164 reason: EndReason::Fin,
1165 stats,
1166 history: Default::default(),
1167 l4: None,
1168 });
1169 assert!(matches!(
1170 closed,
1171 Some(SessionEvent::Closed { key: 1, reason: EndReason::Fin, ts, .. }) if ts == Timestamp::new(7, 0)
1172 ));
1173 let tick: Option<SessionEvent<u8, ()>> = SessionEvent::from_flow_event(FlowEvent::Tick {
1174 key: 2u8,
1175 stats: FlowStats::default(),
1176 ts: Timestamp::new(8, 0),
1177 });
1178 assert!(matches!(tick, Some(SessionEvent::Tick { key: 2, .. })));
1179 }
1180
1181 #[derive(Default, Clone)]
1182 struct CountParser {
1183 init_bytes: usize,
1184 resp_bytes: usize,
1185 }
1186
1187 impl SessionParser for CountParser {
1188 type Message = (FlowSide, usize);
1189 fn feed_initiator(&mut self, b: &[u8], _ts: Timestamp, out: &mut Vec<Self::Message>) {
1190 self.init_bytes += b.len();
1191 out.push((FlowSide::Initiator, self.init_bytes));
1192 }
1193 fn feed_responder(&mut self, b: &[u8], _ts: Timestamp, out: &mut Vec<Self::Message>) {
1194 self.resp_bytes += b.len();
1195 out.push((FlowSide::Responder, self.resp_bytes));
1196 }
1197 }
1198
1199 #[test]
1200 fn auto_impl_session_parser_factory() {
1201 // CountParser is Default + Clone + SessionParser → automatic factory.
1202 let mut f: CountParser = CountParser::default();
1203 let mut p: CountParser = SessionParserFactory::<u32>::new_parser(&mut f, &7);
1204 let mut m = Vec::new();
1205 p.feed_initiator(b"abc", Timestamp::default(), &mut m);
1206 assert_eq!(m, vec![(FlowSide::Initiator, 3)]);
1207 }
1208
1209 #[derive(Default, Clone)]
1210 struct EchoDgram;
1211 impl DatagramParser for EchoDgram {
1212 type Message = (FlowSide, Vec<u8>);
1213 fn parse(
1214 &mut self,
1215 payload: &[u8],
1216 side: FlowSide,
1217 _ts: Timestamp,
1218 out: &mut Vec<Self::Message>,
1219 ) {
1220 out.push((side, payload.to_vec()));
1221 }
1222 }
1223
1224 #[test]
1225 fn auto_impl_datagram_parser_factory() {
1226 let mut f = EchoDgram;
1227 let mut p: EchoDgram = DatagramParserFactory::<()>::new_parser(&mut f, &());
1228 let mut m = Vec::new();
1229 p.parse(b"hello", FlowSide::Responder, Timestamp::default(), &mut m);
1230 assert_eq!(m, vec![(FlowSide::Responder, b"hello".to_vec())]);
1231 }
1232}