stackforge_core/layer/quic/headers.rs
1//! QUIC packet header structures (RFC 9000 Section 17).
2//!
3//! QUIC supports two header forms:
4//!
5//! * **Long Header** (bit 7 = 1): used for Initial, 0-RTT, Handshake, Retry, and Version
6//! Negotiation packets.
7//! * **Short Header** (bit 7 = 0): used for 1-RTT (data) packets after the handshake.
8//!
9//! Long Header format (RFC 9000 Section 17.2):
10//! ```text
11//! Byte 0: | Header Form (1) | Fixed Bit (1) | Long Packet Type (2) | Type-Specific (4) |
12//! Bytes 1-4: Version (big-endian u32)
13//! Byte 5: Destination Connection ID Length
14//! N bytes: Destination Connection ID
15//! Byte: Source Connection ID Length
16//! M bytes: Source Connection ID
17//! (type-specific fields follow)
18//! ```
19//!
20//! Version Negotiation packets have bit 6 = 0 (Fixed Bit cleared).
21//! All other long-header packets have bit 6 = 1.
22
23/// QUIC packet type, derived from the first byte (and version field for Version Negotiation).
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum QuicPacketType {
26 /// Long header, type 0x00 — Initial packet (RFC 9000 Section 17.2.2).
27 Initial,
28 /// Long header, type 0x01 — 0-RTT packet (RFC 9000 Section 17.2.3).
29 ZeroRtt,
30 /// Long header, type 0x02 — Handshake packet (RFC 9000 Section 17.2.4).
31 Handshake,
32 /// Long header, type 0x03 — Retry packet (RFC 9000 Section 17.2.5).
33 Retry,
34 /// Short header — 1-RTT data packet (RFC 9000 Section 17.3).
35 OneRtt,
36 /// Long header with Fixed Bit = 0 — Version Negotiation (RFC 9000 Section 17.2.1).
37 VersionNeg,
38}
39
40impl QuicPacketType {
41 /// Human-readable name for this packet type.
42 pub fn name(&self) -> &'static str {
43 match self {
44 Self::Initial => "Initial",
45 Self::ZeroRtt => "0-RTT",
46 Self::Handshake => "Handshake",
47 Self::Retry => "Retry",
48 Self::OneRtt => "1-RTT",
49 Self::VersionNeg => "Version Negotiation",
50 }
51 }
52}
53
54/// Determine the QUIC packet type from the first byte.
55///
56/// `version` should be the 4-byte version field (bytes 1-4 of a long-header
57/// packet). For short-header detection the caller can pass 0.
58///
59/// Returns `None` if the buffer has fewer than 1 byte.
60pub fn packet_type(first_byte: u8, _version: u32) -> QuicPacketType {
61 let is_long = first_byte & 0x80 != 0;
62 if !is_long {
63 return QuicPacketType::OneRtt;
64 }
65
66 // Long header
67 let fixed_bit = first_byte & 0x40 != 0;
68 if !fixed_bit {
69 // Version Negotiation packet has Fixed Bit cleared.
70 return QuicPacketType::VersionNeg;
71 }
72
73 let long_type = (first_byte & 0x30) >> 4;
74 match long_type {
75 0 => QuicPacketType::Initial,
76 1 => QuicPacketType::ZeroRtt,
77 2 => QuicPacketType::Handshake,
78 3 => QuicPacketType::Retry,
79 _ => unreachable!("long_type is always 0..=3"),
80 }
81}
82
83/// Parsed QUIC Long Header.
84#[derive(Debug, Clone)]
85pub struct QuicLongHeader {
86 /// The packet type.
87 pub packet_type: QuicPacketType,
88 /// QUIC version (big-endian u32 from bytes 1-4).
89 pub version: u32,
90 /// Destination Connection ID bytes.
91 pub dst_conn_id: Vec<u8>,
92 /// Source Connection ID bytes.
93 pub src_conn_id: Vec<u8>,
94 /// Total byte length of the long header (up to and including `src_conn_id`).
95 /// Type-specific fields (token, length, packet number) are NOT included.
96 pub header_len: usize,
97}
98
99impl QuicLongHeader {
100 /// Minimum bytes required for a valid long header (before connection IDs).
101 ///
102 /// 1 (first byte) + 4 (version) + 1 (DCIL) = 6 bytes minimum.
103 const MIN_LEN: usize = 6;
104
105 /// Parse a QUIC Long Header from `buf`.
106 ///
107 /// Returns `None` if:
108 /// - the buffer is too short,
109 /// - bit 7 of the first byte is 0 (short header), or
110 /// - the connection ID lengths extend beyond the buffer.
111 pub fn parse(buf: &[u8]) -> Option<Self> {
112 if buf.len() < Self::MIN_LEN {
113 return None;
114 }
115
116 let first_byte = buf[0];
117 if first_byte & 0x80 == 0 {
118 // Short header, not a long header.
119 return None;
120 }
121
122 let version = u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]);
123 let pkt_type = packet_type(first_byte, version);
124
125 // Byte 5: Destination Connection ID length
126 let dcil = buf[5] as usize;
127 let mut pos = 6;
128
129 if pos + dcil > buf.len() {
130 return None;
131 }
132 let dst_conn_id = buf[pos..pos + dcil].to_vec();
133 pos += dcil;
134
135 // Next byte: Source Connection ID length
136 if pos >= buf.len() {
137 return None;
138 }
139 let scil = buf[pos] as usize;
140 pos += 1;
141
142 if pos + scil > buf.len() {
143 return None;
144 }
145 let src_conn_id = buf[pos..pos + scil].to_vec();
146 pos += scil;
147
148 Some(Self {
149 packet_type: pkt_type,
150 version,
151 dst_conn_id,
152 src_conn_id,
153 header_len: pos,
154 })
155 }
156}
157
158/// Parsed QUIC Short Header (1-RTT).
159///
160/// In practice the destination connection ID length is negotiated during the
161/// handshake and is not encoded in the short header. For pcap-level parsing
162/// where this length is unknown, we default to a 0-byte connection ID.
163#[derive(Debug, Clone)]
164pub struct QuicShortHeader {
165 /// Destination Connection ID bytes (may be empty if length is unknown).
166 pub dst_conn_id: Vec<u8>,
167 /// Total byte length consumed by the short header (first byte + connection ID).
168 pub header_len: usize,
169}
170
171impl QuicShortHeader {
172 /// Parse a QUIC Short Header from `buf`.
173 ///
174 /// `conn_id_len` is the negotiated destination connection ID length. When
175 /// parsing captured packets without prior handshake context, pass `0`.
176 ///
177 /// Returns `None` if the buffer is too short or bit 7 of the first byte is
178 /// set (long header).
179 pub fn parse(buf: &[u8]) -> Option<Self> {
180 Self::parse_with_conn_id_len(buf, 0)
181 }
182
183 /// Parse a QUIC Short Header, specifying the known connection ID length.
184 pub fn parse_with_conn_id_len(buf: &[u8], conn_id_len: usize) -> Option<Self> {
185 if buf.is_empty() {
186 return None;
187 }
188
189 let first_byte = buf[0];
190 if first_byte & 0x80 != 0 {
191 // Long header, not a short header.
192 return None;
193 }
194
195 let header_len = 1 + conn_id_len;
196 if buf.len() < header_len {
197 return None;
198 }
199
200 let dst_conn_id = buf[1..1 + conn_id_len].to_vec();
201
202 Some(Self {
203 dst_conn_id,
204 header_len,
205 })
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_packet_type_short_header() {
215 // bit 7 = 0 => short header (1-RTT)
216 assert_eq!(packet_type(0x40, 0), QuicPacketType::OneRtt);
217 assert_eq!(packet_type(0x00, 0), QuicPacketType::OneRtt);
218 }
219
220 #[test]
221 fn test_packet_type_version_neg() {
222 // bit 7 = 1, bit 6 = 0 => Version Negotiation
223 assert_eq!(packet_type(0x80, 0), QuicPacketType::VersionNeg);
224 }
225
226 #[test]
227 fn test_packet_type_initial() {
228 // bit 7 = 1, bit 6 = 1, bits 5-4 = 00 => Initial
229 assert_eq!(packet_type(0xC0, 1), QuicPacketType::Initial);
230 }
231
232 #[test]
233 fn test_packet_type_handshake() {
234 // bit 7 = 1, bit 6 = 1, bits 5-4 = 10 => Handshake
235 assert_eq!(packet_type(0xE0, 1), QuicPacketType::Handshake);
236 }
237
238 #[test]
239 fn test_packet_type_retry() {
240 // bit 7 = 1, bit 6 = 1, bits 5-4 = 11 => Retry
241 assert_eq!(packet_type(0xF0, 1), QuicPacketType::Retry);
242 }
243
244 #[test]
245 fn test_long_header_parse_initial() {
246 // Initial packet: 0xC0 | pkt_num_len=0 => 0xC0
247 // Version = 1 (0x00000001)
248 // DCID len = 8, SCID len = 0
249 let mut buf = vec![
250 0xC0u8, // first byte: long, fixed, Initial
251 0x00, 0x00, 0x00, 0x01, // version = 1
252 0x08, // DCIL = 8
253 ];
254 buf.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); // DCID
255 buf.push(0x00); // SCIL = 0
256
257 let hdr = QuicLongHeader::parse(&buf).unwrap();
258 assert_eq!(hdr.packet_type, QuicPacketType::Initial);
259 assert_eq!(hdr.version, 1);
260 assert_eq!(hdr.dst_conn_id, vec![1, 2, 3, 4, 5, 6, 7, 8]);
261 assert_eq!(hdr.src_conn_id, vec![]);
262 assert_eq!(hdr.header_len, 6 + 8 + 1); // first+version+dcil + dcid + scil
263 }
264
265 #[test]
266 fn test_long_header_parse_too_short() {
267 let buf = [0xC0u8, 0x00, 0x00]; // way too short
268 assert!(QuicLongHeader::parse(&buf).is_none());
269 }
270
271 #[test]
272 fn test_long_header_parse_short_header_rejected() {
273 // bit 7 = 0 => short header, should return None for long header parser
274 let buf = [0x40u8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00];
275 assert!(QuicLongHeader::parse(&buf).is_none());
276 }
277
278 #[test]
279 fn test_short_header_parse() {
280 let buf = [0x40u8, 0xAA, 0xBB]; // short header, no conn id
281 let hdr = QuicShortHeader::parse(&buf).unwrap();
282 assert_eq!(hdr.dst_conn_id, vec![]);
283 assert_eq!(hdr.header_len, 1);
284 }
285
286 #[test]
287 fn test_short_header_parse_with_conn_id() {
288 let buf = [0x40u8, 0x01, 0x02, 0x03, 0x04]; // short header + 4-byte conn id
289 let hdr = QuicShortHeader::parse_with_conn_id_len(&buf, 4).unwrap();
290 assert_eq!(hdr.dst_conn_id, vec![1, 2, 3, 4]);
291 assert_eq!(hdr.header_len, 5);
292 }
293
294 #[test]
295 fn test_short_header_parse_long_rejected() {
296 // bit 7 = 1 => long header, should return None for short header parser
297 let buf = [0xC0u8, 0x01, 0x02];
298 assert!(QuicShortHeader::parse(&buf).is_none());
299 }
300
301 #[test]
302 fn test_packet_type_names() {
303 assert_eq!(QuicPacketType::Initial.name(), "Initial");
304 assert_eq!(QuicPacketType::ZeroRtt.name(), "0-RTT");
305 assert_eq!(QuicPacketType::Handshake.name(), "Handshake");
306 assert_eq!(QuicPacketType::Retry.name(), "Retry");
307 assert_eq!(QuicPacketType::OneRtt.name(), "1-RTT");
308 assert_eq!(QuicPacketType::VersionNeg.name(), "Version Negotiation");
309 }
310}