use std::time::Duration;
use quic_parser::reassemble_crypto_stream;
use super::parser::{CryptoFrame, build_from_stream, decode_frames};
use super::types::QuicInitial;
use crate::Timestamp;
use crate::event::FlowSide;
use crate::session::DatagramParser;
pub const QUIC_PORT: u16 = 443;
const DEFAULT_MAX_PENDING: usize = 1024;
const DEFAULT_PENDING_TTL: Duration = Duration::from_secs(5);
#[derive(Clone)]
struct Pending {
frames: Vec<CryptoFrame>,
last_seen: Timestamp,
}
#[derive(Default, Clone)]
pub struct QuicUdpParser {
pending: Vec<(Vec<u8>, Pending)>,
}
impl QuicUdpParser {
pub fn new() -> Self {
Self::default()
}
fn evict_stale(&mut self, now: Timestamp) {
self.pending.retain(|(_, p)| {
now.to_duration().saturating_sub(p.last_seen.to_duration()) <= DEFAULT_PENDING_TTL
});
}
fn accumulate(&mut self, dcid: Vec<u8>, frames: Vec<CryptoFrame>, ts: Timestamp) -> Vec<u8> {
if let Some((_, p)) = self.pending.iter_mut().find(|(k, _)| *k == dcid) {
p.frames.extend(frames);
p.last_seen = ts;
reassemble_crypto_stream(&p.frames)
} else {
if self.pending.len() >= DEFAULT_MAX_PENDING
&& let Some(idx) = self
.pending
.iter()
.enumerate()
.min_by_key(|(_, (_, p))| p.last_seen.to_duration())
.map(|(i, _)| i)
{
self.pending.remove(idx);
}
let stream = reassemble_crypto_stream(&frames);
self.pending.push((
dcid,
Pending {
frames,
last_seen: ts,
},
));
stream
}
}
fn forget(&mut self, dcid: &[u8]) {
self.pending.retain(|(k, _)| k != dcid);
}
}
impl DatagramParser for QuicUdpParser {
type Message = QuicInitial;
fn parser_kind(&self) -> crate::ParserKind {
crate::ParserKind::Quic
}
fn parse(
&mut self,
payload: &[u8],
_side: FlowSide,
ts: Timestamp,
out: &mut Vec<Self::Message>,
) {
let Ok((meta, frames)) = decode_frames(payload) else {
return;
};
let dcid = meta.dcid.clone();
let stream = self.accumulate(dcid.clone(), frames, ts);
let initial = build_from_stream(meta, &stream);
if client_hello_complete(&initial) {
self.forget(&dcid);
}
out.push(initial);
}
fn on_tick(&mut self, now: Timestamp, _out: &mut Vec<Self::Message>) {
self.evict_stale(now);
}
}
fn client_hello_complete(initial: &QuicInitial) -> bool {
#[cfg(feature = "tls")]
{
initial.client_hello.is_some()
}
#[cfg(not(feature = "tls"))]
{
initial.sni.is_some() || !initial.alpn.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parser_kind_and_port() {
let p = QuicUdpParser::new();
assert_eq!(p.parser_kind().as_str(), "quic");
assert_eq!(QUIC_PORT, 443);
}
#[test]
fn empty_payload_yields_no_message() {
let mut p = QuicUdpParser::new();
let mut out = Vec::new();
p.parse(&[], FlowSide::Initiator, Timestamp::default(), &mut out);
assert!(out.is_empty());
}
#[test]
fn accumulate_reassembles_crypto_across_datagrams() {
let mut p = QuicUdpParser::new();
let dcid = vec![9u8; 8];
let s1 = p.accumulate(
dcid.clone(),
vec![CryptoFrame {
offset: 0,
data: b"hello ".to_vec(),
}],
Timestamp::new(1, 0),
);
assert_eq!(s1, b"hello ");
let s2 = p.accumulate(
dcid.clone(),
vec![CryptoFrame {
offset: 6,
data: b"world".to_vec(),
}],
Timestamp::new(2, 0),
);
assert_eq!(s2, b"hello world", "frames from both datagrams joined");
let other = p.accumulate(
vec![7u8; 8],
vec![CryptoFrame {
offset: 0,
data: b"xyz".to_vec(),
}],
Timestamp::new(3, 0),
);
assert_eq!(other, b"xyz");
}
#[test]
fn on_tick_evicts_stale_pending() {
let mut p = QuicUdpParser::new();
p.pending.push((
vec![1, 2, 3, 4],
Pending {
frames: Vec::new(),
last_seen: Timestamp::new(0, 0),
},
));
assert_eq!(p.pending.len(), 1);
let mut out = Vec::new();
p.on_tick(Timestamp::new(10, 0), &mut out);
assert_eq!(p.pending.len(), 0, "stale pending evicted after TTL");
}
}