Skip to main content

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}