summaryrefslogtreecommitdiffstats
path: root/rust/src/quic
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--rust/src/quic/crypto.rs200
-rw-r--r--rust/src/quic/cyu.rs195
-rw-r--r--rust/src/quic/detect.rs121
-rw-r--r--rust/src/quic/error.rs65
-rw-r--r--rust/src/quic/frames.rs521
-rw-r--r--rust/src/quic/logger.rs157
-rw-r--r--rust/src/quic/mod.rs27
-rw-r--r--rust/src/quic/parser.rs502
-rw-r--r--rust/src/quic/quic.rs502
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.");
+ }
+}