Fix: http/3 alpn bugs
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
.map_err(|err| HttpError::Request(err.to_string()))?;
|
let mut connection = None;
|
||||||
endpoint.set_default_client_config(quinn_config);
|
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()))?;
|
||||||
|
endpoint.set_default_client_config(quinn_config.clone());
|
||||||
|
let connect_start = Instant::now();
|
||||||
|
let connecting = match endpoint.connect(SocketAddr::new(ip, port), host) {
|
||||||
|
Ok(connecting) => connecting,
|
||||||
|
Err(err) => {
|
||||||
|
warnings.push(format!("http3 connect failed to {ip}: {err}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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 connect_start = Instant::now();
|
let connection = connection.ok_or_else(|| {
|
||||||
let connecting = endpoint
|
HttpError::Request("http3 connect failed for all resolved IPs".to_string())
|
||||||
.connect(SocketAddr::new(ip, port), host)
|
})?;
|
||||||
.map_err(|err| HttpError::Request(err.to_string()))?;
|
let connect_ms = connect_ms.unwrap_or_default();
|
||||||
let connection = timeout(Duration::from_millis(opts.timeout_ms), connecting)
|
|
||||||
.await
|
|
||||||
.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();
|
|
||||||
|
|
||||||
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,30 +593,30 @@ 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 {
|
||||||
HttpMethod::Head => "HEAD".to_string(),
|
HttpMethod::Head => "HEAD".to_string(),
|
||||||
HttpMethod::Get => "GET".to_string(),
|
HttpMethod::Get => "GET".to_string(),
|
||||||
},
|
|
||||||
status: Some(status.as_u16()),
|
|
||||||
http_version: Some("HTTP/3".to_string()),
|
|
||||||
resolved_ips: resolved_ips.to_vec(),
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
warnings: Vec::new(),
|
|
||||||
timing: HttpTiming {
|
|
||||||
total_ms,
|
|
||||||
dns_ms: Some(dns_ms),
|
|
||||||
connect_ms: Some(connect_ms),
|
|
||||||
tls_ms: None,
|
|
||||||
ttfb_ms: Some(ttfb_ms),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
warnings,
|
status: Some(status.as_u16()),
|
||||||
))
|
http_version: Some("HTTP/3".to_string()),
|
||||||
|
resolved_ips: resolved_ips.to_vec(),
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
warnings: Vec::new(),
|
||||||
|
timing: HttpTiming {
|
||||||
|
total_ms,
|
||||||
|
dns_ms: Some(dns_ms),
|
||||||
|
connect_ms: Some(connect_ms),
|
||||||
|
tls_ms: None,
|
||||||
|
ttfb_ms: Some(ttfb_ms),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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));
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user