diff options
Diffstat (limited to '')
-rw-r--r-- | rust/src/quic/crypto.rs | 200 | ||||
-rw-r--r-- | rust/src/quic/cyu.rs | 195 | ||||
-rw-r--r-- | rust/src/quic/detect.rs | 121 | ||||
-rw-r--r-- | rust/src/quic/error.rs | 65 | ||||
-rw-r--r-- | rust/src/quic/frames.rs | 521 | ||||
-rw-r--r-- | rust/src/quic/logger.rs | 157 | ||||
-rw-r--r-- | rust/src/quic/mod.rs | 27 | ||||
-rw-r--r-- | rust/src/quic/parser.rs | 502 | ||||
-rw-r--r-- | rust/src/quic/quic.rs | 502 |
9 files changed, 2290 insertions, 0 deletions
diff --git a/rust/src/quic/crypto.rs b/rust/src/quic/crypto.rs new file mode 100644 index 0000000..d40e845 --- /dev/null +++ b/rust/src/quic/crypto.rs @@ -0,0 +1,200 @@ +/* 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 aes::cipher::generic_array::GenericArray; +use aes::Aes128; +use aes::BlockEncrypt; +use aes::NewBlockCipher; +use aes_gcm::AeadInPlace; +use aes_gcm::Aes128Gcm; +use aes_gcm::NewAead; +use hkdf::Hkdf; +use sha2::Sha256; + +pub const AES128_KEY_LEN: usize = 16; +pub const AES128_TAG_LEN: usize = 16; +pub const AES128_IV_LEN: usize = 12; + +pub struct HeaderProtectionKey(Aes128); + +impl HeaderProtectionKey { + fn new(secret: &[u8], version: u32) -> Self { + let hk = Hkdf::<Sha256>::from_prk(secret).unwrap(); + let mut secret = [0u8; AES128_KEY_LEN]; + let quichp = if version == 0x6b3343cf { + b"quicv2 hp" as &[u8] + } else { + b"quic hp" as &[u8] + }; + hkdf_expand_label(&hk, quichp, &mut secret, AES128_KEY_LEN as u16); + return Self(Aes128::new(GenericArray::from_slice(&secret))); + } + + pub fn decrypt_in_place( + &self, sample: &[u8], first: &mut u8, packet_number: &mut [u8], + ) -> Result<(), ()> { + let mut mask = GenericArray::clone_from_slice(sample); + self.0.encrypt_block(&mut mask); + + let (first_mask, pn_mask) = mask.split_first().unwrap(); + + let bits = if (*first & 0x80) != 0 { + 0x0f // Long header: 4 bits masked + } else { + 0x1f // Short header: 5 bits masked + }; + + *first ^= first_mask & bits; + let pn_len = (*first & 0x03) as usize + 1; + + for (dst, m) in packet_number.iter_mut().zip(pn_mask).take(pn_len) { + *dst ^= m; + } + + Ok(()) + } +} + +pub struct PacketKey { + key: Aes128Gcm, + iv: [u8; AES128_IV_LEN], +} + +impl PacketKey { + fn new(secret: &[u8], version: u32) -> Self { + let hk = Hkdf::<Sha256>::from_prk(secret).unwrap(); + let mut secret = [0u8; AES128_KEY_LEN]; + let quickey = if version == 0x6b3343cf { + b"quicv2 key" as &[u8] + } else { + b"quic key" as &[u8] + }; + hkdf_expand_label(&hk, quickey, &mut secret, AES128_KEY_LEN as u16); + let key = Aes128Gcm::new(GenericArray::from_slice(&secret)); + + let mut r = PacketKey { + key, + iv: [0u8; AES128_IV_LEN], + }; + let quiciv = if version == 0x6b3343cf { + b"quicv2 iv" as &[u8] + } else { + b"quic iv" as &[u8] + }; + hkdf_expand_label(&hk, quiciv, &mut r.iv, AES128_IV_LEN as u16); + return r; + } + + pub fn decrypt_in_place<'a>( + &self, packet_number: u64, header: &[u8], payload: &'a mut [u8], + ) -> Result<&'a [u8], ()> { + if payload.len() < AES128_TAG_LEN { + return Err(()); + } + let mut nonce = [0; AES128_IV_LEN]; + nonce[4..].copy_from_slice(&packet_number.to_be_bytes()); + for (nonce, inp) in nonce.iter_mut().zip(self.iv.iter()) { + *nonce ^= inp; + } + let tag_pos = payload.len() - AES128_TAG_LEN; + let (buffer, tag) = payload.split_at_mut(tag_pos); + let taga = GenericArray::from_slice(tag); + self.key + .decrypt_in_place_detached(GenericArray::from_slice(&nonce), header, buffer, taga) + .map_err(|_| ())?; + Ok(&payload[..tag_pos]) + } +} + +pub struct DirectionalKeys { + pub header: HeaderProtectionKey, + pub packet: PacketKey, +} + +impl DirectionalKeys { + fn new(secret: &[u8], version: u32) -> Self { + Self { + header: HeaderProtectionKey::new(secret, version), + packet: PacketKey::new(secret, version), + } + } +} + +pub struct QuicKeys { + pub local: DirectionalKeys, + pub remote: DirectionalKeys, +} + +fn hkdf_expand_label(hk: &Hkdf<Sha256>, label: &[u8], okm: &mut [u8], olen: u16) { + const LABEL_PREFIX: &[u8] = b"tls13 "; + + let output_len = u16::to_be_bytes(olen); + let label_len = u8::to_be_bytes((LABEL_PREFIX.len() + label.len()) as u8); + let context_len = u8::to_be_bytes(0); + + let info = &[ + &output_len[..], + &label_len[..], + LABEL_PREFIX, + label, + &context_len[..], + ]; + + hk.expand_multi_info(info, okm).unwrap(); +} + +pub fn quic_keys_initial(version: u32, client_dst_connection_id: &[u8]) -> Option<QuicKeys> { + let salt = match version { + 0x51303530 => &[ + 0x50, 0x45, 0x74, 0xEF, 0xD0, 0x66, 0xFE, 0x2F, 0x9D, 0x94, 0x5C, 0xFC, 0xDB, 0xD3, + 0xA7, 0xF0, 0xD3, 0xB5, 0x6B, 0x45, + ], + 0xff00_001d..=0xff00_0020 => &[ + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#section-5.2 + 0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, + 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99, + ], + 0xfaceb002 | 0xff00_0017..=0xff00_001c => &[ + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-23#section-5.2 + 0xc3, 0xee, 0xf7, 0x12, 0xc7, 0x2e, 0xbb, 0x5a, 0x11, 0xa7, 0xd2, 0x43, 0x2b, 0xb4, + 0x63, 0x65, 0xbe, 0xf9, 0xf5, 0x02, + ], + 0x0000_0001 | 0xff00_0021..=0xff00_0022 => &[ + // https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets + 0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, + 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a, + ], + 0x6b3343cf => &[ + // https://www.rfc-editor.org/rfc/rfc9369.html#section-3.3.1 + 0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, + 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9, + ], + _ => { + return None; + } + }; + let hk = Hkdf::<Sha256>::new(Some(salt), client_dst_connection_id); + let mut client_secret = [0u8; 32]; + hkdf_expand_label(&hk, b"client in", &mut client_secret, 32); + let mut server_secret = [0u8; 32]; + hkdf_expand_label(&hk, b"server in", &mut server_secret, 32); + + return Some(QuicKeys { + local: DirectionalKeys::new(&server_secret, version), + remote: DirectionalKeys::new(&client_secret, version), + }); +} diff --git a/rust/src/quic/cyu.rs b/rust/src/quic/cyu.rs new file mode 100644 index 0000000..0264678 --- /dev/null +++ b/rust/src/quic/cyu.rs @@ -0,0 +1,195 @@ +/* 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::Frame, + parser::{QuicHeader, QuicVersion}, +}; +use md5::{Digest, Md5}; + +#[derive(Debug, PartialEq, Eq)] +pub struct Cyu { + pub string: String, + pub hash: String, +} + +impl Cyu { + pub(crate) fn new(string: String, hash: String) -> Self { + Self { string, hash } + } + + pub(crate) fn generate(header: &QuicHeader, frames: &[Frame]) -> Vec<Cyu> { + let version = match header.version { + QuicVersion::Q043 => Some("43"), + QuicVersion::Q044 => Some("44"), + QuicVersion::Q045 => Some("44"), + QuicVersion::Q046 => Some("46"), + _ => { + SCLogDebug!( + "Cannot match QUIC version {:?} to CYU version", + header.version + ); + None + } + }; + + let mut cyu_hashes = Vec::new(); + + if let Some(version) = version { + for frame in frames { + if let Frame::Stream(stream) = frame { + if let Some(tags) = &stream.tags { + let tags = tags + .iter() + .map(|(tag, _value)| tag.to_string()) + .collect::<Vec<String>>() + .join("-"); + + let cyu_string = format!("{},{}", version, tags); + + let mut hasher = Md5::new(); + hasher.update(cyu_string.as_bytes()); + let hash = hasher.finalize(); + + let cyu_hash = format!("{:x}", hash); + + cyu_hashes.push(Cyu::new(cyu_string, cyu_hash)); + } + } + } + } + + cyu_hashes + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::quic::frames::{Frame, Stream, StreamTag}; + use crate::quic::parser::{PublicFlags, QuicType}; + use test_case::test_case; + + macro_rules! mock_header_and_frames { + ($version:expr, $($variants:expr),+) => {{ + let header = QuicHeader::new( + PublicFlags::new(0x80), + QuicType::Initial, + $version, + vec![], + vec![], + ); + + let frames = vec![ + Frame::Stream(Stream { + fin: false, + stream_id: vec![], + offset: vec![], + tags: Some(vec![$(($variants, vec![])),*]) + }) + ]; + + (header, frames) + }}; + } + + // Salesforce tests here: + // https://engineering.salesforce.com/gquic-protocol-analysis-and-fingerprinting-in-zeek-a4178855d75f + #[test_case( + mock_header_and_frames!( + // version + QuicVersion::Q046, + // tags + StreamTag::Pad, StreamTag::Sni, + StreamTag::Stk, StreamTag::Ver, + StreamTag::Ccs, StreamTag::Nonc, + StreamTag::Aead, StreamTag::Uaid, + StreamTag::Scid, StreamTag::Tcid, + StreamTag::Pdmd, StreamTag::Smhl, + StreamTag::Icsl, StreamTag::Nonp, + StreamTag::Pubs, StreamTag::Mids, + StreamTag::Scls, StreamTag::Kexs, + StreamTag::Xlct, StreamTag::Csct, + StreamTag::Copt, StreamTag::Ccrt, + StreamTag::Irtt, StreamTag::Cfcw, + StreamTag::Sfcw + ), + Cyu { + string: "46,PAD-SNI-STK-VER-CCS-NONC-AEAD-UAID-SCID-TCID-PDMD-SMHL-ICSL-NONP-PUBS-MIDS-SCLS-KEXS-XLCT-CSCT-COPT-CCRT-IRTT-CFCW-SFCW".to_string(), + hash: "a46560d4548108cf99308319b3b85346".to_string(), + }; "test cyu 1" + )] + #[test_case( + mock_header_and_frames!( + // version + QuicVersion::Q043, + // tags + StreamTag::Pad, StreamTag::Sni, + StreamTag::Ver, StreamTag::Ccs, + StreamTag::Pdmd, StreamTag::Icsl, + StreamTag::Mids, StreamTag::Cfcw, + StreamTag::Sfcw + ), + Cyu { + string: "43,PAD-SNI-VER-CCS-PDMD-ICSL-MIDS-CFCW-SFCW".to_string(), + hash: "e030dea1f2eea44ac7db5fe4de792acd".to_string(), + }; "test cyu 2" + )] + #[test_case( + mock_header_and_frames!( + // version + QuicVersion::Q043, + // tags + StreamTag::Pad, StreamTag::Sni, + StreamTag::Stk, StreamTag::Ver, + StreamTag::Ccs, StreamTag::Scid, + StreamTag::Pdmd, StreamTag::Icsl, + StreamTag::Mids, StreamTag::Cfcw, + StreamTag::Sfcw + ), + Cyu { + string: "43,PAD-SNI-STK-VER-CCS-SCID-PDMD-ICSL-MIDS-CFCW-SFCW".to_string(), + hash: "0811fab28e41e8c8a33e220a15b964d9".to_string(), + }; "test cyu 3" + )] + #[test_case( + mock_header_and_frames!( + // version + QuicVersion::Q043, + // tags + StreamTag::Pad, StreamTag::Sni, + StreamTag::Stk, StreamTag::Ver, + StreamTag::Ccs, StreamTag::Nonc, + StreamTag::Aead, StreamTag::Scid, + StreamTag::Pdmd, StreamTag::Icsl, + StreamTag::Pubs, StreamTag::Mids, + StreamTag::Kexs, StreamTag::Xlct, + StreamTag::Cfcw, StreamTag::Sfcw + ), + Cyu { + string: "43,PAD-SNI-STK-VER-CCS-NONC-AEAD-SCID-PDMD-ICSL-PUBS-MIDS-KEXS-XLCT-CFCW-SFCW".to_string(), + hash: "d8b208b236d176c89407500dbefb04c2".to_string(), + }; "test cyu 4" + )] + fn test_cyu_generate(input: (QuicHeader, Vec<Frame>), expected: Cyu) { + let (header, frames) = input; + + let cyu = Cyu::generate(&header, &frames); + assert_eq!(1, cyu.len()); + assert_eq!(expected, cyu[0]); + } +} diff --git a/rust/src/quic/detect.rs b/rust/src/quic/detect.rs new file mode 100644 index 0000000..7e9019b --- /dev/null +++ b/rust/src/quic/detect.rs @@ -0,0 +1,121 @@ +/* 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 crate::quic::quic::QuicTransaction; +use std::ptr; + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_tx_get_ua( + tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + if let Some(ua) = &tx.ua { + *buffer = ua.as_ptr(); + *buffer_len = ua.len() as u32; + 1 + } else { + *buffer = ptr::null(); + *buffer_len = 0; + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_tx_get_sni( + tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + if let Some(sni) = &tx.sni { + *buffer = sni.as_ptr(); + *buffer_len = sni.len() as u32; + 1 + } else { + *buffer = ptr::null(); + *buffer_len = 0; + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_tx_get_ja3( + tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + if let Some(ja3) = &tx.ja3 { + *buffer = ja3.as_ptr(); + *buffer_len = ja3.len() as u32; + 1 + } else { + *buffer = ptr::null(); + *buffer_len = 0; + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_tx_get_version( + tx: &QuicTransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + if tx.header.flags.is_long { + let s = &tx.header.version_buf; + *buffer = s.as_ptr(); + *buffer_len = s.len() as u32; + 1 + } else { + *buffer = ptr::null(); + *buffer_len = 0; + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_tx_get_cyu_hash( + tx: &QuicTransaction, i: u32, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + if (i as usize) < tx.cyu.len() { + let cyu = &tx.cyu[i as usize]; + + let p = &cyu.hash; + + *buffer = p.as_ptr(); + *buffer_len = p.len() as u32; + + 1 + } else { + *buffer = ptr::null(); + *buffer_len = 0; + + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_tx_get_cyu_string( + tx: &QuicTransaction, i: u32, buffer: *mut *const u8, buffer_len: *mut u32, +) -> u8 { + if (i as usize) < tx.cyu.len() { + let cyu = &tx.cyu[i as usize]; + + let p = &cyu.string; + + *buffer = p.as_ptr(); + *buffer_len = p.len() as u32; + 1 + } else { + *buffer = ptr::null(); + *buffer_len = 0; + + 0 + } +} diff --git a/rust/src/quic/error.rs b/rust/src/quic/error.rs new file mode 100644 index 0000000..89ac1fb --- /dev/null +++ b/rust/src/quic/error.rs @@ -0,0 +1,65 @@ +/* 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 nom7::error::{ErrorKind, ParseError}; +use std::error::Error; +use std::fmt; + +#[derive(Debug, PartialEq, Eq)] +pub enum QuicError { + StreamTagNoMatch(u32), + InvalidPacket, + Incomplete, + Unhandled, + NomError(ErrorKind), +} + +impl<I> ParseError<I> for QuicError { + fn from_error_kind(_input: I, kind: ErrorKind) -> Self { + QuicError::NomError(kind) + } + + fn append(_input: I, kind: ErrorKind, _other: Self) -> Self { + QuicError::NomError(kind) + } +} + +impl fmt::Display for QuicError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + QuicError::StreamTagNoMatch(tag) => { + write!(f, "Could not match stream tag: 0x{:x}", tag) + } + QuicError::Incomplete => write!(f, "Incomplete data"), + QuicError::InvalidPacket => write!(f, "Invalid packet"), + QuicError::Unhandled => write!(f, "Unhandled packet"), + QuicError::NomError(e) => write!(f, "Internal parser error {:?}", e), + } + } +} + +impl Error for QuicError {} + +impl From<nom7::Err<QuicError>> for QuicError { + fn from(err: nom7::Err<QuicError>) -> Self { + match err { + nom7::Err::Incomplete(_) => QuicError::Incomplete, + nom7::Err::Error(e) => e, + nom7::Err::Failure(e) => e, + } + } +} diff --git a/rust/src/quic/frames.rs b/rust/src/quic/frames.rs new file mode 100644 index 0000000..e1fb7d0 --- /dev/null +++ b/rust/src/quic/frames.rs @@ -0,0 +1,521 @@ +/* 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::error::QuicError; +use crate::quic::parser::quic_var_uint; +use nom7::bytes::complete::take; +use nom7::combinator::{all_consuming, complete}; +use nom7::multi::{count, many0}; +use nom7::number::complete::{be_u16, be_u32, be_u8, le_u16, le_u32}; +use nom7::sequence::pair; +use nom7::IResult; +use num::FromPrimitive; +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, +}; + +/// Tuple of StreamTag and offset +type TagOffset = (StreamTag, u32); + +/// Tuple of StreamTag and value +type TagValue = (StreamTag, Vec<u8>); + +#[derive(Debug, PartialEq)] +pub(crate) struct Stream { + pub fin: bool, + pub stream_id: Vec<u8>, + pub offset: Vec<u8>, + pub tags: Option<Vec<TagValue>>, +} + +#[repr(u32)] +#[derive(Debug, PartialEq, Clone, Copy, FromPrimitive)] +pub(crate) enum StreamTag { + Aead = 0x41454144, + Ccrt = 0x43435254, + Ccs = 0x43435300, + Cetv = 0x43455456, + Cfcw = 0x43464357, + Chlo = 0x43484c4f, + Copt = 0x434f5054, + Csct = 0x43534354, + Ctim = 0x4354494d, + Icsl = 0x4943534c, + Irtt = 0x49525454, + Kexs = 0x4b455853, + Mids = 0x4d494453, + Mspc = 0x4d535043, + Nonc = 0x4e4f4e43, + Nonp = 0x4e4f4e50, + Pad = 0x50414400, + Pdmd = 0x50444d44, + Pubs = 0x50554253, + Scid = 0x53434944, + Scls = 0x53434c53, + Sfcw = 0x53464357, + Smhl = 0x534d484c, + Sni = 0x534e4900, + Sno = 0x534e4f00, + Stk = 0x53544b00, + Tcid = 0x54434944, + Uaid = 0x55414944, + Ver = 0x56455200, + Xlct = 0x584c4354, +} + +impl fmt::Display for StreamTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + StreamTag::Aead => "AEAD", + StreamTag::Ccrt => "CCRT", + StreamTag::Ccs => "CCS", + StreamTag::Cetv => "CETV", + StreamTag::Cfcw => "CFCW", + StreamTag::Chlo => "CHLO", + StreamTag::Copt => "COPT", + StreamTag::Csct => "CSCT", + StreamTag::Ctim => "CTIM", + StreamTag::Icsl => "ICSL", + StreamTag::Irtt => "IRTT", + StreamTag::Kexs => "KEXS", + StreamTag::Mids => "MIDS", + StreamTag::Mspc => "MSPC", + StreamTag::Nonc => "NONC", + StreamTag::Nonp => "NONP", + StreamTag::Pad => "PAD", + StreamTag::Pdmd => "PDMD", + StreamTag::Pubs => "PUBS", + StreamTag::Scid => "SCID", + StreamTag::Scls => "SCLS", + StreamTag::Sfcw => "SFCW", + StreamTag::Smhl => "SMHL", + StreamTag::Sni => "SNI", + StreamTag::Sno => "SNO", + StreamTag::Stk => "STK", + StreamTag::Tcid => "TCID", + StreamTag::Uaid => "UAID", + StreamTag::Ver => "VER", + StreamTag::Xlct => "XLCT", + } + ) + } +} + +#[derive(Debug, PartialEq)] +pub(crate) struct Ack { + pub largest_acknowledged: u64, + pub ack_delay: u64, + pub ack_range_count: u64, + pub first_ack_range: u64, +} + +#[derive(Debug, PartialEq)] +pub(crate) struct Crypto { + pub ciphers: Vec<TlsCipherSuiteID>, + // We remap the Vec<TlsExtension> from tls_parser::parse_tls_extensions because of + // the lifetime of TlsExtension due to references to the slice used for parsing + pub extv: Vec<QuicTlsExtension>, + pub ja3: String, +} + +#[derive(Debug, PartialEq)] +pub(crate) struct CryptoFrag { + pub offset: u64, + pub length: u64, + pub data: Vec<u8>, +} + +#[derive(Debug, PartialEq)] +pub(crate) enum Frame { + Padding, + Ping, + Ack(Ack), + // this is more than a crypto frame : it contains a fully parsed tls hello + Crypto(Crypto), + // this is a regular quic crypto frame : they can be reassembled + // in order to parse a tls hello + CryptoFrag(CryptoFrag), + Stream(Stream), + Unknown(Vec<u8>), +} + +fn parse_padding_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> { + // nom take_while: cannot infer type for type parameter `Error` declared on the function `take_while` + let mut offset = 0; + while offset < input.len() { + if input[offset] != 0 { + break; + } + offset += 1; + } + return Ok((&input[offset..], Frame::Padding)); +} + +fn parse_ack_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> { + let (rest, largest_acknowledged) = quic_var_uint(input)?; + let (rest, ack_delay) = quic_var_uint(rest)?; + let (rest, ack_range_count) = quic_var_uint(rest)?; + let (mut rest, first_ack_range) = quic_var_uint(rest)?; + + for _ in 0..ack_range_count { + //RFC9000 section 19.3.1. ACK Ranges + let (rest1, _gap) = quic_var_uint(rest)?; + let (rest1, _ack_range_length) = quic_var_uint(rest1)?; + rest = rest1; + } + + Ok(( + rest, + Frame::Ack(Ack { + largest_acknowledged, + ack_delay, + ack_range_count, + first_ack_range, + }), + )) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct QuicTlsExtension { + pub etype: TlsExtensionType, + pub values: Vec<Vec<u8>>, +} + +fn quic_tls_ja3_client_extends(ja3: &mut String, exts: Vec<TlsExtension>) { + ja3.push(','); + let mut dash = false; + for e in &exts { + if let TlsExtension::EllipticCurves(x) = e { + for ec in x { + if dash { + ja3.push('-'); + } else { + dash = true; + } + ja3.push_str(&ec.0.to_string()); + } + } + } + ja3.push(','); + dash = false; + for e in &exts { + if let TlsExtension::EcPointFormats(x) = e { + for ec in *x { + if dash { + ja3.push('-'); + } else { + dash = true; + } + ja3.push_str(&ec.to_string()); + } + } + } +} + +// get interesting stuff out of parsed tls extensions +fn quic_get_tls_extensions( + input: Option<&[u8]>, ja3: &mut String, client: bool, +) -> Vec<QuicTlsExtension> { + let mut extv = Vec::new(); + if let Some(extr) = input { + if let Ok((_, exts)) = parse_tls_extensions(extr) { + let mut dash = false; + for e in &exts { + let etype = TlsExtensionType::from(e); + if dash { + ja3.push('-'); + } else { + dash = true; + } + ja3.push_str(&u16::from(etype).to_string()); + let mut values = Vec::new(); + match e { + TlsExtension::SNI(x) => { + for sni in x { + let mut value = Vec::new(); + value.extend_from_slice(sni.1); + values.push(value); + } + } + TlsExtension::ALPN(x) => { + for alpn in x { + let mut value = Vec::new(); + value.extend_from_slice(alpn); + values.push(value); + } + } + _ => {} + } + extv.push(QuicTlsExtension { etype, values }) + } + if client { + quic_tls_ja3_client_extends(ja3, exts); + } + } + } + return extv; +} + +fn parse_quic_handshake(msg: TlsMessage) -> Option<Frame> { + if let Handshake(hs) = msg { + match hs { + ClientHello(ch) => { + let mut ja3 = String::with_capacity(256); + ja3.push_str(&u16::from(ch.version).to_string()); + ja3.push(','); + let mut dash = false; + for c in &ch.ciphers { + if dash { + ja3.push('-'); + } else { + dash = true; + } + ja3.push_str(&u16::from(*c).to_string()); + } + ja3.push(','); + let ciphers = ch.ciphers; + let extv = quic_get_tls_extensions(ch.ext, &mut ja3, true); + return Some(Frame::Crypto(Crypto { ciphers, extv, ja3 })); + } + ServerHello(sh) => { + let mut ja3 = String::with_capacity(256); + ja3.push_str(&u16::from(sh.version).to_string()); + ja3.push(','); + ja3.push_str(&u16::from(sh.cipher).to_string()); + ja3.push(','); + let ciphers = vec![sh.cipher]; + let extv = quic_get_tls_extensions(sh.ext, &mut ja3, false); + return Some(Frame::Crypto(Crypto { ciphers, extv, ja3 })); + } + _ => {} + } + } + return None; +} + +fn parse_crypto_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> { + let (rest, offset) = quic_var_uint(input)?; + 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(nom7::Err::Error(QuicError::InvalidPacket)); +} + +fn parse_tag(input: &[u8]) -> IResult<&[u8], StreamTag, QuicError> { + let (rest, tag) = be_u32(input)?; + + let tag = StreamTag::from_u32(tag).ok_or(nom7::Err::Error(QuicError::StreamTagNoMatch(tag)))?; + + Ok((rest, tag)) +} + +fn parse_tag_and_offset(input: &[u8]) -> IResult<&[u8], TagOffset, QuicError> { + pair(parse_tag, le_u32)(input) +} + +fn parse_crypto_stream(input: &[u8]) -> IResult<&[u8], Vec<TagValue>, QuicError> { + // [message tag][number of tag entries: N][pad][[tag][end offset], ...N][value data] + let (rest, _message_tag) = parse_tag(input)?; + + let (rest, num_entries) = le_u16(rest)?; + let (rest, _padding) = take(2usize)(rest)?; + + let (rest, tags_offset) = count(complete(parse_tag_and_offset), num_entries.into())(rest)?; + + // Convert (Tag, Offset) to (Tag, Value) + let mut tags = Vec::new(); + let mut previous_offset = 0; + let mut rest = rest; + for (tag, offset) in tags_offset { + // offsets should be increasing + let value_len = offset + .checked_sub(previous_offset) + .ok_or(nom7::Err::Error(QuicError::InvalidPacket))?; + let (new_rest, value) = take(value_len)(rest)?; + + previous_offset = offset; + rest = new_rest; + + tags.push((tag, value.to_vec())) + } + + Ok((rest, tags)) +} + +fn parse_stream_frame(input: &[u8], frame_ty: u8) -> IResult<&[u8], Frame, QuicError> { + // 0b1_f_d_ooo_ss + let fin = frame_ty & 0x40 == 0x40; + let has_data_length = frame_ty & 0x20 == 0x20; + + let offset_hdr_length = { + let mut offset_length = (frame_ty & 0x1c) >> 2; + if offset_length != 0 { + offset_length += 1; + } + offset_length + }; + + let stream_id_hdr_length = usize::from((frame_ty & 0x03) + 1); + + let (rest, stream_id) = take(stream_id_hdr_length)(input)?; + let (rest, offset) = take(offset_hdr_length)(rest)?; + + let (rest, data_length) = if has_data_length { + let (rest, data_length) = be_u16(rest)?; + + (rest, usize::from(data_length)) + } else { + (rest, rest.len()) + }; + + let (rest, stream_data) = take(data_length)(rest)?; + + let tags = if let Ok((_, tags)) = all_consuming(parse_crypto_stream)(stream_data) { + Some(tags) + } else { + None + }; + + Ok(( + rest, + Frame::Stream(Stream { + fin, + stream_id: stream_id.to_vec(), + offset: offset.to_vec(), + tags, + }), + )) +} + +fn parse_crypto_stream_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> { + let (rest, _offset) = quic_var_uint(input)?; + let (rest, data_length) = quic_var_uint(rest)?; + if data_length > u32::MAX as u64 { + return Err(nom7::Err::Error(QuicError::Unhandled)); + } + let (rest, stream_data) = take(data_length as u32)(rest)?; + + let tags = if let Ok((_, tags)) = all_consuming(parse_crypto_stream)(stream_data) { + Some(tags) + } else { + None + }; + + Ok(( + rest, + Frame::Stream(Stream { + fin: false, + stream_id: Vec::new(), + offset: Vec::new(), + tags, + }), + )) +} + +impl Frame { + fn decode_frame(input: &[u8]) -> IResult<&[u8], Frame, QuicError> { + let (rest, frame_ty) = be_u8(input)?; + + // Special frame types + let (rest, value) = if frame_ty & 0x80 == 0x80 { + // STREAM + parse_stream_frame(rest, frame_ty)? + } else { + match frame_ty { + 0x00 => parse_padding_frame(rest)?, + 0x01 => (rest, Frame::Ping), + 0x02 => parse_ack_frame(rest)?, + 0x06 => parse_crypto_frame(rest)?, + 0x08 => parse_crypto_stream_frame(rest)?, + _ => ([].as_ref(), Frame::Unknown(rest.to_vec())), + } + }; + + Ok((rest, value)) + } + + pub(crate) fn decode_frames(input: &[u8]) -> IResult<&[u8], Vec<Frame>, QuicError> { + let (rest, mut frames) = many0(complete(Frame::decode_frame))(input)?; + + // reassemble crypto fragments : first find total size + let mut crypto_max_size = 0; + let mut crypto_total_size = 0; + 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_total_size == crypto_max_size { + // we have some, and no gaps from offset 0 + let mut d = vec![0; crypto_max_size as usize]; + 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); + } + } + if let Ok((_, msg)) = parse_tls_message_handshake(&d) { + if let Some(c) = parse_quic_handshake(msg) { + // add a parsed crypto frame + frames.push(c); + } + } + } + + Ok((rest, frames)) + } +} diff --git a/rust/src/quic/logger.rs b/rust/src/quic/logger.rs new file mode 100644 index 0000000..e03ebdd --- /dev/null +++ b/rust/src/quic/logger.rs @@ -0,0 +1,157 @@ +/* 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::parser::QuicType; +use super::quic::QuicTransaction; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use digest::Digest; +use digest::Update; +use md5::Md5; + +fn quic_tls_extension_name(e: u16) -> Option<String> { + match e { + 0 => Some("server_name".to_string()), + 1 => Some("max_fragment_length".to_string()), + 2 => Some("client_certificate_url".to_string()), + 3 => Some("trusted_ca_keys".to_string()), + 4 => Some("truncated_hmac".to_string()), + 5 => Some("status_request".to_string()), + 6 => Some("user_mapping".to_string()), + 7 => Some("client_authz".to_string()), + 8 => Some("server_authz".to_string()), + 9 => Some("cert_type".to_string()), + 10 => Some("supported_groups".to_string()), + 11 => Some("ec_point_formats".to_string()), + 12 => Some("srp".to_string()), + 13 => Some("signature_algorithms".to_string()), + 14 => Some("use_srtp".to_string()), + 15 => Some("heartbeat".to_string()), + 16 => Some("alpn".to_string()), + 17 => Some("status_request_v2".to_string()), + 18 => Some("signed_certificate_timestamp".to_string()), + 19 => Some("client_certificate_type".to_string()), + 20 => Some("server_certificate_type".to_string()), + 21 => Some("padding".to_string()), + 22 => Some("encrypt_then_mac".to_string()), + 23 => Some("extended_master_secret".to_string()), + 24 => Some("token_binding".to_string()), + 25 => Some("cached_info".to_string()), + 26 => Some("tls_lts".to_string()), + 27 => Some("compress_certificate".to_string()), + 28 => Some("record_size_limit".to_string()), + 29 => Some("pwd_protect".to_string()), + 30 => Some("pwd_clear".to_string()), + 31 => Some("password_salt".to_string()), + 32 => Some("ticket_pinning".to_string()), + 33 => Some("tls_cert_with_extern_psk".to_string()), + 34 => Some("delegated_credentials".to_string()), + 35 => Some("session_ticket".to_string()), + 36 => Some("tlmsp".to_string()), + 37 => Some("tlmsp_proxying".to_string()), + 38 => Some("tlmsp_delegate".to_string()), + 39 => Some("supported_ekt_ciphers".to_string()), + 41 => Some("pre_shared_key".to_string()), + 42 => Some("early_data".to_string()), + 43 => Some("supported_versions".to_string()), + 44 => Some("cookie".to_string()), + 45 => Some("psk_key_exchange_modes".to_string()), + 47 => Some("certificate_authorities".to_string()), + 48 => Some("oid_filters".to_string()), + 49 => Some("post_handshake_auth".to_string()), + 50 => Some("signature_algorithms_cert".to_string()), + 51 => Some("key_share".to_string()), + 52 => Some("transparency_info".to_string()), + 53 => Some("connection_id_deprecated".to_string()), + 54 => Some("connection_id".to_string()), + 55 => Some("external_id_hash".to_string()), + 56 => Some("external_session_id".to_string()), + 57 => Some("quic_transport_parameters".to_string()), + 58 => Some("ticket_request".to_string()), + 59 => Some("dnssec_chain".to_string()), + 13172 => Some("next_protocol_negotiation".to_string()), + 65281 => Some("renegotiation_info".to_string()), + _ => None, + } +} + +fn log_template(tx: &QuicTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("quic")?; + if tx.header.ty != QuicType::Short { + js.set_string("version", String::from(tx.header.version).as_str())?; + + if let Some(sni) = &tx.sni { + js.set_string("sni", &String::from_utf8_lossy(sni))?; + } + if let Some(ua) = &tx.ua { + js.set_string("ua", &String::from_utf8_lossy(ua))?; + } + } + if !tx.cyu.is_empty() { + js.open_array("cyu")?; + for cyu in &tx.cyu { + js.start_object()?; + js.set_string("hash", &cyu.hash)?; + js.set_string("string", &cyu.string)?; + js.close()?; + } + js.close()?; + } + + if let Some(ja3) = &tx.ja3 { + if tx.client { + js.open_object("ja3")?; + } else { + js.open_object("ja3s")?; + } + let hash = format!("{:x}", Md5::new().chain(ja3).finalize()); + js.set_string("hash", &hash)?; + js.set_string("string", ja3)?; + js.close()?; + } + if !tx.extv.is_empty() { + js.open_array("extensions")?; + for e in &tx.extv { + js.start_object()?; + let etype = u16::from(e.etype); + if let Some(s) = quic_tls_extension_name(etype) { + js.set_string("name", &s)?; + } + js.set_uint("type", etype.into())?; + + if !e.values.is_empty() { + js.open_array("values")?; + for i in 0..e.values.len() { + js.append_string(&String::from_utf8_lossy(&e.values[i]))?; + } + js.close()?; + } + js.close()?; + } + js.close()?; + } + + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_to_json( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, QuicTransaction); + log_template(tx, js).is_ok() +} diff --git a/rust/src/quic/mod.rs b/rust/src/quic/mod.rs new file mode 100644 index 0000000..8a8f1bb --- /dev/null +++ b/rust/src/quic/mod.rs @@ -0,0 +1,27 @@ +/* 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. + */ + +//! QUIC application layer, parser, detection and logger module. + +mod crypto; +mod cyu; +pub mod detect; +mod error; +mod frames; +mod logger; +mod parser; +pub mod quic; diff --git a/rust/src/quic/parser.rs b/rust/src/quic/parser.rs new file mode 100644 index 0000000..1269736 --- /dev/null +++ b/rust/src/quic/parser.rs @@ -0,0 +1,502 @@ +/* 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::error::QuicError; +use super::frames::Frame; +use nom7::bytes::complete::take; +use nom7::combinator::{all_consuming, map}; +use nom7::number::complete::{be_u24, be_u32, be_u8}; +use nom7::IResult; +use std::convert::TryFrom; + +/* + gQUIC is the Google version of QUIC. + + The following docs were referenced when writing this parser + + References: + - https://docs.google.com/document/d/1WJvyZflAO2pq77yOLbp9NsGjC1CHetAXV8I0fQe-B_U/edit + - https://docs.google.com/document/d/1g5nIXAIkN_Y-7XJW5K45IblHd_L2f5LTaDUDwvZ5L6g/edit + - https://www.slideshare.net/shigeki_ohtsu/quic-overview + - https://tools.ietf.org/html/draft-ietf-quic-transport-17#section-19.8 + - https://github.com/salesforce/GQUIC_Protocol_Analyzer/blob/master/src/gquic-protocol.pac +*/ + +// List of accepted and tested quic versions format +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct QuicVersion(pub u32); + +impl QuicVersion { + pub const Q043: QuicVersion = QuicVersion(0x51303433); + pub const Q044: QuicVersion = QuicVersion(0x51303434); + pub const Q045: QuicVersion = QuicVersion(0x51303435); + pub const Q046: QuicVersion = QuicVersion(0x51303436); + pub const V2: QuicVersion = QuicVersion(0x6b3343cf); + + fn is_gquic(&self) -> bool { + *self == QuicVersion::Q043 + || *self == QuicVersion::Q044 + || *self == QuicVersion::Q045 + || *self == QuicVersion::Q046 + } +} + +impl From<QuicVersion> for String { + fn from(from: QuicVersion) -> Self { + match from { + QuicVersion(0x51303339) => "Q039".to_string(), + QuicVersion(0x51303433) => "Q043".to_string(), + QuicVersion(0x51303434) => "Q044".to_string(), + QuicVersion(0x51303435) => "Q045".to_string(), + QuicVersion(0x51303436) => "Q046".to_string(), + QuicVersion(0x6b3343cf) => "v2".to_string(), + QuicVersion(x) => format!("{:x}", x), + } + } +} +impl From<QuicVersion> for u32 { + fn from(from: QuicVersion) -> Self { + from.0 + } +} + +impl From<u32> for QuicVersion { + fn from(from: u32) -> Self { + QuicVersion(from) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum QuicType { + Initial, + Retry, + Handshake, + ZeroRTT, + VersionNegotiation, + Short, +} + +const QUIC_FLAG_MULTIPATH: u8 = 0x40; +const QUIC_FLAG_DCID_LEN: u8 = 0x8; +const QUIC_FLAG_NONCE: u8 = 0x4; +const QUIC_FLAG_VERSION: u8 = 0x1; + +#[derive(Debug, PartialEq, Eq)] +pub struct PublicFlags { + pub is_long: bool, + pub raw: u8, +} + +impl PublicFlags { + pub fn new(value: u8) -> Self { + let is_long = value & 0x80 == 0x80; + let raw = value; + + PublicFlags { is_long, raw } + } +} + +pub fn quic_pkt_num(input: &[u8]) -> u64 { + // There must be a more idiomatic way sigh... + match input.len() { + 1 => { + return input[0] as u64; + } + 2 => { + return ((input[0] as u64) << 8) | (input[1] as u64); + } + 3 => { + return ((input[0] as u64) << 16) | ((input[1] as u64) << 8) | (input[2] as u64); + } + 4 => { + return ((input[0] as u64) << 24) + | ((input[1] as u64) << 16) + | ((input[2] as u64) << 8) + | (input[3] as u64); + } + _ => { + // should not be reachable + debug_validate_fail!("Unexpected length for quic pkt num"); + return 0; + } + } +} + +pub fn quic_var_uint(input: &[u8]) -> IResult<&[u8], u64, QuicError> { + let (rest, first) = be_u8(input)?; + let msb = first >> 6; + let lsb = (first & 0x3F) as u64; + match msb { + 3 => { + // nom does not have be_u56 + let (rest, second) = be_u24(rest)?; + let (rest, third) = be_u32(rest)?; + return Ok((rest, (lsb << 56) | ((second as u64) << 32) | (third as u64))); + } + 2 => { + let (rest, second) = be_u24(rest)?; + return Ok((rest, (lsb << 24) | (second as u64))); + } + 1 => { + let (rest, second) = be_u8(rest)?; + return Ok((rest, (lsb << 8) | (second as u64))); + } + _ => { + // only remaining case is msb==0 + return Ok((rest, lsb)); + } + } +} + +/// A QUIC packet's header. +#[derive(Debug, PartialEq, Eq)] +pub struct QuicHeader { + pub flags: PublicFlags, + pub ty: QuicType, + pub version: QuicVersion, + pub version_buf: Vec<u8>, + pub dcid: Vec<u8>, + pub scid: Vec<u8>, + pub length: u16, +} + +#[derive(Debug, PartialEq)] +pub(crate) struct QuicData { + pub frames: Vec<Frame>, +} + +impl QuicHeader { + #[cfg(test)] + pub(crate) fn new( + flags: PublicFlags, ty: QuicType, version: QuicVersion, dcid: Vec<u8>, scid: Vec<u8>, + ) -> Self { + Self { + flags, + ty, + version, + version_buf: Vec::new(), + dcid, + scid, + length: 0, + } + } + + pub(crate) fn from_bytes( + input: &[u8], _dcid_len: usize, + ) -> IResult<&[u8], QuicHeader, QuicError> { + let (rest, first) = be_u8(input)?; + let flags = PublicFlags::new(first); + + if !flags.is_long && (flags.raw & QUIC_FLAG_MULTIPATH) == 0 { + let (mut rest, dcid) = if (flags.raw & QUIC_FLAG_DCID_LEN) != 0 { + take(8_usize)(rest)? + } else { + take(0_usize)(rest)? + }; + if (flags.raw & QUIC_FLAG_NONCE) != 0 && (flags.raw & QUIC_FLAG_DCID_LEN) == 0 { + let (rest1, _) = take(32_usize)(rest)?; + rest = rest1; + } + let version_buf: &[u8]; + let version; + if (flags.raw & QUIC_FLAG_VERSION) != 0 { + let (_, version_buf1) = take(4_usize)(rest)?; + let (rest1, version1) = map(be_u32, QuicVersion)(rest)?; + rest = rest1; + version = version1; + version_buf = version_buf1; + } else { + version = QuicVersion(0); + version_buf = &rest[..0]; + } + let pkt_num_len = 1 + ((flags.raw & 0x30) >> 4); + let (mut rest, _pkt_num) = take(pkt_num_len)(rest)?; + if (flags.raw & QUIC_FLAG_DCID_LEN) != 0 { + let (rest1, _msg_auth_hash) = take(12_usize)(rest)?; + rest = rest1; + } + let ty = if (flags.raw & QUIC_FLAG_VERSION) != 0 { + QuicType::Initial + } else { + QuicType::Short + }; + if let Ok(plength) = u16::try_from(rest.len()) { + return Ok(( + rest, + QuicHeader { + flags, + ty, + version, + version_buf: version_buf.to_vec(), + dcid: dcid.to_vec(), + scid: Vec::new(), + length: plength, + }, + )); + } else { + return Err(nom7::Err::Error(QuicError::InvalidPacket)); + } + } else if !flags.is_long { + // Decode short header + // depends on version let (rest, dcid) = take(_dcid_len)(rest)?; + + if let Ok(plength) = u16::try_from(rest.len()) { + return Ok(( + rest, + QuicHeader { + flags, + ty: QuicType::Short, + version: QuicVersion(0), + version_buf: Vec::new(), + dcid: Vec::new(), + scid: Vec::new(), + length: plength, + }, + )); + } else { + return Err(nom7::Err::Error(QuicError::InvalidPacket)); + } + } else { + // Decode Long header + let (_, version_buf) = take(4_usize)(rest)?; + let (rest, version) = map(be_u32, QuicVersion)(rest)?; + + let ty = if version == QuicVersion(0) { + QuicType::VersionNegotiation + } else { + // Q046 is when they started using IETF + if version.is_gquic() && version != QuicVersion::Q046 { + match first & 0x7f { + 0x7f => QuicType::Initial, + 0x7e => QuicType::Retry, + 0x7d => QuicType::Handshake, + 0x7c => QuicType::ZeroRTT, + _ => { + return Err(nom7::Err::Error(QuicError::InvalidPacket)); + } + } + } else if version == QuicVersion::V2 { + match (first & 0x30) >> 4 { + 0x01 => QuicType::Initial, + 0x02 => QuicType::ZeroRTT, + 0x03 => QuicType::Handshake, + 0x00 => QuicType::Retry, + _ => { + return Err(nom7::Err::Error(QuicError::InvalidPacket)); + } + } + } else { + // consider as Quic version 1 (and latest drafts) + match (first & 0x30) >> 4 { + 0x00 => QuicType::Initial, + 0x01 => QuicType::ZeroRTT, + 0x02 => QuicType::Handshake, + 0x03 => QuicType::Retry, + _ => { + return Err(nom7::Err::Error(QuicError::InvalidPacket)); + } + } + } + }; + + let (rest, dcid, scid) = if version.is_gquic() { + // [DCID_LEN (4)][SCID_LEN (4)] + let (rest, lengths) = be_u8(rest)?; + + let mut dcid_len = (lengths & 0xF0) >> 4; + let mut scid_len = lengths & 0x0F; + + // Decode dcid length if not 0 + if dcid_len != 0 { + dcid_len += 3; + } + + // Decode scid length if not 0 + if scid_len != 0 { + scid_len += 3; + } + + let (rest, dcid) = take(dcid_len as usize)(rest)?; + let (rest, scid) = take(scid_len as usize)(rest)?; + + (rest, dcid.to_vec(), scid.to_vec()) + } else { + let (rest, dcid_len) = be_u8(rest)?; + let (rest, dcid) = take(dcid_len as usize)(rest)?; + + let (rest, scid_len) = be_u8(rest)?; + let (rest, scid) = take(scid_len as usize)(rest)?; + (rest, dcid.to_vec(), scid.to_vec()) + }; + + let mut has_length = false; + let rest = match ty { + QuicType::Initial => { + if version.is_gquic() { + let (rest, _pkt_num) = be_u32(rest)?; + let (rest, _msg_auth_hash) = take(12usize)(rest)?; + + rest + } else { + let (rest, token_length) = quic_var_uint(rest)?; + let (rest, _token) = take(token_length as usize)(rest)?; + has_length = true; + rest + } + } + _ => rest, + }; + let (rest, length) = if has_length { + let (rest2, plength) = quic_var_uint(rest)?; + if plength > rest2.len() as u64 { + return Err(nom7::Err::Error(QuicError::InvalidPacket)); + } + if let Ok(length) = u16::try_from(plength) { + (rest2, length) + } else { + return Err(nom7::Err::Error(QuicError::InvalidPacket)); + } + } else if let Ok(length) = u16::try_from(rest.len()) { + (rest, length) + } else { + return Err(nom7::Err::Error(QuicError::InvalidPacket)); + }; + + Ok(( + rest, + QuicHeader { + flags, + ty, + version, + version_buf: version_buf.to_vec(), + dcid, + scid, + length, + }, + )) + } + } +} + +impl QuicData { + pub(crate) fn from_bytes(input: &[u8]) -> Result<QuicData, QuicError> { + let (_, frames) = all_consuming(Frame::decode_frames)(input)?; + Ok(QuicData { frames }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::quic::frames::{Frame, Stream, StreamTag}; + + #[test] + fn public_flags_test() { + let pf = PublicFlags::new(0xcb); + assert_eq!( + PublicFlags { + is_long: true, + raw: 0xcb + }, + pf + ); + } + + const TEST_DEFAULT_CID_LENGTH: usize = 8; + + #[test] + fn test_parse_gquic_unknown_version() { + // Test that we can parse unknown versions + let data = hex::decode("cbff00001d1091d0b10ac886039973885dfa07c469431409b15e86dd0990aaf906c5de620c4538398ffa58004482d2b19e732fc58818e57cb63569ce11abc00ea4fbac0725c5690a0838c27faf88663b48eca63baf0fba52af4eff7b4117384457c5cf1c1c0e52a1843c7676a4a35cf60c4c179e7186274c121110ace964771f31090f586b283bddbf82e9dd1d6a0e41fbaf243540dfb64f4543e1e87857c77cfc1ee9f883b97b89b6321ce30436119acfdbf2b31f4d0dbac0e5ea740ee59c8619d7c431320504c67f5c3aa9be5192f28ae378e0c8305fb95f01e7cb47c27f92cad7e8d55f699a41df3afe3894939f79e5f164771a6fe987602d975a06bfe8e6906b23601d08bcf2026eac25eca958a7b19ed7ba415e4d31b474264a479c53f01e1d35745ae62a9b148e39e2d7d33176f384d6ce4beb25d2177a8e0fbe5503ea034c9a645e5a8c98098bc5db4e11a351ac72b7079db1a858e11a6c6a4a1f44e1073903029cc08e82c48e6de00f5da7a546db371a4e49d4a213339ca074832cfeb4c39731f98a1683d7fb7db8a5b48c763440d6003fdfadd6a7fb23a62074064aafd585f6a887d5648ce71099d7d21e5cc1e14645f066016a7570d885bde4f239226884ee64fb8ec1218efec83d46ca104d9104bf46637ba3a3d8d6a88967859d60f46198e3a8495f2f211d717c6ca39987d2f4f971b502809932d973736dac67e5e28152c23d844d99fe7a5def822ca97aa433603423ee7fef57e6daa4579bb8f4f14a93663c54db415da5e8b9000d673d99c065c5922ba193eada475f2366b422d42dd86dd3b86fdef67d0e71cd200e3e24b77578f90e0e60717e3a1d6078b836d262028fc73efe7b3684b635f3001225acfd249fbe950dae7c539f015a0ce51c983c4d8e01d7e73e16946e681b2148d0ae4e72fb44d1048eb25572dae0a8016434b8c9e3fd3c93045b8afe67adc6cf7ce61a46819b712a8c24980e6c75bf007adf8910badfa102cd60c96238c8719b5e2b405905cfa6840176c7f71b7d9a2f480c36806f415b93b72821f0547b06f298584be093710a381fa352c34ba221cbcf1bbcd0b7d1aea354e460f6824df14d4bf4377a4503397e70f9993a55905ba298e798d9c69386eae8d0ebf6d871ff75e2d5a546bb8ee6ad9c92d88f950e2d8bc371aaad0d948e9f81c8151c51ee17c9257df4fd27cfeb9944b301a0fff1cb0a1b18836969457edd42f6ba370ecc2e5700bbb9fc15dc9f88c9bfc12c7dda64d423179c1eff8c53cca97056e09a07e29d02b4e553141b78d224cd79ae8056d923d41bc67eec00c943e3a62304487261d0877d54c40b7453c52e6c02141c2fa6601a357d53dddf39ae6e152501813e562a0613ca727ef3b0548c1f5a7e5036a8da84e166cec45de83bf217fb8f6c9a0ea20db0b16d1d2bb9e5e305e9d1f35e3065ab7188f79b9a841d1f6000ea744df1ba49116f7558feedf70677e35985d71b1c87c988d0b1ef2e436a54a77397546513c82bf307fc4b29152cafab11c8527eeda2addd00081c3b7b836a39920322a405c4e3774f20feda9998bf703fd10e93748b7834f3f3794d5b1f3f3099c608e84b025f5675b1526e8feee91ed04f4e91e37bd8e7089ec5a48edc2537bcddbd9d118d7937e2c25fa383186efd2f48fa3f5ebe7eaf544835bb330b61af1a95158c5e").unwrap(); + let (_rest, value) = + QuicHeader::from_bytes(data.as_ref(), TEST_DEFAULT_CID_LENGTH).unwrap(); + assert_eq!( + QuicHeader { + flags: PublicFlags { + is_long: true, + raw: 0xcb + }, + ty: QuicType::Initial, + version: QuicVersion(0xff00001d), + version_buf: vec![0xff, 0x00, 0x00, 0x1d], + dcid: hex::decode("91d0b10ac886039973885dfa07c46943") + .unwrap() + .to_vec(), + scid: hex::decode("09b15e86dd0990aaf906c5de620c4538398ffa58") + .unwrap() + .to_vec(), + length: 1154, + }, + value + ); + } + + #[test] + fn test_parse_gquic_q044() { + let test_data = hex::decode("ff513034345005cad2cc06c4d0e400000001afac230bc5b56fb89800171b800143484c4f09000000504144008f030000534e490098030000564552009c03000043435300ac03000050444d44b00300004943534cb40300004d494453b803000043464357bc03000053464357c003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003132372e302e302e310000000001e8816092921ae87eed8086a215829158353039803a09006400000000c0000000800000").unwrap(); + let (rest, header) = + QuicHeader::from_bytes(test_data.as_ref(), TEST_DEFAULT_CID_LENGTH).unwrap(); + + assert_eq!( + QuicHeader { + flags: PublicFlags { + is_long: true, + raw: 0xff + }, + ty: QuicType::Initial, + version: QuicVersion::Q044, + version_buf: vec![0x51, 0x30, 0x34, 0x34], + dcid: hex::decode("05cad2cc06c4d0e4").unwrap().to_vec(), + scid: Vec::new(), + length: 1042, + }, + header + ); + + let data = QuicData::from_bytes(rest).unwrap(); + assert_eq!( + QuicData { + frames: vec![Frame::Stream(Stream { + fin: false, + stream_id: vec![0x1], + offset: vec![], + tags: Some(vec![ + (StreamTag::Pad, [0x0; 911].to_vec()), + ( + StreamTag::Sni, + vec![0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31] + ), + (StreamTag::Ver, vec![0x0, 0x0, 0x0, 0x0]), + ( + StreamTag::Ccs, + vec![ + 0x1, 0xe8, 0x81, 0x60, 0x92, 0x92, 0x1a, 0xe8, 0x7e, 0xed, 0x80, + 0x86, 0xa2, 0x15, 0x82, 0x91 + ] + ), + (StreamTag::Pdmd, vec![0x58, 0x35, 0x30, 0x39]), + (StreamTag::Icsl, vec![0x80, 0x3a, 0x9, 0x0]), + (StreamTag::Mids, vec![0x64, 0x0, 0x0, 0x0]), + (StreamTag::Cfcw, vec![0x0, 0xc0, 0x0, 0x0]), + (StreamTag::Sfcw, vec![0x0, 0x80, 0x0, 0x0]), + ]) + })] + }, + data, + ); + } +} diff --git a/rust/src/quic/quic.rs b/rust/src/quic/quic.rs new file mode 100644 index 0000000..8e3ea6f --- /dev/null +++ b/rust/src/quic/quic.rs @@ -0,0 +1,502 @@ +/* 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::{quic_keys_initial, QuicKeys, AES128_KEY_LEN}, + cyu::Cyu, + frames::{Frame, QuicTlsExtension, StreamTag}, + parser::{quic_pkt_num, QuicData, QuicHeader, QuicType}, +}; +use crate::applayer::{self, *}; +use crate::core::{AppProto, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_UDP, Direction}; +use std::collections::VecDeque; +use std::ffi::CString; +use tls_parser::TlsExtensionType; + +static mut ALPROTO_QUIC: AppProto = ALPROTO_UNKNOWN; + +const DEFAULT_DCID_LEN: usize = 16; +const PKT_NUM_BUF_MAX_LEN: usize = 4; + +#[derive(FromPrimitive, Debug, AppLayerEvent)] +pub enum QuicEvent { + FailedDecrypt, + ErrorOnData, + ErrorOnHeader, +} + +#[derive(Debug)] +pub struct QuicTransaction { + tx_id: u64, + pub header: QuicHeader, + pub cyu: Vec<Cyu>, + pub sni: Option<Vec<u8>>, + pub ua: Option<Vec<u8>>, + pub extv: Vec<QuicTlsExtension>, + pub ja3: Option<String>, + pub client: bool, + tx_data: AppLayerTxData, +} + +impl QuicTransaction { + fn new( + header: QuicHeader, data: QuicData, sni: Option<Vec<u8>>, ua: Option<Vec<u8>>, + extv: Vec<QuicTlsExtension>, ja3: Option<String>, client: bool, + ) -> Self { + let direction = if client { Direction::ToServer } else { Direction::ToClient }; + let cyu = Cyu::generate(&header, &data.frames); + QuicTransaction { + tx_id: 0, + header, + cyu, + sni, + ua, + extv, + ja3, + client, + tx_data: AppLayerTxData::for_direction(direction), + } + } + + fn new_empty(client: bool, header: QuicHeader) -> Self { + let direction = if client { Direction::ToServer } else { Direction::ToClient }; + QuicTransaction { + tx_id: 0, + header, + cyu: Vec::new(), + sni: None, + ua: None, + extv: Vec::new(), + ja3: None, + client, + tx_data: AppLayerTxData::for_direction(direction), + } + } +} + +pub struct QuicState { + state_data: AppLayerStateData, + max_tx_id: u64, + keys: Option<QuicKeys>, + hello_tc: bool, + hello_ts: bool, + transactions: VecDeque<QuicTransaction>, +} + +impl Default for QuicState { + fn default() -> Self { + Self { + state_data: AppLayerStateData::new(), + max_tx_id: 0, + keys: None, + hello_tc: false, + hello_ts: false, + transactions: VecDeque::new(), + } + } +} + +impl QuicState { + fn new() -> Self { + Self::default() + } + + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + let tx = self + .transactions + .iter() + .position(|tx| tx.tx_id == tx_id + 1); + if let Some(idx) = tx { + let _ = self.transactions.remove(idx); + } + } + + fn get_tx(&mut self, tx_id: u64) -> Option<&QuicTransaction> { + self.transactions.iter().find(|&tx| tx.tx_id == tx_id + 1) + } + + fn new_tx( + &mut self, header: QuicHeader, data: QuicData, sni: Option<Vec<u8>>, ua: Option<Vec<u8>>, + extb: Vec<QuicTlsExtension>, ja3: Option<String>, client: bool, + ) { + let mut tx = QuicTransaction::new(header, data, sni, ua, extb, ja3, client); + self.max_tx_id += 1; + tx.tx_id = self.max_tx_id; + self.transactions.push_back(tx); + } + + fn tx_iterator( + &mut self, min_tx_id: u64, state: &mut u64, + ) -> Option<(&QuicTransaction, u64, bool)> { + let mut index = *state as usize; + let len = self.transactions.len(); + + while index < len { + let tx = &self.transactions[index]; + if tx.tx_id < min_tx_id + 1 { + index += 1; + continue; + } + *state = index as u64; + return Some((tx, tx.tx_id - 1, (len - index) > 1)); + } + + return None; + } + + 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) { + let mut sni: Option<Vec<u8>> = None; + let mut ua: Option<Vec<u8>> = None; + let mut ja3: Option<String> = None; + let mut extv: Vec<QuicTlsExtension> = Vec::new(); + for frame in &data.frames { + match frame { + Frame::Stream(s) => { + if let Some(tags) = &s.tags { + for (tag, value) in tags { + if tag == &StreamTag::Sni { + sni = Some(value.to_vec()); + } else if tag == &StreamTag::Uaid { + ua = Some(value.to_vec()); + } + if sni.is_some() && ua.is_some() { + break; + } + } + } + } + Frame::Crypto(c) => { + ja3 = Some(c.ja3.clone()); + for e in &c.extv { + if e.etype == TlsExtensionType::ServerName && !e.values.is_empty() { + sni = Some(e.values[0].to_vec()); + } + } + extv.extend_from_slice(&c.extv); + if to_server { + self.hello_ts = true + } else { + self.hello_tc = true + } + } + _ => {} + } + } + self.new_tx(header, data, sni, ua, extv, ja3, to_server); + } + + fn set_event_notx(&mut self, event: QuicEvent, header: QuicHeader, client: bool) { + let mut tx = QuicTransaction::new_empty(client, header); + self.max_tx_id += 1; + tx.tx_id = self.max_tx_id; + tx.tx_data.set_event(event as u8); + self.transactions.push_back(tx); + } + + fn parse(&mut self, input: &[u8], to_server: bool) -> bool { + // so as to loop over multiple quic headers in one packet + 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; + } + + // unprotect/decrypt packet + if self.keys.is_none() && header.ty == QuicType::Initial { + self.keys = quic_keys_initial(u32::from(header.version), &header.dcid); + } + // header.length was checked against rest.len() during parsing + let (mut framebuf, next_buf) = rest.split_at(header.length.into()); + let hlen = buf.len() - rest.len(); + let mut output; + if self.keys.is_some() { + 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; + } + framebuf = &output; + } + buf = next_buf; + + if header.ty != QuicType::Initial { + // only version is interesting, no frames + self.new_tx( + header, + QuicData { frames: Vec::new() }, + None, + None, + Vec::new(), + None, + to_server, + ); + continue; + } + + match QuicData::from_bytes(framebuf) { + Ok(data) => { + self.handle_frames(data, header, to_server); + } + Err(_e) => { + self.set_event_notx(QuicEvent::ErrorOnData, header, to_server); + return false; + } + } + } + Err(_e) => { + // should we make an event with an empty header ? + return false; + } + } + } + return true; + } +} + +#[no_mangle] +pub extern "C" fn rs_quic_state_new( + _orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto, +) -> *mut std::os::raw::c_void { + let state = QuicState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut _; +} + +#[no_mangle] +pub extern "C" fn rs_quic_state_free(state: *mut std::os::raw::c_void) { + // Just unbox... + std::mem::drop(unsafe { Box::from_raw(state as *mut QuicState) }); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_state_tx_free(state: *mut std::os::raw::c_void, tx_id: u64) { + let state = cast_pointer!(state, QuicState); + state.free_tx(tx_id); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_probing_parser( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + let slice = build_slice!(input, input_len as usize); + + if QuicHeader::from_bytes(slice, DEFAULT_DCID_LEN).is_ok() { + return ALPROTO_QUIC; + } else { + return ALPROTO_FAILED; + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_parse_tc( + _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, + stream_slice: StreamSlice, _data: *const std::os::raw::c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, QuicState); + let buf = stream_slice.as_slice(); + + if state.parse(buf, false) { + return AppLayerResult::ok(); + } else { + return AppLayerResult::err(); + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_parse_ts( + _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, + stream_slice: StreamSlice, _data: *const std::os::raw::c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, QuicState); + let buf = stream_slice.as_slice(); + + if state.parse(buf, true) { + return AppLayerResult::ok(); + } else { + return AppLayerResult::err(); + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_state_get_tx( + state: *mut std::os::raw::c_void, tx_id: u64, +) -> *mut std::os::raw::c_void { + let state = cast_pointer!(state, QuicState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_state_get_tx_count(state: *mut std::os::raw::c_void) -> u64 { + let state = cast_pointer!(state, QuicState); + return state.max_tx_id; +} + +#[no_mangle] +pub extern "C" fn rs_quic_state_progress_completion_status(_direction: u8) -> std::os::raw::c_int { + // This parser uses 1 to signal transaction completion status. + return 1; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_tx_get_alstate_progress( + tx: *mut std::os::raw::c_void, _direction: u8, +) -> std::os::raw::c_int { + let _tx = cast_pointer!(tx, QuicTransaction); + return 1; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_state_get_tx_iterator( + _ipproto: u8, _alproto: AppProto, state: *mut std::os::raw::c_void, min_tx_id: u64, + _max_tx_id: u64, istate: &mut u64, +) -> applayer::AppLayerGetTxIterTuple { + let state = cast_pointer!(state, QuicState); + match state.tx_iterator(min_tx_id, istate) { + Some((tx, out_tx_id, has_next)) => { + let c_tx = tx as *const _ as *mut _; + let ires = applayer::AppLayerGetTxIterTuple::with_values(c_tx, out_tx_id, has_next); + return ires; + } + None => { + return applayer::AppLayerGetTxIterTuple::not_found(); + } + } +} + +export_tx_data_get!(rs_quic_get_tx_data, QuicTransaction); +export_state_data_get!(rs_quic_get_state_data, QuicState); + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"quic\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_quic_register_parser() { + let default_port = CString::new("[443,80]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_UDP, + probe_ts: Some(rs_quic_probing_parser), + probe_tc: Some(rs_quic_probing_parser), + min_depth: 0, + max_depth: 16, + state_new: rs_quic_state_new, + state_free: rs_quic_state_free, + tx_free: rs_quic_state_tx_free, + parse_ts: rs_quic_parse_ts, + parse_tc: rs_quic_parse_tc, + get_tx_count: rs_quic_state_get_tx_count, + get_tx: rs_quic_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_quic_tx_get_alstate_progress, + get_eventinfo: Some(QuicEvent::get_event_info), + get_eventinfo_byid: Some(QuicEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some(rs_quic_state_get_tx_iterator), + get_tx_data: rs_quic_get_tx_data, + get_state_data: rs_quic_get_state_data, + apply_tx_config: None, + flags: 0, + truncate: None, + get_frame_id_by_name: None, + get_frame_name_by_id: None, + }; + + let ip_proto_str = CString::new("udp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_QUIC = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + SCLogDebug!("Rust quic parser registered."); + } else { + SCLogDebug!("Protocol detector and parser disabled for quic."); + } +} |