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 { Ok(rustls::client::danger::ServerCertVerified::assertion()) } fn verify_tls12_signature( &self, _message: &[u8], _cert: &rustls::pki_types::CertificateDer<'_>, _dss: &rustls::DigitallySignedStruct, ) -> Result { Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) } fn verify_tls13_signature( &self, _message: &[u8], _cert: &rustls::pki_types::CertificateDer<'_>, _dss: &rustls::DigitallySignedStruct, ) -> Result { Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) } fn supported_verify_schemes(&self) -> Vec { vec![ rustls::SignatureScheme::RSA_PKCS1_SHA256, rustls::SignatureScheme::ECDSA_NISTP256_SHA256, rustls::SignatureScheme::ECDSA_NISTP384_SHA384, rustls::SignatureScheme::ED25519, rustls::SignatureScheme::RSA_PSS_SHA256, ] } }