v0.32.4
This commit is contained in:
1 parent
b0f211555a
commit
8b9ca4db44
39 files changed
+1481
-303
No files matched your search
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user