tokio_quiche/http3/
settings.rs

1// Copyright (C) 2025, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27use std::future::poll_fn;
28use std::task::Context;
29use std::task::Poll;
30use std::time::Duration;
31
32use crate::http3::driver::H3ConnectionError;
33use crate::quic::QuicheConnection;
34
35use foundations::telemetry::log;
36use tokio_util::time::delay_queue::DelayQueue;
37use tokio_util::time::delay_queue::{
38    self,
39};
40
41/// Unified configuration parameters for
42/// [H3Driver](crate::http3::driver::H3Driver)s.
43#[derive(Default, Clone, Debug)]
44pub struct Http3Settings {
45    /// Maximum number of requests a
46    /// [ServerH3Driver](crate::http3::driver::ServerH3Driver) allows per
47    /// connection.
48    pub max_requests_per_connection: Option<u64>,
49    /// Maximum size of a single HEADERS frame, in bytes.
50    pub max_header_list_size: Option<u64>,
51    /// Maximum value the QPACK encoder is permitted to set for the dynamic
52    /// table capcity. See <https://www.rfc-editor.org/rfc/rfc9204.html#name-maximum-dynamic-table-capac>
53    pub qpack_max_table_capacity: Option<u64>,
54    /// Upper bound on the number of streams that can be blocked on the QPACK
55    /// decoder. See <https://www.rfc-editor.org/rfc/rfc9204.html#name-blocked-streams>
56    pub qpack_blocked_streams: Option<u64>,
57    /// Timeout between starting the QUIC handshake and receiving the first
58    /// request on a connection. Only applicable to
59    /// [ServerH3Driver](crate::http3::driver::ServerH3Driver).
60    pub post_accept_timeout: Option<Duration>,
61    /// Set the `SETTINGS_ENABLE_CONNECT_PROTOCOL` HTTP/3 setting.
62    /// See <https://www.rfc-editor.org/rfc/rfc9220#section-3-2>
63    pub enable_extended_connect: bool,
64}
65
66impl From<&Http3Settings> for quiche::h3::Config {
67    fn from(value: &Http3Settings) -> Self {
68        let mut config = Self::new().unwrap();
69
70        if let Some(v) = value.max_header_list_size {
71            config.set_max_field_section_size(v);
72        }
73
74        if let Some(v) = value.qpack_max_table_capacity {
75            config.set_qpack_max_table_capacity(v);
76        }
77
78        if let Some(v) = value.qpack_blocked_streams {
79            config.set_qpack_blocked_streams(v);
80        }
81
82        if value.enable_extended_connect {
83            config.enable_extended_connect(value.enable_extended_connect)
84        }
85
86        config
87    }
88}
89
90/// Opaque handle to an entry in [`Http3Timeouts`].
91pub(crate) struct TimeoutKey(delay_queue::Key);
92
93pub(crate) struct Http3SettingsEnforcer {
94    limits: Http3Limits,
95    timeouts: Http3Timeouts,
96}
97
98impl From<&Http3Settings> for Http3SettingsEnforcer {
99    fn from(value: &Http3Settings) -> Self {
100        Self {
101            limits: Http3Limits {
102                max_requests_per_connection: value.max_requests_per_connection,
103            },
104            timeouts: Http3Timeouts {
105                post_accept_timeout: value.post_accept_timeout,
106                delay_queue: DelayQueue::new(),
107            },
108        }
109    }
110}
111
112impl Http3SettingsEnforcer {
113    /// Returns a boolean indicating whether or not the connection should be
114    /// closed due to a violation of the request count limit.
115    pub fn enforce_requests_limit(&self, request_count: u64) -> bool {
116        if let Some(limit) = self.limits.max_requests_per_connection {
117            return request_count >= limit;
118        }
119
120        false
121    }
122
123    /// Returns the configured post-accept timeout.
124    pub fn post_accept_timeout(&self) -> Option<Duration> {
125        self.timeouts.post_accept_timeout
126    }
127
128    /// Registers a timeout of `typ` in this [Http3SettingsEnforcer].
129    pub fn add_timeout(
130        &mut self, typ: Http3TimeoutType, duration: Duration,
131    ) -> TimeoutKey {
132        let key = self.timeouts.delay_queue.insert(typ, duration);
133        TimeoutKey(key)
134    }
135
136    /// Checks whether the [Http3SettingsEnforcer] has any pending timeouts.
137    /// This should be used to selectively poll `enforce_timeouts`.
138    pub fn has_pending_timeouts(&self) -> bool {
139        !self.timeouts.delay_queue.is_empty()
140    }
141
142    /// Checks which timeouts have expired.
143    fn poll_timeouts(&mut self, cx: &mut Context) -> Poll<TimeoutCheckResult> {
144        let mut changed = false;
145        let mut result = TimeoutCheckResult::default();
146
147        while let Poll::Ready(Some(exp)) =
148            self.timeouts.delay_queue.poll_expired(cx)
149        {
150            changed |= result.set_expired(exp.into_inner());
151        }
152
153        if changed {
154            return Poll::Ready(result);
155        }
156        Poll::Pending
157    }
158
159    /// Waits for at least one registered timeout to expire.
160    ///
161    /// This function will automatically call `close()` on the underlying
162    /// [quiche::Connection].
163    pub async fn enforce_timeouts(
164        &mut self, qconn: &mut QuicheConnection,
165    ) -> Result<(), H3ConnectionError> {
166        let result = poll_fn(|cx| self.poll_timeouts(cx)).await;
167
168        if result.connection_timed_out {
169            log::debug!("connection timed out due to post-accept-timeout"; "scid" => ?qconn.source_id());
170            qconn.close(true, quiche::h3::WireErrorCode::NoError as u64, &[])?;
171        }
172
173        Ok(())
174    }
175
176    /// Cancels a timeout that was previously registered with `add_timeout`.
177    pub fn cancel_timeout(&mut self, key: TimeoutKey) {
178        self.timeouts.delay_queue.remove(&key.0);
179    }
180}
181
182// TODO(rmehra): explore if these should really be Options, or if we
183// should enforce sane defaults
184struct Http3Limits {
185    max_requests_per_connection: Option<u64>,
186}
187
188struct Http3Timeouts {
189    post_accept_timeout: Option<Duration>,
190    delay_queue: DelayQueue<Http3TimeoutType>,
191}
192
193#[derive(Clone, Copy, Debug)]
194pub(crate) enum Http3TimeoutType {
195    PostAccept,
196}
197
198#[derive(Default, Eq, PartialEq)]
199struct TimeoutCheckResult {
200    connection_timed_out: bool,
201}
202
203impl TimeoutCheckResult {
204    fn set_expired(&mut self, typ: Http3TimeoutType) -> bool {
205        use Http3TimeoutType::*;
206        let field = match typ {
207            PostAccept => &mut self.connection_timed_out,
208        };
209
210        *field = true;
211        true
212    }
213}