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;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct QuicConfig {
pub max_pending_connections: usize,
pub pending_ttl: Duration,
pub max_crypto_bytes: usize,
pub max_crypto_frames: usize,
}
impl Default for QuicConfig {
fn default() -> Self {
Self {
max_pending_connections: 1024,
pending_ttl: Duration::from_secs(5),
max_crypto_bytes: 64 * 1024,
max_crypto_frames: 64,
}
}
}
impl QuicConfig {
#[must_use]
pub fn with_max_pending_connections(mut self, n: usize) -> Self {
self.max_pending_connections = n;
self
}
#[must_use]
pub fn with_pending_ttl(mut self, ttl: Duration) -> Self {
self.pending_ttl = ttl;
self
}
#[must_use]
pub fn with_max_crypto_bytes(mut self, bytes: usize) -> Self {
self.max_crypto_bytes = bytes;
self
}
#[must_use]
pub fn with_max_crypto_frames(mut self, n: usize) -> Self {
self.max_crypto_frames = n;
self
}
}
#[derive(Clone)]
struct Pending {
frames: Vec<CryptoFrame>,
bytes: usize,
prefix_len: usize,
last_seen: Timestamp,
}
#[derive(Default, Clone)]
pub struct QuicUdpParser {
config: QuicConfig,
pending: Vec<(Vec<u8>, Pending)>,
pending_dropped: u64,
}
impl QuicUdpParser {
pub fn new() -> Self {
Self::default()
}
pub fn with_config(config: QuicConfig) -> Self {
Self {
config,
pending: Vec::new(),
pending_dropped: 0,
}
}
pub fn tracked(&self) -> usize {
self.pending.len()
}
pub fn pending_dropped(&self) -> u64 {
self.pending_dropped
}
fn evict_stale(&mut self, now: Timestamp) {
let ttl = self.config.pending_ttl;
let dropped = &mut self.pending_dropped;
self.pending.retain(|(_, p)| {
let alive = now.to_duration().saturating_sub(p.last_seen.to_duration()) <= ttl;
if !alive {
*dropped += p.frames.len() as u64;
}
alive
});
}
fn accumulate(&mut self, dcid: Vec<u8>, frames: Vec<CryptoFrame>, ts: Timestamp) -> Vec<u8> {
let max_bytes = self.config.max_crypto_bytes;
let max_frames = self.config.max_crypto_frames;
if let Some((_, p)) = self.pending.iter_mut().find(|(k, _)| *k == dcid) {
for f in frames {
if p.frames.len() >= max_frames || p.bytes + f.data.len() > max_bytes {
self.pending_dropped += 1;
continue;
}
p.bytes += f.data.len();
p.frames.push(f);
}
let stream = reassemble_crypto_stream(&p.frames);
if stream.len() > p.prefix_len {
p.prefix_len = stream.len();
p.last_seen = ts;
}
stream
} else {
if self.pending.len() >= self.config.max_pending_connections
&& let Some(idx) = self
.pending
.iter()
.enumerate()
.min_by_key(|(_, (_, p))| p.last_seen.to_duration())
.map(|(i, _)| i)
{
self.pending_dropped += self.pending[idx].1.frames.len() as u64;
self.pending.remove(idx);
}
let mut kept: Vec<CryptoFrame> = Vec::new();
let mut bytes = 0usize;
for f in frames {
if kept.len() >= max_frames || bytes + f.data.len() > max_bytes {
self.pending_dropped += 1;
continue;
}
bytes += f.data.len();
kept.push(f);
}
let stream = reassemble_crypto_stream(&kept);
self.pending.push((
dcid,
Pending {
frames: kept,
bytes,
prefix_len: stream.len(),
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(),
bytes: 0,
prefix_len: 0,
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");
}
fn frame(offset: u64, len: usize) -> CryptoFrame {
CryptoFrame {
offset,
data: vec![0xAB; len],
}
}
#[test]
fn replaying_the_same_frames_does_not_refresh_the_ttl() {
let mut p = QuicUdpParser::new();
let dcid = vec![1u8; 8];
p.accumulate(dcid.clone(), vec![frame(0, 16)], Timestamp::new(0, 0));
assert_eq!(p.tracked(), 1);
for sec in 1..20 {
p.accumulate(dcid.clone(), vec![frame(0, 16)], Timestamp::new(sec, 0));
}
p.on_tick(Timestamp::new(20, 0), &mut Vec::new());
assert_eq!(
p.tracked(),
0,
"an entry that never made progress must age out"
);
}
#[test]
fn progress_refreshes_the_ttl() {
let mut p = QuicUdpParser::new();
let dcid = vec![2u8; 8];
p.accumulate(dcid.clone(), vec![frame(0, 16)], Timestamp::new(0, 0));
p.accumulate(dcid.clone(), vec![frame(16, 16)], Timestamp::new(9, 0));
p.on_tick(Timestamp::new(11, 0), &mut Vec::new());
assert_eq!(p.tracked(), 1, "the attempt is still advancing");
}
#[test]
fn crypto_bytes_are_capped_per_connection() {
let mut p = QuicUdpParser::with_config(
QuicConfig::default()
.with_max_crypto_bytes(1024)
.with_max_crypto_frames(1000),
);
let dcid = vec![3u8; 8];
for i in 0..64u64 {
p.accumulate(
dcid.clone(),
vec![frame(i * 100, 100)],
Timestamp::new(1, 0),
);
}
let held: usize = p.pending[0].1.bytes;
assert!(held <= 1024, "held {held} bytes, cap is 1024");
assert!(p.pending_dropped() > 0, "and the refusals are counted");
}
#[test]
fn crypto_frames_are_capped_per_connection() {
let mut p = QuicUdpParser::with_config(
QuicConfig::default()
.with_max_crypto_frames(8)
.with_max_crypto_bytes(1024 * 1024),
);
let dcid = vec![4u8; 8];
for i in 0..500u64 {
p.accumulate(dcid.clone(), vec![frame(i, 1)], Timestamp::new(1, 0));
}
assert_eq!(p.pending[0].1.frames.len(), 8);
assert_eq!(p.pending_dropped(), 492);
}
#[test]
fn distinct_connections_are_capped() {
let mut p =
QuicUdpParser::with_config(QuicConfig::default().with_max_pending_connections(16));
for i in 0..1000u32 {
p.accumulate(
i.to_be_bytes().to_vec(),
vec![frame(0, 64)],
Timestamp::new(1 + i, 0),
);
}
assert_eq!(p.tracked(), 16);
assert!(p.pending_dropped() > 0);
}
}