196 lines
6.9 KiB
Rust
196 lines
6.9 KiB
Rust
mod common;
|
|
|
|
use common::*;
|
|
use rustproxy::RustProxy;
|
|
use rustproxy_config::{RustProxyOptions, TransportProtocol, RouteUdp, RouteQuic};
|
|
use bytes::Buf;
|
|
use std::sync::Arc;
|
|
|
|
/// Build a route that listens on UDP with HTTP/3 enabled and TLS terminate.
|
|
fn make_h3_route(
|
|
port: u16,
|
|
target_host: &str,
|
|
target_port: u16,
|
|
cert_pem: &str,
|
|
key_pem: &str,
|
|
) -> rustproxy_config::RouteConfig {
|
|
let mut route = make_tls_terminate_route(port, "localhost", target_host, target_port, cert_pem, key_pem);
|
|
route.route_match.transport = Some(TransportProtocol::Udp);
|
|
// Keep domain="localhost" from make_tls_terminate_route — needed for TLS cert extraction
|
|
route.action.udp = Some(RouteUdp {
|
|
session_timeout: None,
|
|
max_sessions_per_ip: None,
|
|
max_datagram_size: None,
|
|
quic: Some(RouteQuic {
|
|
max_idle_timeout: Some(30000),
|
|
max_concurrent_bidi_streams: None,
|
|
max_concurrent_uni_streams: None,
|
|
enable_http3: Some(true),
|
|
alt_svc_port: None,
|
|
alt_svc_max_age: None,
|
|
initial_congestion_window: None,
|
|
}),
|
|
});
|
|
route
|
|
}
|
|
|
|
/// Build a quinn client endpoint with insecure TLS for testing.
|
|
fn make_h3_client_endpoint() -> quinn::Endpoint {
|
|
let mut tls_config = rustls::ClientConfig::builder()
|
|
.dangerous()
|
|
.with_custom_certificate_verifier(Arc::new(InsecureVerifier))
|
|
.with_no_client_auth();
|
|
tls_config.alpn_protocols = vec![b"h3".to_vec()];
|
|
|
|
let quic_client_config = quinn::crypto::rustls::QuicClientConfig::try_from(tls_config)
|
|
.expect("Failed to build QUIC client config");
|
|
let client_config = quinn::ClientConfig::new(Arc::new(quic_client_config));
|
|
|
|
let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap())
|
|
.expect("Failed to create QUIC client endpoint");
|
|
endpoint.set_default_client_config(client_config);
|
|
endpoint
|
|
}
|
|
|
|
/// Test that HTTP/3 response streams properly finish (FIN is received by client).
|
|
///
|
|
/// This is the critical regression test for the FIN bug: the proxy must send
|
|
/// a QUIC stream FIN after the response body so the client's `recv_data()`
|
|
/// returns `None` instead of hanging forever.
|
|
#[tokio::test]
|
|
async fn test_h3_response_stream_finishes() {
|
|
let backend_port = next_port();
|
|
let proxy_port = next_port();
|
|
let body_text = "Hello from HTTP/3 backend! This body has a known length for testing.";
|
|
|
|
// 1. Start plain HTTP backend with known body + content-length
|
|
let _backend = start_http_server(backend_port, 200, body_text).await;
|
|
|
|
// 2. Generate self-signed cert and configure H3 route
|
|
let (cert_pem, key_pem) = generate_self_signed_cert("localhost");
|
|
let route = make_h3_route(proxy_port, "127.0.0.1", backend_port, &cert_pem, &key_pem);
|
|
|
|
let options = RustProxyOptions {
|
|
routes: vec![route],
|
|
..Default::default()
|
|
};
|
|
|
|
// 3. Start proxy and wait for UDP bind
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
proxy.start().await.unwrap();
|
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
|
|
|
// 4. Connect QUIC/H3 client
|
|
let endpoint = make_h3_client_endpoint();
|
|
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", proxy_port).parse().unwrap();
|
|
let connection = endpoint
|
|
.connect(addr, "localhost")
|
|
.expect("Failed to initiate QUIC connection")
|
|
.await
|
|
.expect("QUIC handshake failed");
|
|
|
|
let (mut driver, mut send_request) = h3::client::new(
|
|
h3_quinn::Connection::new(connection),
|
|
)
|
|
.await
|
|
.expect("H3 connection setup failed");
|
|
|
|
// Drive the H3 connection in background
|
|
tokio::spawn(async move {
|
|
let _ = driver.wait_idle().await;
|
|
});
|
|
|
|
// 5. Send GET request
|
|
let req = http::Request::builder()
|
|
.method("GET")
|
|
.uri("https://localhost/")
|
|
.header("host", "localhost")
|
|
.body(())
|
|
.unwrap();
|
|
|
|
let mut stream = send_request.send_request(req).await
|
|
.expect("Failed to send H3 request");
|
|
stream.finish().await
|
|
.expect("Failed to finish sending H3 request body");
|
|
|
|
// 6. Read response headers
|
|
let resp = stream.recv_response().await
|
|
.expect("Failed to receive H3 response");
|
|
assert_eq!(resp.status(), http::StatusCode::OK,
|
|
"Expected 200 OK, got {}", resp.status());
|
|
|
|
// 7. Read body and verify stream ends (FIN received)
|
|
// This is the critical assertion: recv_data() must return None (stream ended)
|
|
// within the timeout, NOT hang forever waiting for a FIN that never arrives.
|
|
let result = with_timeout(async {
|
|
let mut total = 0usize;
|
|
while let Some(chunk) = stream.recv_data().await.expect("H3 data receive error") {
|
|
total += chunk.remaining();
|
|
}
|
|
// recv_data() returned None => stream ended (FIN received)
|
|
total
|
|
}, 10)
|
|
.await;
|
|
|
|
let bytes_received = result.expect(
|
|
"TIMEOUT: H3 stream never ended (FIN not received by client). \
|
|
The proxy sent all response data but failed to send the QUIC stream FIN."
|
|
);
|
|
assert_eq!(
|
|
bytes_received,
|
|
body_text.len(),
|
|
"Expected {} bytes, got {}",
|
|
body_text.len(),
|
|
bytes_received
|
|
);
|
|
|
|
// 8. Cleanup
|
|
endpoint.close(quinn::VarInt::from_u32(0), b"test done");
|
|
proxy.stop().await.unwrap();
|
|
}
|
|
|
|
/// Insecure TLS verifier that accepts any certificate (for tests only).
|
|
#[derive(Debug)]
|
|
struct InsecureVerifier;
|
|
|
|
impl rustls::client::danger::ServerCertVerifier for InsecureVerifier {
|
|
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::RSA_PKCS1_SHA256,
|
|
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
|
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
|
rustls::SignatureScheme::ED25519,
|
|
rustls::SignatureScheme::RSA_PSS_SHA256,
|
|
]
|
|
}
|
|
}
|