diff --git a/Cargo.toml b/Cargo.toml index 85329699..c7ff21bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "examples/log_http", "examples/log_dns", "examples/log_tls", + "examples/log_quic", "examples/pcap_dump", "examples/spin", "examples/video", diff --git a/core/src/filter/ast.rs b/core/src/filter/ast.rs index 0f2dafcd..66c88698 100644 --- a/core/src/filter/ast.rs +++ b/core/src/filter/ast.rs @@ -24,6 +24,7 @@ lazy_static! { let http = g.add_node(protocol!("http")); let ssh = g.add_node(protocol!("ssh")); let dns = g.add_node(protocol!("dns")); + let quic = g.add_node(protocol!("quic")); // define valid outer layers for each protocol header g.extend_with_edges([ (ipv4, ethernet), @@ -34,6 +35,7 @@ lazy_static! { (http, tcp), (ssh, tcp), (dns, udp), (dns, tcp), + (quic, udp), //TODO: tls over quic ]); g }; @@ -250,6 +252,8 @@ mod tests { assert!(!has_path(&protocol!("ipv4"), &protocol!("ipv4"))); assert!(!has_path(&protocol!("http"), &protocol!("udp"))); assert!(!has_path(&protocol!("tls"), &protocol!("ssh"))); + assert!(has_path(&protocol!("quic"), &protocol!("udp"))); + assert!(!has_path(&protocol!("quic"), &protocol!("dns"))); } #[test] diff --git a/core/src/protocols/stream/mod.rs b/core/src/protocols/stream/mod.rs index 226ea78e..676c7dea 100644 --- a/core/src/protocols/stream/mod.rs +++ b/core/src/protocols/stream/mod.rs @@ -6,10 +6,12 @@ pub mod dns; pub mod http; +pub mod quic; pub mod tls; use self::dns::{parser::DnsParser, Dns}; use self::http::{parser::HttpParser, Http}; +use self::quic::{parser::QuicParser, QuicPacket}; use self::tls::{parser::TlsParser, Tls}; use crate::conntrack::conn::conn_info::ConnState; use crate::conntrack::conn_id::FiveTuple; @@ -193,6 +195,7 @@ pub enum SessionData { Tls(Box), Dns(Box), Http(Box), + Quic(Box), Null, } @@ -235,6 +238,7 @@ pub enum ConnParser { Tls(TlsParser), Dns(DnsParser), Http(HttpParser), + Quic(QuicParser), Unknown, } @@ -245,6 +249,7 @@ impl ConnParser { ConnParser::Tls(_) => ConnParser::Tls(TlsParser::default()), ConnParser::Dns(_) => ConnParser::Dns(DnsParser::default()), ConnParser::Http(_) => ConnParser::Http(HttpParser::default()), + ConnParser::Quic(_) => ConnParser::Quic(QuicParser::default()), ConnParser::Unknown => ConnParser::Unknown, } } @@ -255,6 +260,7 @@ impl ConnParser { ConnParser::Tls(parser) => parser.parse(pdu), ConnParser::Dns(parser) => parser.parse(pdu), ConnParser::Http(parser) => parser.parse(pdu), + ConnParser::Quic(parser) => parser.parse(pdu), ConnParser::Unknown => ParseResult::Skipped, } } @@ -265,6 +271,7 @@ impl ConnParser { ConnParser::Tls(parser) => parser.probe(pdu), ConnParser::Dns(parser) => parser.probe(pdu), ConnParser::Http(parser) => parser.probe(pdu), + ConnParser::Quic(parser) => parser.probe(pdu), ConnParser::Unknown => ProbeResult::Error, } } @@ -276,6 +283,7 @@ impl ConnParser { ConnParser::Tls(parser) => parser.remove_session(session_id), ConnParser::Dns(parser) => parser.remove_session(session_id), ConnParser::Http(parser) => parser.remove_session(session_id), + ConnParser::Quic(parser) => parser.remove_session(session_id), ConnParser::Unknown => None, } } @@ -286,6 +294,7 @@ impl ConnParser { ConnParser::Tls(parser) => parser.drain_sessions(), ConnParser::Dns(parser) => parser.drain_sessions(), ConnParser::Http(parser) => parser.drain_sessions(), + ConnParser::Quic(parser) => parser.drain_sessions(), ConnParser::Unknown => vec![], } } @@ -296,6 +305,7 @@ impl ConnParser { ConnParser::Tls(parser) => parser.session_match_state(), ConnParser::Dns(parser) => parser.session_match_state(), ConnParser::Http(parser) => parser.session_match_state(), + ConnParser::Quic(parser) => parser.session_match_state(), ConnParser::Unknown => ConnState::Remove, } } @@ -306,6 +316,7 @@ impl ConnParser { ConnParser::Tls(parser) => parser.session_nomatch_state(), ConnParser::Dns(parser) => parser.session_nomatch_state(), ConnParser::Http(parser) => parser.session_nomatch_state(), + ConnParser::Quic(parser) => parser.session_nomatch_state(), ConnParser::Unknown => ConnState::Remove, } } diff --git a/core/src/protocols/stream/quic/header.rs b/core/src/protocols/stream/quic/header.rs new file mode 100644 index 00000000..76fd011b --- /dev/null +++ b/core/src/protocols/stream/quic/header.rs @@ -0,0 +1,24 @@ +//! Quic header types + +use serde::Serialize; + +/// Quic Long Header +#[derive(Debug, Serialize, Clone)] +pub struct QuicLongHeader { + pub packet_type: u8, + pub type_specific: u8, + pub version: u32, + pub dcid_len: u8, // length of dcid in bytes + pub dcid: String, // hex string + pub scid_len: u8, // length of scid in bytes + pub scid: String, // hex string +} + +/// Quic Short Header +#[derive(Debug, Serialize, Clone)] +pub struct QuicShortHeader { + pub dcid: Option, // optional. If not pre-existing cid then none. + + #[serde(skip)] + pub dcid_bytes: Vec, +} diff --git a/core/src/protocols/stream/quic/mod.rs b/core/src/protocols/stream/quic/mod.rs new file mode 100644 index 00000000..27c5e9ec --- /dev/null +++ b/core/src/protocols/stream/quic/mod.rs @@ -0,0 +1,108 @@ +//! QUIC protocol parser. +//! +//! ## Remarks +//! [QUIC-INVARIANTS] https://datatracker.ietf.org/doc/rfc8999/ +//! [QUIC-RFC9000] https://datatracker.ietf.org/doc/rfc9000/ (Quic V1) +//! Retina currently only parses Quic Long and Short Headers and does not attempt to parse TLS or HTTP/3 out of +//! Quic packets. The Quic protocol parser makes several assumptions about the way that quic +//! packets will behave: +//! - Assume that the Quic version is one as listed in the QuicVersion Enum in the quic/parser.rs file +//! - Assume that the dcid of a short header is a maximum of 20 bytes. +//! - Assume that the packet will not try to grease the fixed bit. +//! [QUIC-GREASE](https://www.rfc-editor.org/rfc/rfc9287.html) +//! +//! Additionally, there are a couple decisions made in the design of the quic parser: +//! - The parser will not parse a short header dcid if it is not a part of a pre-identified connection +//! - The payload bytes count is a lazy counter which does not try to exclude tokens for encryption, +//! which is a process that happens in wireshark. +/* +TODO: support parsing the tls out of the initial quic packet setup +TODO support dns over quic +TODO: support HTTP/3 +*/ +pub(crate) mod parser; + +pub use self::header::{QuicLongHeader, QuicShortHeader}; +use serde::Serialize; +pub(crate) mod header; + +/// Parsed Quic Packet contents +#[derive(Debug, Serialize, Clone)] +pub struct QuicPacket { + /// Quic Short header + pub short_header: Option, + + /// Quic Long header + pub long_header: Option, + + /// The number of bytes contained in the estimated payload + pub payload_bytes_count: u16, +} + +impl QuicPacket { + /// Returns the header type of the Quic packet (ie. "long" or "short") + pub fn header_type(&self) -> &str { + match &self.long_header { + Some(_) => "long", + None => match &self.short_header { + Some(_) => "short", + None => "", + }, + } + } + + /// Returns the packet type of the Quic packet + pub fn packet_type(&self) -> u8 { + match &self.long_header { + Some(long_header) => long_header.packet_type, + None => 0, + } + } + + /// Returns the version of the Quic packet + pub fn version(&self) -> u32 { + match &self.long_header { + Some(long_header) => long_header.version, + None => 0, + } + } + + /// Returns the destination connection ID of the Quic packet or an empty string if it does not exist + pub fn dcid(&self) -> &str { + match &self.long_header { + Some(long_header) => { + if long_header.dcid_len > 0 { + &long_header.dcid + } else { + "" + } + } + None => { + if let Some(short_header) = &self.short_header { + short_header.dcid.as_deref().unwrap_or("") + } else { + "" + } + } + } + } + + /// Returns the source connection ID of the Quic packet or an empty string if it does not exist + pub fn scid(&self) -> &str { + match &self.long_header { + Some(long_header) => { + if long_header.scid_len > 0 { + &long_header.scid + } else { + "" + } + } + None => "", + } + } + + /// Returns the number of bytes in the payload of the Quic packet + pub fn payload_bytes_count(&self) -> u16 { + self.payload_bytes_count + } +} diff --git a/core/src/protocols/stream/quic/parser.rs b/core/src/protocols/stream/quic/parser.rs new file mode 100644 index 00000000..063c7805 --- /dev/null +++ b/core/src/protocols/stream/quic/parser.rs @@ -0,0 +1,224 @@ +//! Quic Header parser +//! Custom Quic Parser with many design choices borrowed from +//! [Wireshark Quic Disector](https://gitlab.com/wireshark/wireshark/-/blob/master/epan/dissectors/packet-quic.c) +//! +use crate::protocols::stream::quic::header::{QuicLongHeader, QuicShortHeader}; +use crate::protocols::stream::quic::QuicPacket; +use crate::protocols::stream::{ + ConnParsable, ConnState, L4Pdu, ParseResult, ProbeResult, Session, SessionData, +}; +use std::collections::HashMap; + +#[derive(Default, Debug)] +pub struct QuicParser { + /// Maps session ID to Quic transaction + sessions: HashMap, + /// Total sessions ever seen (Running session ID) + cnt: usize, +} + +impl ConnParsable for QuicParser { + fn parse(&mut self, pdu: &L4Pdu) -> ParseResult { + let offset = pdu.offset(); + let length = pdu.length(); + if length == 0 { + return ParseResult::Skipped; + } + + if let Ok(data) = (pdu.mbuf_ref()).get_data_slice(offset, length) { + self.process(data) + } else { + log::warn!("Malformed packet on parse"); + ParseResult::Skipped + } + } + + fn probe(&self, pdu: &L4Pdu) -> ProbeResult { + if pdu.length() < 5 { + return ProbeResult::Unsure; + } + + let offset = pdu.offset(); + let length = pdu.length(); + + if let Ok(data) = (pdu.mbuf).get_data_slice(offset, length) { + // Check if Fixed Bit is set + if (data[0] & 0x40) == 0 { + return ProbeResult::NotForUs; + } + + if (data[0] & 0x80) != 0 { + // Potential Long Header + if data.len() < 6 { + return ProbeResult::Unsure; + } + + // Check if version is known + let version = ((data[1] as u32) << 24) + | ((data[2] as u32) << 16) + | ((data[3] as u32) << 8) + | (data[4] as u32); + match QuicVersion::from_u32(version) { + QuicVersion::Unknown => ProbeResult::NotForUs, + _ => ProbeResult::Certain, + } + } else { + ProbeResult::Unsure + } + } else { + log::warn!("Malformed packet"); + ProbeResult::Error + } + } + + fn remove_session(&mut self, session_id: usize) -> Option { + self.sessions.remove(&session_id).map(|quic| Session { + data: SessionData::Quic(Box::new(quic)), + id: session_id, + }) + } + + fn drain_sessions(&mut self) -> Vec { + self.sessions + .drain() + .map(|(session_id, quic)| Session { + data: SessionData::Quic(Box::new(quic)), + id: session_id, + }) + .collect() + } + + fn session_match_state(&self) -> ConnState { + ConnState::Parsing + } + fn session_nomatch_state(&self) -> ConnState { + ConnState::Parsing + } +} + +/// Supported Quic Versions +#[derive(Debug, PartialEq, Eq, Hash)] +enum QuicVersion { + ReservedNegotiation = 0x00000000, + Rfc9000 = 0x00000001, // Quic V1 + Rfc9369 = 0x6b3343cf, // Quic V2 + Unknown, +} + +impl QuicVersion { + fn from_u32(version: u32) -> Self { + match version { + 0x00000000 => QuicVersion::ReservedNegotiation, + 0x00000001 => QuicVersion::Rfc9000, + 0x6b3343cf => QuicVersion::Rfc9369, + _ => QuicVersion::Unknown, + } + } +} + +/// Errors Thrown by Quic Parser. These are handled by retina and used to skip packets. +#[derive(Debug)] +pub enum QuicError { + FixedBitNotSet, + PacketTooShort, + UnknownVersion, + ShortHeader, +} + +impl QuicPacket { + /// Processes the connection ID bytes array to a hex string + pub fn vec_u8_to_hex_string(vec: &[u8]) -> String { + vec.iter() + .map(|&byte| format!("{:02x}", byte)) + .collect::>() + .join("") + } + + /// Parses Quic packet from bytes + pub fn parse_from(data: &[u8]) -> Result { + if data.len() <= 2 { + return Err(QuicError::PacketTooShort); + } + if (data[0] & 0x40) == 0 { + return Err(QuicError::FixedBitNotSet); + } + if (data[0] & 0x80) != 0 { + // Long Header + if data.len() < 7 { + return Err(QuicError::PacketTooShort); + } + let version = ((data[1] as u32) << 24) + | ((data[2] as u32) << 16) + | ((data[3] as u32) << 8) + | (data[4] as u32); + if QuicVersion::from_u32(version) == QuicVersion::Unknown { + return Err(QuicError::UnknownVersion); + } + + let packet_type = (data[0] & 0x30) >> 4; + let type_specific = data[0] & 0x0F; + + let dcid_len = data[5]; + let dcid_start = 6; + // There's a +2 in this size check because we need enough space to check the SCID length + if data.len() < (dcid_start + dcid_len as usize + 2) { + return Err(QuicError::PacketTooShort); + } + let dcid_bytes = &data[dcid_start..dcid_start + dcid_len as usize]; + let dcid = QuicPacket::vec_u8_to_hex_string(dcid_bytes); + let scid_len = data[dcid_start + dcid_len as usize]; + let scid_start = dcid_start + dcid_len as usize + 1; + if data.len() < (scid_start + scid_len as usize + 1) { + return Err(QuicError::PacketTooShort); + } + let scid_bytes = &data[scid_start..scid_start + scid_len as usize]; + let scid = QuicPacket::vec_u8_to_hex_string(scid_bytes); + + // Counts all bytes remaining + let payload_bytes_count = data.len() - scid_start - scid_len as usize; + Ok(QuicPacket { + payload_bytes_count: payload_bytes_count as u16, + short_header: None, + long_header: Some(QuicLongHeader { + packet_type, + type_specific, + version, + dcid_len, + dcid, + scid_len, + scid, + }), + }) + } else { + // Short Header + let mut max_dcid_len = 20; + if data.len() < 1 + max_dcid_len { + max_dcid_len = data.len() - 1; + } + let dcid_bytes = data[1..1 + max_dcid_len].to_vec(); + // Counts all bytes remaining + let payload_bytes_count = data.len() - 1 - max_dcid_len; + Ok(QuicPacket { + short_header: Some(QuicShortHeader { + dcid: None, + dcid_bytes, + }), + long_header: None, + payload_bytes_count: payload_bytes_count as u16, + }) + } + } +} + +impl QuicParser { + fn process(&mut self, data: &[u8]) -> ParseResult { + if let Ok(quic) = QuicPacket::parse_from(data) { + let session_id = self.cnt; + self.sessions.insert(session_id, quic); + self.cnt += 1; + ParseResult::Done(session_id) + } else { + ParseResult::Skipped + } + } +} diff --git a/core/src/subscription/mod.rs b/core/src/subscription/mod.rs index 30223d22..e31c9f7e 100644 --- a/core/src/subscription/mod.rs +++ b/core/src/subscription/mod.rs @@ -10,6 +10,7 @@ pub mod connection_frame; pub mod dns_transaction; pub mod frame; pub mod http_transaction; +pub mod quic_stream; pub mod tls_handshake; pub mod zc_frame; @@ -19,6 +20,7 @@ pub use self::connection_frame::ConnectionFrame; pub use self::dns_transaction::DnsTransaction; pub use self::frame::Frame; pub use self::http_transaction::HttpTransaction; +pub use self::quic_stream::QuicStream; pub use self::tls_handshake::TlsHandshake; pub use self::zc_frame::ZcFrame; diff --git a/core/src/subscription/quic_stream.rs b/core/src/subscription/quic_stream.rs new file mode 100644 index 00000000..84762e8f --- /dev/null +++ b/core/src/subscription/quic_stream.rs @@ -0,0 +1,156 @@ +//! QUIC streams. +//! +//! This is a session-level subscription that delivers parsed QUIC stream records and associated +//! connection metadata. +//! +//! ## Example +//! Prints QUIC connections that use long headers: +//! ``` +//! #[filter("quic.header_type = 'long'")] +//! fn main() { +//! let config = default_config(); +//! let cb = |quic: QuicStream| { +//! println!("{}", quic.data); +//! }; +//! let mut runtime = Runtime::new(config, filter, cb).unwrap(); +//! runtime.run(); +//! } + +use crate::conntrack::conn_id::FiveTuple; +use crate::conntrack::pdu::{L4Context, L4Pdu}; +use crate::conntrack::ConnTracker; +use crate::filter::FilterResult; +use crate::memory::mbuf::Mbuf; +use crate::protocols::stream::quic::{parser::QuicParser, QuicPacket}; +use crate::protocols::stream::{ConnParser, Session, SessionData}; +use crate::subscription::{Level, Subscribable, Subscription, Trackable}; +use std::collections::HashSet; + +use serde::Serialize; + +use std::net::SocketAddr; + +/// A parsed QUIC stream and connection metadata. +#[derive(Debug, Serialize)] +pub struct QuicStream { + pub five_tuple: FiveTuple, + pub data: QuicPacket, +} + +impl QuicStream { + /// Returns the QUIC client's socket address. + #[inline] + pub fn client(&self) -> SocketAddr { + self.five_tuple.orig + } + + /// Returns the QUIC server's socket address. + #[inline] + pub fn server(&self) -> SocketAddr { + self.five_tuple.resp + } +} + +impl Subscribable for QuicStream { + type Tracked = TrackedQuic; + + fn level() -> Level { + Level::Session + } + + fn parsers() -> Vec { + vec![ConnParser::Quic(QuicParser::default())] + } + + fn process_packet( + mbuf: Mbuf, + subscription: &Subscription, + conn_tracker: &mut ConnTracker, + ) { + match subscription.filter_packet(&mbuf) { + FilterResult::MatchTerminal(idx) | FilterResult::MatchNonTerminal(idx) => { + if let Ok(ctxt) = L4Context::new(&mbuf, idx) { + conn_tracker.process(mbuf, ctxt, subscription); + } + } + FilterResult::NoMatch => drop(mbuf), + } + } +} + +/// Represents a QUIC connection during the connection lifetime. +/// +/// ## Remarks +/// Retina uses an internal parser to track and filter application-layer protocols, and transfers +/// session ownership to the subscription to invoke the callback on a filter match. This is an +/// optimization to avoid double-parsing: once for the filter and once for the subscription data. +/// This is why most `Trackable` trait methods for this type are unimplemented. +/// +/// ## Note +/// Internal connection state is an associated type of a `pub` trait, and therefore must also be +/// public. Documentation is hidden by default to avoid confusing users. +#[doc(hidden)] +pub struct TrackedQuic { + five_tuple: FiveTuple, + connection_id: HashSet, +} + +impl TrackedQuic { + fn get_connection_id(&self, dcid_bytes: &[u8]) -> Option { + let dcid_hex = QuicPacket::vec_u8_to_hex_string(dcid_bytes); + for dcid in &self.connection_id { + if dcid_hex.starts_with(dcid) { + return Some(dcid.clone()); + } + } + None + } +} + +impl Trackable for TrackedQuic { + type Subscribed = QuicStream; + + fn new(five_tuple: FiveTuple) -> Self { + TrackedQuic { + five_tuple, + connection_id: HashSet::new(), + } + } + + fn pre_match(&mut self, _pdu: L4Pdu, _session_id: Option) {} + + fn on_match(&mut self, session: Session, subscription: &Subscription) { + if let SessionData::Quic(quic) = session.data { + let mut quic_clone = (*quic).clone(); + + if let Some(long_header) = &quic_clone.long_header { + if long_header.dcid_len > 0 { + self.connection_id.insert(long_header.dcid.clone()); + } + if long_header.scid_len > 0 { + self.connection_id.insert(long_header.scid.clone()); + } + } else { + if let Some(ref mut short_header_value) = quic_clone.short_header { + short_header_value.dcid = + self.get_connection_id(&short_header_value.dcid_bytes); + } + return subscription.invoke(QuicStream { + five_tuple: self.five_tuple, + data: quic_clone, + }); + } + + subscription.invoke(QuicStream { + five_tuple: self.five_tuple, + data: quic_clone, + }); + } + } + + fn post_match(&mut self, _pdu: L4Pdu, _subscription: &Subscription) {} + + fn on_terminate(&mut self, _subscription: &Subscription) { + self.connection_id.clear(); + } +} diff --git a/examples/log_quic/Cargo.toml b/examples/log_quic/Cargo.toml new file mode 100644 index 00000000..c168caeb --- /dev/null +++ b/examples/log_quic/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "log_quic" +version = "0.1.0" +edition = "2021" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.70" +clap = { version = "3.2.23", features = ["derive"] } +env_logger = "0.8.4" +jsonl = "4.0.1" +lazy_static = "1.4.0" +log = { version = "0.4", features = ["release_max_level_info"] } +regex = "1.7.3" +retina-core = { path = "../../core" } +retina-filtergen = { path = "../../filtergen" } +serde_json = "1.0.96" diff --git a/examples/log_quic/README.md b/examples/log_quic/README.md new file mode 100644 index 00000000..54740d74 --- /dev/null +++ b/examples/log_quic/README.md @@ -0,0 +1,9 @@ +# Quic Logger + +Demonstrates logging Quic transactions to a file. + +### Build and run +``` +cargo build --release --bin log_quic +sudo env LD_LIBRARY_PATH=$LD_LIBRARY_PATH RUST_LOG=error ./target/release/log_quic -c +``` \ No newline at end of file diff --git a/examples/log_quic/src/main.rs b/examples/log_quic/src/main.rs new file mode 100644 index 00000000..c1bca47b --- /dev/null +++ b/examples/log_quic/src/main.rs @@ -0,0 +1,55 @@ +use retina_core::config::load_config; +use retina_core::subscription::QuicStream; +use retina_core::Runtime; +use retina_filtergen::filter; + +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; + +use anyhow::Result; +use clap::Parser; + +// Define command-line arguments. +#[derive(Parser, Debug)] +struct Args { + #[clap(short, long, parse(from_os_str), value_name = "FILE")] + config: PathBuf, + #[clap( + short, + long, + parse(from_os_str), + value_name = "FILE", + default_value = "quic.jsonl" + )] + outfile: PathBuf, +} + +#[filter("quic")] +fn main() -> Result<()> { + env_logger::init(); + let args = Args::parse(); + let config = load_config(&args.config); + + // Use `BufWriter` to improve the speed of repeated write calls to the same file. + let file = Mutex::new(BufWriter::new(File::create(&args.outfile)?)); + let cnt = AtomicUsize::new(0); + + let callback = |quic: QuicStream| { + if let Ok(serialized) = serde_json::to_string(&quic) { + let mut wtr = file.lock().unwrap(); + wtr.write_all(serialized.as_bytes()).unwrap(); + wtr.write_all(b"\n").unwrap(); + cnt.fetch_add(1, Ordering::Relaxed); + } + }; + let mut runtime = Runtime::new(config, filter, callback)?; + runtime.run(); + + let mut wtr = file.lock().unwrap(); + wtr.flush()?; + println!("Done. Logged {:?} Quic stream to {:?}", cnt, &args.outfile); + Ok(()) +} diff --git a/traces/quic.pcap b/traces/quic.pcap new file mode 100644 index 00000000..41fc73b6 Binary files /dev/null and b/traces/quic.pcap differ