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    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}