mirror of
https://github.com/n0-computer/noq.git
synced 2026-10-04 21:43:54 +00:00
908 lines
28 KiB
Rust
908 lines
28 KiB
Rust
#![allow(clippy::mutex_atomic)]
|
|
|
|
use std::{
|
|
env,
|
|
net::{SocketAddr, ToSocketAddrs},
|
|
sync::{Arc, Mutex},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use anyhow::{anyhow, Context, Result};
|
|
use futures::future;
|
|
use hyper::body::HttpBody as _;
|
|
use lazy_static::lazy_static;
|
|
use quinn_h3::Settings;
|
|
use structopt::StructOpt;
|
|
use tracing::{debug, error, info, info_span, warn};
|
|
use tracing_futures::Instrument as _;
|
|
|
|
#[derive(StructOpt, Debug)]
|
|
#[structopt(name = "interop")]
|
|
struct Opt {
|
|
host: Option<String>,
|
|
#[structopt(short, long)]
|
|
name: Option<String>,
|
|
#[structopt(long)]
|
|
port: Option<u16>,
|
|
#[structopt(long)]
|
|
retry_port: Option<u16>,
|
|
#[structopt(long)]
|
|
h3: bool,
|
|
#[structopt(long)]
|
|
hq: bool,
|
|
#[structopt(long)]
|
|
seq: bool,
|
|
/// Enable key logging
|
|
#[structopt(long = "keylog")]
|
|
keylog: bool,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum Alpn {
|
|
Hq,
|
|
H3,
|
|
HqH3,
|
|
}
|
|
|
|
impl From<&Alpn> for Vec<Vec<u8>> {
|
|
fn from(alpn: &Alpn) -> Vec<Vec<u8>> {
|
|
match alpn {
|
|
Alpn::H3 => vec![quinn_h3::ALPN.into()],
|
|
Alpn::Hq => vec![b"hq-27"[..].into()],
|
|
Alpn::HqH3 => vec![b"hq-27"[..].into(), quinn_h3::ALPN.into()],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct Peer {
|
|
name: String,
|
|
host: String,
|
|
port: u16,
|
|
retry_port: u16,
|
|
alpn: Alpn,
|
|
sequential: bool,
|
|
}
|
|
|
|
impl Peer {
|
|
fn new<T: Into<String>>(host: T) -> Self {
|
|
let host_str = host.into();
|
|
Self {
|
|
name: host_str.clone(),
|
|
host: host_str,
|
|
port: 4433,
|
|
retry_port: 4434,
|
|
alpn: Alpn::HqH3,
|
|
sequential: false,
|
|
}
|
|
}
|
|
|
|
fn name<T: Into<String>>(mut self, name: T) -> Self {
|
|
self.name = name.into();
|
|
self
|
|
}
|
|
|
|
fn port(mut self, port: u16) -> Self {
|
|
self.port = port;
|
|
self
|
|
}
|
|
|
|
fn retry_port(mut self, port: u16) -> Self {
|
|
self.retry_port = port;
|
|
self
|
|
}
|
|
|
|
fn h3(mut self) -> Self {
|
|
self.alpn = Alpn::H3;
|
|
self
|
|
}
|
|
|
|
fn hq(mut self) -> Self {
|
|
self.alpn = Alpn::Hq;
|
|
self
|
|
}
|
|
|
|
fn uri(&self, path: &str) -> http::Uri {
|
|
http::Uri::builder()
|
|
.scheme("https")
|
|
.authority(self.host.as_str())
|
|
.path_and_query(path)
|
|
.build()
|
|
.expect("invalid uri")
|
|
}
|
|
}
|
|
|
|
lazy_static! {
|
|
static ref PEERS: Vec<Peer> = vec![
|
|
Peer::new("quant.eggert.org").name("quant").hq(),
|
|
Peer::new("nghttp2.org").name("nghttp2").h3(),
|
|
Peer::new("fb.mvfst.net").name("mvfst").h3(),
|
|
Peer::new("test.privateoctopus.com").name("picoquic").h3(),
|
|
Peer::new("quic.westus.cloudapp.azure.com")
|
|
.name("msquic")
|
|
.h3()
|
|
.port(443),
|
|
Peer::new("quic.westus.cloudapp.azure.com")
|
|
.name("msquic-hq")
|
|
.hq(),
|
|
Peer::new("f5quic.com").name("f5"),
|
|
Peer::new("quic.ogre.com").name("ATS"),
|
|
Peer::new("quic.tech").name("quiche-http/0.9").hq(),
|
|
Peer::new("quic.tech")
|
|
.name("quiche")
|
|
.h3()
|
|
.port(8443)
|
|
.retry_port(8444),
|
|
Peer::new("http3-test.litespeedtech.com")
|
|
.name("lsquic")
|
|
.h3(),
|
|
Peer::new("cloudflare-quic.com")
|
|
.name("ngx_quic")
|
|
.h3()
|
|
.port(443),
|
|
Peer::new("quic.aiortc.org").name("aioquic"),
|
|
Peer::new("quic.rocks").name("gQuic"),
|
|
];
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
let opt = Opt::from_args();
|
|
let mut code = 0;
|
|
if env::var("RUST_LOG").is_err() {
|
|
env::set_var("RUST_LOG", "info");
|
|
}
|
|
tracing::subscriber::set_global_default(
|
|
tracing_subscriber::FmtSubscriber::builder()
|
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
|
.finish(),
|
|
)
|
|
.unwrap();
|
|
let peers = if let Some(host) = opt.host {
|
|
vec![Peer {
|
|
name: host.clone(),
|
|
host,
|
|
port: opt.port.unwrap_or(4433),
|
|
retry_port: opt.retry_port.unwrap_or(4434),
|
|
sequential: opt.seq,
|
|
alpn: match (opt.h3, opt.hq) {
|
|
(false, true) => Alpn::Hq,
|
|
(true, false) => Alpn::H3,
|
|
_ => Alpn::HqH3,
|
|
},
|
|
}]
|
|
} else if opt.name.is_some() {
|
|
let name = opt.name.as_ref().unwrap();
|
|
let mut peers: Vec<Peer> = PEERS[..]
|
|
.iter()
|
|
.filter(|p| &p.name == name)
|
|
.cloned()
|
|
.collect();
|
|
|
|
peers.iter_mut().for_each(|mut x| {
|
|
match (opt.h3, opt.hq) {
|
|
(false, true) => x.alpn = Alpn::Hq,
|
|
(true, false) => x.alpn = Alpn::H3,
|
|
_ => (),
|
|
}
|
|
if let Some(port) = opt.port {
|
|
x.port = port;
|
|
}
|
|
if let Some(retry_port) = opt.retry_port {
|
|
x.retry_port = retry_port;
|
|
}
|
|
x.sequential = opt.seq;
|
|
});
|
|
peers
|
|
} else {
|
|
Vec::from(&PEERS[..])
|
|
};
|
|
|
|
let keylog = opt.keylog;
|
|
let results = future::join_all(peers.into_iter().map(|peer| async move {
|
|
let span = info_span!("peer", name = peer.name.as_str());
|
|
let result = run(&peer, keylog).instrument(span).await;
|
|
(peer, result)
|
|
}));
|
|
for (peer, result) in results.await {
|
|
match result {
|
|
Ok(r) => println!("{}: {}", peer.name, r),
|
|
Err(e) => {
|
|
println!("ERROR: {}: {}", peer.name, e);
|
|
code = 1;
|
|
}
|
|
}
|
|
}
|
|
::std::process::exit(code);
|
|
}
|
|
|
|
async fn run(peer: &Peer, keylog: bool) -> Result<String> {
|
|
let state = State::try_new(&peer, keylog)?;
|
|
let result = match peer.alpn {
|
|
Alpn::Hq => state.run_hq().instrument(info_span!("hq")).await?,
|
|
_ => state.run_h3().instrument(info_span!("h3")).await?,
|
|
};
|
|
Ok(result.format())
|
|
}
|
|
|
|
struct State {
|
|
endpoint: quinn::Endpoint,
|
|
client_config: quinn::ClientConfig,
|
|
remote: SocketAddr,
|
|
host: String,
|
|
peer: Peer,
|
|
h3_client: Option<quinn_h3::client::Client>,
|
|
}
|
|
|
|
impl State {
|
|
async fn run_hq(self) -> Result<InteropResult> {
|
|
// We run core on its own first to ensure the 0-RTT token can be used reliably
|
|
let core = self.core_hq().instrument(info_span!("core")).await;
|
|
if self.peer.sequential {
|
|
Ok(build_result(
|
|
core,
|
|
self.key_update_hq()
|
|
.instrument(info_span!("key_update"))
|
|
.await,
|
|
self.rebind_hq().instrument(info_span!("rebind")).await,
|
|
self.retry_hq().instrument(info_span!("retry")).await,
|
|
self.throughput_hq()
|
|
.instrument(info_span!("throughput"))
|
|
.await,
|
|
None,
|
|
None,
|
|
))
|
|
} else {
|
|
let (key_update, rebind, retry, throughput) = tokio::join!(
|
|
self.key_update_hq().instrument(info_span!("key_update")),
|
|
self.rebind_hq().instrument(info_span!("rebind")),
|
|
self.retry_hq().instrument(info_span!("retry")),
|
|
self.throughput_hq().instrument(info_span!("throughput"))
|
|
);
|
|
Ok(build_result(
|
|
core, key_update, rebind, retry, throughput, None, None,
|
|
))
|
|
}
|
|
}
|
|
|
|
async fn run_h3(self) -> Result<InteropResult> {
|
|
// We run core on its own first to ensure the 0-RTT token can be used reliably
|
|
let core = self.core_h3().instrument(info_span!("core")).await;
|
|
if self.peer.sequential {
|
|
Ok(build_result(
|
|
core,
|
|
self.key_update_h3()
|
|
.instrument(info_span!("key_update"))
|
|
.await,
|
|
self.rebind_h3().instrument(info_span!("rebind")).await,
|
|
self.retry_h3().instrument(info_span!("retry")).await,
|
|
self.throughput_h3()
|
|
.instrument(info_span!("throughput"))
|
|
.await,
|
|
Some(self.h3().instrument(info_span!("h3")).await),
|
|
Some(
|
|
self.dynamic_encoding()
|
|
.instrument(info_span!("dynamic_encoding"))
|
|
.await,
|
|
),
|
|
))
|
|
} else {
|
|
let (key_update, rebind, retry, throughput, h3, dynamic_encoding) = tokio::join!(
|
|
self.key_update_h3().instrument(info_span!("key_update")),
|
|
self.rebind_h3().instrument(info_span!("rebind")),
|
|
self.retry_h3().instrument(info_span!("retry")),
|
|
self.throughput_h3().instrument(info_span!("throughput")),
|
|
self.h3().instrument(info_span!("h3")),
|
|
self.dynamic_encoding()
|
|
.instrument(info_span!("dynamic_encoding")),
|
|
);
|
|
Ok(build_result(
|
|
core,
|
|
key_update,
|
|
rebind,
|
|
retry,
|
|
throughput,
|
|
Some(h3),
|
|
Some(dynamic_encoding),
|
|
))
|
|
}
|
|
}
|
|
|
|
fn try_new(peer: &Peer, keylog: bool) -> Result<Self> {
|
|
let remote = format!("{}:{}", peer.host, peer.port)
|
|
.to_socket_addrs()?
|
|
.next()
|
|
.ok_or_else(|| anyhow!("couldn't resolve to an address"))?;
|
|
let host = if webpki::DNSNameRef::try_from_ascii_str(&peer.host).is_ok() {
|
|
&peer.host
|
|
} else {
|
|
warn!("invalid hostname, using \"example.com\"");
|
|
"example.com"
|
|
};
|
|
|
|
let mut tls_config = rustls::ClientConfig::new();
|
|
tls_config.versions = vec![rustls::ProtocolVersion::TLSv1_3];
|
|
tls_config.enable_early_data = true;
|
|
tls_config
|
|
.dangerous()
|
|
.set_certificate_verifier(Arc::new(InteropVerifier(Arc::new(Mutex::new(false)))));
|
|
tls_config.alpn_protocols = (&peer.alpn).into();
|
|
if keylog {
|
|
tls_config.key_log = Arc::new(rustls::KeyLogFile::new());
|
|
}
|
|
let mut transport = quinn::TransportConfig::default();
|
|
transport.initial_window(1024 * 1024 * 5);
|
|
transport.send_window(1024 * 1024 * 2);
|
|
transport.receive_window(1024 * 1024 * 2);
|
|
transport
|
|
.max_idle_timeout(Some(Duration::from_secs(1)))
|
|
.unwrap();
|
|
let client_config = quinn::ClientConfig {
|
|
crypto: Arc::new(tls_config),
|
|
transport: Arc::new(transport),
|
|
};
|
|
|
|
let mut endpoint = quinn::Endpoint::builder();
|
|
endpoint.default_client_config(client_config.clone());
|
|
|
|
let (endpoint, _) = endpoint.bind(&"[::]:0".parse().unwrap())?;
|
|
|
|
let h3_client = match peer.alpn {
|
|
Alpn::Hq => None,
|
|
_ => {
|
|
let mut h3_client = quinn_h3::client::Builder::default();
|
|
h3_client.settings(Settings::new());
|
|
Some(h3_client.endpoint(endpoint.clone()))
|
|
}
|
|
};
|
|
Ok(State {
|
|
h3_client,
|
|
host: host.into(),
|
|
peer: peer.clone(),
|
|
client_config,
|
|
endpoint,
|
|
remote,
|
|
})
|
|
}
|
|
|
|
async fn core_hq(&self) -> Result<InteropResult> {
|
|
let mut result = InteropResult::default();
|
|
let new_conn = self
|
|
.endpoint
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("failed to connect: {}", e))?;
|
|
result.handshake = true;
|
|
let stream = new_conn
|
|
.connection
|
|
.open_bi()
|
|
.await
|
|
.map_err(|e| anyhow!("failed to open stream: {}", e))?;
|
|
hq_get(stream, "/")
|
|
.await
|
|
.map_err(|e| anyhow!("simple request failed: {}", e))?;
|
|
result.stream_data = true;
|
|
new_conn.connection.close(0u32.into(), b"done");
|
|
|
|
let saw_cert = Arc::new(Mutex::new(false));
|
|
let quinn::ClientConfig {
|
|
mut crypto,
|
|
transport,
|
|
} = self.client_config.clone();
|
|
Arc::make_mut(&mut crypto)
|
|
.dangerous()
|
|
.set_certificate_verifier(Arc::new(InteropVerifier(saw_cert.clone())));
|
|
let client_config = quinn::ClientConfig { crypto, transport };
|
|
|
|
let conn = match self
|
|
.endpoint
|
|
.connect_with(client_config, &self.remote, &self.host)?
|
|
.into_0rtt()
|
|
{
|
|
Ok((new_conn, _)) => {
|
|
let stream = new_conn
|
|
.connection
|
|
.open_bi()
|
|
.await
|
|
.map_err(|e| anyhow!("failed to open 0-RTT stream: {}", e))?;
|
|
hq_get(stream, "/")
|
|
.await
|
|
.map_err(|e| anyhow!("0-RTT request failed: {}", e))?;
|
|
result.zero_rtt = true;
|
|
new_conn.connection
|
|
}
|
|
Err(conn) => {
|
|
info!("0-RTT unsupported");
|
|
let new_conn = conn
|
|
.await
|
|
.map_err(|e| anyhow!("failed to connect: {}", e))?;
|
|
new_conn.connection
|
|
}
|
|
};
|
|
result.resumption = !*saw_cert.lock().unwrap();
|
|
conn.close(0u32.into(), b"done");
|
|
|
|
self.endpoint.wait_idle().await;
|
|
|
|
result.close = true;
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
async fn key_update_hq(&self) -> Result<()> {
|
|
let new_conn = self
|
|
.endpoint
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("failed to connect: {}", e))?;
|
|
let conn = new_conn.connection;
|
|
// Make sure some traffic has gone both ways before the key update
|
|
let stream = conn
|
|
.open_bi()
|
|
.await
|
|
.map_err(|e| anyhow!("failed to open stream: {}", e))?;
|
|
hq_get(stream, "/").await?;
|
|
conn.force_key_update();
|
|
let stream = conn
|
|
.open_bi()
|
|
.await
|
|
.map_err(|e| anyhow!("failed to open stream: {}", e))?;
|
|
hq_get(stream, "/").await?;
|
|
conn.close(0u32.into(), b"done");
|
|
Ok(())
|
|
}
|
|
|
|
async fn retry_hq(&self) -> Result<()> {
|
|
let mut remote = self.remote;
|
|
remote.set_port(self.peer.retry_port);
|
|
|
|
let new_conn = self
|
|
.endpoint
|
|
.connect(&remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("failed to connect: {}", e))?;
|
|
let stream = new_conn
|
|
.connection
|
|
.open_bi()
|
|
.await
|
|
.map_err(|e| anyhow!("failed to open stream: {}", e))?;
|
|
hq_get(stream, "/").await?;
|
|
new_conn.connection.close(0u32.into(), b"done");
|
|
Ok(())
|
|
}
|
|
|
|
async fn rebind_hq(&self) -> Result<()> {
|
|
let mut endpoint = quinn::Endpoint::builder();
|
|
endpoint.default_client_config(self.client_config.clone());
|
|
let (endpoint, _) = endpoint.bind(&"[::]:0".parse().unwrap())?;
|
|
|
|
let new_conn = endpoint
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("failed to connect: {}", e))?;
|
|
let socket = std::net::UdpSocket::bind("[::]:0").unwrap();
|
|
endpoint.rebind(socket)?;
|
|
let stream = new_conn
|
|
.connection
|
|
.open_bi()
|
|
.await
|
|
.map_err(|e| anyhow!("failed to open stream: {}", e))?;
|
|
hq_get(stream, "/").await?;
|
|
new_conn.connection.close(0u32.into(), b"done");
|
|
Ok(())
|
|
}
|
|
|
|
async fn throughput_hq(&self) -> Result<()> {
|
|
for size in [5_000_000, 10_000_000usize].iter() {
|
|
let uri = self.peer.uri(&format!("/{}", size));
|
|
let conn = self
|
|
.endpoint
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("failed to connect: {}", e))?
|
|
.connection;
|
|
|
|
let start = Instant::now();
|
|
let stream = conn
|
|
.open_bi()
|
|
.await
|
|
.map_err(|e| anyhow!("failed to open stream: {}", e))?;
|
|
let body = hq_get(stream, &format!("/{}", size)).await?;
|
|
if body.len() != *size {
|
|
return Err(anyhow!("hq {} responded {} B", uri, body.len()));
|
|
}
|
|
let elapsed_hq = start.elapsed();
|
|
conn.close(0x100u16.into(), b"NO_ERROR");
|
|
|
|
let mut h2 = hyper::client::Builder::default();
|
|
h2.http2_only(true);
|
|
let mut http = hyper::client::connect::HttpConnector::new();
|
|
http.enforce_http(false);
|
|
let client = h2.build::<_, hyper::Body>(hyper_rustls::HttpsConnector::from((
|
|
http,
|
|
self.client_config.crypto.clone(),
|
|
)));
|
|
let response = client.get(self.peer.uri("/")).await?;
|
|
let _ = hyper::body::to_bytes(response).await?;
|
|
|
|
let start = Instant::now();
|
|
let mut response = client.get(uri.clone()).await?;
|
|
let mut total_len = 0usize;
|
|
while let Some(b) = response.body_mut().data().await {
|
|
total_len += b?.len()
|
|
}
|
|
if total_len != *size {
|
|
return Err(anyhow!("h2 {} responded {} B", uri, total_len));
|
|
}
|
|
let elapsed_h2 = start.elapsed();
|
|
|
|
let percentage = (elapsed_hq.as_nanos() as f64 - elapsed_h2.as_nanos() as f64)
|
|
/ elapsed_h2.as_nanos() as f64
|
|
* 100.0;
|
|
info!(
|
|
size = size,
|
|
h3 = elapsed_hq.as_millis() as usize,
|
|
h2 = elapsed_h2.as_millis() as usize,
|
|
"hq time {:+.2}% of h2's",
|
|
percentage
|
|
);
|
|
if percentage > 10.0 {
|
|
return Err(anyhow!("Throughput {} is {:+.2}% slower", size, percentage));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn h3(&self) -> Result<()> {
|
|
if let Alpn::Hq = self.peer.alpn {
|
|
return Err(anyhow!("H3 not implemented on this peer"));
|
|
}
|
|
let conn = self
|
|
.h3_client
|
|
.as_ref()
|
|
.unwrap()
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("h3 failed to connect: {}", e))?;
|
|
|
|
h3_get(&conn, &self.peer.uri("/"))
|
|
.await
|
|
.map_err(|e| anyhow!("h3 request failed: {}", e))?;
|
|
conn.close();
|
|
Ok(())
|
|
}
|
|
|
|
async fn throughput_h3(&self) -> Result<()> {
|
|
if let Alpn::Hq = self.peer.alpn {
|
|
return Err(anyhow!("H3 not implemented on this peer"));
|
|
}
|
|
|
|
for size in [5_000_000, 10_000_000usize].iter() {
|
|
let uri = self.peer.uri(&format!("/{}", size));
|
|
let conn = self
|
|
.h3_client
|
|
.as_ref()
|
|
.unwrap()
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("h3 failed to connect: {}", e))?;
|
|
|
|
let start = Instant::now();
|
|
h3_get(&conn, &uri).await.context("h3 request failed")?;
|
|
let elapsed_h3 = start.elapsed();
|
|
conn.close();
|
|
|
|
let mut h2 = hyper::client::Builder::default();
|
|
h2.http2_only(true);
|
|
let mut http = hyper::client::connect::HttpConnector::new();
|
|
http.enforce_http(false);
|
|
let client = h2.build::<_, hyper::Body>(hyper_rustls::HttpsConnector::from((
|
|
http,
|
|
self.client_config.crypto.clone(),
|
|
)));
|
|
let response = client.get(self.peer.uri("/")).await?;
|
|
let _ = hyper::body::to_bytes(response).await?;
|
|
|
|
let start = Instant::now();
|
|
let mut response = client.get(uri.clone()).await?;
|
|
let mut total_len = 0usize;
|
|
while let Some(b) = response.body_mut().data().await {
|
|
total_len += b?.len()
|
|
}
|
|
if total_len != *size {
|
|
return Err(anyhow!("h2 {} responded {} B", uri, total_len));
|
|
}
|
|
let elapsed_h2 = start.elapsed();
|
|
|
|
let percentage = (elapsed_h3.as_nanos() as f64 - elapsed_h2.as_nanos() as f64)
|
|
/ elapsed_h2.as_nanos() as f64
|
|
* 100.0;
|
|
info!(
|
|
size = size,
|
|
h3 = elapsed_h3.as_millis() as usize,
|
|
h2 = elapsed_h2.as_millis() as usize,
|
|
"h3 time {:+.2}% of h2's",
|
|
percentage
|
|
);
|
|
if percentage > 10.0 {
|
|
return Err(anyhow!("Throughput {} is {:+.2}% slower", size, percentage));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn core_h3(&self) -> Result<InteropResult> {
|
|
let mut result = InteropResult::default();
|
|
|
|
let conn = self
|
|
.h3_client
|
|
.as_ref()
|
|
.unwrap()
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("h3 failed to connect: {}", e))?;
|
|
result.handshake = true;
|
|
h3_get(&conn, &self.peer.uri("/"))
|
|
.await
|
|
.map_err(|e| anyhow!("simple request failed: {}", e))?;
|
|
result.stream_data = true;
|
|
conn.close();
|
|
|
|
let saw_cert = Arc::new(Mutex::new(false));
|
|
let quinn::ClientConfig {
|
|
mut crypto,
|
|
transport,
|
|
} = self.client_config.clone();
|
|
Arc::make_mut(&mut crypto)
|
|
.dangerous()
|
|
.set_certificate_verifier(Arc::new(InteropVerifier(saw_cert.clone())));
|
|
let client_config = quinn::ClientConfig { crypto, transport };
|
|
|
|
let conn = match self
|
|
.h3_client
|
|
.as_ref()
|
|
.unwrap()
|
|
.connect_with(client_config, &self.remote, &self.host)?
|
|
.into_0rtt()
|
|
{
|
|
Ok((conn, _)) => {
|
|
h3_get(&conn, &self.peer.uri("/"))
|
|
.await
|
|
.map_err(|e| anyhow!("0-RTT request failed: {}", e))?;
|
|
result.zero_rtt = true;
|
|
conn
|
|
}
|
|
Err(connecting) => {
|
|
info!("0-RTT unsupported");
|
|
connecting
|
|
.await
|
|
.map_err(|e| anyhow!("failed to connect: {}", e))?
|
|
}
|
|
};
|
|
result.resumption = !*saw_cert.lock().unwrap();
|
|
conn.close();
|
|
|
|
self.endpoint.wait_idle().await;
|
|
|
|
result.close = true;
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
async fn key_update_h3(&self) -> Result<()> {
|
|
let conn = self
|
|
.h3_client
|
|
.as_ref()
|
|
.unwrap()
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("h3 failed to connect: {}", e))?;
|
|
// Make sure some traffic has gone both ways before the key update
|
|
h3_get(&conn, &self.peer.uri("/"))
|
|
.await
|
|
.map_err(|e| anyhow!("request failed before key update: {}", e))?;
|
|
conn.force_key_update();
|
|
h3_get(&conn, &self.peer.uri("/"))
|
|
.await
|
|
.map_err(|e| anyhow!("request failed after key update: {}", e))?;
|
|
conn.close();
|
|
Ok(())
|
|
}
|
|
|
|
async fn retry_h3(&self) -> Result<()> {
|
|
let mut remote = self.remote;
|
|
remote.set_port(self.peer.retry_port);
|
|
|
|
let conn = self
|
|
.h3_client
|
|
.as_ref()
|
|
.unwrap()
|
|
.connect(&remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("h3 failed to connect: {}", e))?;
|
|
h3_get(&conn, &self.peer.uri("/"))
|
|
.await
|
|
.map_err(|e| anyhow!("request failed on retry port: {}", e))?;
|
|
conn.close();
|
|
Ok(())
|
|
}
|
|
|
|
async fn rebind_h3(&self) -> Result<()> {
|
|
let (endpoint, _) = quinn::Endpoint::builder().bind(&"[::]:0".parse().unwrap())?;
|
|
|
|
let h3_client = quinn_h3::client::Builder::default().endpoint(self.endpoint.clone());
|
|
let conn = h3_client
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("h3 failed to connect: {}", e))?;
|
|
let socket = std::net::UdpSocket::bind("[::]:0").unwrap();
|
|
endpoint.rebind(socket)?;
|
|
h3_get(&conn, &self.peer.uri("/"))
|
|
.await
|
|
.map_err(|e| anyhow!("request failed on retry port: {}", e))?;
|
|
conn.close();
|
|
Ok(())
|
|
}
|
|
|
|
async fn dynamic_encoding(&self) -> Result<()> {
|
|
let conn = self
|
|
.h3_client
|
|
.as_ref()
|
|
.unwrap()
|
|
.connect(&self.remote, &self.host)?
|
|
.await
|
|
.map_err(|e| anyhow!("h3 failed to connect: {}", e))?;
|
|
h3_get(&conn, &self.peer.uri("/"))
|
|
.await
|
|
.map_err(|e| anyhow!("request failed before key update: {}", e))?;
|
|
h3_get(&conn, &self.peer.uri("/"))
|
|
.await
|
|
.map_err(|e| anyhow!("request failed before key update: {}", e))?;
|
|
if !conn.had_refs() {
|
|
return Err(anyhow!("No dynamic table entry was referenced"));
|
|
}
|
|
conn.close();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct InteropResult {
|
|
handshake: bool,
|
|
stream_data: bool,
|
|
close: bool,
|
|
resumption: bool,
|
|
key_update: bool,
|
|
rebinding: bool,
|
|
zero_rtt: bool,
|
|
retry: bool,
|
|
throughput: bool,
|
|
h3: bool,
|
|
dynamic_encoding: bool,
|
|
}
|
|
|
|
impl InteropResult {
|
|
fn format(&self) -> String {
|
|
let mut string = String::with_capacity(10);
|
|
if self.handshake {
|
|
string.push_str("VH");
|
|
}
|
|
if self.stream_data {
|
|
string.push_str("D");
|
|
}
|
|
if self.close {
|
|
string.push_str("C");
|
|
}
|
|
if self.resumption {
|
|
string.push_str("R");
|
|
}
|
|
if self.zero_rtt {
|
|
string.push_str("Z");
|
|
}
|
|
if self.retry {
|
|
string.push_str("S");
|
|
}
|
|
if self.rebinding {
|
|
string.push_str("B");
|
|
}
|
|
if self.key_update {
|
|
string.push_str("U");
|
|
}
|
|
if self.throughput {
|
|
string.push_str("T");
|
|
}
|
|
if self.h3 {
|
|
string.push_str("3");
|
|
}
|
|
if self.dynamic_encoding {
|
|
string.push_str("d");
|
|
}
|
|
string
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::cognitive_complexity)]
|
|
fn build_result(
|
|
core: Result<InteropResult>,
|
|
key_update: Result<()>,
|
|
rebind: Result<()>,
|
|
retry: Result<()>,
|
|
throughput: Result<()>,
|
|
h3: Option<Result<()>>,
|
|
dynamic_encoding: Option<Result<()>>,
|
|
) -> InteropResult {
|
|
let mut result = core.unwrap_or_else(|e| {
|
|
error!("core functionality failed: {}", e);
|
|
InteropResult::default()
|
|
});
|
|
match key_update {
|
|
Ok(_) => result.key_update = true,
|
|
Err(e) => error!("key update failed: {}", e),
|
|
};
|
|
match rebind {
|
|
Ok(_) => result.rebinding = true,
|
|
Err(e) => error!("rebinding failed: {}", e),
|
|
}
|
|
match retry {
|
|
Ok(_) => result.retry = true,
|
|
Err(e) => error!("retry failed: {}", e),
|
|
}
|
|
match throughput {
|
|
Ok(_) => result.throughput = true,
|
|
Err(e) => error!("throughput failed: {}", e),
|
|
}
|
|
match h3 {
|
|
Some(Ok(_)) => result.h3 = true,
|
|
Some(Err(e)) => error!("retry failed: {}", e),
|
|
None => (),
|
|
}
|
|
match dynamic_encoding {
|
|
Some(Ok(_)) => result.dynamic_encoding = true,
|
|
Some(Err(e)) => error!("dynamic encoding failed: {}", e),
|
|
None => (),
|
|
}
|
|
result
|
|
}
|
|
|
|
async fn h3_get(conn: &quinn_h3::client::Connection, uri: &http::Uri) -> Result<usize> {
|
|
let (response, _) = conn.send_request(http::Request::get(uri).body(())?).await?;
|
|
|
|
let (resp, mut recv_body) = response.await?;
|
|
debug!("resp: {:?}", resp);
|
|
let mut total_len = 0usize;
|
|
while let Some(d) = recv_body.data().await {
|
|
total_len += d?.len()
|
|
}
|
|
|
|
Ok(total_len)
|
|
}
|
|
|
|
async fn hq_get(stream: (quinn::SendStream, quinn::RecvStream), path: &str) -> Result<Vec<u8>> {
|
|
let (mut send, recv) = stream;
|
|
send.write_all(&format!("GET {}\r\n", path).as_bytes())
|
|
.await
|
|
.map_err(|e| anyhow!("failed to send request: {}", e))?;
|
|
send.finish()
|
|
.await
|
|
.map_err(|e| anyhow!("failed to shutdown stream: {}", e))?;
|
|
let response = recv
|
|
.read_to_end(usize::max_value())
|
|
.await
|
|
.map_err(|e| anyhow!("failed to read response: {}", e))?;
|
|
Ok(response)
|
|
}
|
|
|
|
struct InteropVerifier(Arc<Mutex<bool>>);
|
|
impl rustls::ServerCertVerifier for InteropVerifier {
|
|
fn verify_server_cert(
|
|
&self,
|
|
_roots: &rustls::RootCertStore,
|
|
_presented_certs: &[rustls::Certificate],
|
|
_dns_name: webpki::DNSNameRef,
|
|
_ocsp_response: &[u8],
|
|
) -> std::result::Result<rustls::ServerCertVerified, rustls::TLSError> {
|
|
*self.0.lock().unwrap() = true;
|
|
Ok(rustls::ServerCertVerified::assertion())
|
|
}
|
|
}
|