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::{any::Any, sync::Arc};
11use trillium::{Handler, KnownHeaderName, Listener, Transport, 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.as_ref().map(Listener::unshared_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    // Each stream task holds the handler. Cloning a connection-local `Arc` for each keeps that
86    // refcount traffic on this connection, rather than on the one count every core's streams
87    // would otherwise share.
88    handle_inbound_bidi_streams(
89        connection,
90        h3.clone(),
91        ArcHandler::new(handler),
92        runtime,
93        wt_dispatcher,
94        listener,
95        local_alt_svc,
96        priorities,
97    )
98    .await;
99}
100
101#[allow(clippy::too_many_arguments)]
102async fn handle_inbound_bidi_streams(
103    connection: QuicConnection,
104    h3: Arc<H3Connection>,
105    handler: ArcHandler<impl Handler>,
106    runtime: impl RuntimeTrait,
107    wt_dispatcher: Option<WebTransportDispatcher>,
108    listener: Option<Listener>,
109    local_alt_svc: Option<&'static str>,
110    priorities: PriorityRegistry,
111) {
112    loop {
113        match h3.swansong().interrupt(connection.accept_bidi()).await {
114            None => {
115                log::trace!("H3 bidi accept loop: interrupted by swansong shutdown");
116                break;
117            }
118            Some(Err(e)) => {
119                log::debug!("H3 bidi accept loop: accept_bidi error: {e}");
120                break;
121            }
122            Some(Ok((stream_id, transport))) => {
123                handle_bidi_stream(
124                    stream_id,
125                    transport,
126                    &h3,
127                    &handler,
128                    &connection,
129                    &runtime,
130                    &wt_dispatcher,
131                    listener.clone(),
132                    local_alt_svc,
133                    &priorities,
134                );
135            }
136        }
137    }
138
139    h3.shut_down();
140}
141
142#[allow(clippy::too_many_arguments)]
143fn handle_bidi_stream(
144    stream_id: u64,
145    transport: BoxedBidiStream,
146    h3: &Arc<H3Connection>,
147    handler: &ArcHandler<impl Handler>,
148    connection: &QuicConnection,
149    runtime: &impl RuntimeTrait,
150    wt_dispatcher: &Option<WebTransportDispatcher>,
151    listener: Option<Listener>,
152    local_alt_svc: Option<&'static str>,
153    priorities: &PriorityRegistry,
154) {
155    log::trace!("H3 bidi stream {stream_id}: spawning handler task");
156    let (h3, handler, connection, wt_dispatcher, priorities) = (
157        h3.clone(),
158        handler.clone(),
159        connection.clone(),
160        wt_dispatcher.clone(),
161        priorities.clone(),
162    );
163
164    // Wrap the stream so RFC 9218 priority signals routed to its slot are applied to the QUIC
165    // send stream as it writes. trillium-http emits the initial priority and any PRIORITY_UPDATE
166    // to the connection callback, which stores into this slot.
167    let slot = priorities.register(stream_id);
168
169    // Taken before the stream is handed off, while the send half is still reachable: this is
170    // the only channel through which the layers above can observe a QUIC peer's departure.
171    let peer_gone = transport.stopped();
172
173    // Erased once, as the `Box<dyn Transport>` that `trillium::Conn` holds, so the conn moves into
174    // and out of trillium without being boxed again. The paths that need QUIC stream operations
175    // downcast back to `RequestStream`.
176    let transport: Box<dyn Transport> = Box::new(RequestStream::new(transport, slot, stream_id));
177
178    runtime.spawn_detached(async move {
179        // Unmapped for the same reason the TCP accept loop unmaps: the QUIC listener is bound
180        // dual-stack too, so an IPv4 client over HTTP/3 arrives as `::ffff:a.b.c.d`. Without this
181        // the same client would key and log differently over h3 than over h1/h2.
182        let peer_ip = unmap_ipv4(connection.remote_address().ip());
183        let quic_connection = connection.clone();
184        let wt_dispatcher = wt_dispatcher.clone();
185
186        let handler_fn = {
187            let handler = handler.clone();
188            let wt_dispatcher = wt_dispatcher.clone();
189            move |mut conn: trillium_http::Conn<_>| async move {
190                conn.set_peer_ip(Some(peer_ip));
191                conn.set_secure(true);
192
193                let state = conn.state_mut();
194                state.insert(quic_connection);
195                state.insert(StreamId(stream_id));
196                if let Some(listener) = listener {
197                    if let Some(addr) = listener.socket_addr() {
198                        state.insert(addr);
199                    }
200                    state.insert(listener);
201                }
202                if let Some(dispatcher) = wt_dispatcher {
203                    state.insert(dispatcher);
204                }
205                if let Some(alt_svc) = local_alt_svc {
206                    conn.response_headers_mut()
207                        .try_insert(KnownHeaderName::AltSvc, alt_svc);
208                }
209
210                let conn = handler.run(conn.into()).await;
211                let conn = handler.before_send(conn).await;
212
213                conn.into_inner()
214            }
215        };
216
217        let result = h3
218            .clone()
219            .process_inbound_bidi(transport, handler_fn, stream_id)
220            .with_reset(|transport, code| {
221                // RFC 9114 §4.1.2: stream-level protocol errors (notably H3_MESSAGE_ERROR)
222                // MUST RST the stream. We stop the recv side and reset the send side with
223                // the same code so the peer sees the error on whichever direction it's
224                // listening on.
225                if let Some(stream) = request_stream(transport) {
226                    let raw = u64::from(code);
227                    stream.stop(raw);
228                    stream.reset(raw);
229                }
230            });
231
232        let result = match peer_gone {
233            Some(peer_gone) => result.with_peer_gone(peer_gone).await,
234            None => result.await,
235        };
236
237        match result {
238            Ok(H3StreamResult::Request(conn)) if conn.should_upgrade() => {
239                let upgrade = Upgrade::from(conn);
240                if handler.has_upgrade(&upgrade) {
241                    log::debug!("upgrading h3 stream");
242                    handler.upgrade(upgrade).await;
243                } else {
244                    log::error!("h3 upgrade specified but no upgrade handler provided");
245                }
246            }
247
248            Ok(H3StreamResult::Request(_)) => {}
249
250            Ok(H3StreamResult::WebTransport {
251                session_id,
252                transport,
253                buffer,
254            }) => {
255                if let Some(mut stream) = into_request_stream(transport) {
256                    if let Some(dispatcher) = &wt_dispatcher {
257                        dispatcher.dispatch(WebTransportStream::Bidi {
258                            session_id,
259                            stream,
260                            buffer: buffer.into(),
261                        });
262                    } else {
263                        stream.stop(H3ErrorCode::StreamCreationError.into());
264                        stream.reset(H3ErrorCode::StreamCreationError.into());
265                    }
266                }
267            }
268
269            Err(error) => {
270                log::debug!("H3 bidi stream {stream_id}: error: {error}");
271                handle_h3_error(error, &connection, &h3);
272            }
273        }
274
275        priorities.deregister(stream_id);
276    });
277}
278
279/// The concrete type behind every h3 request stream's `Box<dyn Transport>`.
280type RequestStream = PrioritizedStream<BoxedBidiStream>;
281
282fn request_stream(transport: &mut Box<dyn Transport>) -> Option<&mut RequestStream> {
283    (&mut **transport as &mut dyn Any).downcast_mut()
284}
285
286fn into_request_stream(transport: Box<dyn Transport>) -> Option<Box<RequestStream>> {
287    (transport as Box<dyn Any>).downcast().ok()
288}
289
290fn spawn_inbound_uni_streams(
291    connection: &QuicConnection,
292    h3: &Arc<H3Connection>,
293    runtime: &impl RuntimeTrait,
294    wt_dispatcher: &Option<WebTransportDispatcher>,
295) {
296    let (connection, h3, runtime, wt_dispatcher) = (
297        connection.clone(),
298        h3.clone(),
299        runtime.clone(),
300        wt_dispatcher.clone(),
301    );
302    runtime.clone().spawn_detached(async move {
303        while let Some(Ok((_stream_id, recv))) =
304            h3.swansong().interrupt(connection.accept_uni()).await
305        {
306            let (connection, h3, wt_dispatcher) =
307                (connection.clone(), h3.clone(), wt_dispatcher.clone());
308
309            runtime.spawn_detached(async move {
310                // RFC 9114 §8.1 / RFC 9204 §6 connection-level errors must close the
311                // QUIC connection while the recv stream is still alive — otherwise
312                // quinn's RecvStream::drop sends STOP_SENDING, and the peer's malformed
313                // RESET_STREAM response can race ahead and override our app error code
314                // with FINAL_SIZE_ERROR on the wire. The closure fires inside
315                // process_inbound_uni_with_close before stream drops, so the close sets
316                // quinn's conn.error first and the drop becomes a no-op.
317                let close_connection = {
318                    let connection = connection.clone();
319                    let h3 = h3.clone();
320                    move |code: H3ErrorCode| {
321                        connection.close(code.into(), code.reason().as_bytes());
322                        h3.shut_down();
323                    }
324                };
325                let result = h3
326                    .process_inbound_uni_with_close(recv, close_connection)
327                    .await;
328
329                match result {
330                    Ok(UniStreamResult::Handled) => {}
331                    Ok(UniStreamResult::WebTransport {
332                        session_id,
333                        mut stream,
334                        buffer,
335                    }) => {
336                        if let Some(dispatcher) = &wt_dispatcher {
337                            dispatcher.dispatch(WebTransportStream::Uni {
338                                session_id,
339                                stream: Box::new(stream),
340                                buffer: buffer.into(),
341                            });
342                        } else {
343                            stream.stop(H3ErrorCode::StreamCreationError.into());
344                        }
345                    }
346
347                    Ok(UniStreamResult::Unknown { mut stream, .. }) => {
348                        stream.stop(H3ErrorCode::StreamCreationError.into());
349                    }
350
351                    Err(error) => {
352                        // Connection-level protocol errors already fired the close
353                        // callback above; this call is a no-op for the close path
354                        // (idempotent) and still useful for logging plus I/O errors.
355                        handle_h3_error(error, &connection, &h3);
356                    }
357                }
358            });
359        }
360
361        h3.shut_down();
362    });
363}
364
365fn spawn_qpack_decoder_stream(
366    connection: &QuicConnection,
367    h3: &Arc<H3Connection>,
368    runtime: &impl RuntimeTrait,
369) {
370    let (connection, h3) = (connection.clone(), h3.clone());
371
372    runtime.spawn_detached(async move {
373        log::trace!("H3: opening outbound QPACK decoder stream");
374        let stream = match connection.open_uni().await {
375            Ok((_stream_id, stream)) => stream,
376            Err(err) => {
377                log::error!("H3: open_uni for QPACK decoder stream failed: {err:?}");
378                h3.shut_down();
379                return;
380            }
381        };
382
383        let result = h3.run_decoder(stream).await;
384
385        if let Err(error) = result {
386            handle_h3_error(error, &connection, &h3);
387        }
388        // run_decoder shuts the connection down on return.
389    });
390}
391
392fn spawn_qpack_encoder_stream(
393    connection: &QuicConnection,
394    h3: &Arc<H3Connection>,
395    runtime: &impl RuntimeTrait,
396) {
397    let (connection, h3) = (connection.clone(), h3.clone());
398    runtime.spawn_detached(async move {
399        log::trace!("H3: opening outbound QPACK encoder stream");
400        let stream = match connection.open_uni().await {
401            Ok((_stream_id, stream)) => stream,
402            Err(err) => {
403                log::error!("H3: open_uni for QPACK encoder stream failed: {err:?}");
404                h3.shut_down();
405                return;
406            }
407        };
408
409        let result = h3.run_encoder(stream).await;
410
411        if let Err(error) = result {
412            handle_h3_error(error, &connection, &h3);
413        }
414        // run_encoder shuts the connection down on return.
415    });
416}
417
418fn spawn_outbound_control_stream(
419    connection: &QuicConnection,
420    h3: &Arc<H3Connection>,
421    runtime: &impl RuntimeTrait,
422) {
423    let (connection, h3) = (connection.clone(), h3.clone());
424    runtime.spawn_detached(async move {
425        log::trace!("H3: opening outbound control stream");
426        let stream = match connection.open_uni().await {
427            Ok((_stream_id, stream)) => stream,
428            Err(err) => {
429                log::error!("H3: open_uni for outbound control stream failed: {err:?}");
430                h3.shut_down();
431                return;
432            }
433        };
434
435        let result = h3.run_outbound_control(stream).await;
436
437        if let Err(error) = result {
438            handle_h3_error(error, &connection, &h3);
439        }
440        // run_outbound_control shuts the connection down on return.
441    });
442}
443
444fn handle_h3_error(error: H3Error, connection: &QuicConnection, h3: &H3Connection) {
445    log::debug!("H3 error: {error}");
446    if let H3Error::Protocol(code) = error
447        && code.is_connection_error()
448    {
449        connection.close(code.into(), code.reason().as_bytes());
450        h3.shut_down();
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::{RequestStream, into_request_stream, priority::PriorityRegistry, request_stream};
457    use crate::{BoxedBidiStream, QuicTransportBidi, QuicTransportReceive, QuicTransportSend};
458    use futures_lite::{AsyncRead, AsyncWrite};
459    use std::{
460        io,
461        pin::Pin,
462        sync::{Arc, Mutex},
463        task::{Context, Poll},
464    };
465    use trillium::Transport;
466
467    /// A bidi stream that records the stream-level signals sent through it.
468    struct RecordingBidi(Arc<Mutex<Vec<String>>>);
469
470    impl AsyncRead for RecordingBidi {
471        fn poll_read(
472            self: Pin<&mut Self>,
473            _: &mut Context<'_>,
474            _: &mut [u8],
475        ) -> Poll<io::Result<usize>> {
476            Poll::Ready(Ok(0))
477        }
478    }
479
480    impl AsyncWrite for RecordingBidi {
481        fn poll_write(
482            self: Pin<&mut Self>,
483            _: &mut Context<'_>,
484            buf: &[u8],
485        ) -> Poll<io::Result<usize>> {
486            Poll::Ready(Ok(buf.len()))
487        }
488
489        fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
490            Poll::Ready(Ok(()))
491        }
492
493        fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
494            Poll::Ready(Ok(()))
495        }
496    }
497
498    impl QuicTransportReceive for RecordingBidi {
499        fn stop(&mut self, code: u64) {
500            self.0.lock().unwrap().push(format!("stop {code}"));
501        }
502    }
503
504    impl QuicTransportSend for RecordingBidi {
505        fn reset(&mut self, code: u64) {
506            self.0.lock().unwrap().push(format!("reset {code}"));
507        }
508    }
509
510    impl Transport for RecordingBidi {}
511    impl QuicTransportBidi for RecordingBidi {}
512
513    #[test]
514    fn a_request_stream_is_recoverable_from_its_erased_transport() {
515        let signals = Arc::new(Mutex::new(Vec::new()));
516        let registry = PriorityRegistry::default();
517        let quic_stream: BoxedBidiStream = Box::new(RecordingBidi(Arc::clone(&signals)));
518        let mut transport: Box<dyn Transport> =
519            Box::new(RequestStream::new(quic_stream, registry.register(0), 0));
520
521        let stream = request_stream(&mut transport).expect("erased as a RequestStream");
522        stream.stop(1);
523        stream.reset(2);
524        assert_eq!(*signals.lock().unwrap(), ["stop 1", "reset 2"]);
525
526        assert!(into_request_stream(transport).is_some());
527    }
528}