quic: track concurrent handshakes per UDP flow

Keep independent Initial keys, connection IDs, CRYPTO reassembly,
Hello progress and Retry state for concurrent QUIC connections sharing
a UDP five-tuple. Route packets to matching contexts without replacing
another unfinished handshake, and validate Retry integrity per connection.

Track received CRYPTO byte ranges so duplicates cannot fill gaps and
conflicting overlaps are rejected. Bound retained contexts and fragment
buffers, expire idle contexts, and report quic.too_many_connections when
no completed context can be evicted.

Keep application parsing active under track-only handling so later
connections can still be inspected. Explicit bypass waits for all known
contexts to complete both Hellos and remains a flow-wide opt-out.

Add tests for interleaved connections, CID changes and empty IDs, Retry,
resource limits, expiry and QUIC v2. Cross-flow migration and Handshake
or 1-RTT decryption remain outside the scope of this change.

Feature: #9228.
https://redmine.openinfosecfoundation.org/issues/9228
Signed-off-by: Shaoqi Jiang <jiangshaoqi9@163.com>
pull/16511/head
Shaoqi Jiang 3 days ago
parent e16087fab4
commit 75b2c525d0

@ -5,3 +5,4 @@
alert quic any any -> any any (msg:"SURICATA QUIC failed decrypt"; app-layer-event:quic.failed_decrypt; classtype:protocol-command-decode; sid:2231000; rev:1;)
alert quic any any -> any any (msg:"SURICATA QUIC error on data"; app-layer-event:quic.error_on_data; classtype:protocol-command-decode; sid:2231001; rev:1;)
alert quic any any -> any any (msg:"SURICATA QUIC crypto fragments too long"; app-layer-event:quic.crypto_frag_too_long; classtype:protocol-command-decode; sid:2231002; rev:1;)
alert quic any any -> any any (msg:"SURICATA QUIC too many connections"; app-layer-event:quic.too_many_connections; classtype:protocol-command-decode; sid:2231003; rev:1;)

