Skip to main content

tokio_quiche/quic/
mod.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
27//! `async`-ified QUIC connections powered by [quiche].
28//!
29//! Hooking up a [quiche::Connection] to [tokio]'s executor and IO primitives
30//! requires an [`ApplicationOverQuic`] to control the connection. The
31//! application exposes a small number of callbacks which are executed whenever
32//! there is work to do with the connection.
33//!
34//! The primary entrypoints to set up a connection are [`listen`][listen] for
35//! servers and [`connect`] for clients.
36//! [`listen_with_capabilities`](crate::listen_with_capabilities)
37//! and [`connect_with_config`] exist for scenarios that require more in-depth
38//! configuration. Lastly, the [`raw`] submodule allows users to take full
39//! control over connection creation and its ingress path.
40//!
41//! # QUIC Connection Internals
42//!
43//! ![QUIC Worker Setup](https://github.com/cloudflare/quiche/blob/master/tokio-quiche/docs/worker.png?raw=true)
44//!
45//! *Note: Internal details are subject to change between minor versions.*
46//!
47//! tokio-quiche conceptually separates a network socket into a `recv` half and
48//! a `send` half. The `recv` half can only sensibly be used by one async task
49//! at a time, while many tasks can `send` packets on the socket concurrently.
50//! Thus, we spawn a dedicated `InboundPacketRouter` task for each socket which
51//! becomes the sole owner of the socket's `recv` half. It decodes the QUIC
52//! header in each packet, looks up the destination connection ID (DCID), and
53//! forwards the packet to the connection's `IoWorker` task.
54//!
55//! If the packet initiates a new connection, it is passed to an
56//! `InitialPacketHandler` with logic for either the client- or server-side
57//! connection setup. The purple `ConnectionAcceptor` depicted above is the
58//! server-side implementation. It optionally validates the client's IP
59//! address with a `RETRY` packet before packaging the nascent connection into
60//! an [`InitialQuicConnection`][iqc] and sending it to the
61//! [`QuicConnectionStream`] returned by [`listen`][listen].
62//!
63//! At this point the caller of [`listen`][listen] has control of the
64//! [`InitialQuicConnection`][iqc] (`IQC`). Now an `IoWorker` task needs to be
65//! spawned to continue driving the connection. This is possible with
66//! `IQC::handshake` or `IQC::start` (see the [`InitialQuicConnection`][iqc]
67//! docs). Client-side connections use the same infrastructure (except for the
68//! `InitialPacketHandler`), but [`connect`] immediately consumes the
69//! [`QuicConnectionStream`] and calls `IQC::start`.
70//!
71//! `IoWorker` is responsible for feeding inbound packets into the underlying
72//! [`quiche::Connection`], executing the [`ApplicationOverQuic`] callbacks, and
73//! flushing outbound packets to the network via the socket's shared `send`
74//! half. It loops through these operations in the order shown above, yielding
75//! only when sending packets and on `wait_for_data` calls. New inbound packets
76//! or a timeout can also restart the loop while `wait_for_data` is pending.
77//! This continues until the connection is closed or the [`ApplicationOverQuic`]
78//! returns an error.
79//!
80//! [listen]: crate::listen
81//! [iqc]: crate::InitialQuicConnection
82
83use std::sync::Arc;
84use std::time::Duration;
85
86use datagram_socket::DatagramSocketRecv;
87use datagram_socket::DatagramSocketSend;
88use foundations::telemetry::log;
89use qlog::writer::make_qlog_writer_from_path;
90use qlog::writer::qlog_file_name;
91
92use crate::http3::settings::Http3Settings;
93use crate::metrics::DefaultMetrics;
94use crate::metrics::Metrics;
95use crate::settings::Config;
96use crate::socket::QuicListener;
97use crate::socket::Socket;
98use crate::ClientH3Controller;
99use crate::ClientH3Driver;
100use crate::ConnectionParams;
101use crate::QuicConnectionStream;
102use crate::QuicResult;
103use crate::QuicResultExt;
104
105mod addr_validation_token;
106pub(crate) mod connection;
107mod dscp;
108mod hooks;
109mod io;
110pub mod raw;
111mod router;
112
113use self::connection::ApplicationOverQuic;
114use self::connection::ConnectionIdGenerator as _;
115use self::connection::QuicConnection;
116use self::router::acceptor::ConnectionAcceptor;
117use self::router::acceptor::ConnectionAcceptorConfig;
118use self::router::connector::ClientConnector;
119use self::router::InboundPacketRouter;
120
121pub use self::connection::ConnectionShutdownBehaviour;
122pub use self::connection::HandshakeError;
123pub use self::connection::HandshakeInfo;
124pub use self::connection::Incoming;
125pub use self::connection::QuicCommand;
126pub use self::connection::QuicConnectionStats;
127pub use self::connection::SimpleConnectionIdGenerator;
128pub use self::dscp::Dscp;
129pub use self::dscp::DscpHandle;
130pub use self::hooks::ConnectionHook;
131
132/// Alias of [quiche::Connection] used internally by the crate.
133pub type QuicheConnection = quiche::Connection<crate::buf_factory::BufFactory>;
134
135/// Connects to an HTTP/3 server using `socket` and the default client
136/// configuration.
137///
138/// This function always uses the [`ApplicationOverQuic`] provided in
139/// [`http3::driver`](crate::http3::driver) and returns a corresponding
140/// [ClientH3Controller]. To specify a different implementation or customize the
141/// configuration, use [`connect_with_config`].
142///
143/// # Note
144/// tokio-quiche currently only supports one client connection per socket.
145/// Sharing a socket among multiple connections will lead to lost packets as
146/// both connections try to read from the shared socket.
147pub async fn connect<Tx, Rx, S>(
148    socket: S, host: Option<&str>,
149) -> QuicResult<(QuicConnection, ClientH3Controller)>
150where
151    Tx: DatagramSocketSend + Send + 'static,
152    Rx: DatagramSocketRecv + Unpin + 'static,
153    S: TryInto<Socket<Tx, Rx>>,
154    S::Error: std::error::Error + Send + Sync + 'static,
155{
156    // Don't apply_max_capabilities(): some NICs don't support GSO
157    let socket: Socket<Tx, Rx> = socket.try_into()?;
158
159    let (h3_driver, h3_controller) =
160        ClientH3Driver::new(Http3Settings::default());
161    let mut params = ConnectionParams::default();
162    params.settings.max_idle_timeout = Some(Duration::from_secs(30));
163
164    Ok((
165        connect_with_config(socket, host, &params, h3_driver).await?,
166        h3_controller,
167    ))
168}
169
170/// Connects to a QUIC server using `socket` and the provided
171/// [`ApplicationOverQuic`].
172///
173/// When the future resolves, the connection has completed its handshake and
174/// `app` is running in the worker task. In case the handshake failed, we close
175/// the connection automatically and the future will resolve with an error.
176///
177/// # Note
178/// tokio-quiche currently only supports one client connection per socket.
179/// Sharing a socket among multiple connections will lead to lost packets as
180/// both connections try to read from the shared socket.
181pub async fn connect_with_config<Tx, Rx, App>(
182    socket: Socket<Tx, Rx>, host: Option<&str>, params: &ConnectionParams<'_>,
183    app: App,
184) -> QuicResult<QuicConnection>
185where
186    Tx: DatagramSocketSend + Send + 'static,
187    Rx: DatagramSocketRecv + Unpin + 'static,
188    App: ApplicationOverQuic,
189{
190    let mut client_config = Config::new(params, socket.capabilities)?;
191    let scid = SimpleConnectionIdGenerator.new_connection_id();
192
193    #[cfg(feature = "custom-client-dcid")]
194    let mut quiche_conn = if let Some(dcid) = &params.dcid {
195        quiche::connect_with_dcid_and_buffer_factory(
196            host,
197            &scid,
198            dcid,
199            socket.local_addr,
200            socket.peer_addr,
201            client_config.as_mut(),
202        )?
203    } else {
204        quiche::connect_with_buffer_factory(
205            host,
206            &scid,
207            socket.local_addr,
208            socket.peer_addr,
209            client_config.as_mut(),
210        )?
211    };
212
213    #[cfg(not(feature = "custom-client-dcid"))]
214    let mut quiche_conn = quiche::connect_with_buffer_factory(
215        host,
216        &scid,
217        socket.local_addr,
218        socket.peer_addr,
219        client_config.as_mut(),
220    )?;
221
222    #[cfg(feature = "custom-client-dcid")]
223    log::info!("created unestablished quiche::Connection"; "scid" => ?scid, "provided_dcid" => ?params.dcid);
224    #[cfg(not(feature = "custom-client-dcid"))]
225    log::info!("created unestablished quiche::Connection"; "scid" => ?scid);
226
227    if let Some(session) = &params.session {
228        quiche_conn.set_session(session).map_err(|error| {
229            log::error!("application provided an invalid session"; "error"=>?error);
230            quiche::Error::CryptoFail
231        })?;
232    }
233
234    // Set the qlog writer here instead of in the `ClientConnector` to avoid
235    // missing logs from early in the connection
236    if let Some(qlog_dir) = &client_config.qlog_dir {
237        log::info!("setting up qlogs"; "qlog_dir"=>qlog_dir);
238        let id = format!("{:?}", scid);
239        let path = std::path::Path::new(qlog_dir)
240            .join(qlog_file_name(&id, client_config.qlog_compression));
241        if let Ok(writer) =
242            make_qlog_writer_from_path(&path, client_config.qlog_compression)
243        {
244            quiche_conn.set_qlog(
245                writer,
246                "tokio-quiche qlog".to_string(),
247                format!("tokio-quiche qlog id={id}"),
248            );
249        }
250    }
251
252    // Set the keylog file here for the same reason
253    if let Some(keylog_file) = &client_config.keylog_file {
254        log::info!("setting up keylog file");
255        if let Ok(keylog_clone) = keylog_file.try_clone() {
256            quiche_conn.set_keylog(Box::new(keylog_clone));
257        }
258    }
259
260    let socket_tx = Arc::new(socket.send);
261    let socket_rx = socket.recv;
262
263    let (router, mut quic_connection_stream) = InboundPacketRouter::new(
264        client_config,
265        Arc::clone(&socket_tx),
266        socket_rx,
267        socket.local_addr,
268        ClientConnector::new(socket_tx, Box::new(quiche_conn)),
269        DefaultMetrics,
270    );
271
272    // drive the packet router:
273    tokio::spawn(async move {
274        match router.await {
275            Ok(()) => {
276                log::debug!("incoming packet router finished");
277            },
278            Err(error) => {
279                log::error!("incoming packet router failed"; "error"=>error);
280            },
281        }
282    });
283
284    Ok(quic_connection_stream
285        .recv()
286        .await
287        .ok_or("unable to establish connection")??
288        .start(app))
289}
290
291pub(crate) fn start_listener<M>(
292    socket: QuicListener, params: &ConnectionParams, metrics: M,
293) -> std::io::Result<QuicConnectionStream<M>>
294where
295    M: Metrics,
296{
297    #[cfg(unix)]
298    assert!(
299        datagram_socket::is_nonblocking(&socket).unwrap_or_default(),
300        "O_NONBLOCK should be set for the listening socket"
301    );
302
303    let config = Config::new(params, socket.capabilities).into_io()?;
304
305    let local_addr = socket.socket.local_addr()?;
306    let socket_tx = Arc::new(socket.socket);
307    let socket_rx = Arc::clone(&socket_tx);
308
309    let acceptor = ConnectionAcceptor::new(
310        ConnectionAcceptorConfig {
311            disable_client_ip_validation: config.disable_client_ip_validation,
312            enable_per_connection_dscp: params.enable_per_connection_dscp,
313            qlog_dir: config.qlog_dir.clone(),
314            qlog_compression: config.qlog_compression,
315            keylog_file: config
316                .keylog_file
317                .as_ref()
318                .and_then(|f| f.try_clone().ok()),
319            #[cfg(target_os = "linux")]
320            with_pktinfo: if local_addr.is_ipv4() {
321                config.has_ippktinfo
322            } else {
323                config.has_ipv6pktinfo
324            },
325        },
326        Arc::clone(&socket_tx),
327        Default::default(),
328        socket.cid_generator,
329        metrics.clone(),
330    );
331
332    let (socket_driver, accept_stream) = InboundPacketRouter::new(
333        config,
334        socket_tx,
335        socket_rx,
336        local_addr,
337        acceptor,
338        metrics.clone(),
339    );
340
341    crate::metrics::tokio_task::spawn("quic_udp_listener", metrics, async move {
342        match socket_driver.await {
343            Ok(()) => {
344                log::trace!("incoming packet router finished");
345            },
346            Err(error) => {
347                log::error!("incoming packet router failed"; "error"=>error);
348            },
349        }
350    });
351    Ok(QuicConnectionStream::new(accept_stream))
352}