Skip to main content

quiche_apps/
client.rs

1// Copyright (C) 2020, 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 crate::args::*;
28use crate::common::*;
29
30use std::io::prelude::*;
31
32use std::rc::Rc;
33
34use std::cell::RefCell;
35
36use ring::rand::*;
37
38const MAX_DATAGRAM_SIZE: usize = 1350;
39
40#[derive(Debug)]
41pub enum ClientError {
42    HandshakeFail,
43    HttpFail,
44    Other(String),
45}
46
47pub fn connect(
48    args: ClientArgs, conn_args: CommonArgs,
49    output_sink: impl FnMut(String) + 'static,
50) -> Result<(), ClientError> {
51    let mut buf = [0; 65535];
52    let mut out = [0; MAX_DATAGRAM_SIZE];
53
54    let output_sink =
55        Rc::new(RefCell::new(output_sink)) as Rc<RefCell<dyn FnMut(_)>>;
56
57    // Setup the event loop.
58    let mut poll = mio::Poll::new().unwrap();
59    let mut events = mio::Events::with_capacity(1024);
60
61    // We'll only connect to the first server provided in URL list.
62    let connect_url = &args.urls[0];
63
64    // Resolve server address.
65    let peer_addr = if let Some(addr) = &args.connect_to {
66        addr.parse().expect("--connect-to is expected to be a string containing an IPv4 or IPv6 address with a port. E.g. 192.0.2.0:443")
67    } else {
68        *connect_url.socket_addrs(|| None).unwrap().first().unwrap()
69    };
70
71    // Bind to INADDR_ANY or IN6ADDR_ANY depending on the IP family of the
72    // server address. This is needed on macOS and BSD variants that don't
73    // support binding to IN6ADDR_ANY for both v4 and v6.
74    let bind_addr = match peer_addr {
75        std::net::SocketAddr::V4(_) => format!("0.0.0.0:{}", args.source_port),
76        std::net::SocketAddr::V6(_) => format!("[::]:{}", args.source_port),
77    };
78
79    // Create the UDP socket backing the QUIC connection, and register it with
80    // the event loop.
81    let mut socket =
82        mio::net::UdpSocket::bind(bind_addr.parse().unwrap()).unwrap();
83    poll.registry()
84        .register(&mut socket, mio::Token(0), mio::Interest::READABLE)
85        .unwrap();
86
87    let migrate_socket = if args.perform_migration {
88        let mut socket =
89            mio::net::UdpSocket::bind(bind_addr.parse().unwrap()).unwrap();
90        poll.registry()
91            .register(&mut socket, mio::Token(1), mio::Interest::READABLE)
92            .unwrap();
93
94        Some(socket)
95    } else {
96        None
97    };
98
99    // Create the configuration for the QUIC connection.
100    let mut config = quiche::Config::new(args.version).unwrap();
101
102    if let Some(ref trust_origin_ca_pem) = args.trust_origin_ca_pem {
103        config
104            .load_verify_locations_from_file(trust_origin_ca_pem)
105            .map_err(|e| {
106                ClientError::Other(format!("error loading origin CA file : {e}"))
107            })?;
108    } else {
109        config.verify_peer(!args.no_verify);
110    }
111
112    config.set_application_protos(&conn_args.alpns).unwrap();
113
114    config.set_initial_rtt(conn_args.initial_rtt);
115    config.set_max_idle_timeout(conn_args.idle_timeout);
116    config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE);
117    config.set_max_send_udp_payload_size(MAX_DATAGRAM_SIZE);
118    config.set_initial_max_data(conn_args.max_data);
119    config.set_initial_max_stream_data_bidi_local(conn_args.max_stream_data);
120    config.set_initial_max_stream_data_bidi_remote(conn_args.max_stream_data);
121    config.set_initial_max_stream_data_uni(conn_args.max_stream_data);
122    config.set_initial_max_streams_bidi(conn_args.max_streams_bidi);
123    config.set_initial_max_streams_uni(conn_args.max_streams_uni);
124    config.set_disable_active_migration(!conn_args.enable_active_migration);
125    config.set_active_connection_id_limit(conn_args.max_active_cids);
126    config.set_initial_congestion_window_packets(
127        usize::try_from(conn_args.initial_cwnd_packets).unwrap(),
128    );
129
130    config.set_max_connection_window(conn_args.max_window);
131    config.set_max_stream_window(conn_args.max_stream_window);
132
133    let mut keylog = None;
134
135    if let Some(keylog_path) = std::env::var_os("SSLKEYLOGFILE") {
136        let file = std::fs::OpenOptions::new()
137            .create(true)
138            .append(true)
139            .open(keylog_path)
140            .unwrap();
141
142        keylog = Some(file);
143
144        config.log_keys();
145    }
146
147    if conn_args.no_grease {
148        config.grease(false);
149    }
150
151    if conn_args.early_data {
152        config.enable_early_data();
153    }
154
155    config
156        .set_cc_algorithm_name(&conn_args.cc_algorithm)
157        .unwrap();
158
159    if conn_args.disable_hystart {
160        config.enable_hystart(false);
161    }
162
163    if conn_args.dgrams_enabled {
164        config.enable_dgram(true, 1000, 1000);
165    }
166
167    let mut http_conn: Option<Box<dyn HttpConn>> = None;
168
169    let mut app_proto_selected = false;
170
171    // Generate a random source connection ID for the connection.
172    let rng = SystemRandom::new();
173
174    let scid = if !cfg!(feature = "fuzzing") {
175        let mut conn_id = [0; quiche::MAX_CONN_ID_LEN];
176        rng.fill(&mut conn_id[..]).unwrap();
177
178        conn_id.to_vec()
179    } else {
180        // When fuzzing use an all zero connection ID.
181        [0; quiche::MAX_CONN_ID_LEN].to_vec()
182    };
183
184    let scid = quiche::ConnectionId::from_ref(&scid);
185
186    let local_addr = socket.local_addr().unwrap();
187
188    // Create a QUIC connection and initiate handshake.
189    let mut conn = quiche::connect(
190        connect_url.domain(),
191        &scid,
192        local_addr,
193        peer_addr,
194        &mut config,
195    )
196    .unwrap();
197
198    if let Some(keylog) = &mut keylog {
199        if let Ok(keylog) = keylog.try_clone() {
200            conn.set_keylog(Box::new(keylog));
201        }
202    }
203
204    // Only bother with qlog if the user specified it.
205    #[cfg(feature = "qlog")]
206    {
207        if let Some(dir) = std::env::var_os("QLOGDIR") {
208            let id = format!("{scid:?}");
209            let writer = make_qlog_writer(&dir, "client", &id);
210
211            conn.set_qlog(
212                std::boxed::Box::new(writer),
213                "quiche-client qlog".to_string(),
214                format!("{} id={}", "quiche-client qlog", id),
215            );
216        }
217    }
218
219    if let Some(session_file) = &args.session_file {
220        if let Ok(session) = std::fs::read(session_file) {
221            conn.set_session(&session).ok();
222        }
223    }
224
225    info!(
226        "connecting to {:} from {:} with scid {:?}",
227        peer_addr,
228        socket.local_addr().unwrap(),
229        scid,
230    );
231
232    let (write, send_info) = conn.send(&mut out).expect("initial send failed");
233
234    while let Err(e) = socket.send_to(&out[..write], send_info.to) {
235        if e.kind() == std::io::ErrorKind::WouldBlock {
236            trace!(
237                "{} -> {}: send() would block",
238                socket.local_addr().unwrap(),
239                send_info.to
240            );
241            continue;
242        }
243
244        return Err(ClientError::Other(format!("send() failed: {e:?}")));
245    }
246
247    trace!("written {write}");
248
249    let app_data_start = std::time::Instant::now();
250
251    let mut pkt_count = 0;
252
253    let mut scid_sent = false;
254    let mut new_path_probed = false;
255    let mut migrated = false;
256
257    loop {
258        if !conn.is_in_early_data() || app_proto_selected {
259            poll.poll(&mut events, conn.timeout()).unwrap();
260        }
261
262        // If the event loop reported no events, it means that the timeout
263        // has expired, so handle it without attempting to read packets. We
264        // will then proceed with the send loop.
265        if events.is_empty() {
266            trace!("timed out");
267
268            conn.on_timeout();
269        }
270
271        // Read incoming UDP packets from the socket and feed them to quiche,
272        // until there are no more packets to read.
273        for event in &events {
274            let socket = match event.token() {
275                mio::Token(0) => &socket,
276
277                mio::Token(1) => migrate_socket.as_ref().unwrap(),
278
279                _ => unreachable!(),
280            };
281
282            let local_addr = socket.local_addr().unwrap();
283            'read: loop {
284                let (len, from) = match socket.recv_from(&mut buf) {
285                    Ok(v) => v,
286
287                    Err(e) => {
288                        // There are no more UDP packets to read on this socket.
289                        // Process subsequent events.
290                        if e.kind() == std::io::ErrorKind::WouldBlock {
291                            trace!("{local_addr}: recv() would block");
292                            break 'read;
293                        }
294
295                        return Err(ClientError::Other(format!(
296                            "{local_addr}: recv() failed: {e:?}"
297                        )));
298                    },
299                };
300
301                trace!("got {len} bytes from {from} to {local_addr}");
302
303                if let Some(target_path) = conn_args.dump_packet_path.as_ref() {
304                    let path = format!("{target_path}/{pkt_count}.pkt");
305
306                    if let Ok(f) = std::fs::File::create(path) {
307                        let mut f = std::io::BufWriter::new(f);
308                        f.write_all(&buf[..len]).ok();
309                    }
310                }
311
312                pkt_count += 1;
313
314                let recv_info = quiche::RecvInfo {
315                    to: local_addr,
316                    from,
317                };
318
319                // Process potentially coalesced packets.
320                let read = match conn.recv(&mut buf[..len], recv_info) {
321                    Ok(v) => v,
322
323                    Err(e) => {
324                        error!("{local_addr}: recv failed: {e:?}");
325                        continue 'read;
326                    },
327                };
328
329                trace!("{local_addr}: processed {read} bytes");
330            }
331        }
332
333        trace!("done reading");
334
335        if conn.is_closed() {
336            info!(
337                "connection closed, {:?} {:?}",
338                conn.stats(),
339                conn.path_stats().collect::<Vec<quiche::PathStats>>()
340            );
341
342            if !conn.is_established() {
343                error!(
344                    "connection timed out after {:?}",
345                    app_data_start.elapsed(),
346                );
347
348                return Err(ClientError::HandshakeFail);
349            }
350
351            if let Some(session_file) = &args.session_file {
352                if let Some(session) = conn.session() {
353                    std::fs::write(session_file, session).ok();
354                }
355            }
356
357            if let Some(h_conn) = http_conn {
358                if h_conn.report_incomplete(&app_data_start) {
359                    return Err(ClientError::HttpFail);
360                }
361            }
362
363            break;
364        }
365
366        // Create a new application protocol session once the QUIC connection is
367        // established.
368        if (conn.is_established() || conn.is_in_early_data()) &&
369            (!args.perform_migration || migrated) &&
370            !app_proto_selected
371        {
372            // At this stage the ALPN negotiation succeeded and selected a
373            // single application protocol name. We'll use this to construct
374            // the correct type of HttpConn but `application_proto()`
375            // returns a slice, so we have to convert it to a str in order
376            // to compare to our lists of protocols. We `unwrap()` because
377            // we need the value and if something fails at this stage, there
378            // is not much anyone can do to recover.
379
380            let app_proto = conn.application_proto();
381
382            if alpns::HTTP_09.contains(&app_proto) {
383                http_conn = Some(Http09Conn::with_urls(
384                    &args.urls,
385                    args.reqs_cardinal,
386                    Rc::clone(&output_sink),
387                ));
388
389                app_proto_selected = true;
390            } else if alpns::HTTP_3.contains(&app_proto) {
391                let dgram_sender = if conn_args.dgrams_enabled {
392                    Some(Http3DgramSender::new(
393                        conn_args.dgram_count,
394                        conn_args.dgram_data.clone(),
395                        0,
396                    ))
397                } else {
398                    None
399                };
400
401                http_conn = Some(Http3Conn::with_urls(
402                    &mut conn,
403                    &args.urls,
404                    args.reqs_cardinal,
405                    &args.req_headers,
406                    &args.body,
407                    &args.method,
408                    args.send_priority_update,
409                    conn_args.max_field_section_size,
410                    conn_args.qpack_max_table_capacity,
411                    conn_args.qpack_blocked_streams,
412                    args.dump_json,
413                    dgram_sender,
414                    Rc::clone(&output_sink),
415                ));
416
417                app_proto_selected = true;
418            }
419        }
420
421        // If we have an HTTP connection, first issue the requests then
422        // process received data.
423        if let Some(h_conn) = http_conn.as_mut() {
424            h_conn.send_requests(&mut conn, &args.dump_response_path);
425            h_conn.handle_responses(&mut conn, &mut buf, &app_data_start);
426        }
427
428        // Handle path events.
429        while let Some(qe) = conn.path_event_next() {
430            match qe {
431                quiche::PathEvent::New(..) => unreachable!(),
432
433                quiche::PathEvent::Validated(local_addr, peer_addr) => {
434                    info!("Path ({local_addr}, {peer_addr}) is now validated");
435                    conn.migrate(local_addr, peer_addr).unwrap();
436                    migrated = true;
437                },
438
439                quiche::PathEvent::FailedValidation(local_addr, peer_addr) => {
440                    info!("Path ({local_addr}, {peer_addr}) failed validation");
441                },
442
443                quiche::PathEvent::Closed(local_addr, peer_addr) => {
444                    info!(
445                        "Path ({local_addr}, {peer_addr}) is now closed and unusable"
446                    );
447                },
448
449                quiche::PathEvent::ReusedSourceConnectionId(
450                    cid_seq,
451                    old,
452                    new,
453                ) => {
454                    info!(
455                        "Peer reused cid seq {cid_seq} (initially {old:?}) on {new:?}"
456                    );
457                },
458
459                quiche::PathEvent::PeerMigrated(..) => unreachable!(),
460            }
461        }
462
463        // See whether source Connection IDs have been retired.
464        while let Some(retired_scid) = conn.retired_scid_next() {
465            info!("Retiring source CID {retired_scid:?}");
466        }
467
468        // Provides as many CIDs as possible.
469        while conn.scids_left() > 0 {
470            let (scid, reset_token) = generate_cid_and_reset_token(&rng);
471
472            if conn.new_scid(&scid, reset_token, false).is_err() {
473                break;
474            }
475
476            scid_sent = true;
477        }
478
479        if args.perform_migration &&
480            !new_path_probed &&
481            scid_sent &&
482            conn.available_dcids() > 0
483        {
484            let additional_local_addr =
485                migrate_socket.as_ref().unwrap().local_addr().unwrap();
486            conn.probe_path(additional_local_addr, peer_addr).unwrap();
487
488            new_path_probed = true;
489        }
490
491        // Generate outgoing QUIC packets and send them on the UDP socket, until
492        // quiche reports that there are no more packets to be sent.
493        let mut sockets = vec![&socket];
494        if let Some(migrate_socket) = migrate_socket.as_ref() {
495            sockets.push(migrate_socket);
496        }
497
498        for socket in sockets {
499            let local_addr = socket.local_addr().unwrap();
500
501            for peer_addr in conn.paths_iter(local_addr) {
502                loop {
503                    let (write, send_info) = match conn.send_on_path(
504                        &mut out,
505                        Some(local_addr),
506                        Some(peer_addr),
507                    ) {
508                        Ok(v) => v,
509
510                        Err(quiche::Error::Done) => {
511                            trace!("{local_addr} -> {peer_addr}: done writing");
512                            break;
513                        },
514
515                        Err(e) => {
516                            error!(
517                                "{local_addr} -> {peer_addr}: send failed: {e:?}"
518                            );
519
520                            conn.close(false, 0x1, b"fail").ok();
521                            break;
522                        },
523                    };
524
525                    if let Err(e) = socket.send_to(&out[..write], send_info.to) {
526                        if e.kind() == std::io::ErrorKind::WouldBlock {
527                            trace!(
528                                "{} -> {}: send() would block",
529                                local_addr,
530                                send_info.to
531                            );
532                            break;
533                        }
534
535                        return Err(ClientError::Other(format!(
536                            "{} -> {}: send() failed: {:?}",
537                            local_addr, send_info.to, e
538                        )));
539                    }
540
541                    trace!(
542                        "written {write} bytes from {local_addr} to {}",
543                        send_info.to
544                    );
545                }
546            }
547        }
548
549        if conn.is_closed() {
550            info!(
551                "connection closed, {:?} {:?}",
552                conn.stats(),
553                conn.path_stats().collect::<Vec<quiche::PathStats>>()
554            );
555
556            if !conn.is_established() {
557                error!(
558                    "connection timed out after {:?}",
559                    app_data_start.elapsed(),
560                );
561
562                return Err(ClientError::HandshakeFail);
563            }
564
565            if let Some(session_file) = &args.session_file {
566                if let Some(session) = conn.session() {
567                    std::fs::write(session_file, session).ok();
568                }
569            }
570
571            if let Some(h_conn) = http_conn {
572                if h_conn.report_incomplete(&app_data_start) {
573                    return Err(ClientError::HttpFail);
574                }
575            }
576
577            break;
578        }
579    }
580
581    Ok(())
582}