@ -0,0 +1,149 @@
/* Copyright (C) 2021 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use super::{
crypto::{decrypt_initial, quic_keys_initial, verify_retry, QuicKeys},
frames::Frame,
parser::{QuicData, QuicHeader},
quic::QuicEvent,
reassembly::CryptoReassembly,
};
use std::time::Duration;
/// A connection is local to its UDP flow; CID migration across flows is not tracked.
pub(super) struct QuicConnection {
pub version: u32,
pub client_scid: Vec<u8>,
pub original_dcid: Vec<u8>,
initial_dcid: Vec<u8>,
server_scid: Option<Vec<u8>>,
keys: Option<QuicKeys>,
crypto: [CryptoReassembly; 2],
hello: [bool; 2],
has_retried: bool,
pub last_seen: Duration,
}
impl QuicConnection {
pub fn new(header: &QuicHeader, now: Duration) -> Self {
Self {
version: header.version.into(),
client_scid: header.scid.clone(),
original_dcid: header.dcid.clone(),
initial_dcid: header.dcid.clone(),
server_scid: None,
keys: quic_keys_initial(header.version.into(), &header.dcid),
crypto: Default::default(),
hello: [false; 2],
has_retried: false,
last_seen: now,
}
}
pub fn supported(&self, header: &QuicHeader) -> bool {
self.keys.is_some() || header.version.is_gquic() || !header.flags.is_long
}
pub fn matches(&self, header: &QuicHeader, to_server: bool) -> bool {
if self.version != u32::from(header.version) {
return false;
}
if self.keys.is_none() {
// Legacy gQUIC has no Initial key derivation and may omit SCIDs.
return self.initial_dcid == header.dcid
|| (!to_server && self.initial_dcid == header.scid);
}
if to_server {
self.client_scid == header.scid
&& (self.initial_dcid == header.dcid
|| self.original_dcid == header.dcid
|| self.server_scid.as_deref() == Some(header.dcid.as_slice()))
} else {
self.client_scid == header.dcid
&& self
.server_scid
.as_deref()
.is_none_or(|scid| scid == header.scid)
}
}
pub fn hello_done(&self, to_server: bool) -> bool {
self.hello[usize::from(!to_server)]
}
pub fn complete(&self) -> bool {
self.hello[0] && self.hello[1]
}
pub fn decode(
&self, header: &QuicHeader, to_server: bool, packet: &[u8], payload: &[u8], hlen: usize,
) -> Result<QuicData, QuicEvent> {
let mut output = Vec::new();
let data = if let Some(keys) = &self.keys {
let len = decrypt_initial(keys, to_server, header, payload, packet, hlen, &mut output)
.map_err(|_| QuicEvent::FailedDecrypt)?;
output.truncate(len);
output.as_slice()
} else {
payload
};
QuicData::from_bytes(data).map_err(|_| QuicEvent::ErrorOnData)
}
pub fn ingest(
&mut self, data: &mut QuicData, header: &QuicHeader, to_server: bool, now: Duration,
) -> Result<(), QuicEvent> {
let dir = usize::from(!to_server);
if !data.frames.iter().any(|f| matches!(f, Frame::Crypto(_))) {
for frame in &data.frames {
if let Frame::CryptoFrag(frag) = frame {
self.crypto[dir].insert(frag.offset, &frag.data)?;
}
}
if let Some(hello) = self.crypto[dir].hello()? {
data.frames.push(hello);
}
}
if data.frames.iter().any(|f| matches!(f, Frame::Crypto(_))) {
self.hello[dir] = true;
self.crypto[dir].clear();
}
if !to_server && self.keys.is_some() {
self.server_scid = Some(header.scid.clone());
}
self.last_seen = self.last_seen.max(now);
Ok(())
}
pub fn accepts_retry(&self, header: &QuicHeader, packet: &[u8]) -> bool {
self.version == u32::from(header.version)
&& header.dcid == self.client_scid
&& !self.has_retried
&& self.server_scid.is_none()
&& header.scid != self.initial_dcid
&& verify_retry(self.version, &self.original_dcid, packet)
}
pub fn retry(&mut self, header: &QuicHeader, now: Duration) {
self.initial_dcid.clone_from(&header.scid);
self.keys = quic_keys_initial(self.version, &self.initial_dcid);
self.crypto = Default::default();
self.hello = [false; 2];
self.has_retried = true;
self.last_seen = self.last_seen.max(now);
}
}

@ -15,6 +15,7 @@
* 02110-1301, USA.
*/
use super::parser::{quic_pkt_num, QuicHeader};
use aes::cipher::{BlockEncrypt, KeyInit};
use aes::Aes128;
use aes_gcm::aead::AeadInPlace;
@ -26,6 +27,11 @@ use std::convert::TryInto;
pub const AES128_KEY_LEN: usize = 16;
pub const AES128_TAG_LEN: usize = 16;
pub const AES128_IV_LEN: usize = 12;
const PKT_NUM_BUF_MAX_LEN: usize = 4;
// RFC 9001 section 5.8.
const RETRY_KEY_V1: [u8; 16] = [
0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e,
];
pub struct HeaderProtectionKey(Aes128);
@ -196,3 +202,189 @@ pub fn quic_keys_initial(version: u32, client_dst_connection_id: &[u8]) -> Optio
remote: DirectionalKeys::new(&client_secret, version),
});
}
pub(super) fn decrypt_initial<'a>(
keys: &QuicKeys, to_server: bool, header: &QuicHeader, framebuf: &'a [u8], buf: &'a [u8],
hlen: usize, output: &'a mut Vec<u8>,
) -> Result<usize, ()> {
let hkey = if to_server {
&keys.remote.header
} else {
&keys.local.header
};
if framebuf.len() < PKT_NUM_BUF_MAX_LEN + AES128_KEY_LEN {
return Err(());
}
let h2len = hlen + usize::from(header.length);
let mut h2 = Vec::with_capacity(h2len);
h2.extend_from_slice(&buf[..h2len]);
let mut h20 = h2[0];
let mut pktnum_buf = Vec::with_capacity(PKT_NUM_BUF_MAX_LEN);
pktnum_buf.extend_from_slice(&h2[hlen..hlen + PKT_NUM_BUF_MAX_LEN]);
let r1 = hkey.decrypt_in_place(
&h2[hlen + PKT_NUM_BUF_MAX_LEN..hlen + PKT_NUM_BUF_MAX_LEN + AES128_KEY_LEN],
&mut h20,
&mut pktnum_buf,
);
if r1.is_err() {
return Err(());
}
// mutate one at a time
h2[0] = h20;
let _ = &h2[hlen..hlen + 1 + ((h20 & 3) as usize)]
.copy_from_slice(&pktnum_buf[..1 + ((h20 & 3) as usize)]);
let pkt_num = quic_pkt_num(&h2[hlen..hlen + 1 + ((h20 & 3) as usize)]);
if framebuf.len() < 1 + ((h20 & 3) as usize) {
return Err(());
}
output.extend_from_slice(&framebuf[1 + ((h20 & 3) as usize)..]);
let pkey = if to_server {
&keys.remote.packet
} else {
&keys.local.packet
};
let r = pkey.decrypt_in_place(pkt_num, &h2[..hlen + 1 + ((h20 & 3) as usize)], output);
if let Ok(r2) = r {
return Ok(r2.len());
}
Err(())
}
/// Verify the Retry pseudo-packet, including the original client DCID.
pub(super) fn verify_retry(version: u32, odcid: &[u8], packet: &[u8]) -> bool {
let (key, nonce) = match version {
1 | 0xff00_0021..=0xff00_0022 => (
RETRY_KEY_V1,
[
0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb,
],
),
0x6b3343cf => (
[
0x8f, 0xb4, 0xb0, 0x1b, 0x56, 0xac, 0x48, 0xe2, 0x60, 0xfb, 0xcb, 0xce, 0xad, 0x7c,
0xcc, 0x92,
],
[
0xd8, 0x69, 0x69, 0xbc, 0x2d, 0x7c, 0x6d, 0x99, 0x90, 0xef, 0xb0, 0x4a,
],
),
0xff00_001d..=0xff00_0020 => (
[
0xcc, 0xce, 0x18, 0x7e, 0xd0, 0x9a, 0x09, 0xd0, 0x57, 0x28, 0x15, 0x5a, 0x6c, 0xb9,
0x6b, 0xe1,
],
[
0xe5, 0x49, 0x30, 0xf9, 0x7f, 0x21, 0x36, 0xf0, 0x53, 0x0a, 0x8c, 0x1c,
],
),
_ => return false,
};
if packet.len() < AES128_TAG_LEN || odcid.len() > 20 {
return false;
}
let tag_pos = packet.len() - AES128_TAG_LEN;
let mut pseudo = Vec::with_capacity(1 + odcid.len() + tag_pos);
pseudo.push(odcid.len() as u8);
pseudo.extend_from_slice(odcid);
pseudo.extend_from_slice(&packet[..tag_pos]);
Aes128Gcm::new((&key).into())
.decrypt_in_place_detached((&nonce).into(), &pseudo, &mut [], packet[tag_pos..].into())
.is_ok()
}
#[cfg(test)]
pub(super) mod test_packets {
use super::*;
pub fn varint(value: usize) -> Vec<u8> {
if value < 64 {
vec![value as u8]
} else {
((value as u16) | 0x4000).to_be_bytes().to_vec()
}
}
pub fn initial(
version: u32, key_dcid: &[u8], dcid: &[u8], scid: &[u8], number: u32, client: bool,
offset: usize, data: &[u8],
) -> Vec<u8> {
let keys = quic_keys_initial(version, key_dcid).unwrap();
let keys = if client { keys.remote } else { keys.local };
let mut plain = vec![6];
plain.extend(varint(offset));
plain.extend(varint(data.len()));
plain.extend_from_slice(data);
plain.resize(1160.max(plain.len()), 0);
let mut packet = vec![if version == 0x6b3343cf { 0xd3 } else { 0xc3 }];
packet.extend_from_slice(&version.to_be_bytes());
packet.push(dcid.len() as u8);
packet.extend_from_slice(dcid);
packet.push(scid.len() as u8);
packet.extend_from_slice(scid);
packet.push(0); // token length
packet.extend(varint(4 + plain.len() + 16));
let pn_offset = packet.len();
packet.extend_from_slice(&number.to_be_bytes());
let mut nonce = keys.packet.iv;
for (dst, src) in nonce[8..].iter_mut().zip(number.to_be_bytes()) {
*dst ^= src;
}
let tag = keys
.packet
.key
.encrypt_in_place_detached((&nonce).into(), &packet, &mut plain)
.unwrap();
packet.extend(plain);
packet.extend_from_slice(&tag);
let mut mask: [u8; 16] = packet[pn_offset + 4..pn_offset + 20].try_into().unwrap();
keys.header.0.encrypt_block((&mut mask).into());
packet[0] ^= mask[0] & 15;
for i in 0..4 {
packet[pn_offset + i] ^= mask[i + 1];
}
packet
}
pub fn retry(dcid: &[u8], scid: &[u8], odcid: &[u8]) -> Vec<u8> {
let mut p = vec![0xf0, 0, 0, 0, 1, dcid.len() as u8];
p.extend_from_slice(dcid);
p.push(scid.len() as u8);
p.extend_from_slice(scid);
p.extend_from_slice(b"retry-token");
let mut pseudo = vec![odcid.len() as u8];
pseudo.extend_from_slice(odcid);
pseudo.extend_from_slice(&p);
let nonce = [
0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb,
];
let tag = Aes128Gcm::new((&RETRY_KEY_V1).into())
.encrypt_in_place_detached((&nonce).into(), &pseudo, &mut [])
.unwrap();
p.extend_from_slice(&tag);
p
}
}
#[cfg(test)]
mod retry_vectors {
use super::verify_retry;
#[test]
fn rfc9001_and_rfc9369_retry_vectors() {
let odcid = hex::decode("8394c8f03e515708").unwrap();
for (version, text) in [
(
1,
"ff000000010008f067a5502a4262b5746f6b656e04a265ba2eff4d829058fb3f0f2496ba",
),
(
0x6b3343cf,
"cf6b3343cf0008f067a5502a4262b5746f6b656ec8646ce8bfe33952d955543665dcc7b6",
),
] {
let packet = hex::decode(text).unwrap();
assert!(verify_retry(version, &odcid, &packet));
assert!(!verify_retry(version, &[0; 8], &packet));
}
}
}

