Skip to main content

h3i/client/
async_client.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//! Responsible for creating a [tokio_quiche::quic::QuicheConnection] and
28//! yielding I/O to tokio-quiche.
29
30use log;
31use quiche::PathStats;
32use quiche::Stats;
33use std::future::Future;
34use std::pin::Pin;
35use std::task::Context;
36use std::task::Poll;
37use std::time::Duration;
38use tokio::select;
39use tokio::sync::mpsc;
40use tokio::sync::oneshot;
41use tokio::time::sleep;
42use tokio::time::sleep_until;
43use tokio::time::Instant;
44use tokio_quiche::metrics::Metrics;
45use tokio_quiche::quic::HandshakeInfo;
46use tokio_quiche::quic::QuicheConnection;
47use tokio_quiche::settings::Hooks;
48use tokio_quiche::settings::QuicSettings;
49use tokio_quiche::socket::Socket;
50use tokio_quiche::ApplicationOverQuic;
51use tokio_quiche::ConnectionParams;
52use tokio_quiche::QuicResult;
53
54use crate::actions::h3::Action;
55use crate::actions::h3::WaitType;
56use crate::actions::h3::WaitingFor;
57use crate::client::execute_action;
58use crate::client::parse_args;
59use crate::client::parse_streams;
60use crate::client::ClientError;
61use crate::client::CloseTriggerFrames;
62use crate::client::ConnectionSummary;
63use crate::client::ParsedArgs;
64use crate::client::StreamMap;
65use crate::client::MAX_DATAGRAM_SIZE;
66use crate::config::Config as H3iConfig;
67use crate::frame::H3iFrame;
68use crate::quiche;
69
70use super::Client;
71use super::ConnectionCloseDetails;
72use super::StreamParserMap;
73
74/// Connect to the socket.
75pub async fn connect(
76    args: &H3iConfig, frame_actions: Vec<Action>,
77    close_trigger_frames: Option<CloseTriggerFrames>,
78) -> std::result::Result<BuildingConnectionSummary, ClientError> {
79    let quic_settings = create_config(args);
80    let mut connection_params =
81        ConnectionParams::new_client(quic_settings, None, Hooks::default());
82
83    connection_params.session = args.session.clone();
84
85    let ParsedArgs {
86        connect_url,
87        bind_addr,
88        peer_addr,
89    } = parse_args(args);
90
91    let socket = tokio::net::UdpSocket::bind(bind_addr).await.unwrap();
92    socket.connect(peer_addr).await.unwrap();
93
94    log::info!(
95        "connecting to {:} from {:}",
96        peer_addr,
97        socket.local_addr().unwrap()
98    );
99
100    let (h3i, conn_summary_fut) =
101        H3iDriver::new(frame_actions, close_trigger_frames);
102    match tokio_quiche::quic::connect_with_config(
103        Socket::try_from(socket).unwrap(),
104        connect_url,
105        &connection_params,
106        h3i,
107    )
108    .await
109    {
110        Ok(_) => Ok(conn_summary_fut),
111        Err(_) => Err(ClientError::HandshakeFail),
112    }
113}
114
115fn create_config(args: &H3iConfig) -> QuicSettings {
116    let mut quic_settings = QuicSettings::default();
117
118    quic_settings.verify_peer = args.verify_peer;
119    quic_settings.max_idle_timeout =
120        Some(Duration::from_millis(args.idle_timeout));
121    quic_settings.send_capacity_factor = args.send_capacity_factor;
122    quic_settings.max_recv_udp_payload_size = MAX_DATAGRAM_SIZE;
123    quic_settings.max_send_udp_payload_size = MAX_DATAGRAM_SIZE;
124    quic_settings.initial_max_data = 10_000_000;
125    quic_settings.initial_max_stream_data_bidi_local =
126        args.max_stream_data_bidi_local;
127    quic_settings.initial_max_stream_data_bidi_remote =
128        args.max_stream_data_bidi_remote;
129    quic_settings.initial_max_stream_data_uni = args.max_stream_data_uni;
130    quic_settings.initial_max_streams_bidi = args.max_streams_bidi;
131    quic_settings.initial_max_streams_uni = args.max_streams_uni;
132    quic_settings.disable_active_migration = true;
133    quic_settings.active_connection_id_limit = 0;
134    quic_settings.max_connection_window = args.max_window;
135    quic_settings.max_stream_window = args.max_stream_window;
136    quic_settings.enable_send_streams_blocked = true;
137    quic_settings.grease = false;
138
139    quic_settings.capture_quiche_logs = true;
140    quic_settings.keylog_file = std::env::var_os("SSLKEYLOGFILE")
141        .and_then(|os_str| os_str.into_string().ok());
142
143    quic_settings.enable_dgram = args.enable_dgram;
144    quic_settings.dgram_recv_max_queue_len = args.dgram_recv_queue_len;
145    quic_settings.dgram_send_max_queue_len = args.dgram_send_queue_len;
146
147    quic_settings
148}
149
150/// The [`Future`] used to build a [`ConnectionSummary`].
151///
152/// At a high level, [`H3iDriver`] will interact with the UDP socket directly,
153/// sending and receiving data as necessary. As new data is received, it will
154/// send [`ConnectionRecord`]s to this struct, which uses these records to
155/// construct the [`ConnectionSummary`].
156#[must_use = "must await to get a ConnectionSummary"]
157pub struct BuildingConnectionSummary {
158    rx: mpsc::UnboundedReceiver<ConnectionRecord>,
159    summary: Option<ConnectionSummary>,
160    seen_all_close_trigger_frames: Option<oneshot::Sender<()>>,
161}
162
163impl BuildingConnectionSummary {
164    fn new(
165        rx: mpsc::UnboundedReceiver<ConnectionRecord>,
166        close_trigger_frames: Option<CloseTriggerFrames>,
167        trigger_frame_tx: oneshot::Sender<()>,
168    ) -> Self {
169        let summary = ConnectionSummary {
170            stream_map: StreamMap::new(close_trigger_frames),
171            ..Default::default()
172        };
173
174        Self {
175            rx,
176            summary: Some(summary),
177            seen_all_close_trigger_frames: Some(trigger_frame_tx),
178        }
179    }
180}
181
182impl Future for BuildingConnectionSummary {
183    type Output = ConnectionSummary;
184
185    fn poll(
186        mut self: Pin<&mut Self>, cx: &mut Context<'_>,
187    ) -> Poll<Self::Output> {
188        while let Poll::Ready(Some(record)) = self.rx.poll_recv(cx) {
189            // Add all records from the current event loop iteration to the
190            // in-progress summary.
191            let summary = self.summary.as_mut().expect("summary already taken");
192
193            match record {
194                ConnectionRecord::StreamedFrame { stream_id, frame } => {
195                    let stream_map = &mut summary.stream_map;
196                    stream_map.insert(stream_id, frame);
197
198                    if stream_map.all_close_trigger_frames_seen() {
199                        // Signal the H3iDriver task to close the connection.
200                        if let Some(expected_tx) =
201                            self.seen_all_close_trigger_frames.take()
202                        {
203                            let _ = expected_tx.send(());
204                        }
205                    }
206                },
207                ConnectionRecord::ConnectionStats(s) => summary.stats = Some(s),
208                ConnectionRecord::PathStats(ps) => summary.path_stats = ps,
209                ConnectionRecord::Close(d) => summary.conn_close_details = d,
210            };
211        }
212
213        if self.rx.is_closed() {
214            // The sender drops when the Tokio-Quiche IOW finishes, so the
215            // connection is done and we're safe to yield the summary.
216            let summary = self.summary.take().expect("summary already taken");
217            Poll::Ready(summary)
218        } else {
219            Poll::Pending
220        }
221    }
222}
223
224pub struct H3iDriver {
225    actions: Vec<Action>,
226    actions_executed: usize,
227    next_fire_time: Instant,
228    waiting_for_responses: WaitingFor,
229    record_tx: mpsc::UnboundedSender<ConnectionRecord>,
230    stream_parsers: StreamParserMap,
231    close_trigger_seen_rx: oneshot::Receiver<()>,
232}
233
234impl H3iDriver {
235    fn new(
236        actions: Vec<Action>, close_trigger_frames: Option<CloseTriggerFrames>,
237    ) -> (Self, BuildingConnectionSummary) {
238        let (record_tx, record_rx) = mpsc::unbounded_channel();
239        let (close_trigger_seen_tx, close_trigger_seen_rx) = oneshot::channel();
240        let fut = BuildingConnectionSummary::new(
241            record_rx,
242            close_trigger_frames,
243            close_trigger_seen_tx,
244        );
245
246        (
247            Self {
248                actions,
249                actions_executed: 0,
250                next_fire_time: Instant::now(),
251                waiting_for_responses: WaitingFor::default(),
252                record_tx,
253                stream_parsers: StreamParserMap::default(),
254                close_trigger_seen_rx,
255            },
256            fut,
257        )
258    }
259
260    /// If the next action should fire.
261    fn should_fire(&self) -> bool {
262        Instant::now() >= self.next_fire_time
263    }
264
265    /// Insert all waits into the waiting set.
266    fn register_waits(&mut self) {
267        while self.actions_executed < self.actions.len() {
268            if let Action::Wait { wait_type } =
269                &self.actions[self.actions_executed]
270            {
271                self.actions_executed += 1;
272
273                match wait_type {
274                    WaitType::WaitDuration(duration) => {
275                        self.next_fire_time = Instant::now() + *duration;
276
277                        log::debug!(
278                            "h3i: waiting for responses: {:?}",
279                            self.waiting_for_responses
280                        );
281                    },
282                    WaitType::StreamEvent(event) => {
283                        self.waiting_for_responses.add_wait(event);
284                    },
285                    WaitType::CanOpenNumStreams(required_streams) => {
286                        log::info!(
287                            "h3i: waiting for peer_streams_left_bidi >= {required_streams:?}"
288                        );
289                        self.waiting_for_responses
290                            .set_required_stream_quota(*required_streams);
291                    },
292                }
293            } else {
294                break;
295            }
296        }
297    }
298}
299
300impl Client for H3iDriver {
301    fn stream_parsers_mut(&mut self) -> &mut StreamParserMap {
302        &mut self.stream_parsers
303    }
304
305    fn handle_response_frame(
306        &mut self, stream_id: u64, frame: crate::frame::H3iFrame,
307    ) {
308        self.record_tx
309            .send(ConnectionRecord::StreamedFrame { stream_id, frame })
310            .expect("H3iDriver task dropped")
311    }
312}
313
314impl ApplicationOverQuic for H3iDriver {
315    fn on_conn_established(
316        &mut self, _qconn: &mut QuicheConnection, _handshake_info: &HandshakeInfo,
317    ) -> QuicResult<()> {
318        log::info!("h3i: HTTP/3 connection established");
319        Ok(())
320    }
321
322    fn should_act(&self) -> bool {
323        // Send terminal records even without an established connection.
324        true
325    }
326
327    fn process_reads(&mut self, qconn: &mut QuicheConnection) -> QuicResult<()> {
328        log::trace!("h3i: process_reads");
329
330        // Register waits during `process_reads()` so `work_loop()` can clear
331        // them during the current event loop iteration. Registering them during
332        // `process_writes()` could miss waits and hang the client.
333        self.register_waits();
334
335        let stream_events = parse_streams(qconn, self);
336        for event in stream_events {
337            self.waiting_for_responses.remove_wait(event);
338        }
339
340        self.waiting_for_responses.check_can_open_num_streams(qconn);
341
342        Ok(())
343    }
344
345    fn process_writes(&mut self, qconn: &mut QuicheConnection) -> QuicResult<()> {
346        log::trace!("h3i: process_writes");
347
348        if !self.waiting_for_responses.is_empty() {
349            log::debug!(
350                "awaiting responses on streams {:?}, skipping further action",
351                self.waiting_for_responses
352            );
353
354            return Ok(());
355        }
356
357        // Re-create the iterator so we can mutably borrow the stream parser map
358        let iter = self.actions.clone().into_iter().skip(self.actions_executed);
359
360        for action in iter {
361            match action {
362                Action::SendFrame { .. } |
363                Action::StreamBytes { .. } |
364                Action::SendDatagram { .. } |
365                Action::ResetStream { .. } |
366                Action::StopSending { .. } |
367                Action::OpenUniStream { .. } |
368                Action::ConnectionClose { .. } |
369                Action::SendHeadersFrame { .. } => {
370                    if self.should_fire() {
371                        // Reset the fire time such that the next action will
372                        // still fire.
373                        self.next_fire_time = Instant::now();
374
375                        execute_action(&action, qconn, self.stream_parsers_mut());
376                        self.actions_executed += 1;
377                    } else {
378                        break;
379                    }
380                },
381                Action::Wait { .. } => {
382                    // Waits are registered during the read phase. Stop here so
383                    // that phase can register this wait and increment
384                    // `actions_executed`.
385                    break;
386                },
387                Action::FlushPackets => {
388                    self.actions_executed += 1;
389                    break;
390                },
391            }
392        }
393
394        Ok(())
395    }
396
397    async fn wait_for_data(
398        &mut self, qconn: &mut QuicheConnection,
399    ) -> QuicResult<()> {
400        log::trace!("h3i: wait_for_data");
401
402        let sleep_fut = if !self.should_fire() {
403            sleep_until(self.next_fire_time)
404        } else {
405            // If there is nothing to send, let the IOW resolve `wait_for_data`
406            // through a QUIC timer or incoming data.
407            sleep(Duration::MAX)
408        };
409
410        select! {
411            rx = &mut self.close_trigger_seen_rx, if !self.close_trigger_seen_rx.is_terminated() => {
412                // NOTE: wait_for_data can be called again after all close triggers have been seen,
413                // depending on how long it takes quiche to mark the connection as closed.
414                // Therefore we can't re-poll the receiver or we'd panic.
415                if rx.is_ok() {
416                    // TODO: customizable close trigger frames
417                    let _ = qconn.close(true, quiche::h3::WireErrorCode::NoError as u64, b"saw all expected frames");
418                }
419            }
420            _ = sleep_fut => {}
421        }
422
423        Ok(())
424    }
425
426    fn on_conn_close<M: Metrics>(
427        &mut self, qconn: &mut QuicheConnection, _metrics: &M,
428        _work_loop_result: &QuicResult<()>,
429    ) {
430        let _ = self
431            .record_tx
432            .send(ConnectionRecord::Close(ConnectionCloseDetails::new(qconn)));
433
434        let _ = self
435            .record_tx
436            .send(ConnectionRecord::ConnectionStats(qconn.stats()));
437
438        let conn_path_stats = qconn.path_stats().collect::<Vec<PathStats>>();
439        let _ = self
440            .record_tx
441            .send(ConnectionRecord::PathStats(conn_path_stats));
442    }
443}
444
445pub enum ConnectionRecord {
446    StreamedFrame { stream_id: u64, frame: H3iFrame },
447    Close(ConnectionCloseDetails),
448    PathStats(Vec<PathStats>),
449    ConnectionStats(Stats),
450}