Skip to main content

trillium_server_common/
h3.rs

1//! HTTP/3 specific exports
2
3mod priority;
4pub mod web_transport;
5use crate::{
6    ArcHandler, ArcedQuicEndpoint, BoxedBidiStream, QuicConnection, QuicTransportReceive,
7    QuicTransportSend, RuntimeTrait, unmap_ipv4,
8};
9use priority::{PrioritizedStream, PriorityRegistry, transport_priority};
10use std::sync::Arc;
11use trillium::{Handler, KnownHeaderName, Listener, Upgrade};
12use trillium_http::{
13    HttpContext,
14    h3::{H3Connection, H3Error, H3ErrorCode, H3StreamResult, UniStreamResult},
15};
16use web_transport::{WebTransportDispatcher, WebTransportStream};
17
18/// A QUIC stream identifier
19#[derive(Clone, Copy, Debug)]
20pub struct StreamId(u64);
21impl From<StreamId> for u64 {
22    fn from(val: StreamId) -> Self {
23        val.0
24    }
25}
26
27impl From<u64> for StreamId {
28    fn from(value: u64) -> Self {
29        Self(value)
30    }
31}
32
33pub(crate) async fn run_h3(
34    quic_binding: ArcedQuicEndpoint,
35    context: Arc<HttpContext>,
36    handler: ArcHandler<impl Handler>,
37    runtime: impl RuntimeTrait,
38    listener: Option<Listener>,
39    local_alt_svc: Option<&'static str>,
40) {
41    let swansong = context.swansong();
42    while let Some(connection) = swansong.interrupt(quic_binding.accept()).await.flatten() {
43        let h3 = H3Connection::new(context.clone());
44        let handler = handler.clone();
45        let runtime = runtime.clone();
46        runtime.clone().spawn_detached(run_h3_connection(
47            connection,
48            h3,
49            handler,
50            runtime,
51            listener.clone(),
52            local_alt_svc,
53        ));
54    }
55}
56
57async fn run_h3_connection(
58    connection: QuicConnection,
59    h3: Arc<H3Connection>,
60    handler: ArcHandler<impl Handler>,
61    runtime: impl RuntimeTrait,
62    listener: Option<Listener>,
63    local_alt_svc: Option<&'static str>,
64) {
65    let wt_dispatcher = h3
66        .context()
67        .config()
68        .webtransport_enabled()
69        .then(WebTransportDispatcher::new);
70
71    log::trace!("new quic connection from {}", connection.remote_address());
72
73    let priorities = PriorityRegistry::default();
74    h3.register_priority_callback({
75        let priorities = priorities.clone();
76        move |stream_id, priority, is_update| {
77            priorities.apply(stream_id, transport_priority(priority), is_update)
78        }
79    });
80
81    spawn_outbound_control_stream(&connection, &h3, &runtime);
82    spawn_qpack_encoder_stream(&connection, &h3, &runtime);
83    spawn_qpack_decoder_stream(&connection, &h3, &runtime);
84    spawn_inbound_uni_streams(&connection, &h3, &runtime, &wt_dispatcher);
85    handle_inbound_bidi_streams(
86        connection,
87        h3.clone(),
88        handler,
89        runtime,
90        wt_dispatcher,
91        listener,
92        local_alt_svc,
93        priorities,
94    )
95    .await;
96}
97
98#[allow(clippy::too_many_arguments)]
99async fn handle_inbound_bidi_streams(
100    connection: QuicConnection,
101    h3: Arc<H3Connection>,
102    handler: ArcHandler<impl Handler>,
103    runtime: impl RuntimeTrait,
104    wt_dispatcher: Option<WebTransportDispatcher>,
105    listener: Option<Listener>,
106    local_alt_svc: Option<&'static str>,
107    priorities: PriorityRegistry,
108) {
109    loop {
110        match h3.swansong().interrupt(connection.accept_bidi()).await {
111            None => {
112                log::trace!("H3 bidi accept loop: interrupted by swansong shutdown");
113                break;
114            }
115            Some(Err(e)) => {
116                log::debug!("H3 bidi accept loop: accept_bidi error: {e}");
117                break;
118            }
119            Some(Ok((stream_id, transport))) => {
120                handle_bidi_stream(
121                    stream_id,
122                    transport,
123                    &h3,
124                    &handler,
125                    &connection,
126                    &runtime,
127                    &wt_dispatcher,
128                    listener.clone(),
129                    local_alt_svc,
130                    &priorities,
131                );
132            }
133        }
134    }
135
136    h3.shut_down();
137}
138
139#[allow(clippy::too_many_arguments)]
140fn handle_bidi_stream(
141    stream_id: u64,
142    transport: BoxedBidiStream,
143    h3: &Arc<H3Connection>,
144    handler: &ArcHandler<impl Handler>,
145    connection: &QuicConnection,
146    runtime: &impl RuntimeTrait,
147    wt_dispatcher: &Option<WebTransportDispatcher>,
148    listener: Option<Listener>,
149    local_alt_svc: Option<&'static str>,
150    priorities: &PriorityRegistry,
151) {
152    log::trace!("H3 bidi stream {stream_id}: spawning handler task");
153    let (h3, handler, connection, wt_dispatcher, priorities) = (
154        h3.clone(),
155        handler.clone(),
156        connection.clone(),
157        wt_dispatcher.clone(),
158        priorities.clone(),
159    );
160
161    // Wrap the stream so RFC 9218 priority signals routed to its slot are applied to the QUIC
162    // send stream as it writes. trillium-http emits the initial priority and any PRIORITY_UPDATE
163    // to the connection callback, which stores into this slot.
164    let slot = priorities.register(stream_id);
165
166    // Taken before the stream is handed off, while the send half is still reachable: this is
167    // the only channel through which the layers above can observe a QUIC peer's departure.
168    let peer_gone = transport.stopped();
169
170    let transport: BoxedBidiStream = Box::new(PrioritizedStream::new(transport, slot, stream_id));
171
172    runtime.spawn_detached(async move {
173        // Unmapped for the same reason the TCP accept loop unmaps: the QUIC listener is bound
174        // dual-stack too, so an IPv4 client over HTTP/3 arrives as `::ffff:a.b.c.d`. Without this
175        // the same client would key and log differently over h3 than over h1/h2.
176        let peer_ip = unmap_ipv4(connection.remote_address().ip());
177        let quic_connection = connection.clone();
178        let wt_dispatcher = wt_dispatcher.clone();
179
180        let handler_fn = {
181            let handler = handler.clone();
182            let wt_dispatcher = wt_dispatcher.clone();
183            move |mut conn: trillium_http::Conn<_>| async move {
184                conn.set_peer_ip(Some(peer_ip));
185                conn.set_secure(true);
186
187                let state = conn.state_mut();
188                state.insert(quic_connection);
189                state.insert(StreamId(stream_id));
190                if let Some(listener) = listener {
191                    if let Some(addr) = listener.socket_addr() {
192                        state.insert(addr);
193                    }
194                    state.insert(listener);
195                }
196                if let Some(dispatcher) = wt_dispatcher {
197                    state.insert(dispatcher);
198                }
199                if let Some(alt_svc) = local_alt_svc {
200                    conn.response_headers_mut()
201                        .try_insert(KnownHeaderName::AltSvc, alt_svc);
202                }
203
204                let conn = handler.run(conn.into()).await;
205                let conn = handler.before_send(conn).await;
206
207                conn.into_inner()
208            }
209        };
210
211        let result = h3
212            .clone()
213            .process_inbound_bidi(transport, handler_fn, stream_id)
214            .with_reset(|t, code| {
215                // RFC 9114 §4.1.2: stream-level protocol errors (notably H3_MESSAGE_ERROR)
216                // MUST RST the stream. We stop the recv side and reset the send side with
217                // the same code so the peer sees the error on whichever direction it's
218                // listening on.
219                let raw = u64::from(code);
220                t.stop(raw);
221                t.reset(raw);
222            });
223
224        let result = match peer_gone {
225            Some(peer_gone) => result.with_peer_gone(peer_gone).await,
226            None => result.await,
227        };
228
229        match result {
230            Ok(H3StreamResult::Request(conn)) if conn.should_upgrade() => {
231                let upgrade = Upgrade::from(conn);
232                if handler.has_upgrade(&upgrade) {
233                    log::debug!("upgrading h3 stream");
234                    handler.upgrade(upgrade).await;
235                } else {
236                    log::error!("h3 upgrade specified but no upgrade handler provided");
237                }
238            }
239
240            Ok(H3StreamResult::Request(_)) => {}
241
242            Ok(H3StreamResult::WebTransport {
243                session_id,
244                mut transport,
245                buffer,
246            }) => {
247                if let Some(dispatcher) = &wt_dispatcher {
248                    dispatcher.dispatch(WebTransportStream::Bidi {
249                        session_id,
250                        stream: Box::new(transport),
251                        buffer: buffer.into(),
252                    });
253                } else {
254                    transport.stop(H3ErrorCode::StreamCreationError.into());
255                    transport.reset(H3ErrorCode::StreamCreationError.into());
256                }
257            }
258
259            Err(error) => {
260                log::debug!("H3 bidi stream {stream_id}: error: {error}");
261                handle_h3_error(error, &connection, &h3);
262            }
263        }
264
265        priorities.deregister(stream_id);
266    });
267}
268
269fn spawn_inbound_uni_streams(
270    connection: &QuicConnection,
271    h3: &Arc<H3Connection>,
272    runtime: &impl RuntimeTrait,
273    wt_dispatcher: &Option<WebTransportDispatcher>,
274) {
275    let (connection, h3, runtime, wt_dispatcher) = (
276        connection.clone(),
277        h3.clone(),
278        runtime.clone(),
279        wt_dispatcher.clone(),
280    );
281    runtime.clone().spawn_detached(async move {
282        while let Some(Ok((_stream_id, recv))) =
283            h3.swansong().interrupt(connection.accept_uni()).await
284        {
285            let (connection, h3, wt_dispatcher) =
286                (connection.clone(), h3.clone(), wt_dispatcher.clone());
287
288            runtime.spawn_detached(async move {
289                // RFC 9114 §8.1 / RFC 9204 §6 connection-level errors must close the
290                // QUIC connection while the recv stream is still alive — otherwise
291                // quinn's RecvStream::drop sends STOP_SENDING, and the peer's malformed
292                // RESET_STREAM response can race ahead and override our app error code
293                // with FINAL_SIZE_ERROR on the wire. The closure fires inside
294                // process_inbound_uni_with_close before stream drops, so the close sets
295                // quinn's conn.error first and the drop becomes a no-op.
296                let close_connection = {
297                    let connection = connection.clone();
298                    let h3 = h3.clone();
299                    move |code: H3ErrorCode| {
300                        connection.close(code.into(), code.reason().as_bytes());
301                        h3.shut_down();
302                    }
303                };
304                let result = h3
305                    .process_inbound_uni_with_close(recv, close_connection)
306                    .await;
307
308                match result {
309                    Ok(UniStreamResult::Handled) => {}
310                    Ok(UniStreamResult::WebTransport {
311                        session_id,
312                        mut stream,
313                        buffer,
314                    }) => {
315                        if let Some(dispatcher) = &wt_dispatcher {
316                            dispatcher.dispatch(WebTransportStream::Uni {
317                                session_id,
318                                stream: Box::new(stream),
319                                buffer: buffer.into(),
320                            });
321                        } else {
322                            stream.stop(H3ErrorCode::StreamCreationError.into());
323                        }
324                    }
325
326                    Ok(UniStreamResult::Unknown { mut stream, .. }) => {
327                        stream.stop(H3ErrorCode::StreamCreationError.into());
328                    }
329
330                    Err(error) => {
331                        // Connection-level protocol errors already fired the close
332                        // callback above; this call is a no-op for the close path
333                        // (idempotent) and still useful for logging plus I/O errors.
334                        handle_h3_error(error, &connection, &h3);
335                    }
336                }
337            });
338        }
339
340        h3.shut_down();
341    });
342}
343
344fn spawn_qpack_decoder_stream(
345    connection: &QuicConnection,
346    h3: &Arc<H3Connection>,
347    runtime: &impl RuntimeTrait,
348) {
349    let (connection, h3) = (connection.clone(), h3.clone());
350
351    runtime.spawn_detached(async move {
352        log::trace!("H3: opening outbound QPACK decoder stream");
353        let stream = match connection.open_uni().await {
354            Ok((_stream_id, stream)) => stream,
355            Err(err) => {
356                log::error!("H3: open_uni for QPACK decoder stream failed: {err:?}");
357                h3.shut_down();
358                return;
359            }
360        };
361
362        let result = h3.run_decoder(stream).await;
363
364        if let Err(error) = result {
365            handle_h3_error(error, &connection, &h3);
366        }
367        // run_decoder shuts the connection down on return.
368    });
369}
370
371fn spawn_qpack_encoder_stream(
372    connection: &QuicConnection,
373    h3: &Arc<H3Connection>,
374    runtime: &impl RuntimeTrait,
375) {
376    let (connection, h3) = (connection.clone(), h3.clone());
377    runtime.spawn_detached(async move {
378        log::trace!("H3: opening outbound QPACK encoder stream");
379        let stream = match connection.open_uni().await {
380            Ok((_stream_id, stream)) => stream,
381            Err(err) => {
382                log::error!("H3: open_uni for QPACK encoder stream failed: {err:?}");
383                h3.shut_down();
384                return;
385            }
386        };
387
388        let result = h3.run_encoder(stream).await;
389
390        if let Err(error) = result {
391            handle_h3_error(error, &connection, &h3);
392        }
393        // run_encoder shuts the connection down on return.
394    });
395}
396
397fn spawn_outbound_control_stream(
398    connection: &QuicConnection,
399    h3: &Arc<H3Connection>,
400    runtime: &impl RuntimeTrait,
401) {
402    let (connection, h3) = (connection.clone(), h3.clone());
403    runtime.spawn_detached(async move {
404        log::trace!("H3: opening outbound control stream");
405        let stream = match connection.open_uni().await {
406            Ok((_stream_id, stream)) => stream,
407            Err(err) => {
408                log::error!("H3: open_uni for outbound control stream failed: {err:?}");
409                h3.shut_down();
410                return;
411            }
412        };
413
414        let result = h3.run_outbound_control(stream).await;
415
416        if let Err(error) = result {
417            handle_h3_error(error, &connection, &h3);
418        }
419        // run_outbound_control shuts the connection down on return.
420    });
421}
422
423fn handle_h3_error(error: H3Error, connection: &QuicConnection, h3: &H3Connection) {
424    log::debug!("H3 error: {error}");
425    if let H3Error::Protocol(code) = error
426        && code.is_connection_error()
427    {
428        connection.close(code.into(), code.reason().as_bytes());
429        h3.shut_down();
430    }
431}