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)]
44#[non_exhaustive]
45pub struct Http3Settings {
46 /// Maximum number of requests a
47 /// [ServerH3Driver](crate::http3::driver::ServerH3Driver) allows per
48 /// connection.
49 pub max_requests_per_connection: Option<u64>,
50 /// Maximum size of a single HEADERS frame, in bytes.
51 pub max_header_list_size: Option<u64>,
52 /// Maximum value the QPACK encoder is permitted to set for the dynamic
53 /// table capcity. See <https://www.rfc-editor.org/rfc/rfc9204.html#name-maximum-dynamic-table-capac>
54 pub qpack_max_table_capacity: Option<u64>,
55 /// Upper bound on the number of streams that can be blocked on the QPACK
56 /// decoder. See <https://www.rfc-editor.org/rfc/rfc9204.html#name-blocked-streams>
57 pub qpack_blocked_streams: Option<u64>,
58 /// Timeout between starting the QUIC handshake and receiving the first
59 /// request on a connection. Only applicable to
60 /// [ServerH3Driver](crate::http3::driver::ServerH3Driver).
61 pub post_accept_timeout: Option<Duration>,
62 /// Set the `SETTINGS_ENABLE_CONNECT_PROTOCOL` HTTP/3 setting.
63 /// See <https://www.rfc-editor.org/rfc/rfc9220#section-3-2>
64 pub enable_extended_connect: bool,
65 /// Maximum size, in bytes, of the buffer the driver uses to read HTTP/3
66 /// body data out of quiche before forwarding it upstream.
67 ///
68 /// The buffer is sized dynamically to the amount of data currently
69 /// readable on a stream (which is itself bounded by the QUIC flow-control
70 /// window and the size of the buffered QUIC STREAM frames), but a single
71 /// read will never allocate more than this cap. This bounds the memory a
72 /// single (potentially adversarial) stream can force the driver to
73 /// allocate.
74 ///
75 /// When unset or `Some(0)`, the driver defaults to 16 KiB.
76 pub max_recv_body_buf_size: Option<usize>,
77}
78
79impl From<&Http3Settings> for quiche::h3::Config {
80 fn from(value: &Http3Settings) -> Self {
81 let mut config = Self::new().unwrap();
82
83 if let Some(v) = value.max_header_list_size {
84 config.set_max_field_section_size(v);
85 }
86
87 if let Some(v) = value.qpack_max_table_capacity {
88 config.set_qpack_max_table_capacity(v);
89 }
90
91 if let Some(v) = value.qpack_blocked_streams {
92 config.set_qpack_blocked_streams(v);
93 }
94
95 if value.enable_extended_connect {
96 config.enable_extended_connect(value.enable_extended_connect)
97 }
98
99 config
100 }
101}
102
103/// Opaque handle to an entry in [`Http3Timeouts`].
104pub(crate) struct TimeoutKey(delay_queue::Key);
105
106pub(crate) struct Http3SettingsEnforcer {
107 limits: Http3Limits,
108 timeouts: Http3Timeouts,
109}
110
111impl From<&Http3Settings> for Http3SettingsEnforcer {
112 fn from(value: &Http3Settings) -> Self {
113 Self {
114 limits: Http3Limits {
115 max_requests_per_connection: value.max_requests_per_connection,
116 },
117 timeouts: Http3Timeouts {
118 post_accept_timeout: value.post_accept_timeout,
119 delay_queue: DelayQueue::new(),
120 },
121 }
122 }
123}
124
125impl Http3SettingsEnforcer {
126 /// Returns a boolean indicating whether or not the connection should be
127 /// closed due to a violation of the request count limit.
128 pub fn enforce_requests_limit(&self, request_count: u64) -> bool {
129 if let Some(limit) = self.limits.max_requests_per_connection {
130 return request_count >= limit;
131 }
132
133 false
134 }
135
136 /// Returns the configured post-accept timeout.
137 pub fn post_accept_timeout(&self) -> Option<Duration> {
138 self.timeouts.post_accept_timeout
139 }
140
141 /// Registers a timeout of `typ` in this [Http3SettingsEnforcer].
142 pub fn add_timeout(
143 &mut self, typ: Http3TimeoutType, duration: Duration,
144 ) -> TimeoutKey {
145 let key = self.timeouts.delay_queue.insert(typ, duration);
146 TimeoutKey(key)
147 }
148
149 /// Checks whether the [Http3SettingsEnforcer] has any pending timeouts.
150 /// This should be used to selectively poll `enforce_timeouts`.
151 pub fn has_pending_timeouts(&self) -> bool {
152 !self.timeouts.delay_queue.is_empty()
153 }
154
155 /// Checks which timeouts have expired.
156 fn poll_timeouts(&mut self, cx: &mut Context) -> Poll<TimeoutCheckResult> {
157 let mut changed = false;
158 let mut result = TimeoutCheckResult::default();
159
160 while let Poll::Ready(Some(exp)) =
161 self.timeouts.delay_queue.poll_expired(cx)
162 {
163 changed |= result.set_expired(exp.into_inner());
164 }
165
166 if changed {
167 return Poll::Ready(result);
168 }
169 Poll::Pending
170 }
171
172 /// Waits for at least one registered timeout to expire.
173 ///
174 /// This function will automatically call `close()` on the underlying
175 /// [quiche::Connection].
176 pub async fn enforce_timeouts(
177 &mut self, qconn: &mut QuicheConnection,
178 ) -> Result<(), H3ConnectionError> {
179 let result = poll_fn(|cx| self.poll_timeouts(cx)).await;
180
181 if result.connection_timed_out {
182 log::debug!("connection timed out due to post-accept-timeout"; "scid" => ?qconn.source_id());
183 qconn.close(true, quiche::h3::WireErrorCode::NoError as u64, &[])?;
184 }
185
186 Ok(())
187 }
188
189 /// Cancels a timeout that was previously registered with `add_timeout`.
190 pub fn cancel_timeout(&mut self, key: TimeoutKey) {
191 self.timeouts.delay_queue.remove(&key.0);
192 }
193}
194
195// TODO(rmehra): explore if these should really be Options, or if we
196// should enforce sane defaults
197struct Http3Limits {
198 max_requests_per_connection: Option<u64>,
199}
200
201struct Http3Timeouts {
202 post_accept_timeout: Option<Duration>,
203 delay_queue: DelayQueue<Http3TimeoutType>,
204}
205
206#[derive(Clone, Copy, Debug)]
207pub(crate) enum Http3TimeoutType {
208 PostAccept,
209}
210
211#[derive(Default, Eq, PartialEq)]
212struct TimeoutCheckResult {
213 connection_timed_out: bool,
214}
215
216impl TimeoutCheckResult {
217 fn set_expired(&mut self, typ: Http3TimeoutType) -> bool {
218 use Http3TimeoutType::*;
219 let field = match typ {
220 PostAccept => &mut self.connection_timed_out,
221 };
222
223 *field = true;
224 true
225 }
226}