blob: 9d2c9531d443480a213946fb9794a36434b03fcc [file] [log] [blame]
// Copyright (C) 2020, Cloudflare, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#[macro_use]
extern crate log;
use std::net::ToSocketAddrs;
use std::io::prelude::*;
use ring::rand::*;
use quiche_apps::*;
const MAX_DATAGRAM_SIZE: usize = 1350;
const USAGE: &str = "Usage:
quiche-client [options] URL...
quiche-client -h | --help
Options:
--method METHOD Use the given HTTP request method [default: GET].
--body FILE Send the given file as request body.
--max-data BYTES Connection-wide flow control limit [default: 10000000].
--max-stream-data BYTES Per-stream flow control limit [default: 1000000].
--max-streams-bidi STREAMS Number of allowed concurrent streams [default: 100].
--max-streams-uni STREAMS Number of allowed concurrent streams [default: 100].
--idle-timeout TIMEOUT Idle timeout in milliseconds [default: 30000].
--wire-version VERSION The version number to send to the server [default: babababa].
--http-version VERSION HTTP version to use [default: all].
--dgram-proto PROTO DATAGRAM application protocol to use [default: none].
--dgram-count COUNT Number of DATAGRAMs to send [default: 0].
--dgram-data DATA Data to send for certain types of DATAGRAM application protocol [default: quack].
--dump-packets PATH Dump the incoming packets as files in the given directory.
--dump-responses PATH Dump response payload as files in the given directory.
--dump-json Dump response headers and payload to stdout.
--connect-to ADDRESS Override ther server's address.
--no-verify Don't verify server's certificate.
--no-grease Don't send GREASE.
--cc-algorithm NAME Specify which congestion control algorithm to use [default: cubic].
--disable-hystart Disable HyStart++.
-H --header HEADER ... Add a request header.
-n --requests REQUESTS Send the given number of identical requests [default: 1].
-h --help Show this screen.
";
fn main() {
let mut buf = [0; 65535];
let mut out = [0; MAX_DATAGRAM_SIZE];
env_logger::builder()
.default_format_timestamp_nanos(true)
.init();
// Parse CLI parameters.
let docopt = docopt::Docopt::new(USAGE).unwrap();
let conn_args = CommonArgs::with_docopt(&docopt);
let args = ClientArgs::with_docopt(&docopt);
// Setup the event loop.
let poll = mio::Poll::new().unwrap();
let mut events = mio::Events::with_capacity(1024);
// We'll only connect to the first server provided in URL list.
let connect_url = &args.urls[0];
// Resolve server address.
let peer_addr = if let Some(addr) = &args.connect_to {
addr.parse().unwrap()
} else {
connect_url.to_socket_addrs().unwrap().next().unwrap()
};
// Bind to INADDR_ANY or IN6ADDR_ANY depending on the IP family of the
// server address. This is needed on macOS and BSD variants that don't
// support binding to IN6ADDR_ANY for both v4 and v6.
let bind_addr = match peer_addr {
std::net::SocketAddr::V4(_) => "0.0.0.0:0",
std::net::SocketAddr::V6(_) => "[::]:0",
};
// Create the UDP socket backing the QUIC connection, and register it with
// the event loop.
let socket = std::net::UdpSocket::bind(bind_addr).unwrap();
socket.connect(peer_addr).unwrap();
let socket = mio::net::UdpSocket::from_socket(socket).unwrap();
poll.register(
&socket,
mio::Token(0),
mio::Ready::readable(),
mio::PollOpt::edge(),
)
.unwrap();
// Create the configuration for the QUIC connection.
let mut config = quiche::Config::new(args.version).unwrap();
config.verify_peer(!args.no_verify);
config.set_application_protos(&conn_args.alpns).unwrap();
config.set_max_idle_timeout(conn_args.idle_timeout);
config.set_max_udp_payload_size(MAX_DATAGRAM_SIZE as u64);
config.set_initial_max_data(conn_args.max_data);
config.set_initial_max_stream_data_bidi_local(conn_args.max_stream_data);
config.set_initial_max_stream_data_bidi_remote(conn_args.max_stream_data);
config.set_initial_max_stream_data_uni(conn_args.max_stream_data);
config.set_initial_max_streams_bidi(conn_args.max_streams_bidi);
config.set_initial_max_streams_uni(conn_args.max_streams_uni);
config.set_disable_active_migration(true);
let mut keylog = None;
if let Some(keylog_path) = std::env::var_os("SSLKEYLOGFILE") {
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(keylog_path)
.unwrap();
keylog = Some(file);
config.log_keys();
}
if conn_args.no_grease {
config.grease(false);
}
config
.set_cc_algorithm_name(&conn_args.cc_algorithm)
.unwrap();
if conn_args.disable_hystart {
config.enable_hystart(false);
}
if conn_args.dgrams_enabled {
config.enable_dgram(true, 1000, 1000);
}
let mut http_conn: Option<Box<dyn HttpConn>> = None;
let mut siduck_conn: Option<SiDuckConn> = None;
let mut app_proto_selected = false;
// Generate a random source connection ID for the connection.
let mut scid = [0; quiche::MAX_CONN_ID_LEN];
SystemRandom::new().fill(&mut scid[..]).unwrap();
// Create a QUIC connection and initiate handshake.
let mut conn =
quiche::connect(connect_url.domain(), &scid, &mut config).unwrap();
if let Some(keylog) = &mut keylog {
if let Ok(keylog) = keylog.try_clone() {
conn.set_keylog(Box::new(keylog));
}
}
// Only bother with qlog if the user specified it.
#[cfg(feature = "qlog")]
{
if let Some(dir) = std::env::var_os("QLOGDIR") {
let id = hex_dump(&scid);
let writer = make_qlog_writer(&dir, "client", &id);
conn.set_qlog(
std::boxed::Box::new(writer),
"quiche-client qlog".to_string(),
format!("{} id={}", "quiche-client qlog", id),
);
}
}
info!(
"connecting to {:} from {:} with scid {}",
peer_addr,
socket.local_addr().unwrap(),
hex_dump(&scid)
);
let write = conn.send(&mut out).expect("initial send failed");
while let Err(e) = socket.send(&out[..write]) {
if e.kind() == std::io::ErrorKind::WouldBlock {
trace!("send() would block");
continue;
}
panic!("send() failed: {:?}", e);
}
trace!("written {}", write);
let app_data_start = std::time::Instant::now();
let mut pkt_count = 0;
loop {
poll.poll(&mut events, conn.timeout()).unwrap();
// Read incoming UDP packets from the socket and feed them to quiche,
// until there are no more packets to read.
'read: loop {
// If the event loop reported no events, it means that the timeout
// has expired, so handle it without attempting to read packets. We
// will then proceed with the send loop.
if events.is_empty() {
trace!("timed out");
conn.on_timeout();
break 'read;
}
let len = match socket.recv(&mut buf) {
Ok(v) => v,
Err(e) => {
// There are no more UDP packets to read, so end the read
// loop.
if e.kind() == std::io::ErrorKind::WouldBlock {
trace!("recv() would block");
break 'read;
}
panic!("recv() failed: {:?}", e);
},
};
trace!("got {} bytes", len);
if let Some(target_path) = conn_args.dump_packet_path.as_ref() {
let path = format!("{}/{}.pkt", target_path, pkt_count);
if let Ok(f) = std::fs::File::create(&path) {
let mut f = std::io::BufWriter::new(f);
f.write_all(&buf[..len]).ok();
}
}
pkt_count += 1;
// Process potentially coalesced packets.
let read = match conn.recv(&mut buf[..len]) {
Ok(v) => v,
Err(e) => {
error!("recv failed: {:?}", e);
continue 'read;
},
};
trace!("processed {} bytes", read);
}
trace!("done reading");
if conn.is_closed() {
info!("connection closed, {:?}", conn.stats());
if let Some(h_conn) = http_conn {
h_conn.report_incomplete(&app_data_start);
}
if let Some(si_conn) = siduck_conn {
si_conn.report_incomplete(&app_data_start);
}
break;
}
// Create a new application protocol session once the QUIC connection is
// established.
if conn.is_established() && !app_proto_selected {
// At this stage the ALPN negotiation succeeded and selected a
// single application protocol name. We'll use this to construct
// the correct type of HttpConn but `application_proto()`
// returns a slice, so we have to convert it to a str in order
// to compare to our lists of protocols. We `unwrap()` because
// we need the value and if something fails at this stage, there
// is not much anyone can do to recover.
let app_proto = conn.application_proto();
let app_proto = &std::str::from_utf8(&app_proto).unwrap();
if alpns::HTTP_09.contains(app_proto) {
http_conn =
Some(Http09Conn::with_urls(&args.urls, args.reqs_cardinal));
app_proto_selected = true;
} else if alpns::HTTP_3.contains(app_proto) {
let dgram_sender = if conn_args.dgrams_enabled {
Some(Http3DgramSender::new(
conn_args.dgram_count,
conn_args.dgram_data.clone(),
0,
))
} else {
None
};
http_conn = Some(Http3Conn::with_urls(
&mut conn,
&args.urls,
args.reqs_cardinal,
&args.req_headers,
&args.body,
&args.method,
args.dump_json,
dgram_sender,
));
app_proto_selected = true;
} else if alpns::SIDUCK.contains(app_proto) {
siduck_conn = Some(SiDuckConn::new(
conn_args.dgram_count,
conn_args.dgram_data.clone(),
));
app_proto_selected = true;
}
}
// If we have an HTTP connection, first issue the requests then
// process received data.
if let Some(h_conn) = http_conn.as_mut() {
h_conn.send_requests(&mut conn, &args.dump_response_path);
h_conn.handle_responses(&mut conn, &mut buf, &app_data_start);
}
// If we have a siduck connection, first issue the quacks then
// process received data.
if let Some(si_conn) = siduck_conn.as_mut() {
si_conn.send_quacks(&mut conn);
si_conn.handle_quack_acks(&mut conn, &mut buf, &app_data_start);
}
// Generate outgoing QUIC packets and send them on the UDP socket, until
// quiche reports that there are no more packets to be sent.
loop {
let write = match conn.send(&mut out) {
Ok(v) => v,
Err(quiche::Error::Done) => {
trace!("done writing");
break;
},
Err(e) => {
error!("send failed: {:?}", e);
conn.close(false, 0x1, b"fail").ok();
break;
},
};
if let Err(e) = socket.send(&out[..write]) {
if e.kind() == std::io::ErrorKind::WouldBlock {
trace!("send() would block");
break;
}
panic!("send() failed: {:?}", e);
}
trace!("written {}", write);
}
if conn.is_closed() {
info!("connection closed, {:?}", conn.stats());
if let Some(h_conn) = http_conn {
h_conn.report_incomplete(&app_data_start);
}
break;
}
}
}
/// Application-specific arguments that compliment the `CommonArgs`.
struct ClientArgs {
version: u32,
dump_response_path: Option<String>,
dump_json: bool,
urls: Vec<url::Url>,
reqs_cardinal: u64,
req_headers: Vec<String>,
no_verify: bool,
body: Option<Vec<u8>>,
method: String,
connect_to: Option<String>,
}
impl Args for ClientArgs {
fn with_docopt(docopt: &docopt::Docopt) -> Self {
let args = docopt.parse().unwrap_or_else(|e| e.exit());
let version = args.get_str("--wire-version");
let version = u32::from_str_radix(version, 16).unwrap();
let dump_response_path = if args.get_str("--dump-responses") != "" {
Some(args.get_str("--dump-responses").to_string())
} else {
None
};
let dump_json = args.get_bool("--dump-json");
// URLs (can be multiple).
let urls: Vec<url::Url> = args
.get_vec("URL")
.into_iter()
.map(|x| url::Url::parse(x).unwrap())
.collect();
// Request headers (can be multiple).
let req_headers = args
.get_vec("--header")
.into_iter()
.map(|x| x.to_string())
.collect();
let reqs_cardinal = args.get_str("--requests");
let reqs_cardinal = u64::from_str_radix(reqs_cardinal, 10).unwrap();
let no_verify = args.get_bool("--no-verify");
let body = if args.get_bool("--body") {
std::fs::read(args.get_str("--body")).ok()
} else {
None
};
let method = args.get_str("--method").to_string();
let connect_to = if args.get_bool("--connect-to") {
Some(args.get_str("--connect-to").to_string())
} else {
None
};
ClientArgs {
version,
dump_response_path,
dump_json,
urls,
req_headers,
reqs_cardinal,
no_verify,
body,
method,
connect_to,
}
}
}