This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 22:54:52 +02:00
1 parent b0f211555a
commit 8b9ca4db44
39 files changed
+1481 -303

No files matched your search

+17
View File
@@ -364,10 +364,27 @@ fn build_raw_client(factory: &ClientFactory) -> Result<RawProxyClient> {
)
.http2_keep_alive_timeout(Duration::from_secs(10))
.http2_keep_alive_while_idle(false);
// The request URI is free to carry the preserved public H2 :authority,
// while TLS SNI/verification uses tls_server_name (or backend URL) and
// DNS/TCP remains restricted to backend.url by GuardedResolver.
let origin = url::Url::parse(&backend.url)?;
let network_host = match origin.host().context("backend host missing")? {
url::Host::Ipv6(ip) => format!("[{ip}]"),
host => host.to_string(),
};
let port = origin.port_or_known_default().context("backend port missing")?;
let tls_name = backend.tls_server_name
.as_deref()
.map(crate::config::normalize_host)
.transpose()?
.unwrap_or(network_host);
let destination: http::Uri = format!("{}://{tls_name}:{port}", origin.scheme())
.parse().context("invalid configured backend transport authority")?;
Ok(builder.build(TimedConnector {
inner,
policy: policy.clone(),
timeout,
destination,
}))
}
+151
View File
@@ -221,3 +221,154 @@ fn default_protocol_and_serialization_are_backward_compatible() {
);
}
}
// Exercise real TLS ALPN and the actual Hyper connector rather than only the
// local authority helper. The public Host must never become a DNS/TCP target.
async fn tls_authority_origin(
offer_h2: bool,
) -> (
std::net::SocketAddr,
tokio::sync::oneshot::Receiver<(Version, Option<String>, Option<String>, Option<String>)>,
tokio::task::JoinHandle<()>,
) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let pair = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
let key = rustls::pki_types::PrivatePkcs8KeyDer::from(pair.signing_key.serialize_der());
let provider = Arc::new(rustls::crypto::ring::default_provider());
let mut config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions().unwrap()
.with_no_client_auth()
.with_single_cert(vec![pair.cert.der().clone()], key.into()).unwrap();
config.alpn_protocols = if offer_h2 {
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
} else {
vec![b"http/1.1".to_vec()]
};
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
let (observed_tx, observed_rx) = tokio::sync::oneshot::channel();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let tls = acceptor.accept(stream).await.unwrap();
let sni = tls.get_ref().1.server_name().map(str::to_owned);
let observed = Arc::new(std::sync::Mutex::new(Some(observed_tx)));
let service = service_fn(move |request: Request<hyper::body::Incoming>| {
let observed = observed.clone();
let sni = sni.clone();
async move {
if let Some(tx) = observed.lock().unwrap().take() {
let authority = request.uri().authority().map(|v| v.as_str().to_owned());
let host = request.headers().get("host")
.map(|v| v.to_str().unwrap().to_owned());
let _ = tx.send((request.version(), authority, host, sni));
}
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(b"ok"))))
}
});
if offer_h2 {
let _ = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(tls), service).await;
} else {
let _ = hyper::server::conn::http1::Builder::new()
.serve_connection(TokioIo::new(tls), service).await;
}
});
(address, observed_rx, server)
}
#[tokio::test]
async fn strict_h2_preserves_public_authority_while_tcp_and_sni_use_private_origin() {
let (address, seen, server) = tls_authority_origin(true).await;
let mut backend = protocol_backend(address, "https", "h2");
backend.tls_server_name = Some("localhost".into());
backend.preserve_host = true;
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
let request = Request::builder().version(Version::HTTP_2)
.uri("https://unitraklub.pl/block_refresh/block/10")
.body(Body::empty()).unwrap();
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
.await.unwrap().unwrap();
assert_eq!(response.version(), Version::HTTP_2);
let (version, authority, host, sni) = seen.await.unwrap();
assert_eq!(version, Version::HTTP_2);
assert_eq!(authority.as_deref(), Some("unitraklub.pl"));
assert!(host.is_none(), "Strict H2 must not duplicate :authority with Host");
assert_eq!(sni.as_deref(), Some("localhost"));
server.abort();
}
#[tokio::test]
async fn auto_h1_fallback_preserves_public_host_without_resolving_it() {
let (address, seen, server) = tls_authority_origin(false).await;
let mut backend = protocol_backend(address, "https", "auto");
backend.tls_server_name = Some("localhost".into());
backend.preserve_host = true;
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
let request = Request::builder().version(Version::HTTP_11)
.uri("https://unitraklub.pl/block_refresh/block/10")
.header("host", "unitraklub.pl")
.body(Body::empty()).unwrap();
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
.await.unwrap().unwrap();
assert_eq!(response.version(), Version::HTTP_11);
let (version, _authority, host, sni) = seen.await.unwrap();
assert_eq!(version, Version::HTTP_11);
assert_eq!(host.as_deref(), Some("unitraklub.pl"));
assert_eq!(sni.as_deref(), Some("localhost"));
server.abort();
}
#[tokio::test]
async fn auto_h2_alpn_retains_same_public_host_and_pinned_tls_origin() {
let (address, seen, server) = tls_authority_origin(true).await;
let mut backend = protocol_backend(address, "https", "auto");
backend.tls_server_name = Some("localhost".into());
backend.preserve_host = true;
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
let request = Request::builder().version(Version::HTTP_11)
.uri("https://unitraklub.pl/block_refresh/block/10")
.header("host", "unitraklub.pl")
.body(Body::empty()).unwrap();
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
.await.unwrap().unwrap();
assert_eq!(response.version(), Version::HTTP_2);
let (version, authority, host, sni) = seen.await.unwrap();
assert_eq!(version, Version::HTTP_2);
// Legacy Hyper may preserve H1-compatible Host on ALPN H2; neither value
// may select a different origin or differ from the intended public name.
assert_eq!(authority.or(host).as_deref(), Some("unitraklub.pl"));
assert_eq!(sni.as_deref(), Some("localhost"));
server.abort();
}
#[tokio::test]
async fn cleartext_h2c_preserves_public_authority_and_pins_socket() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let (tx, observed) = tokio::sync::oneshot::channel();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let result = Arc::new(std::sync::Mutex::new(Some(tx)));
let service = service_fn(move |request: Request<hyper::body::Incoming>| {
let result = result.clone();
async move {
if let Some(tx) = result.lock().unwrap().take() {
let _ = tx.send(request.uri().authority().map(|v| v.as_str().to_owned()));
}
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(b"ok"))))
}
});
let _ = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(stream), service).await;
});
let backend = protocol_backend(address, "http", "h2");
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
let request = Request::builder().version(Version::HTTP_2)
.uri("http://unitraklub.pl/test")
.body(Body::empty()).unwrap();
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
.await.unwrap().unwrap();
assert_eq!(response.version(), Version::HTTP_2);
assert_eq!(observed.await.unwrap().as_deref(), Some("unitraklub.pl"));
server.abort();
}