Skip to main content

leyline/profile/
h3.rs

1use std::collections::HashSet;
2
3use serde::Deserialize;
4
5use crate::profile::TlsProfile;
6
7const LOCAL_TRANSPORT_PARAMS: [u64; 14] = [
8    0x01, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0e, 0x0f, 0x20,
9];
10const ENFORCED_TRANSPORT_PARAMS: [u64; 8] = [0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0e, 0x0f];
11const SERVER_TRANSPORT_PARAMS: [u64; 4] = [0x00, 0x02, 0x0d, 0x10];
12const INITIAL_SOURCE_CONNECTION_ID: u64 = 0x0f;
13const HTTP2_RESERVED_SETTINGS: [u64; 4] = [0x02, 0x03, 0x04, 0x05];
14const MAX_VARINT: u64 = (1 << 62) - 1;
15const MIN_INITIAL_DATAGRAM_SIZE: u16 = 1200;
16
17#[expect(
18    missing_docs,
19    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
20)]
21#[derive(Debug, Clone, Deserialize)]
22#[serde(deny_unknown_fields)]
23#[non_exhaustive]
24pub struct H3Profile {
25    pub initial_max_data: u64,
26    pub initial_max_stream_data_bidi_local: u64,
27    pub initial_max_stream_data_bidi_remote: u64,
28    pub initial_max_stream_data_uni: u64,
29    pub initial_max_streams_bidi: u64,
30    pub initial_max_streams_uni: u64,
31    pub max_idle_timeout_secs: u64,
32    pub max_udp_payload_size: u16,
33    #[serde(default)]
34    pub initial_datagram_size: Option<u16>,
35    #[serde(default)]
36    pub initial_crypto_split: H3CryptoSplit,
37    #[serde(default)]
38    pub initial_crypto_reorder: H3CryptoReorder,
39    pub active_connection_id_limit: u64,
40    pub dcid_length: H3ConnectionIdLength,
41    #[serde(default)]
42    pub scid_length: Option<usize>,
43    #[serde(default)]
44    pub qpack_max_table_capacity: Option<u64>,
45    #[serde(default)]
46    pub qpack_blocked_streams: Option<u64>,
47    #[serde(default)]
48    pub max_field_section_size: Option<u64>,
49    #[serde(default)]
50    pub race: bool,
51    #[serde(default)]
52    pub max_ack_delay_ms: Option<u64>,
53    #[serde(default)]
54    pub transport_parameters: Option<Vec<H3TransportParam>>,
55    #[serde(default)]
56    pub transport_order: H3Order,
57    #[serde(default)]
58    pub settings: Option<Vec<H3Setting>>,
59    #[serde(default)]
60    pub control_grease_frame: Option<H3Grease>,
61    #[serde(default)]
62    pub pseudo_order: Option<Vec<String>>,
63    #[serde(default)]
64    pub priority_update: bool,
65    #[serde(default)]
66    pub tls: Option<TlsProfile>,
67}
68
69#[expect(
70    missing_docs,
71    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
72)]
73#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
74#[serde(untagged)]
75#[non_exhaustive]
76pub enum H3ConnectionIdLength {
77    Fixed(usize),
78    Weighted { weights: Vec<(usize, u32)> },
79}
80
81#[expect(
82    missing_docs,
83    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
84)]
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
86#[serde(rename_all = "snake_case")]
87#[non_exhaustive]
88pub enum H3Order {
89    #[default]
90    Fixed,
91    Shuffle,
92    Rotate,
93}
94
95#[expect(
96    missing_docs,
97    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
98)]
99#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
100#[serde(rename_all = "snake_case")]
101#[non_exhaustive]
102pub enum H3CryptoSplit {
103    #[default]
104    Fill,
105    Even,
106}
107
108#[expect(
109    missing_docs,
110    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
111)]
112#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
113#[serde(rename_all = "snake_case")]
114#[non_exhaustive]
115pub enum H3CryptoReorder {
116    #[default]
117    None,
118    SniMidpoint,
119}
120
121#[expect(
122    missing_docs,
123    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
124)]
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
126#[serde(deny_unknown_fields)]
127#[non_exhaustive]
128pub struct H3Grease {
129    pub id_bits: u32,
130    #[serde(default)]
131    pub max_len: usize,
132    #[serde(default)]
133    pub value_bits: u32,
134}
135
136#[expect(
137    missing_docs,
138    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
139)]
140#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
141#[serde(deny_unknown_fields)]
142#[non_exhaustive]
143pub struct H3TransportParam {
144    #[serde(default)]
145    pub id: Option<u64>,
146    #[serde(default)]
147    pub varint: Option<u64>,
148    #[serde(default)]
149    pub hex: Option<String>,
150    #[serde(default)]
151    pub grease: Option<H3Grease>,
152    #[serde(default)]
153    pub versions: Option<H3VersionInformation>,
154    #[serde(default)]
155    pub pinned: bool,
156}
157
158#[expect(
159    missing_docs,
160    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
161)]
162#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
163#[serde(deny_unknown_fields)]
164#[non_exhaustive]
165pub struct H3VersionInformation {
166    pub chosen: u32,
167    pub available: Vec<u32>,
168    #[serde(default)]
169    pub grease: Option<H3VersionGrease>,
170}
171
172#[expect(
173    missing_docs,
174    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
175)]
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
177#[serde(rename_all = "snake_case")]
178#[non_exhaustive]
179pub enum H3VersionGrease {
180    First,
181    Random,
182}
183
184#[expect(
185    missing_docs,
186    reason = "profile schema mirrors the embedded TOML tables; variant and field names are the documentation"
187)]
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
189#[serde(deny_unknown_fields)]
190#[non_exhaustive]
191pub struct H3Setting {
192    #[serde(default)]
193    pub id: Option<u64>,
194    #[serde(default)]
195    pub value: Option<u64>,
196    #[serde(default)]
197    pub grease: Option<H3Grease>,
198}
199
200impl H3Profile {
201    pub(crate) fn validate(&self) -> Result<(), String> {
202        let legacy_settings = self.qpack_max_table_capacity.is_some()
203            || self.qpack_blocked_streams.is_some()
204            || self.max_field_section_size.is_some();
205        if self.settings.is_some() == legacy_settings {
206            return Err(
207                "[h3] needs either a settings list or the qpack_max_table_capacity, \
208                 qpack_blocked_streams and max_field_section_size fields, not both"
209                    .into(),
210            );
211        }
212        if let Some(settings) = &self.settings {
213            validate_settings(settings)?;
214        }
215        if let Some(tls) = &self.tls {
216            tls.validate(true)?;
217        }
218        if let Some(params) = &self.transport_parameters {
219            validate_transport_parameters(params)?;
220        }
221        if let Some(size) = self.initial_datagram_size
222            && !(MIN_INITIAL_DATAGRAM_SIZE..=self.max_udp_payload_size).contains(&size)
223        {
224            return Err(format!(
225                "[h3] initial_datagram_size {size} must be {MIN_INITIAL_DATAGRAM_SIZE}..={}",
226                self.max_udp_payload_size
227            ));
228        }
229        if self.initial_crypto_reorder != H3CryptoReorder::None
230            && self.initial_crypto_split != H3CryptoSplit::Even
231        {
232            return Err("[h3] initial_crypto_reorder needs initial_crypto_split = \"even\"".into());
233        }
234        if let H3ConnectionIdLength::Weighted { weights } = &self.dcid_length
235            && weights.iter().all(|(_, weight)| *weight == 0)
236        {
237            return Err("[h3] dcid_length weights must not all be zero".into());
238        }
239        Ok(())
240    }
241}
242
243fn validate_settings(settings: &[H3Setting]) -> Result<(), String> {
244    let mut seen = HashSet::new();
245    for setting in settings {
246        let (id, value) = match (setting.id, setting.value, &setting.grease) {
247            (None, None, Some(_)) => continue,
248            (Some(id), Some(value), None) => (id, value),
249            _ => return Err("[h3] settings entry needs either id and value, or grease".into()),
250        };
251        if id > MAX_VARINT || value > MAX_VARINT {
252            return Err(format!("[h3] setting {id} does not fit in a QUIC varint"));
253        }
254        if HTTP2_RESERVED_SETTINGS.contains(&id) {
255            return Err(format!("[h3] setting {id} is reserved for HTTP/2"));
256        }
257        if !seen.insert(id) {
258            return Err(format!("[h3] setting {id} appears twice"));
259        }
260    }
261    Ok(())
262}
263
264fn validate_transport_parameters(params: &[H3TransportParam]) -> Result<(), String> {
265    let mut seen = HashSet::new();
266    for param in params {
267        param.validate()?;
268        if let Some(id) = param.id
269            && !seen.insert(id)
270        {
271            return Err(format!("[h3] transport parameter {id} appears twice"));
272        }
273    }
274    if !seen.contains(&INITIAL_SOURCE_CONNECTION_ID) {
275        return Err(format!(
276            "[h3] transport_parameters must list initial_source_connection_id \
277             ({INITIAL_SOURCE_CONNECTION_ID})"
278        ));
279    }
280    Ok(())
281}
282
283impl H3TransportParam {
284    fn validate(&self) -> Result<(), String> {
285        let kinds = [
286            self.varint.is_some(),
287            self.hex.is_some(),
288            self.grease.is_some(),
289            self.versions.is_some(),
290        ]
291        .iter()
292        .filter(|set| **set)
293        .count();
294        match (self.id, kinds) {
295            (Some(id), _) if id > MAX_VARINT => Err(format!(
296                "[h3] transport parameter {id} does not fit in a QUIC varint"
297            )),
298            (Some(id), _) if SERVER_TRANSPORT_PARAMS.contains(&id) => Err(format!(
299                "[h3] transport parameter {id} is sent only by servers"
300            )),
301            (Some(id), 0) if LOCAL_TRANSPORT_PARAMS.contains(&id) => Ok(()),
302            (Some(id), 0) => Err(format!(
303                "[h3] transport parameter {id} has no value and is not one Leyline fills in"
304            )),
305            (Some(id), 1) if ENFORCED_TRANSPORT_PARAMS.contains(&id) => Err(format!(
306                "[h3] transport parameter {id} is enforced locally, so it takes no raw value"
307            )),
308            (Some(id), 1) if self.varint.is_some_and(|value| value > MAX_VARINT) => Err(format!(
309                "[h3] transport parameter {id} value does not fit in a QUIC varint"
310            )),
311            (Some(_), 1) if self.versions.is_some() => self
312                .versions
313                .as_ref()
314                .map_or(Ok(()), H3VersionInformation::validate),
315            (Some(_), 1) if self.grease.is_none() => Ok(()),
316            (None, 1) if self.grease.is_some() => Ok(()),
317            _ => Err(format!(
318                "[h3] transport parameter entry {self:?} needs an id with at most one of varint, \
319                 hex or versions, or a grease table alone"
320            )),
321        }
322    }
323}
324
325impl H3VersionInformation {
326    fn validate(&self) -> Result<(), String> {
327        if self.chosen != QUIC_START_VERSION || !self.available.contains(&self.chosen) {
328            return Err(format!(
329                "[h3] version_information chosen must be {QUIC_START_VERSION:#010x}, the version \
330                 the connection starts in, and must appear in available"
331            ));
332        }
333        match std::iter::once(&self.chosen)
334            .chain(&self.available)
335            .find(|version| !quic_version_supported(**version))
336        {
337            Some(version) => Err(format!(
338                "[h3] QUIC version {version:#010x} is not supported by the QUIC stack"
339            )),
340            None => Ok(()),
341        }
342    }
343}
344
345#[cfg(feature = "http3")]
346fn quic_version_supported(version: u32) -> bool {
347    leyline_quiche::version_is_supported(version)
348}
349
350#[cfg(feature = "http3")]
351const QUIC_START_VERSION: u32 = leyline_quiche::PROTOCOL_VERSION;
352
353#[cfg(not(feature = "http3"))]
354const QUIC_START_VERSION: u32 = 1;
355
356#[cfg(not(feature = "http3"))]
357fn quic_version_supported(_version: u32) -> bool {
358    true
359}