#[cfg(not(feature = "quic"))]
fn main() {
eprintln!("❌ The `quic` feature is not enabled.");
eprintln!(" Rebuild with: cargo run --bin h3_client --features quic");
std::process::exit(1);
}
#[cfg(feature = "quic")]
fn main() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to build Tokio runtime")
.block_on(run())
.unwrap_or_else(|e| {
eprintln!("❌ Fatal error: {e}");
std::process::exit(1);
});
}
#[cfg(feature = "quic")]
async fn run() -> anyhow::Result<()> {
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Instant;
use anyhow::Context;
use h3_quinn::quinn;
use http::{Method, Uri};
use rustls::RootCertStore;
let _ = rustls::crypto::ring::default_provider().install_default();
let args: Vec<String> = std::env::args().skip(1).collect();
let mut url_str = "https://127.0.0.1:4000/health".to_string();
let mut method_str = "GET".to_string();
let mut body_str = String::new();
let mut verbose = false;
let mut insecure = true; let mut repeat: u32 = 1;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--url" | "-u" => {
i += 1;
url_str = args[i].clone();
}
"--method" | "-X" => {
i += 1;
method_str = args[i].clone();
}
"--body" | "-d" => {
i += 1;
body_str = args[i].clone();
}
"--repeat" | "-n" => {
i += 1;
repeat = args[i].parse().unwrap_or(1);
}
"--verbose" | "-v" => {
verbose = true;
}
"--secure" => {
insecure = false;
}
"--help" | "-h" => {
print_help();
return Ok(());
}
_ => {
url_str = args[i].clone();
}
}
i += 1;
}
let uri: Uri = url_str.parse().context("Invalid URL")?;
let host = uri.host().context("URL has no host")?.to_string();
let port = uri.port_u16().unwrap_or(4000);
let method: Method = method_str.parse().context("Invalid HTTP method")?;
let server_addr: SocketAddr = format!("{host}:{port}")
.parse()
.unwrap_or_else(|_| SocketAddr::from(([127, 0, 0, 1], port)));
println!("╔══════════════════════════════════════════════════════════════╗");
println!("║ GBP Router · HTTP/3 QUIC Test Client ║");
println!("╠══════════════════════════════════════════════════════════════╣");
println!("║ Target : {url_str:<51}║");
println!("║ Method : {method:<51}║");
println!("║ Server : {server_addr:<51}║");
println!(
"║ TLS : {} (self-signed ok) ║",
if insecure {
"⚠️ INSECURE"
} else {
"🔒 Verified "
}
);
if repeat > 1 {
println!("║ Repeat : {repeat:<51}║");
}
println!("╚══════════════════════════════════════════════════════════════╝");
println!();
let tls_config = if insecure {
let mut cfg =
rustls::ClientConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
.with_no_client_auth();
cfg.alpn_protocols = vec![b"h3".to_vec()];
cfg
} else {
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let mut cfg =
rustls::ClientConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
.with_root_certificates(root_store)
.with_no_client_auth();
cfg.alpn_protocols = vec![b"h3".to_vec()];
cfg
};
let quic_client_cfg = quinn::ClientConfig::new(Arc::new(
quinn::crypto::rustls::QuicClientConfig::try_from(tls_config)
.context("Failed to build QUIC client config")?,
));
let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap())
.context("Failed to create QUIC client endpoint")?;
endpoint.set_default_client_config(quic_client_cfg);
let mut total_bytes: u64 = 0;
let mut total_ms: u128 = 0;
let mut successes = 0u32;
for req_num in 1..=repeat {
if repeat > 1 {
println!("── Request {req_num}/{repeat} ────────────────────────────────────────────");
}
let t0 = Instant::now();
match send_h3_request(
&endpoint,
server_addr,
&host,
uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/"),
method.clone(),
if body_str.is_empty() {
None
} else {
Some(body_str.as_bytes().to_vec())
},
verbose,
)
.await
{
Ok((status, headers, body)) => {
let elapsed = t0.elapsed();
total_ms += elapsed.as_millis();
total_bytes += body.len() as u64;
successes += 1;
println!("✅ HTTP/3 {status} ({} ms)", elapsed.as_millis());
if verbose {
println!(
"\n── Response Headers ──────────────────────────────────────────────"
);
for (k, v) in &headers {
println!(" {k}: {}", v.to_str().unwrap_or("<binary>"));
}
}
println!("\n── Response Body ─────────────────────────────────────────────────");
let body_str = std::str::from_utf8(&body).unwrap_or("<binary>");
if let Ok(json) = serde_json::from_str::<serde_json::Value>(body_str) {
println!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
} else {
println!("{body_str}");
}
println!();
}
Err(e) => {
let elapsed = t0.elapsed();
total_ms += elapsed.as_millis();
eprintln!("❌ Request failed ({} ms): {e}", elapsed.as_millis());
if verbose {
eprintln!(" Cause: {e:?}");
}
}
}
}
if repeat > 1 {
println!("── Summary ───────────────────────────────────────────────────────");
println!(" Requests : {repeat}");
println!(" Successes : {successes}");
println!(" Total time : {} ms", total_ms);
println!(" Avg time : {} ms / req", total_ms / repeat as u128);
println!(" Total bytes: {total_bytes}");
println!(" Protocol : HTTP/3 (QUIC, RFC 9114)");
println!(" Transport : UDP (RFC 9000)");
println!(" TLS : 1.3 (RFC 8446)");
println!(" ALPN : h3");
}
endpoint.wait_idle().await;
Ok(())
}
#[cfg(feature = "quic")]
async fn send_h3_request(
endpoint: &h3_quinn::quinn::Endpoint,
addr: std::net::SocketAddr,
host: &str,
path: &str,
method: http::Method,
body: Option<Vec<u8>>,
verbose: bool,
) -> anyhow::Result<(u16, http::HeaderMap, bytes::Bytes)> {
use anyhow::Context;
use bytes::{Buf, BytesMut};
let conn = endpoint
.connect(addr, host)
.context("Failed to initiate QUIC connection")?
.await
.context("QUIC handshake failed")?;
if verbose {
println!("🤝 QUIC handshake complete — TLS 1.3, ALPN: h3");
println!(" remote: {addr}");
}
let (mut driver, mut send_req) = h3::client::new(h3_quinn::Connection::new(conn))
.await
.context("HTTP/3 connection negotiation failed")?;
let drive = tokio::spawn(async move {
let _ = futures::future::poll_fn(|cx| driver.poll_close(cx)).await;
});
let content_type = if body.is_some() {
"application/json"
} else {
""
};
let mut req_builder = http::Request::builder()
.method(method)
.uri(format!("https://{host}{path}"))
.header("user-agent", "gbp-h3-test-client/1.0")
.header("accept", "application/json");
if !content_type.is_empty() {
req_builder = req_builder.header("content-type", content_type);
}
let request = req_builder
.body(())
.context("Failed to build HTTP/3 request")?;
if verbose {
println!("\n── Request Headers ───────────────────────────────────────────────");
for (k, v) in request.headers() {
println!(" {k}: {}", v.to_str().unwrap_or("<binary>"));
}
}
let mut stream = send_req
.send_request(request)
.await
.context("Failed to send HTTP/3 request headers")?;
if let Some(body_bytes) = body {
stream
.send_data(bytes::Bytes::from(body_bytes))
.await
.context("Failed to send HTTP/3 request body")?;
}
stream
.finish()
.await
.context("Failed to finish HTTP/3 request stream")?;
let response = stream
.recv_response()
.await
.context("Failed to receive HTTP/3 response headers")?;
let status = response.status().as_u16();
let headers = response.headers().clone();
let mut body_buf = BytesMut::new();
while let Some(mut chunk) = stream
.recv_data()
.await
.context("Failed to receive HTTP/3 response body")?
{
let remaining = chunk.remaining();
body_buf.extend_from_slice(&chunk.copy_to_bytes(remaining));
}
drive.abort();
Ok((status, headers, body_buf.freeze()))
}
#[cfg(feature = "quic")]
fn print_help() {
println!("GBP Router — HTTP/3 QUIC Test Client");
println!();
println!("USAGE:");
println!(" cargo run --bin h3_client --features quic -- [OPTIONS]");
println!();
println!("OPTIONS:");
println!(" -u, --url <URL> Target URL [default: https://127.0.0.1:4000/health]");
println!(" -X, --method <METHOD> HTTP method [default: GET]");
println!(" -d, --body <JSON> Request body (for POST requests)");
println!(" -n, --repeat <N> Send N requests (for latency benchmarking)");
println!(" -v, --verbose Show full request/response headers");
println!(" --secure Verify TLS certificate (reject self-signed)");
println!(" -h, --help Show this help");
println!();
println!("EXAMPLES:");
println!(" # Health check");
println!(" cargo run --bin h3_client --features quic");
println!();
println!(" # POST GraphQL query");
println!(" cargo run --bin h3_client --features quic -- \\");
println!(" -X POST -u https://127.0.0.1:4000/graphql \\");
println!(" -d '{{\"query\":\"{{ __typename }}\"}}' -v");
println!();
println!(" # Latency benchmark (100 requests)");
println!(" cargo run --bin h3_client --features quic -- -n 100");
println!();
println!("REQUIRES:");
println!(" Router running with QUIC enabled:");
println!(" cargo run --bin router --features quic -- examples/router-quic-test.yaml");
}
#[cfg(feature = "quic")]
#[derive(Debug)]
struct SkipServerVerification;
#[cfg(feature = "quic")]
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::ED25519,
]
}
}