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