@ -16,7 +16,6 @@
*/
use super::error::QuicError;
use super::quic::QUIC_MAX_CRYPTO_FRAG_LEN;
use crate::handshake::HandshakeParams;
use crate::quic::parser::quic_var_uint;
use nom8::bytes::complete::take;
@ -30,8 +29,7 @@ use std::fmt;
use tls_parser::TlsMessage::Handshake;
use tls_parser::TlsMessageHandshake::{ClientHello, ServerHello};
use tls_parser::{
parse_tls_extensions, parse_tls_message_handshake, TlsCipherSuiteID, TlsExtension,
TlsExtensionType, TlsMessage,
parse_tls_extensions, TlsCipherSuiteID, TlsExtension, TlsExtensionType, TlsMessage,
};
/// Tuple of StreamTag and offset
@ -316,7 +314,7 @@ fn quic_get_tls_extensions(
return extv;
}
fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
pub(super) fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> {
if let Handshake(hs) = msg {
match hs {
ClientHello(ch) => {
@ -385,37 +383,14 @@ fn parse_crypto_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> {
let (rest, length) = quic_var_uint(rest)?;
let (rest, data) = take(length as usize)(rest)?;
if offset > 0 {
return Ok((
rest,
Frame::CryptoFrag(CryptoFrag {
offset,
length,
data: data.to_vec(),
}),
));
}
// if we have offset 0, try quick path : parse directly
match parse_tls_message_handshake(data) {
Ok((_, msg)) => {
if let Some(c) = parse_quic_handshake(msg) {
return Ok((rest, c));
}
}
Err(nom7::Err::Incomplete(_)) => {
// offset 0 but incomplete : save it as a fragment for later reassembly
return Ok((
rest,
Frame::CryptoFrag(CryptoFrag {
offset,
length,
data: data.to_vec(),
}),
));
}
_ => {}
}
return Err(nom8::Err::Error(QuicError::InvalidPacket));
Ok((
rest,
Frame::CryptoFrag(CryptoFrag {
offset,
length,
data: data.to_vec(),
}),
))
}
fn parse_tag(input: &[u8]) -> IResult<&[u8], StreamTag, QuicError> {
@ -553,72 +528,7 @@ impl Frame {
Ok((rest, value))
}
pub(crate) fn decode_frames<'a>(
input: &'a [u8], past_frag: &'a [u8], past_fraglen: u32,
) -> IResult<&'a [u8], Vec<Frame>, QuicError> {
let (rest, mut frames) =
all_consuming(many0(complete(Frame::decode_frame))).parse(input)?;
// we use the already seen past fragment data
let mut crypto_max_size = past_frag.len() as u64;
let mut crypto_total_size = 0;
// reassemble crypto fragments : first find total size
for f in &frames {
if let Frame::CryptoFrag(c) = f {
if crypto_max_size < c.offset + c.length {
crypto_max_size = c.offset + c.length;
}
crypto_total_size += c.length;
}
}
if crypto_max_size > 0 && crypto_max_size < QUIC_MAX_CRYPTO_FRAG_LEN {
// we have some, and no gaps from offset 0
let mut d = vec![0; crypto_max_size as usize];
d[..past_frag.len()].clone_from_slice(past_frag);
for f in &frames {
if let Frame::CryptoFrag(c) = f {
d[c.offset as usize..(c.offset + c.length) as usize].clone_from_slice(&c.data);
}
}
// check that we have enough data, some new data, and data for the first byte
if crypto_total_size + past_fraglen as u64 >= crypto_max_size && crypto_total_size > 0 {
match parse_tls_message_handshake(&d) {
Ok((_, msg)) => {
if let Some(c) = parse_quic_handshake(msg) {
// add a parsed crypto frame
frames.push(c);
}
}
Err(nom7::Err::Incomplete(_)) => {
// this means the current packet does not have all the hanshake data yet
let frag = CryptoFrag {
offset: crypto_total_size + past_fraglen as u64,
length: d.len() as u64,
data: d.to_vec(),
};
frames.push(Frame::CryptoFrag(frag));
}
_ => {}
}
} else {
// pass in offset the number of bytes set in data
let frag = CryptoFrag {
offset: crypto_total_size + past_fraglen as u64,
length: d.len() as u64,
data: d.to_vec(),
};
frames.push(Frame::CryptoFrag(frag));
}
} else if crypto_max_size >= QUIC_MAX_CRYPTO_FRAG_LEN {
// just notice the engine that we have a big crypto fragment without supplying data
let frag = CryptoFrag {
offset: 0,
length: crypto_max_size,
data: Vec::new(),
};
frames.push(Frame::CryptoFrag(frag));
}
Ok((rest, frames))
pub(crate) fn decode_frames(input: &[u8]) -> IResult<&[u8], Vec<Frame>, QuicError> {
all_consuming(many0(complete(Frame::decode_frame))).parse(input)
}
}

@ -17,6 +17,7 @@
//! QUIC application layer, parser, detection and logger module.
mod connection;
mod crypto;
mod cyu;
pub mod detect;
@ -25,3 +26,4 @@ mod frames;
mod logger;
mod parser;
pub mod quic;
mod reassembly;

@ -46,7 +46,7 @@ impl QuicVersion {
pub const Q046: QuicVersion = QuicVersion(0x51303436);
pub const V2: QuicVersion = QuicVersion(0x6b3343cf);
fn is_gquic(&self) -> bool {
pub(super) fn is_gquic(&self) -> bool {
*self == QuicVersion::Q043
|| *self == QuicVersion::Q044
|| *self == QuicVersion::Q045
@ -338,11 +338,17 @@ impl QuicHeader {
let (rest, dcid) = take(dcid_len as usize)(rest)?;
let (rest, scid_len) = be_u8(rest)?;
if (version == QuicVersion(1) || version == QuicVersion::V2)
&& (dcid_len > 20 || scid_len > 20)
{
return Err(nom8::Err::Error(QuicError::InvalidPacket));
}
let (rest, scid) = take(scid_len as usize)(rest)?;
(rest, dcid.to_vec(), scid.to_vec())
};
let mut has_length = false;
let mut has_length =
!version.is_gquic() && matches!(ty, QuicType::Handshake | QuicType::ZeroRTT);
let rest = match ty {
QuicType::Initial => {
if version.is_gquic() {
@ -358,7 +364,10 @@ impl QuicHeader {
}
}
QuicType::Retry => {
// opaque retry token and 16 bytes retry integrity tag
// An IETF Retry requires a nonempty token and a 16-byte tag.
if !version.is_gquic() && rest.len() <= 16 {
return Err(nom8::Err::Error(QuicError::InvalidPacket));
}
&rest[rest.len()..]
}
_ => rest,
@ -396,10 +405,8 @@ impl QuicHeader {
}
impl QuicData {
pub(crate) fn from_bytes(
input: &[u8], past_frag: &[u8], past_fraglen: u32,
) -> Result<QuicData, QuicError> {
let (_, frames) = Frame::decode_frames(input, past_frag, past_fraglen)?;
pub(crate) fn from_bytes(input: &[u8]) -> Result<QuicData, QuicError> {
let (_, frames) = Frame::decode_frames(input)?;
Ok(QuicData { frames })
}
}
@ -473,8 +480,7 @@ mod tests {
header
);
let past_frag = Vec::new();
let data = QuicData::from_bytes(rest, &past_frag, 0).unwrap();
let data = QuicData::from_bytes(rest).unwrap();
assert_eq!(
QuicData {
frames: vec![Frame::Stream(Stream {

@ -16,17 +16,17 @@
*/
use super::{
crypto::{quic_keys_initial, QuicKeys, AES128_KEY_LEN},
connection::QuicConnection,
cyu::Cyu,
frames::{Frame, QuicTlsExtension, StreamTag},
parser::{quic_pkt_num, QuicData, QuicHeader, QuicType},
parser::{QuicData, QuicHeader, QuicType},
};
use crate::conf::conf_get;
use crate::encryption::EncryptionHandling;
use crate::{
applayer::{self, *},
direction::Direction,
flow::Flow,
flow::{flow_get_last_time, Flow},
ja4::JA4,
};
use crate::{
@ -36,6 +36,7 @@ use crate::{
use std::collections::VecDeque;
use std::ffi::CString;
use std::str::FromStr;
use std::time::Duration;
use suricata_sys::sys::{
AppLayerParserState, AppProto, SCAppLayerParserConfParserEnabled,
SCAppLayerParserRegisterLogger, SCAppLayerParserStateSetFlag,
@ -49,7 +50,8 @@ static mut ENCRYPTION_BYPASS_ENABLED: EncryptionHandling =
EncryptionHandling::ENCRYPTION_HANDLING_TRACK_ONLY;
const DEFAULT_DCID_LEN: usize = 16;
const PKT_NUM_BUF_MAX_LEN: usize = 4;
static mut MAX_CONNECTIONS: usize = 64;
static mut CONNECTION_IDLE_TIMEOUT: u64 = 60;
pub(super) const QUIC_MAX_CRYPTO_FRAG_LEN: u64 = 65535;
#[derive(FromPrimitive, Debug, AppLayerEvent)]
@ -58,6 +60,7 @@ pub enum QuicEvent {
ErrorOnData,
ErrorOnHeader,
CryptoFragTooLong,
TooManyConnections,
}
#[derive(Debug)]
@ -123,18 +126,9 @@ impl QuicTransaction {
pub struct QuicState {
state_data: AppLayerStateData,
max_tx_id: u64,
keys: Option<QuicKeys>,
/// crypto fragment data already seen and reassembled to client
crypto_frag_tc: Vec<u8>,
/// number of bytes set in crypto fragment data to client
crypto_fraglen_tc: u32,
/// crypto fragment data already seen and reassembled to server
crypto_frag_ts: Vec<u8>,
/// number of bytes set in crypto fragment data to server
crypto_fraglen_ts: u32,
hello_tc: bool,
hello_ts: bool,
has_retried: bool,
connections: Vec<QuicConnection>,
max_connections: usize,
idle_timeout: Duration,
transactions: VecDeque<QuicTransaction>,
}
@ -143,14 +137,9 @@ impl Default for QuicState {
Self {
state_data: AppLayerStateData::default(),
max_tx_id: 0,
keys: None,
crypto_frag_tc: Vec::new(),
crypto_frag_ts: Vec::new(),
crypto_fraglen_tc: 0,
crypto_fraglen_ts: 0,
hello_tc: false,
hello_ts: false,
has_retried: false,
connections: Vec::new(),
max_connections: unsafe { MAX_CONNECTIONS },
idle_timeout: Duration::from_secs(unsafe { CONNECTION_IDLE_TIMEOUT }),
transactions: VecDeque::new(),
}
}
@ -190,65 +179,12 @@ impl QuicState {
self.transactions.push_back(tx);
}
fn decrypt<'a>(
&mut self, to_server: bool, header: &QuicHeader, framebuf: &'a [u8], buf: &'a [u8],
hlen: usize, output: &'a mut Vec<u8>,
) -> Result<usize, ()> {
if let Some(keys) = &self.keys {
let hkey = if to_server {
&keys.remote.header
} else {
&keys.local.header
};
if framebuf.len() < PKT_NUM_BUF_MAX_LEN + AES128_KEY_LEN {
return Err(());
}
let h2len = hlen + usize::from(header.length);
let mut h2 = Vec::with_capacity(h2len);
h2.extend_from_slice(&buf[..h2len]);
let mut h20 = h2[0];
let mut pktnum_buf = Vec::with_capacity(PKT_NUM_BUF_MAX_LEN);
pktnum_buf.extend_from_slice(&h2[hlen..hlen + PKT_NUM_BUF_MAX_LEN]);
let r1 = hkey.decrypt_in_place(
&h2[hlen + PKT_NUM_BUF_MAX_LEN..hlen + PKT_NUM_BUF_MAX_LEN + AES128_KEY_LEN],
&mut h20,
&mut pktnum_buf,
);
if r1.is_err() {
return Err(());
}
// mutate one at a time
h2[0] = h20;
let _ = &h2[hlen..hlen + 1 + ((h20 & 3) as usize)]
.copy_from_slice(&pktnum_buf[..1 + ((h20 & 3) as usize)]);
let pkt_num = quic_pkt_num(&h2[hlen..hlen + 1 + ((h20 & 3) as usize)]);
if framebuf.len() < 1 + ((h20 & 3) as usize) {
return Err(());
}
output.extend_from_slice(&framebuf[1 + ((h20 & 3) as usize)..]);
let pkey = if to_server {
&keys.remote.packet
} else {
&keys.local.packet
};
let r = pkey.decrypt_in_place(pkt_num, &h2[..hlen + 1 + ((h20 & 3) as usize)], output);
if let Ok(r2) = r {
return Ok(r2.len());
}
}
return Err(());
}
fn handle_frames(
&mut self, data: QuicData, header: QuicHeader, to_server: bool,
pstate: *mut AppLayerParserState,
) {
fn handle_frames(&mut self, data: QuicData, header: QuicHeader, to_server: bool) {
let mut sni: Option<Vec<u8>> = None;
let mut ua: Option<Vec<u8>> = None;
let mut ja3: Option<String> = None;
let mut ja4: Option<JA4> = None;
let mut extv: Vec<QuicTlsExtension> = Vec::new();
let mut frag_long = false;
for frame in &data.frames {
match frame {
Frame::Stream(s) => {
@ -265,24 +201,6 @@ impl QuicState {
}
}
}
Frame::CryptoFrag(frag) => {
// means we had some fragments but not full TLS hello
// save it for a later packet
if to_server {
// use a hardcoded limit to not grow indefinitely
if frag.length < QUIC_MAX_CRYPTO_FRAG_LEN {
self.crypto_frag_ts.clone_from(&frag.data);
self.crypto_fraglen_ts = frag.offset as u32;
} else {
frag_long = true;
}
} else if frag.length < QUIC_MAX_CRYPTO_FRAG_LEN {
self.crypto_frag_tc.clone_from(&frag.data);
self.crypto_fraglen_tc = frag.offset as u32;
} else {
frag_long = true;
}
}
Frame::Crypto(c) => {
if let Some(ja3str) = &c.ja3 {
ja3 = Some(ja3str.clone());
@ -301,32 +219,11 @@ impl QuicState {
}
}
extv.extend_from_slice(&c.extv);
if to_server {
self.hello_ts = true;
} else {
self.hello_tc = true;
}
if self.hello_tc && self.hello_ts {
let flags = match unsafe { ENCRYPTION_BYPASS_ENABLED } {
EncryptionHandling::ENCRYPTION_HANDLING_BYPASS => {
APP_LAYER_PARSER_NO_INSPECTION | APP_LAYER_PARSER_BYPASS_READY
}
EncryptionHandling::ENCRYPTION_HANDLING_TRACK_ONLY => {
APP_LAYER_PARSER_NO_INSPECTION
}
_ => 0,
};
if flags != 0 {
unsafe {
SCAppLayerParserStateSetFlag(pstate, flags);
}
}
}
}
_ => {}
}
}
self.new_tx(header, data, sni, ua, extv, ja3, ja4, to_server, frag_long);
self.new_tx(header, data, sni, ua, extv, ja3, ja4, to_server, false);
}
fn set_event_notx(&mut self, event: QuicEvent, header: QuicHeader, client: bool) {
@ -337,106 +234,204 @@ impl QuicState {
self.transactions.push_back(tx);
}
fn parse(&mut self, input: &[u8], to_server: bool, pstate: *mut AppLayerParserState) -> bool {
// so as to loop over multiple quic headers in one packet
fn parse(
&mut self, input: &[u8], to_server: bool, pstate: *mut AppLayerParserState, now: Duration,
) -> bool {
self.connections
.retain(|c| now.saturating_sub(c.last_seen) < self.idle_timeout);
let mut buf = input;
while !buf.is_empty() {
match QuicHeader::from_bytes(buf, DEFAULT_DCID_LEN) {
Ok((rest, header)) => {
if (to_server && self.hello_ts) || (!to_server && self.hello_tc) {
// payload is encrypted, stop parsing here
return true;
}
if header.ty == QuicType::Short {
// nothing to get
return true;
}
let Ok((rest, header)) = QuicHeader::from_bytes(buf, DEFAULT_DCID_LEN) else {
// The next boundary is unknown; discard this datagram, not the flow.
return true;
};
if header.ty == QuicType::Short {
break;
}
let hlen = buf.len() - rest.len();
let (payload, next) = rest.split_at(header.length as usize);
let packet = &buf[..hlen + payload.len()];
if header.ty == QuicType::Retry && !to_server {
let matches: Vec<_> = self
.connections
.iter()
.enumerate()
.filter(|(_, c)| c.accepts_retry(&header, packet))
.map(|(i, _)| i)
.collect();
if let [i] = matches.as_slice() {
self.connections[*i].retry(&header, now);
}
}
if header.ty != QuicType::Initial {
// Non-Initial packets cannot introduce a connection. A CID
// learned only from encrypted 1-RTT traffic is also unknown here.
if !header.version.is_gquic()
&& !self.connections.is_empty()
&& !self
.connections
.iter()
.any(|c| c.matches(&header, to_server) && !c.hello_done(to_server))
{
buf = next;
continue;
}
self.new_tx(
header,
QuicData { frames: Vec::new() },
None,
None,
Vec::new(),
None,
None,
to_server,
false,
);
buf = next;
continue;
}
// unprotect/decrypt packet
if self.keys.is_none() && header.ty == QuicType::Initial {
self.keys = quic_keys_initial(u32::from(header.version), &header.dcid);
} else if !to_server
&& self.keys.is_some()
&& header.ty == QuicType::Retry
&& !self.has_retried
{
// a retry packet discards the current keys, client will resend an initial packet with new keys
self.hello_ts = false;
self.keys = None;
// RFC 9000 17.2.5.2 After the client has received and processed an Initial or Retry packet
// from the server, it MUST discard any subsequent Retry packets that it receives.
self.has_retried = true;
}
// header.length was checked against rest.len() during parsing
let (mut framebuf, next_buf) = rest.split_at(header.length.into());
if header.ty != QuicType::Initial {
// only version is interesting, no frames
self.new_tx(
header,
QuicData { frames: Vec::new() },
None,
None,
Vec::new(),
None,
None,
to_server,
false,
);
buf = next_buf;
continue;
}
let hlen = buf.len() - rest.len();
let mut output;
if self.keys.is_some() && !framebuf.is_empty() {
output = Vec::with_capacity(framebuf.len() + 4);
if let Ok(dlen) =
self.decrypt(to_server, &header, framebuf, buf, hlen, &mut output)
{
output.resize(dlen, 0);
} else {
self.set_event_notx(QuicEvent::FailedDecrypt, header, to_server);
return false;
let candidates: Vec<_> = self
.connections
.iter()
.enumerate()
.filter(|(_, c)| c.matches(&header, to_server))
.map(|(i, _)| i)
.collect();
if let [i] = candidates.as_slice() {
if self.connections[*i].hello_done(to_server) {
buf = next;
continue;
}
}
let mut selected = None;
let mut ambiguous = false;
let mut error = QuicEvent::FailedDecrypt;
let has_candidates = !candidates.is_empty();
for i in candidates {
match self.connections[i].decode(&header, to_server, packet, payload, hlen) {
Ok(data) => {
if selected.is_some() {
ambiguous = true;
break;
}
framebuf = &output;
selected = Some((i, data));
}
buf = next_buf;
Err(e) => error = e,
}
}
if ambiguous {
self.set_event_notx(QuicEvent::ErrorOnData, header, to_server);
buf = next;
continue;
}
let mut frag = Vec::new();
// take the current fragment and reset it in the state
let past_frag = if to_server {
std::mem::swap(&mut self.crypto_frag_ts, &mut frag);
&frag
} else {
std::mem::swap(&mut self.crypto_frag_tc, &mut frag);
&frag
};
let past_fraglen = if to_server {
self.crypto_fraglen_ts
} else {
self.crypto_fraglen_tc
};
if to_server {
self.crypto_fraglen_ts = 0
} else {
self.crypto_fraglen_tc = 0
let (i, mut data) = if let Some(found) = selected {
found
} else if !has_candidates && (to_server || !header.flags.is_long) {
let mut connection = QuicConnection::new(&header, now);
if !connection.supported(&header) {
self.new_tx(
header,
QuicData { frames: Vec::new() },
None,
None,
Vec::new(),
None,
None,
to_server,
false,
);
buf = next;
continue;
}
let mut data = match connection.decode(&header, to_server, packet, payload, hlen) {
Ok(data) => data,
Err(e) => {
self.set_event_notx(e, header, to_server);
buf = next;
continue;
}
match QuicData::from_bytes(framebuf, past_frag, past_fraglen) {
Ok(data) => {
self.handle_frames(data, header, to_server, pstate);
}
Err(_e) => {
self.set_event_notx(QuicEvent::ErrorOnData, header, to_server);
return false;
}
};
// Validate before admitting a connection or evicting an old record.
if let Err(e) = connection.ingest(&mut data, &header, to_server, now) {
self.set_event_notx(e, header, to_server);
buf = next;
continue;
}
if self.connections.len() >= self.max_connections {
if let Some(oldest) = self
.connections
.iter()
.enumerate()
.filter(|(_, c)| c.complete())
.min_by_key(|(_, c)| c.last_seen)
.map(|(i, _)| i)
{
self.connections.remove(oldest);
} else {
self.set_event_notx(QuicEvent::TooManyConnections, header, to_server);
buf = next;
continue;
}
}
Err(_e) => {
// should we make an event with an empty header ?
return false;
self.connections.push(connection);
self.handle_frames(data, header, to_server);
buf = next;
continue;
} else {
// Without a matching client Initial, server Initial keys cannot
// be recovered. This is expected with partial captures.
if has_candidates {
self.set_event_notx(error, header, to_server);
}
buf = next;
continue;
};
if !self.connections[i].hello_done(to_server) {
match self.connections[i].ingest(&mut data, &header, to_server, now) {
Ok(()) => self.handle_frames(data, header, to_server),
Err(e) => self.set_event_notx(e, header, to_server),
}
}
buf = next;
}
self.apply_encryption_policy(pstate);
true
}
fn apply_encryption_policy(&self, pstate: *mut AppLayerParserState) {
if pstate.is_null()
|| self.connections.is_empty()
|| !self.connections.iter().all(QuicConnection::complete)
{
return;
}
// Keep UDP application parsing alive for future connections. Explicit
// bypass remains a flow-wide opt-out of subsequent connection inspection.
let flags = self.encryption_flags(unsafe { ENCRYPTION_BYPASS_ENABLED });
if flags != 0 {
unsafe {
SCAppLayerParserStateSetFlag(pstate, flags);
}
}
}
fn encryption_flags(&self, handling: EncryptionHandling) -> u16 {
if self.connections.is_empty() || !self.connections.iter().all(QuicConnection::complete) {
return 0;
}
match handling {
EncryptionHandling::ENCRYPTION_HANDLING_TRACK_ONLY => {
APP_LAYER_PARSER_NO_INSPECTION_PAYLOAD
}
EncryptionHandling::ENCRYPTION_HANDLING_BYPASS => {
APP_LAYER_PARSER_NO_INSPECTION | APP_LAYER_PARSER_BYPASS_READY
}
_ => 0,
}
return true;
}
}
@ -474,13 +469,13 @@ unsafe extern "C" fn quic_probing_parser(
}
unsafe extern "C" fn quic_parse_tc(
_flow: *mut Flow, state: *mut std::os::raw::c_void, pstate: *mut AppLayerParserState,
flow: *mut Flow, state: *mut std::os::raw::c_void, pstate: *mut AppLayerParserState,
stream_slice: StreamSlice, _data: *mut std::os::raw::c_void,
) -> AppLayerResult {
let state = cast_pointer!(state, QuicState);
let buf = stream_slice.as_slice();
if state.parse(buf, false, pstate) {
if state.parse(buf, false, pstate, flow_get_last_time(&*flow)) {
return AppLayerResult::ok();
} else {
return AppLayerResult::err();
@ -488,13 +483,13 @@ unsafe extern "C" fn quic_parse_tc(
}
unsafe extern "C" fn quic_parse_ts(
_flow: *mut Flow, state: *mut std::os::raw::c_void, pstate: *mut AppLayerParserState,
flow: *mut Flow, state: *mut std::os::raw::c_void, pstate: *mut AppLayerParserState,
stream_slice: StreamSlice, _data: *mut std::os::raw::c_void,
) -> AppLayerResult {
let state = cast_pointer!(state, QuicState);
let buf = stream_slice.as_slice();
if state.parse(buf, true, pstate) {
if state.parse(buf, true, pstate, flow_get_last_time(&*flow)) {
return AppLayerResult::ok();
} else {
return AppLayerResult::err();
@ -593,6 +588,22 @@ pub unsafe extern "C" fn SCRegisterQuicParser() {
ALPROTO_QUIC = alproto;
if SCAppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
let _ = AppLayerRegisterParser(&parser, alproto);
if let Some(value) = conf_get("app-layer.protocols.quic.max-connections") {
match value.parse::<usize>().ok().filter(|n| *n > 0) {
Some(n) => MAX_CONNECTIONS = n,
None => {
SCFatalErrorOnInit!("Invalid quic.max-connections");
}
}
}
if let Some(value) = conf_get("app-layer.protocols.quic.connection-idle-timeout") {
match value.parse::<u64>().ok().filter(|n| *n > 0) {
Some(n) => CONNECTION_IDLE_TIMEOUT = n,
None => {
SCFatalErrorOnInit!("Invalid quic.connection-idle-timeout");
}
}
}
if let Some(val) = conf_get("app-layer.protocols.quic.encryption-handling") {
if let Ok(eh) = EncryptionHandling::from_str(val) {
unsafe { ENCRYPTION_BYPASS_ENABLED = eh };
@ -607,3 +618,7 @@ pub unsafe extern "C" fn SCRegisterQuicParser() {
SCLogDebug!("Protocol detector and parser disabled for quic.");
}
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;

@ -0,0 +1,116 @@
/* Copyright (C) 2021 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use super::{
frames::{parse_quic_handshake, Frame},
quic::{QuicEvent, QUIC_MAX_CRYPTO_FRAG_LEN},
};
use tls_parser::parse_tls_message_handshake;
/// Initial CRYPTO bytes for one direction of one connection.
#[derive(Default)]
pub(super) struct CryptoReassembly {
data: Vec<u8>,
present: Vec<bool>,
contiguous: usize,
}
impl CryptoReassembly {
pub fn insert(&mut self, offset: u64, data: &[u8]) -> Result<(), QuicEvent> {
let end = offset
.checked_add(data.len() as u64)
.filter(|end| *end < QUIC_MAX_CRYPTO_FRAG_LEN)
.ok_or(QuicEvent::CryptoFragTooLong)? as usize;
let offset = offset as usize;
// Reject conflicting retransmissions before modifying any buffered byte.
for (i, byte) in data.iter().enumerate() {
let i = offset + i;
if i < self.present.len() && self.present[i] && self.data[i] != *byte {
return Err(QuicEvent::ErrorOnData);
}
}
if end > self.data.len() {
self.data.resize(end, 0);
self.present.resize(end, false);
}
self.data[offset..end].copy_from_slice(data);
self.present[offset..end].fill(true);
while self.contiguous < self.present.len() && self.present[self.contiguous] {
self.contiguous += 1;
}
Ok(())
}
pub fn hello(&self) -> Result<Option<Frame>, QuicEvent> {
if self.contiguous < 4 {
return Ok(None);
}
let length = 4
+ ((self.data[1] as usize) << 16)
+ ((self.data[2] as usize) << 8)
+ self.data[3] as usize;
if length as u64 >= QUIC_MAX_CRYPTO_FRAG_LEN {
return Err(QuicEvent::CryptoFragTooLong);
}
if self.contiguous < length {
return Ok(None);
}
let (_, msg) = parse_tls_message_handshake(&self.data[..length])
.map_err(|_| QuicEvent::ErrorOnData)?;
parse_quic_handshake(msg)
.map(Some)
.ok_or(QuicEvent::ErrorOnData)
}
pub fn clear(&mut self) {
*self = Self::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn duplicate_fragments_do_not_fill_a_gap() {
let mut r = CryptoReassembly::default();
r.insert(0, &[1, 0, 0, 8]).unwrap();
r.insert(8, &[1, 2, 3, 4]).unwrap();
r.insert(8, &[1, 2, 3, 4]).unwrap();
assert_eq!(r.contiguous, 4);
assert!(r.hello().unwrap().is_none());
r.insert(4, &[5, 6, 7, 8]).unwrap();
assert_eq!(r.contiguous, 12);
}
#[test]
fn conflicting_overlap_is_rejected() {
let mut r = CryptoReassembly::default();
r.insert(2, &[1, 2, 3]).unwrap();
assert!(matches!(r.insert(3, &[9]), Err(QuicEvent::ErrorOnData)));
}
#[test]
fn offsets_are_bounded_before_allocating() {
let mut r = CryptoReassembly::default();
assert!(matches!(
r.insert(u64::MAX, &[1]),
Err(QuicEvent::CryptoFragTooLong)
));
assert!(r.data.is_empty());
}
}

@ -0,0 +1,376 @@
/* Copyright (C) 2021 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use super::QuicState;
use crate::encryption::EncryptionHandling;
use crate::quic::crypto::test_packets::{initial, retry};
use std::{ptr, time::Duration};
fn hello(name: &str) -> Vec<u8> {
let mut body = vec![3, 3];
body.extend([0; 32]);
body.extend([0, 0, 2, 0x13, 1, 1, 0]); // session, cipher, compression
let mut sni = ((name.len() + 3) as u16).to_be_bytes().to_vec();
sni.push(0);
sni.extend_from_slice(&(name.len() as u16).to_be_bytes());
sni.extend_from_slice(name.as_bytes());
let mut ext = vec![0, 0];
ext.extend_from_slice(&(sni.len() as u16).to_be_bytes());
ext.extend(sni);
body.extend_from_slice(&(ext.len() as u16).to_be_bytes());
body.extend(ext);
let mut result = vec![1, 0, 0, body.len() as u8];
result.extend(body);
result
}
fn server_hello() -> Vec<u8> {
let mut b = vec![2, 0, 0, 40, 3, 3];
b.extend([0; 32]);
b.extend([0, 0x13, 1, 0, 0, 0]);
b
}
fn parse(state: &mut QuicState, packet: &[u8], client: bool, time: u64) {
assert!(state.parse(packet, client, ptr::null_mut(), Duration::from_secs(time)));
}
fn names(state: &QuicState) -> Vec<&[u8]> {
state
.transactions
.iter()
.filter_map(|tx| tx.sni.as_deref())
.collect()
}
#[test]
fn three_interleaved_connections_reassemble_independently() {
let mut s = QuicState::new();
let messages = [hello("a.example"), hello("b.example"), hello("c.example")];
for (i, message) in messages.iter().enumerate() {
let d = [i as u8 + 1; 8];
let c = [i as u8 + 11; 8];
parse(
&mut s,
&initial(1, &d, &d, &c, 0, true, 0, &message[..20]),
true,
1,
);
}
assert!(names(&s).is_empty());
// A repeated tail cannot fill the gap between its first and last fragments.
let a_tail = initial(
1,
&[1; 8],
&[1; 8],
&[11; 8],
1,
true,
40,
&messages[0][40..],
);
parse(&mut s, &a_tail, true, 2);
parse(&mut s, &a_tail, true, 2);
assert!(names(&s).is_empty());
for (i, message) in messages.iter().enumerate().rev() {
let d = [i as u8 + 1; 8];
let c = [i as u8 + 11; 8];
parse(
&mut s,
&initial(1, &d, &d, &c, 2, true, 20, &message[20..]),
true,
3,
);
}
assert_eq!(
names(&s),
vec![b"c.example".as_slice(), b"b.example", b"a.example"]
);
assert_eq!(s.connections.len(), 3);
}
#[test]
fn server_cid_switch_preserves_initial_keys_and_other_connections() {
let mut s = QuicState::new();
let a = hello("a.example");
let b = hello("b.example");
parse(
&mut s,
&initial(1, &[1; 8], &[1; 8], &[11; 8], 0, true, 0, &a[..20]),
true,
1,
);
parse(
&mut s,
&initial(1, &[2; 8], &[2; 8], &[12; 8], 0, true, 0, &b[..20]),
true,
1,
);
parse(
&mut s,
&initial(1, &[1; 8], &[11; 8], &[21; 8], 0, false, 0, &server_hello()),
false,
2,
);
parse(
&mut s,
&initial(1, &[1; 8], &[21; 8], &[11; 8], 1, true, 20, &a[20..]),
true,
3,
);
assert!(s.connections[0].complete());
assert!(!s.connections[1].complete());
assert_eq!(
s.encryption_flags(EncryptionHandling::ENCRYPTION_HANDLING_BYPASS),
0
);
parse(
&mut s,
&initial(1, &[2; 8], &[2; 8], &[12; 8], 1, true, 20, &b[20..]),
true,
4,
);
assert_eq!(names(&s), vec![b"a.example".as_slice(), b"b.example"]);
}
#[test]
fn zero_length_client_ids_use_the_correct_server_initial_keys() {
let mut s = QuicState::new();
for i in 1..=2 {
parse(
&mut s,
&initial(1, &[i; 8], &[i; 8], &[], 0, true, 0, &hello("zero.example")),
true,
1,
);
}
for i in 1..=2 {
parse(
&mut s,
&initial(1, &[i; 8], &[], &[i + 20; 8], 0, false, 0, &server_hello()),
false,
2,
);
}
assert_eq!(s.connections.len(), 2);
assert!(s.connections.iter().all(|c| c.complete()));
}
#[test]
fn retry_resets_only_its_connection_and_requires_integrity() {
let mut s = QuicState::new();
let a = hello("retry.example");
let b = hello("other.example");
parse(
&mut s,
&initial(1, &[1; 8], &[1; 8], &[11; 8], 0, true, 0, &a[..20]),
true,
1,
);
parse(
&mut s,
&initial(1, &[2; 8], &[2; 8], &[12; 8], 0, true, 0, &b[..20]),
true,
1,
);
let good = retry(&[11; 8], &[31; 8], &[1; 8]);
let mut bad = good.clone();
*bad.last_mut().unwrap() ^= 1;
parse(&mut s, &bad, false, 2);
// Invalid Retry must not discard the original CRYPTO prefix.
parse(
&mut s,
&initial(1, &[1; 8], &[1; 8], &[11; 8], 1, true, 20, &a[20..]),
true,
3,
);
parse(&mut s, &good, false, 4);
parse(
&mut s,
&initial(1, &[31; 8], &[31; 8], &[11; 8], 2, true, 0, &a),
true,
5,
);
parse(
&mut s,
&initial(1, &[2; 8], &[2; 8], &[12; 8], 1, true, 20, &b[20..]),
true,
5,
);
assert_eq!(s.connections.len(), 2);
assert_eq!(
names(&s),
vec![
b"retry.example".as_slice(),
b"retry.example",
b"other.example"
]
);
}
#[test]
fn invalid_initial_and_capacity_limit_preserve_unfinished_connections() {
let mut s = QuicState::new();
s.max_connections = 2;
let h = hello("bounded.example");
for i in 1..=2 {
parse(
&mut s,
&initial(1, &[i; 8], &[i; 8], &[i + 10; 8], 0, true, 0, &h[..20]),
true,
1,
);
}
let mut invalid = initial(1, &[3; 8], &[3; 8], &[13; 8], 0, true, 0, &h);
*invalid.last_mut().unwrap() ^= 1;
parse(&mut s, &invalid, true, 2);
parse(
&mut s,
&initial(1, &[3; 8], &[3; 8], &[13; 8], 1, true, 0, &h),
true,
2,
);
assert_eq!(s.connections.len(), 2);
for i in 1..=2 {
parse(
&mut s,
&initial(1, &[i; 8], &[i; 8], &[i + 10; 8], 1, true, 20, &h[20..]),
true,
3,
);
}
assert_eq!(names(&s).len(), 2);
}
#[test]
fn idle_contexts_expire_without_expiring_active_connections() {
let mut s = QuicState::new();
s.idle_timeout = Duration::from_secs(10);
let h = hello("expiry.example");
parse(
&mut s,
&initial(1, &[1; 8], &[1; 8], &[11; 8], 0, true, 0, &h[..20]),
true,
1,
);
parse(
&mut s,
&initial(1, &[2; 8], &[2; 8], &[12; 8], 0, true, 0, &h[..20]),
true,
8,
);
parse(
&mut s,
&initial(1, &[2; 8], &[2; 8], &[12; 8], 1, true, 20, &h[20..]),
true,
12,
);
assert_eq!(s.connections.len(), 1);
assert_eq!(s.connections[0].client_scid, [12; 8]);
assert_eq!(names(&s), vec![b"expiry.example".as_slice()]);
}
#[test]
fn quic_v2_connections_are_independent() {
let mut s = QuicState::new();
let h = hello("v2.example");
for i in 1..=2 {
parse(
&mut s,
&initial(
0x6b3343cf,
&[i; 8],
&[i; 8],
&[i + 10; 8],
0,
true,
0,
&h[..20],
),
true,
1,
);
}
for i in 1..=2 {
parse(
&mut s,
&initial(
0x6b3343cf,
&[i; 8],
&[i; 8],
&[i + 10; 8],
1,
true,
20,
&h[20..],
),
true,
2,
);
}
assert_eq!(names(&s).len(), 2);
assert_eq!(s.connections.len(), 2);
}
// Rust-only tests pass a null parser state. End-to-end tests exercise the real C API.
#[no_mangle]
extern "C" fn SCAppLayerParserStateSetFlag(_: *mut suricata_sys::sys::AppLayerParserState, _: u16) {
panic!("Rust tests must not call the C parser-state API");
}
#[test]
fn encrypted_cid_rotation_after_hello_does_not_create_new_transactions() {
let mut s = QuicState::new();
parse(
&mut s,
&initial(
1,
&[1; 8],
&[1; 8],
&[11; 8],
0,
true,
0,
&hello("done.example"),
),
true,
1,
);
parse(
&mut s,
&initial(1, &[1; 8], &[11; 8], &[21; 8], 0, false, 0, &server_hello()),
false,
2,
);
let count = s.transactions.len();
let mut handshake = vec![0xe0, 0, 0, 0, 1, 8];
handshake.extend([99; 8]);
handshake.push(8);
handshake.extend([11; 8]);
handshake.push(20);
handshake.extend([0; 20]);
parse(&mut s, &handshake, true, 3);
assert_eq!(s.transactions.len(), count);
assert_eq!(
s.encryption_flags(EncryptionHandling::ENCRYPTION_HANDLING_TRACK_ONLY),
crate::applayer::APP_LAYER_PARSER_NO_INSPECTION_PAYLOAD
);
assert_eq!(
s.encryption_flags(EncryptionHandling::ENCRYPTION_HANDLING_FULL),
0
);
}
Loading…
Cancel
Save