use std::collections::HashMap;
use std::hash::Hash;
use ahash::RandomState;
use smallvec::SmallVec;
use crate::Timestamp;
use crate::detect::signatures::{SignatureFn, SignatureMatch};
use crate::event::{AnomalyKind, EndReason, FlowSide, FlowStats};
use crate::extractor::Orientation;
use crate::parser_kind::ParserKind;
use crate::reassembler::{Chunk, Chunks, ReassemblyStop, StreamChunks};
use crate::session::{
DatagramParser, DatagramParserFactory, GapResponse, SessionParser, SessionParserFactory,
Transports,
};
pub(crate) const REASON_MAX_BYTES: usize = 256;
pub const DEFAULT_PROBE_PACKETS: u8 = 4;
pub const PROBE_BUFFER_CAP: usize = 64;
pub(crate) const PROBE_REPLAY_BYTE_CAP: usize = 64 * 1024;
pub(crate) fn truncate_reason(s: &str) -> String {
let mut owned = String::from(s);
if owned.len() > REASON_MAX_BYTES {
let cap = (0..=REASON_MAX_BYTES)
.rev()
.find(|i| owned.is_char_boundary(*i))
.unwrap_or(0);
owned.truncate(cap);
}
owned
}
pub(crate) type Ports = Option<(u16, u16)>;
#[derive(Clone, Copy)]
pub(crate) enum Stream<'a> {
Chunks(&'a StreamChunks),
Bytes(&'a [u8]),
}
impl<'a> Stream<'a> {
pub(crate) fn is_empty(&self) -> bool {
match self {
Stream::Chunks(c) => c.is_empty(),
Stream::Bytes(b) => b.is_empty(),
}
}
pub(crate) fn len(&self) -> usize {
match self {
Stream::Chunks(c) => c.len(),
Stream::Bytes(b) => b.len(),
}
}
pub(crate) fn stop(&self) -> Option<ReassemblyStop> {
match self {
Stream::Chunks(c) => c.stop(),
Stream::Bytes(_) => None,
}
}
pub(crate) fn iter(&self) -> StreamIter<'a> {
match *self {
Stream::Chunks(c) => StreamIter::Chunks(c.iter()),
Stream::Bytes(b) => StreamIter::Bytes((!b.is_empty()).then_some(b)),
}
}
pub(crate) fn to_chunks(self) -> StreamChunks {
match self {
Stream::Chunks(c) => c.clone(),
Stream::Bytes(b) => {
let mut c = StreamChunks::new();
c.push_data(b);
c
}
}
}
}
pub(crate) enum StreamIter<'a> {
Chunks(Chunks<'a>),
Bytes(Option<&'a [u8]>),
}
impl<'a> Iterator for StreamIter<'a> {
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Chunk<'a>> {
match self {
StreamIter::Chunks(c) => c.next(),
StreamIter::Bytes(b) => b.take().map(Chunk::Data),
}
}
}
pub(crate) enum Selector {
All,
Ports(SmallVec<[u16; 4]>),
Signature {
signature: SignatureFn,
max_probe_packets: u8,
},
}
impl Selector {
pub(crate) fn needs_ports(&self) -> bool {
matches!(self, Selector::Ports(_))
}
pub(crate) fn admits(&self, ports: Ports) -> bool {
match self {
Selector::Ports(set) => {
ports.is_some_and(|(s, d)| set.contains(&s) || set.contains(&d))
}
Selector::All | Selector::Signature { .. } => true,
}
}
}
pub(crate) trait Output<K, M> {
fn message(
&mut self,
key: &K,
side: FlowSide,
orientation: Orientation,
message: M,
ts: Timestamp,
parser_kind: ParserKind,
);
fn parser_closed(
&mut self,
key: &K,
parser_kind: ParserKind,
reason: EndReason,
detail: Option<String>,
ts: Timestamp,
at_flow_end: bool,
);
fn parser_side_stopped(
&mut self,
key: &K,
parser_kind: ParserKind,
side: FlowSide,
reason: EndReason,
detail: Option<String>,
ts: Timestamp,
);
fn anomaly(&mut self, key: &K, kind: AnomalyKind, ts: Timestamp);
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Emit {
pub count_anomalies: bool,
pub anomalies: bool,
pub parser_events: bool,
pub flow_end_closes: bool,
}
pub(crate) struct Ctx<'a, K> {
pub key: &'a K,
pub l4: Option<crate::L4Proto>,
pub side: FlowSide,
pub orientation: Orientation,
pub ts: Timestamp,
pub emit: Emit,
}
enum Close {
Poison(Option<String>),
Done,
Gap(FlowSide, u64),
BothSides(EndReason),
}
impl Close {
fn reason(&self) -> EndReason {
match self {
Close::Poison(_) => EndReason::ParseError,
Close::Done => EndReason::ParserDone,
Close::Gap(..) => EndReason::StreamGap,
Close::BothSides(reason) => *reason,
}
}
fn detail(self) -> Option<String> {
match self {
Close::Poison(r) => r,
Close::Done => None,
Close::Gap(side, n) => Some(format!("{n} bytes missing on the {side} side")),
Close::BothSides(_) => Some("both sides stopped".to_owned()),
}
}
}
enum FeedEnd {
Close(Close),
StopSide(EndReason, String),
}
struct Active<P> {
parser: P,
stopped: [bool; 2],
}
fn side_idx(side: FlowSide) -> usize {
match side {
FlowSide::Initiator => 0,
FlowSide::Responder => 1,
}
}
impl<P: SessionParser> Active<P> {
fn new(parser: P) -> Self {
Self {
parser,
stopped: [false, false],
}
}
fn feed<K, O>(
&mut self,
cx: &Ctx<'_, K>,
stream: &Stream<'_>,
scratch: &mut Vec<P::Message>,
out: &mut O,
) -> bool
where
O: Output<K, P::Message>,
{
let si = side_idx(cx.side);
if self.stopped[si] {
return false;
}
let kind = self.parser.parser_kind();
match feed(&mut self.parser, cx, stream, scratch, out) {
None => false,
Some(FeedEnd::Close(close)) => {
emit_close(cx, kind, close, out);
true
}
Some(FeedEnd::StopSide(reason, detail)) => {
self.stopped[si] = true;
crate::obs::record_parser_side_stopped(kind, cx.side, reason);
if cx.emit.parser_events {
out.parser_side_stopped(cx.key, kind, cx.side, reason, Some(detail), cx.ts);
}
if self.stopped == [true, true] {
emit_close(cx, kind, Close::BothSides(reason), out);
return true;
}
false
}
}
}
}
#[derive(Default)]
struct Probe {
packets: u8,
prefix: [SmallVec<[u8; PROBE_BUFFER_CAP]>; 2],
sealed: [bool; 2],
replay: Vec<(FlowSide, Orientation, Timestamp, StreamChunks)>,
replay_bytes: usize,
kept: [bool; 2],
lost: [u64; 2],
}
enum Verdict {
Match,
Reject,
Undecided,
}
impl Probe {
fn step(
&mut self,
cx: &Ctx<'_, impl Sized>,
stream: &Stream<'_>,
signature: SignatureFn,
max_probe_packets: u8,
) -> Verdict {
let si = side_idx(cx.side);
if !self.sealed[si] {
let prefix = &mut self.prefix[si];
for chunk in stream.iter() {
match chunk {
Chunk::Data(d) => {
let room = PROBE_BUFFER_CAP.saturating_sub(prefix.len());
prefix.extend_from_slice(&d[..room.min(d.len())]);
}
Chunk::Gap(_) => {
self.sealed[si] = true;
break;
}
}
}
if stream.stop().is_some() {
self.sealed[si] = true;
}
}
self.packets = self.packets.saturating_add(1);
let len = stream.len();
if !self.kept[si]
|| (self.lost[si] == 0 && self.replay_bytes + len <= PROBE_REPLAY_BYTE_CAP)
{
self.kept[si] = true;
self.replay_bytes += len;
self.replay
.push((cx.side, cx.orientation, cx.ts, stream.to_chunks()));
} else {
let gaps: u64 = stream
.iter()
.map(|c| match c {
Chunk::Gap(n) => n,
Chunk::Data(_) => 0,
})
.sum();
self.lost[si] += len as u64 + gaps;
}
let verdicts = (signature(&self.prefix[0]), signature(&self.prefix[1]));
if matches!(verdicts.0, SignatureMatch::Match)
|| matches!(verdicts.1, SignatureMatch::Match)
{
Verdict::Match
} else if (matches!(verdicts.0, SignatureMatch::NoMatch)
&& matches!(verdicts.1, SignatureMatch::NoMatch))
|| self.packets >= max_probe_packets
{
Verdict::Reject
} else {
Verdict::Undecided
}
}
}
enum SessionFlow<P> {
Probing(Box<Probe>),
Active(Active<P>),
Closed,
}
pub(crate) struct SessionCore<K, F>
where
F: SessionParserFactory<K>,
{
factory: F,
selector: Selector,
flows: HashMap<K, SessionFlow<F::Parser>, RandomState>,
scratch: Vec<<F::Parser as SessionParser>::Message>,
}
impl<K, F> SessionCore<K, F>
where
K: Hash + Eq + Clone,
F: SessionParserFactory<K>,
{
pub(crate) fn new(factory: F, selector: Selector) -> Self {
Self {
factory,
selector,
flows: HashMap::with_hasher(RandomState::new()),
scratch: Vec::new(),
}
}
pub(crate) fn selector(&self) -> &Selector {
&self.selector
}
pub(crate) fn wants(&self, ports: Ports) -> bool {
self.selector.admits(ports)
}
pub(crate) fn on_stream<O>(
&mut self,
cx: &Ctx<'_, K>,
ports: Ports,
chunks: &Stream<'_>,
out: &mut O,
) where
O: Output<K, <F::Parser as SessionParser>::Message>,
{
if chunks.is_empty() {
return;
}
if !self.flows.contains_key(cx.key) {
if !self.selector.admits(ports) {
return;
}
let state = match self.selector {
Selector::Signature { .. } => SessionFlow::Probing(Box::default()),
Selector::All | Selector::Ports(_) => {
SessionFlow::Active(Active::new(self.factory.new_parser(cx.key)))
}
};
self.flows.insert(cx.key.clone(), state);
}
let state = self.flows.get_mut(cx.key).expect("just inserted");
match state {
SessionFlow::Closed => {}
SessionFlow::Active(active) => {
if active.feed(cx, chunks, &mut self.scratch, out) {
*state = SessionFlow::Closed;
}
}
SessionFlow::Probing(probe) => {
let Selector::Signature {
signature,
max_probe_packets,
} = self.selector
else {
unreachable!("only signature cores probe")
};
match probe.step(cx, chunks, signature, max_probe_packets) {
Verdict::Undecided => {}
Verdict::Reject => *state = SessionFlow::Closed,
Verdict::Match => {
let probe = std::mem::take(&mut **probe);
let parser = self.factory.new_parser(cx.key);
*state = match pin(parser, probe, cx, &mut self.scratch, out) {
Some(active) => SessionFlow::Active(active),
None => SessionFlow::Closed,
};
}
}
}
}
}
pub(crate) fn streams_done(&self, key: &K, ports: Ports) -> [bool; 2] {
match self.flows.get(key) {
Some(SessionFlow::Closed) => [true, true],
Some(SessionFlow::Active(a)) => a.stopped,
Some(SessionFlow::Probing(_)) => [false, false],
None => {
let done = !self.selector.admits(ports);
[done, done]
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn on_flow_end<O>(
&mut self,
key: &K,
reason: EndReason,
stats: &FlowStats,
finals: [&StreamChunks; 2],
ports: Ports,
emit: Emit,
out: &mut O,
) where
O: Output<K, <F::Parser as SessionParser>::Message>,
{
let ts = stats.last_seen;
for (side, chunks) in [FlowSide::Initiator, FlowSide::Responder]
.into_iter()
.zip(finals)
{
if chunks.is_empty() {
continue;
}
let cx = Ctx {
key,
l4: Some(crate::L4Proto::Tcp),
side,
orientation: stats.orientation_for(side),
ts,
emit,
};
self.on_stream(&cx, ports, &Stream::Chunks(chunks), out);
}
let Some(SessionFlow::Active(mut active)) = self.flows.remove(key) else {
return;
};
let kind = active.parser.parser_kind();
for side in [FlowSide::Initiator, FlowSide::Responder] {
if active.stopped[side_idx(side)] {
continue;
}
let fin_seen = match side {
FlowSide::Initiator => stats.fin_initiator,
FlowSide::Responder => stats.fin_responder,
};
if reason.is_graceful() || fin_seen {
self.scratch.clear();
match side {
FlowSide::Initiator => active.parser.fin_initiator(&mut self.scratch),
FlowSide::Responder => active.parser.fin_responder(&mut self.scratch),
}
let orientation = stats.orientation_for(side);
for m in self.scratch.drain(..) {
out.message(key, side, orientation, m, ts, kind);
}
} else {
match side {
FlowSide::Initiator => active.parser.rst_initiator(),
FlowSide::Responder => active.parser.rst_responder(),
}
}
}
crate::obs::record_parser_closed(kind, reason);
if emit.flow_end_closes {
out.parser_closed(key, kind, reason, None, ts, true);
}
}
pub(crate) fn on_tick<O>(
&mut self,
now: Timestamp,
stamp: Timestamp,
orientation_of: &dyn Fn(&K) -> Orientation,
emit: Emit,
out: &mut O,
) where
O: Output<K, <F::Parser as SessionParser>::Message>,
{
for (key, state) in self.flows.iter_mut() {
let SessionFlow::Active(Active { parser, .. }) = state else {
continue;
};
let kind = parser.parser_kind();
let orientation = orientation_of(key);
self.scratch.clear();
parser.on_tick(now, &mut self.scratch);
for m in self.scratch.drain(..) {
out.message(key, FlowSide::Initiator, orientation, m, stamp, kind);
}
if let Some(close) = check_close(parser) {
*state = SessionFlow::Closed;
let cx = Ctx {
key,
l4: None,
side: FlowSide::Initiator,
orientation,
ts: stamp,
emit,
};
emit_close(&cx, kind, close, out);
}
}
}
pub(crate) fn retain(&mut self, alive: &dyn Fn(&K) -> bool) {
self.flows.retain(|k, _| alive(k));
}
}
fn pin<K, P, O>(
parser: P,
probe: Probe,
cx: &Ctx<'_, K>,
scratch: &mut Vec<P::Message>,
out: &mut O,
) -> Option<Active<P>>
where
P: SessionParser,
O: Output<K, P::Message>,
{
let mut active = Active::new(parser);
for (side, orientation, ts, chunks) in &probe.replay {
let rcx = Ctx {
key: cx.key,
l4: cx.l4,
side: *side,
orientation: *orientation,
ts: *ts,
emit: cx.emit,
};
if active.feed(&rcx, &Stream::Chunks(chunks), scratch, out) {
return None;
}
}
for (i, side) in [FlowSide::Initiator, FlowSide::Responder]
.into_iter()
.enumerate()
{
if probe.lost[i] == 0 {
continue;
}
let mut gap = StreamChunks::new();
gap.push_gap(probe.lost[i]);
let rcx = Ctx {
key: cx.key,
l4: cx.l4,
side,
orientation: match side == cx.side {
true => cx.orientation,
false => cx.orientation.flipped(),
},
ts: cx.ts,
emit: cx.emit,
};
if active.feed(&rcx, &Stream::Chunks(&gap), scratch, out) {
return None;
}
}
Some(active)
}
fn feed<K, P, O>(
parser: &mut P,
cx: &Ctx<'_, K>,
chunks: &Stream<'_>,
scratch: &mut Vec<P::Message>,
out: &mut O,
) -> Option<FeedEnd>
where
P: SessionParser,
O: Output<K, P::Message>,
{
let kind = parser.parser_kind();
for chunk in chunks.iter() {
scratch.clear();
let gap = match chunk {
Chunk::Data(bytes) => {
match cx.side {
FlowSide::Initiator => parser.feed_initiator(bytes, cx.ts, scratch),
FlowSide::Responder => parser.feed_responder(bytes, cx.ts, scratch),
}
None
}
Chunk::Gap(missing) => Some((missing, parser.on_gap(cx.side, missing, cx.ts, scratch))),
};
for m in scratch.drain(..) {
out.message(cx.key, cx.side, cx.orientation, m, cx.ts, kind);
}
if let Some(close) = check_close(parser) {
return Some(FeedEnd::Close(close));
}
match gap {
Some((missing, GapResponse::Stop)) => {
return Some(FeedEnd::Close(Close::Gap(cx.side, missing)));
}
Some((missing, GapResponse::StopSide)) => {
return Some(FeedEnd::StopSide(
EndReason::StreamGap,
format!("{missing} bytes missing"),
));
}
_ => {}
}
}
chunks.stop().map(|stop| {
FeedEnd::StopSide(
EndReason::BufferOverflow,
format!("reassembly stopped: {stop}"),
)
})
}
fn check_close<P: SessionParser>(parser: &P) -> Option<Close> {
if parser.is_poisoned() {
Some(Close::Poison(parser.poison_reason().map(truncate_reason)))
} else if parser.is_done() {
Some(Close::Done)
} else {
None
}
}
fn emit_close<K, M, O: Output<K, M>>(cx: &Ctx<'_, K>, kind: ParserKind, close: Close, out: &mut O) {
let reason = close.reason();
crate::obs::record_parser_closed(kind, reason);
if let Close::Poison(detail) = &close
&& cx.emit.count_anomalies
{
let anomaly = AnomalyKind::SessionParseError {
side: cx.side,
reason: detail.clone(),
parser_kind: kind,
slot: None,
};
crate::obs::record_anomaly(&anomaly);
crate::obs::trace_anomaly(&anomaly);
if cx.emit.anomalies {
out.anomaly(cx.key, anomaly, cx.ts);
}
}
if cx.emit.parser_events {
out.parser_closed(cx.key, kind, reason, close.detail(), cx.ts, false);
}
}
enum DatagramFlow<P> {
Probing { packets: u8 },
Active(P),
Closed,
}
pub(crate) struct DatagramCore<K, F>
where
F: DatagramParserFactory<K>,
{
factory: F,
selector: Selector,
transports: Transports,
flows: HashMap<K, DatagramFlow<F::Parser>, RandomState>,
scratch: Vec<<F::Parser as DatagramParser>::Message>,
}
impl<K, F> DatagramCore<K, F>
where
K: Hash + Eq + Clone,
F: DatagramParserFactory<K>,
{
pub(crate) fn new(factory: F, selector: Selector) -> Self {
let transports = factory.transports();
Self {
factory,
selector,
transports,
flows: HashMap::with_hasher(RandomState::new()),
scratch: Vec::new(),
}
}
pub(crate) fn selector(&self) -> &Selector {
&self.selector
}
pub(crate) fn wants(&self, ports: Ports, l4: Option<crate::L4Proto>) -> bool {
self.transports.admits(l4) && self.selector.admits(ports)
}
pub(crate) fn on_datagram<O>(
&mut self,
cx: &Ctx<'_, K>,
ports: Ports,
payload: &[u8],
out: &mut O,
) where
O: Output<K, <F::Parser as DatagramParser>::Message>,
{
if !self.transports.admits(cx.l4) {
return;
}
if !self.flows.contains_key(cx.key) {
if !self.selector.admits(ports) {
return;
}
let state = match self.selector {
Selector::Signature { .. } => DatagramFlow::Probing { packets: 0 },
Selector::All | Selector::Ports(_) => {
DatagramFlow::Active(self.factory.new_parser(cx.key))
}
};
self.flows.insert(cx.key.clone(), state);
}
let state = self.flows.get_mut(cx.key).expect("just inserted");
if let DatagramFlow::Probing { packets } = state {
let Selector::Signature {
signature,
max_probe_packets,
} = self.selector
else {
unreachable!("only signature cores probe")
};
match signature(payload) {
SignatureMatch::Match => {
*state = DatagramFlow::Active(self.factory.new_parser(cx.key))
}
verdict => {
*packets = packets.saturating_add(1);
if matches!(verdict, SignatureMatch::NoMatch) || *packets >= max_probe_packets {
*state = DatagramFlow::Closed;
}
return;
}
}
}
let DatagramFlow::Active(parser) = state else {
return;
};
let kind = parser.parser_kind();
self.scratch.clear();
parser.parse(payload, cx.side, cx.ts, &mut self.scratch);
for m in self.scratch.drain(..) {
out.message(cx.key, cx.side, cx.orientation, m, cx.ts, kind);
}
if let Some(close) = check_datagram_close(parser) {
*state = DatagramFlow::Closed;
emit_close(cx, kind, close, out);
}
}
pub(crate) fn on_flow_end<O>(
&mut self,
key: &K,
reason: EndReason,
stats: &FlowStats,
emit: Emit,
out: &mut O,
) where
O: Output<K, <F::Parser as DatagramParser>::Message>,
{
if let Some(DatagramFlow::Active(parser)) = self.flows.remove(key) {
crate::obs::record_parser_closed(parser.parser_kind(), reason);
if emit.flow_end_closes {
out.parser_closed(
key,
parser.parser_kind(),
reason,
None,
stats.last_seen,
true,
);
}
}
}
pub(crate) fn on_tick<O>(
&mut self,
now: Timestamp,
stamp: Timestamp,
orientation_of: &dyn Fn(&K) -> Orientation,
emit: Emit,
out: &mut O,
) where
O: Output<K, <F::Parser as DatagramParser>::Message>,
{
for (key, state) in self.flows.iter_mut() {
let DatagramFlow::Active(parser) = state else {
continue;
};
let kind = parser.parser_kind();
let orientation = orientation_of(key);
self.scratch.clear();
parser.on_tick(now, &mut self.scratch);
for m in self.scratch.drain(..) {
out.message(key, FlowSide::Initiator, orientation, m, stamp, kind);
}
if let Some(close) = check_datagram_close(parser) {
*state = DatagramFlow::Closed;
let cx = Ctx {
key,
l4: None,
side: FlowSide::Initiator,
orientation,
ts: stamp,
emit,
};
emit_close(&cx, kind, close, out);
}
}
}
pub(crate) fn retain(&mut self, alive: &dyn Fn(&K) -> bool) {
self.flows.retain(|k, _| alive(k));
}
}
fn check_datagram_close<P: DatagramParser>(parser: &P) -> Option<Close> {
if parser.is_poisoned() {
Some(Close::Poison(parser.poison_reason().map(truncate_reason)))
} else if parser.is_done() {
Some(Close::Done)
} else {
None
}
}