diff --git a/rules/quic-events.rules b/rules/quic-events.rules index b554ba147e..a7f1cf2ed3 100644 --- a/rules/quic-events.rules +++ b/rules/quic-events.rules @@ -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;) diff --git a/rust/src/quic/connection.rs b/rust/src/quic/connection.rs new file mode 100644 index 0000000000..80decb3ee6 --- /dev/null +++ b/rust/src/quic/connection.rs @@ -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, + pub original_dcid: Vec, + initial_dcid: Vec, + server_scid: Option>, + keys: Option, + 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 { + 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); + } +} diff --git a/rust/src/quic/crypto.rs b/rust/src/quic/crypto.rs index 8cfe4ddc0e..2923c1baf8 100644 --- a/rust/src/quic/crypto.rs +++ b/rust/src/quic/crypto.rs @@ -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, +) -> Result { + 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 { + 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 { + 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 { + 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)); + } + } +} diff --git a/rust/src/quic/frames.rs b/rust/src/quic/frames.rs index 136c1a0c9f..43f8191644 100644 --- a/rust/src/quic/frames.rs +++ b/rust/src/quic/frames.rs @@ -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 { +pub(super) fn parse_quic_handshake(msg: TlsMessage) -> Option { 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, 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, QuicError> { + all_consuming(many0(complete(Frame::decode_frame))).parse(input) } } diff --git a/rust/src/quic/mod.rs b/rust/src/quic/mod.rs index 8a8f1bbd7f..28c82a424b 100644 --- a/rust/src/quic/mod.rs +++ b/rust/src/quic/mod.rs @@ -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; diff --git a/rust/src/quic/parser.rs b/rust/src/quic/parser.rs index 1ddf0dd2db..9c993e5ab6 100644 --- a/rust/src/quic/parser.rs +++ b/rust/src/quic/parser.rs @@ -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 { - let (_, frames) = Frame::decode_frames(input, past_frag, past_fraglen)?; + pub(crate) fn from_bytes(input: &[u8]) -> Result { + 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 { diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs index 616e337b1e..8b96d4a147 100644 --- a/rust/src/quic/quic.rs +++ b/rust/src/quic/quic.rs @@ -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, - /// crypto fragment data already seen and reassembled to client - crypto_frag_tc: Vec, - /// 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, - /// 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, + max_connections: usize, + idle_timeout: Duration, transactions: VecDeque, } @@ -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, - ) -> Result { - 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> = None; let mut ua: Option> = None; let mut ja3: Option = None; let mut ja4: Option = None; let mut extv: Vec = 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::().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::().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; diff --git a/rust/src/quic/reassembly.rs b/rust/src/quic/reassembly.rs new file mode 100644 index 0000000000..6e941ebb4e --- /dev/null +++ b/rust/src/quic/reassembly.rs @@ -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, + present: Vec, + 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, 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()); + } +} diff --git a/rust/src/quic/tests.rs b/rust/src/quic/tests.rs new file mode 100644 index 0000000000..e05f0dcfc8 --- /dev/null +++ b/rust/src/quic/tests.rs @@ -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 { + 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 { + 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 + ); +}