impl std::str::FromStr for EchConfig { type Err = hex::FromHexError; fn from_str(s: &str) -> Result<Self, Self::Err> {
hex::decode(s).map(EchConfig)
}
} use futures::{
FutureExt as _, TryFutureExt as _,
future::{Either, select},
}; use http::Uri as Url; use neqo_common::{Datagram, Role, qdebug, qerror, qinfo, qlog::Qlog}; use neqo_http3::Header; use neqo_transport::{AppError, CloseReason, ConnectionId, OutputBatch, Version}; use neqo_udp::RecvBuf; use nss::{
Cipher, ResumptionToken,
constants::{TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256},
init,
}; use rustc_hash::FxHashMap as HashMap; use thiserror::Error; use tokio::time::Sleep;
#[arg(name = "download-in-series", long)] /// Download resources in series using separate connections.
download_in_series: bool,
#[arg(name = "concurrency", long, default_value = "100")] /// The maximum number of requests to have outstanding at one time.
concurrency: usize,
#[arg(name = "output-read-data", long)] /// Output received data to stdout
output_read_data: bool,
#[arg(name = "output-dir", long)] /// Save contents of fetched URLs to a directory
output_dir: Option<PathBuf>,
#[arg(short = 'r', long, hide = true)] /// Client attempts to resume by making multiple connections to servers. /// Requires that 2 or more URLs are listed for each server. /// Use this for 0-RTT: the stack always attempts 0-RTT on resumption.
resume: bool,
#[arg(long)] /// Save the resumption token to a file after connecting.
save_token: Option<PathBuf>,
#[arg(long)] /// Load a resumption token from a file and attempt 0-RTT.
load_token: Option<PathBuf>,
#[arg(name = "key-update", long, hide = true)] /// Attempt to initiate a key update immediately after confirming the connection.
key_update: bool,
#[arg(name = "ech", long)] /// Enable encrypted client hello (ECH). /// This takes an encoded ECH configuration in hexadecimal format.
ech: Option<EchConfig>,
#[arg(name = "ipv4-only", short = '4', long)] /// Connect only over IPv4
ipv4_only: bool,
#[arg(name = "ipv6-only", short = '6', long)] /// Connect only over IPv6
ipv6_only: bool,
/// The test that this client will run. Currently, we only support "upload". #[arg(name = "test", long)]
test: Option<String>,
/// The request size that will be used for upload test. #[arg(name = "upload-size", long, default_value = "100")]
upload_size: usize,
/// The length of the local connection ID. #[arg(name = "cid-length", short = 'l', long, default_value = "0",
value_parser = clap::value_parser!(u8).range(..=20))]
cid_len: u8,
}
ifself.key_update {
qerror!("internal option key_update set by user");
exit(127)
}
ifself.resume {
qerror!("internal option resume set by user");
exit(127)
}
// Only use v1 for most QNS tests. self.shared.quic_parameters.quic_version = vec![Version::Version1]; // This is the default for all tests except http3. self.shared.alpn = String::from("hq-interop"); // Wireshark can't reassemble sliced CRYPTO frames, which causes tests to fail. // So let's turn that off by default, and only enable for some known-good QNS tests. self.shared.quic_parameters.no_sni_slicing = true; match testcase.as_str() { "http3" => { self.shared.quic_parameters.no_sni_slicing = false; self.shared.alpn = String::from("h3"); iflet Some(testcase) = &self.test { if testcase.as_str() != "upload" {
qerror!("Unsupported test case: {testcase}");
exit(127)
}
self.method = String::from("POST");
}
} "handshake" | "transfer" | "retry" | "ecn" => {} "resumption" => { ifself.urls.len() < 2 {
qerror!("Warning: resumption test won't work without >1 URL");
exit(127);
} self.resume = true;
} "zerortt" => { ifself.urls.len() < 2 {
qerror!("Warning: zerortt test won't work without >1 URL");
exit(127);
} self.shared.quic_parameters.no_sni_slicing = false; self.resume = true; // PMTUD probes inflate what we sent in 1-RTT, causing QNS to fail the test. self.shared.quic_parameters.no_pmtud = true; // If we pace, we might get the initial server flight before sending sufficient // 0-RTT data to pass the QNS check. So let's burst. self.shared.quic_parameters.no_pacing = true;
} "multiconnect" => { self.download_in_series = true;
} "chacha20" => { self.shared.ciphers.clear(); self.shared
.ciphers
.extend_from_slice(&[String::from("TLS_CHACHA20_POLY1305_SHA256")]);
} "keyupdate" => { self.key_update = true;
} "v2" => { self.shared.quic_parameters.no_sni_slicing = false; // Use default version set for this test (which allows compatible vneg.) self.shared.quic_parameters.quic_version.clear();
}
_ => exit(127),
}
}
let url_path = if url.path() == "/" { // If no path is given... call it "root"? "root"
} else { // Omit leading slash
&url.path()[1..]
};
out_path.push(url_path);
if all_paths.contains(&out_path) {
qerror!("duplicate path {}", out_path.display()); return None;
}
// Wait for the socket to be readable or the timeout to fire. asyncfn ready(
socket: &crate::udp::Socket, mut timeout: Option<&mut Pin<Box<Sleep>>>,
) -> Result<Ready, io::Error> { let socket_ready = Box::pin(socket.readable()).map_ok(|()| Ready::Socket); let timeout_ready = timeout
.as_mut()
.map_or_else(|| Either::Right(futures::future::pending()), Either::Left)
.map(|()| Ok(Ready::Timeout));
select(socket_ready, timeout_ready).await.factor_first().0
}
/// Handles a given task on the provided [`Client`]. trait Handler { type Client: Client;
// hostname might be an IPv6 address, e.g. `[::1]`. `:` is an invalid // Windows file name character. #[cfg(windows)] let hostname: String = hostname
.chars()
.map(|c| if c == ':' { '_' } else { c })
.collect();
for ((host, port), mut urls) in urls_by_origin(&args.urls) { if args.resume && urls.len() < 2 {
qerror!("Resumption to {host} cannot work without at least 2 URLs");
exit(127);
}
let remote_addr = format!("{host}:{port}").to_socket_addrs()?.find(|addr| {
!matches!(
(addr, args.ipv4_only, args.ipv6_only),
(SocketAddr::V4(..), false, true) | (SocketAddr::V6(..), true, false)
)
}); let Some(remote_addr) = remote_addr else {
qerror!("No compatible address found for: {host}");
exit(1);
}; letmut socket = crate::udp::Socket::bind(local_addr_for(&remote_addr, 0))?; if socket.may_fragment() {
qinfo!("Datagrams may be fragmented by the IP layer. Disabling PMTUD.");
args.shared.quic_parameters.no_pmtud = true;
} let real_local = socket.local_addr().unwrap();
qinfo!( "{} Client connecting: {real_local:?} -> {remote_addr:?}",
args.shared.alpn
);
letmut token: Option<ResumptionToken> = args
.load_token
.as_ref()
.map(|path| -> Res<_> {
Ok(ResumptionToken::new(
std::fs::read(path)?, // Expiry is a client-side hint only; the TLS ticket itself // carries its own lifetime enforced by the server.
now() + std::time::Duration::from_secs(86400),
))
})
.transpose()?; letmut first = true; while !urls.is_empty() { let to_request = if (args.resume && first) || args.download_in_series {
urls.pop_front().into_iter().collect()
} else {
std::mem::take(&mut urls)
};
first = false;
token = if args.shared.alpn == "h3" { let client = http3::create_client(&args, real_local, remote_addr, &host, token)
.expect("failed to create client");
let handler = http3::Handler::new(to_request, args.clone());
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.