Fix: http/3 alpn bugs

This commit is contained in:
DaZuo0122
2026-01-18 23:05:41 +08:00
parent 9bcb7549f3
commit 7054ff77a7
4 changed files with 82 additions and 47 deletions

2
Cargo.lock generated
View File

@@ -3235,7 +3235,7 @@ dependencies = [
[[package]] [[package]]
name = "wtfnet-cli" name = "wtfnet-cli"
version = "0.1.0" version = "0.4.0"
dependencies = [ dependencies = [
"clap", "clap",
"serde", "serde",

View File

@@ -98,7 +98,8 @@ cmake --build build --target install
``` ```
## HTTP/3 (experimental) ## HTTP/3 (experimental)
HTTP/3 support is feature-gated and incomplete. Do not enable it in production builds yet. HTTP/3 support is feature-gated and best-effort. Enable it only when you want to test QUIC
connectivity.
To enable locally for testing: To enable locally for testing:
```bash ```bash

View File

@@ -21,6 +21,8 @@ use quinn::ClientConfig as QuinnClientConfig;
#[cfg(feature = "http3")] #[cfg(feature = "http3")]
use quinn::Endpoint; use quinn::Endpoint;
#[cfg(feature = "http3")] #[cfg(feature = "http3")]
use quinn::crypto::rustls::QuicClientConfig;
#[cfg(feature = "http3")]
use webpki_roots::TLS_SERVER_ROOTS; use webpki_roots::TLS_SERVER_ROOTS;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -468,26 +470,54 @@ async fn http3_request(
let port = parsed let port = parsed
.port_or_known_default() .port_or_known_default()
.ok_or_else(|| HttpError::Url("missing port".to_string()))?; .ok_or_else(|| HttpError::Url("missing port".to_string()))?;
let ip = resolved_ips
.first()
.and_then(|value| value.parse::<IpAddr>().ok())
.ok_or_else(|| HttpError::Request("no resolved IPs for http3".to_string()))?;
let quinn_config = build_quinn_config()?; let quinn_config = build_quinn_config()?;
let candidates = resolved_ips
.iter()
.filter_map(|value| value.parse::<IpAddr>().ok())
.collect::<Vec<_>>();
if candidates.is_empty() {
return Err(HttpError::Request("no resolved IPs for http3".to_string()));
}
let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap()) let mut endpoint_guard = None;
let mut connection = None;
let mut connect_ms = None;
for ip in candidates {
let bind_addr = match ip {
IpAddr::V4(_) => "0.0.0.0:0",
IpAddr::V6(_) => "[::]:0",
};
let mut endpoint = Endpoint::client(bind_addr.parse().unwrap())
.map_err(|err| HttpError::Request(err.to_string()))?; .map_err(|err| HttpError::Request(err.to_string()))?;
endpoint.set_default_client_config(quinn_config); endpoint.set_default_client_config(quinn_config.clone());
let connect_start = Instant::now(); let connect_start = Instant::now();
let connecting = endpoint let connecting = match endpoint.connect(SocketAddr::new(ip, port), host) {
.connect(SocketAddr::new(ip, port), host) Ok(connecting) => connecting,
.map_err(|err| HttpError::Request(err.to_string()))?; Err(err) => {
let connection = timeout(Duration::from_millis(opts.timeout_ms), connecting) warnings.push(format!("http3 connect failed to {ip}: {err}"));
.await continue;
.map_err(|_| HttpError::Request("http3 connect timed out".to_string()))? }
.map_err(|err| HttpError::Request(err.to_string()))?; };
let connect_ms = connect_start.elapsed().as_millis(); match timeout(Duration::from_millis(opts.timeout_ms), connecting).await {
Ok(Ok(conn)) => {
connect_ms = Some(connect_start.elapsed().as_millis());
connection = Some(conn);
endpoint_guard = Some(endpoint);
break;
}
Ok(Err(err)) => {
warnings.push(format!("http3 connect failed to {ip}: {err}"));
}
Err(_) => {
warnings.push(format!("http3 connect to {ip} timed out"));
}
}
}
let connection = connection.ok_or_else(|| {
HttpError::Request("http3 connect failed for all resolved IPs".to_string())
})?;
let connect_ms = connect_ms.unwrap_or_default();
let conn = h3_quinn::Connection::new(connection); let conn = h3_quinn::Connection::new(connection);
let (mut driver, mut send_request) = h3::client::new(conn) let (mut driver, mut send_request) = h3::client::new(conn)
@@ -563,8 +593,8 @@ async fn http3_request(
warnings.push("http3 timing for tls/connect is best-effort".to_string()); warnings.push("http3 timing for tls/connect is best-effort".to_string());
Ok(( let _endpoint_guard = endpoint_guard;
HttpReport { let report = HttpReport {
url: url.to_string(), url: url.to_string(),
final_url: Some(final_url), final_url: Some(final_url),
method: match opts.method { method: match opts.method {
@@ -584,9 +614,9 @@ async fn http3_request(
tls_ms: None, tls_ms: None,
ttfb_ms: Some(ttfb_ms), ttfb_ms: Some(ttfb_ms),
}, },
}, };
warnings,
)) Ok((report, warnings))
} }
#[cfg(feature = "http3")] #[cfg(feature = "http3")]
@@ -594,10 +624,14 @@ fn build_quinn_config() -> Result<QuinnClientConfig, HttpError> {
let mut roots = quinn::rustls::RootCertStore::empty(); let mut roots = quinn::rustls::RootCertStore::empty();
roots.extend(TLS_SERVER_ROOTS.iter().cloned()); roots.extend(TLS_SERVER_ROOTS.iter().cloned());
let mut client_config = let mut crypto = quinn::rustls::ClientConfig::builder()
QuinnClientConfig::with_root_certificates(Arc::new(roots)).map_err(|err| { .with_root_certificates(roots)
HttpError::Request(format!("quinn config error: {err}")) .with_no_client_auth();
})?; crypto.alpn_protocols = vec![b"h3".to_vec()];
let mut client_config = QuinnClientConfig::new(Arc::new(
QuicClientConfig::try_from(crypto)
.map_err(|err| HttpError::Request(format!("quinn config error: {err}")))?,
));
let mut transport = quinn::TransportConfig::default(); let mut transport = quinn::TransportConfig::default();
transport.keep_alive_interval(Some(Duration::from_secs(5))); transport.keep_alive_interval(Some(Duration::from_secs(5)));
client_config.transport_config(Arc::new(transport)); client_config.transport_config(Arc::new(transport));

View File

@@ -22,7 +22,7 @@ This document tracks current implementation status against the original design i
- DNS watch uses `pnet` and is feature-gated as best-effort. - DNS watch uses `pnet` and is feature-gated as best-effort.
## Gaps vs design (as of now) ## Gaps vs design (as of now)
- HTTP/3 is feature-gated and incomplete; not enabled in default builds. - HTTP/3 is feature-gated and best-effort; not enabled in default builds.
- TLS verification is rustls-based (no OS-native verifier). - TLS verification is rustls-based (no OS-native verifier).
- DNS leak DoH detection is heuristic and currently optional. - DNS leak DoH detection is heuristic and currently optional.