Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fce910795 | |||
| ff09cef350 | |||
| d0148b2ac3 | |||
| 7217e15649 | |||
| bfcf92a855 | |||
| 8e0804cd20 | |||
| c63f6fcd5f | |||
| f3cd4d193e | |||
| 81de611255 | |||
| 91598b3be9 | |||
| 4e3c548012 | |||
| 1a2d7529db | |||
| 31514f54ae | |||
| 247653c9d0 |
43
changelog.md
43
changelog.md
@@ -1,5 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-20 - 25.17.7 - fix(readme)
|
||||
document QUIC and HTTP/3 compatibility caveats
|
||||
|
||||
- Add notes explaining that GREASE frames are disabled on both server and client HTTP/3 paths to avoid interoperability issues
|
||||
- Document that the current HTTP/3 stack depends on pre-1.0 h3 ecosystem components and may still have rough edges
|
||||
|
||||
## 2026-03-20 - 25.17.6 - fix(rustproxy-http)
|
||||
disable HTTP/3 GREASE for client and server connections
|
||||
|
||||
- Switch the HTTP/3 server connection setup to use the builder API with send_grease(false)
|
||||
- Switch the HTTP/3 client handshake to use the builder API with send_grease(false) to improve compatibility
|
||||
|
||||
## 2026-03-20 - 25.17.5 - fix(rustproxy)
|
||||
add HTTP/3 integration test for QUIC response stream FIN handling
|
||||
|
||||
- adds an integration test covering HTTP/3 proxying over QUIC with TLS termination
|
||||
- verifies response bodies fully arrive and the client receives stream termination instead of hanging
|
||||
- adds test-only dependencies for quinn, h3, h3-quinn, rustls, bytes, and http
|
||||
|
||||
## 2026-03-20 - 25.17.4 - fix(rustproxy-http)
|
||||
prevent HTTP/3 response body streaming from hanging on backend completion
|
||||
|
||||
- extract and track Content-Length before consuming the response body
|
||||
- stop the HTTP/3 body loop when the stream reports end-of-stream or the expected byte count has been sent
|
||||
- add a per-frame idle timeout to avoid indefinite waits on stalled or close-delimited backend bodies
|
||||
|
||||
## 2026-03-20 - 25.17.3 - fix(repository)
|
||||
no changes detected
|
||||
|
||||
|
||||
## 2026-03-20 - 25.17.2 - fix(rustproxy-http)
|
||||
enable TLS connections for HTTP/3 upstream requests when backend re-encryption or TLS is configured
|
||||
|
||||
- Pass backend TLS client configuration into the HTTP/3 request handler.
|
||||
- Detect TLS-required upstream targets using route and target TLS settings before connecting.
|
||||
- Build backend request URIs with the correct http or https scheme to match the upstream connection.
|
||||
|
||||
## 2026-03-20 - 25.17.1 - fix(rustproxy-routing)
|
||||
allow QUIC UDP TLS connections without SNI to match domain-restricted routes
|
||||
|
||||
- Exempts UDP transport from the no-SNI rejection logic because QUIC encrypts the TLS ClientHello and SNI is unavailable at accept time
|
||||
- Adds regression tests to confirm QUIC route matching succeeds without SNI while TCP TLS without SNI remains rejected
|
||||
|
||||
## 2026-03-19 - 25.17.0 - feat(rustproxy-passthrough)
|
||||
add PROXY protocol v2 client IP handling for UDP and QUIC listeners
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "25.17.0",
|
||||
"version": "25.17.7",
|
||||
"private": false,
|
||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
||||
@@ -1111,6 +1111,10 @@ SmartProxy searches for the Rust binary in this order:
|
||||
5. Local dev build (`./rust/target/release/rustproxy`)
|
||||
6. System PATH (`rustproxy`)
|
||||
|
||||
### QUIC / HTTP3 Caveats
|
||||
- **GREASE frames are disabled.** The underlying h3 crate sends [GREASE frames](https://www.rfc-editor.org/rfc/rfc9114.html#frame-reserved) by default to test protocol extensibility. However, some HTTP/3 clients and servers don't properly ignore unknown frame types, causing 400/500 errors or stream hangs ([h3#206](https://github.com/hyperium/h3/issues/206)). SmartProxy disables GREASE on both the server side (for incoming H3 requests) and the client side (for H3 backend connections) to maximize compatibility.
|
||||
- **HTTP/3 is pre-release.** The h3 ecosystem (h3 0.0.8, h3-quinn 0.0.10, quinn 0.11) is still pre-1.0. Expect rough edges.
|
||||
|
||||
### Performance Tuning
|
||||
- ✅ Use NFTables forwarding for high-traffic routes (Linux only)
|
||||
- ✅ Enable connection keep-alive where appropriate
|
||||
|
||||
4
rust/Cargo.lock
generated
4
rust/Cargo.lock
generated
@@ -1224,10 +1224,14 @@ dependencies = [
|
||||
"bytes",
|
||||
"clap",
|
||||
"dashmap",
|
||||
"h3",
|
||||
"h3-quinn",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"mimalloc",
|
||||
"quinn",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
|
||||
@@ -36,7 +36,6 @@ pub struct H3ProxyService {
|
||||
protocol_cache: Arc<ProtocolCache>,
|
||||
#[allow(dead_code)]
|
||||
upstream_selector: UpstreamSelector,
|
||||
#[allow(dead_code)]
|
||||
backend_tls_config: Arc<rustls::ClientConfig>,
|
||||
connect_timeout: Duration,
|
||||
}
|
||||
@@ -76,7 +75,9 @@ impl H3ProxyService {
|
||||
debug!("HTTP/3 connection from {} on port {}", remote_addr, port);
|
||||
|
||||
let mut h3_conn: h3::server::Connection<h3_quinn::Connection, Bytes> =
|
||||
h3::server::Connection::new(h3_quinn::Connection::new(connection))
|
||||
h3::server::builder()
|
||||
.send_grease(false)
|
||||
.build(h3_quinn::Connection::new(connection))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("H3 connection setup failed: {}", e))?;
|
||||
|
||||
@@ -98,12 +99,14 @@ impl H3ProxyService {
|
||||
let rm = self.route_manager.load();
|
||||
let pool = Arc::clone(&self.connection_pool);
|
||||
let metrics = Arc::clone(&self.metrics);
|
||||
let backend_tls = Arc::clone(&self.backend_tls_config);
|
||||
let connect_timeout = self.connect_timeout;
|
||||
let client_ip = client_ip.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_h3_request(
|
||||
request, stream, port, &client_ip, &rm, &pool, &metrics, connect_timeout,
|
||||
request, stream, port, &client_ip, &rm, &pool, &metrics,
|
||||
&backend_tls, connect_timeout,
|
||||
).await {
|
||||
debug!("HTTP/3 request error from {}: {}", client_ip, e);
|
||||
}
|
||||
@@ -133,6 +136,7 @@ async fn handle_h3_request(
|
||||
route_manager: &RouteManager,
|
||||
_connection_pool: &ConnectionPool,
|
||||
metrics: &MetricsCollector,
|
||||
backend_tls_config: &Arc<rustls::ClientConfig>,
|
||||
connect_timeout: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
let method = request.method().clone();
|
||||
@@ -173,7 +177,15 @@ async fn handle_h3_request(
|
||||
let backend_port = target.port.resolve(port);
|
||||
let backend_addr = format!("{}:{}", backend_host, backend_port);
|
||||
|
||||
// Connect to backend via TCP HTTP/1.1 with timeout
|
||||
// Determine if backend requires TLS (same logic as proxy_service.rs)
|
||||
let mut use_tls = target.tls.is_some();
|
||||
if let Some(ref tls) = route.action.tls {
|
||||
if tls.mode == rustproxy_config::TlsMode::TerminateAndReencrypt {
|
||||
use_tls = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to backend via TCP with timeout
|
||||
let tcp_stream = tokio::time::timeout(
|
||||
connect_timeout,
|
||||
tokio::net::TcpStream::connect(&backend_addr),
|
||||
@@ -183,15 +195,27 @@ async fn handle_h3_request(
|
||||
|
||||
let _ = tcp_stream.set_nodelay(true);
|
||||
|
||||
let io = hyper_util::rt::TokioIo::new(tcp_stream);
|
||||
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend handshake failed: {}", e))?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = conn.await {
|
||||
debug!("Backend connection closed: {}", e);
|
||||
}
|
||||
});
|
||||
// Branch: wrap in TLS if backend requires it, then HTTP/1.1 handshake.
|
||||
// hyper's SendRequest<B> is NOT generic over the IO type, so both branches
|
||||
// produce the same type and can be unified.
|
||||
let mut sender = if use_tls {
|
||||
let connector = tokio_rustls::TlsConnector::from(Arc::clone(backend_tls_config));
|
||||
let server_name = rustls::pki_types::ServerName::try_from(backend_host.to_string())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid backend SNI '{}': {}", backend_host, e))?;
|
||||
let tls_stream = connector.connect(server_name, tcp_stream).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend TLS handshake to {} failed: {}", backend_addr, e))?;
|
||||
let io = hyper_util::rt::TokioIo::new(tls_stream);
|
||||
let (sender, conn) = hyper::client::conn::http1::handshake(io).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend handshake failed: {}", e))?;
|
||||
tokio::spawn(async move { let _ = conn.await; });
|
||||
sender
|
||||
} else {
|
||||
let io = hyper_util::rt::TokioIo::new(tcp_stream);
|
||||
let (sender, conn) = hyper::client::conn::http1::handshake(io).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend handshake failed: {}", e))?;
|
||||
tokio::spawn(async move { let _ = conn.await; });
|
||||
sender
|
||||
};
|
||||
|
||||
// Stream request body from H3 client to backend via an mpsc channel.
|
||||
// This avoids buffering the entire request body in memory.
|
||||
@@ -214,7 +238,7 @@ async fn handle_h3_request(
|
||||
|
||||
// Create a body that polls from the mpsc receiver
|
||||
let body = H3RequestBody { receiver: body_rx };
|
||||
let backend_req = build_backend_request(&method, &backend_addr, &path, &host, &request, body)?;
|
||||
let backend_req = build_backend_request(&method, &backend_addr, &path, &host, &request, body, use_tls)?;
|
||||
|
||||
let response = sender.send_request(backend_req).await
|
||||
.map_err(|e| anyhow::anyhow!("Backend request failed: {}", e))?;
|
||||
@@ -237,6 +261,12 @@ async fn handle_h3_request(
|
||||
h3_response = h3_response.header(name, value);
|
||||
}
|
||||
|
||||
// Extract content-length for body loop termination (must be before into_body())
|
||||
let content_length: Option<u64> = response.headers()
|
||||
.get(hyper::header::CONTENT_LENGTH)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse().ok());
|
||||
|
||||
// Add Alt-Svc for HTTP/3 advertisement
|
||||
let alt_svc = route.action.udp.as_ref()
|
||||
.and_then(|u| u.quic.as_ref())
|
||||
@@ -257,21 +287,52 @@ async fn handle_h3_request(
|
||||
|
||||
// Stream response body back
|
||||
use http_body_util::BodyExt;
|
||||
use http_body::Body as _;
|
||||
let mut body = response.into_body();
|
||||
let mut total_bytes_out: u64 = 0;
|
||||
while let Some(frame) = body.frame().await {
|
||||
match frame {
|
||||
Ok(frame) => {
|
||||
|
||||
// Per-frame idle timeout: if no frame arrives within this duration, assume
|
||||
// the body is complete (or the backend has stalled). This prevents indefinite
|
||||
// hangs on close-delimited bodies or when hyper's internal trailers oneshot
|
||||
// never resolves after all data has been received.
|
||||
const FRAME_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
loop {
|
||||
// Layer 1: If the body already knows it is finished (Content-Length
|
||||
// bodies track remaining bytes internally), break immediately to
|
||||
// avoid blocking on hyper's internal trailers oneshot.
|
||||
if body.is_end_stream() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Layer 3: Per-frame idle timeout safety net
|
||||
match tokio::time::timeout(FRAME_IDLE_TIMEOUT, body.frame()).await {
|
||||
Ok(Some(Ok(frame))) => {
|
||||
if let Some(data) = frame.data_ref() {
|
||||
total_bytes_out += data.len() as u64;
|
||||
stream.send_data(Bytes::copy_from_slice(data)).await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to send H3 data: {}", e))?;
|
||||
|
||||
// Layer 2: Content-Length byte count check
|
||||
if let Some(cl) = content_length {
|
||||
if total_bytes_out >= cl {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
Ok(Some(Err(e))) => {
|
||||
warn!("Backend body read error: {}", e);
|
||||
break;
|
||||
}
|
||||
Ok(None) => break, // Body ended naturally
|
||||
Err(_) => {
|
||||
debug!(
|
||||
"H3 body frame idle timeout ({:?}) after {} bytes; finishing stream",
|
||||
FRAME_IDLE_TIMEOUT, total_bytes_out
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,10 +355,12 @@ fn build_backend_request<B>(
|
||||
host: &str,
|
||||
original_request: &hyper::Request<()>,
|
||||
body: B,
|
||||
use_tls: bool,
|
||||
) -> anyhow::Result<hyper::Request<B>> {
|
||||
let scheme = if use_tls { "https" } else { "http" };
|
||||
let mut req = hyper::Request::builder()
|
||||
.method(method)
|
||||
.uri(format!("http://{}{}", backend_addr, path))
|
||||
.uri(format!("{}://{}{}", scheme, backend_addr, path))
|
||||
.header("host", host);
|
||||
|
||||
// Forward non-pseudo headers
|
||||
|
||||
@@ -2550,7 +2550,11 @@ impl HttpProxyService {
|
||||
backend_key: &str,
|
||||
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
|
||||
let h3_quinn_conn = h3_quinn::Connection::new(quic_conn.clone());
|
||||
let (mut driver, mut send_request) = match h3::client::new(h3_quinn_conn).await {
|
||||
let (mut driver, mut send_request) = match h3::client::builder()
|
||||
.send_grease(false)
|
||||
.build(h3_quinn_conn)
|
||||
.await
|
||||
{
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
error!(backend = %backend_key, domain = %domain, error = %e, "H3 client handshake failed");
|
||||
|
||||
@@ -122,10 +122,16 @@ impl RouteManager {
|
||||
// This prevents session-ticket resumption from misrouting when clients
|
||||
// omit SNI (RFC 8446 recommends but doesn't mandate SNI on resumption).
|
||||
// Wildcard-only routes (domains: ["*"]) still match since they accept all.
|
||||
let patterns = domains.to_vec();
|
||||
let is_wildcard_only = patterns.iter().all(|d| *d == "*");
|
||||
if !is_wildcard_only {
|
||||
return false;
|
||||
//
|
||||
// Exception: QUIC (UDP transport) encrypts the TLS ClientHello, so SNI
|
||||
// is unavailable at accept time. Domain verification happens per-request
|
||||
// in H3ProxyService via the :authority header.
|
||||
if ctx.transport != Some(TransportProtocol::Udp) {
|
||||
let patterns = domains.to_vec();
|
||||
let is_wildcard_only = patterns.iter().all(|d| *d == "*");
|
||||
if !is_wildcard_only {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -997,4 +1003,52 @@ mod tests {
|
||||
let result = manager.find_route(&udp_ctx).unwrap();
|
||||
assert_eq!(result.route.name.as_deref(), Some("udp-route"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quic_tls_no_sni_matches_domain_restricted_route() {
|
||||
// QUIC accept-level matching: is_tls=true, domain=None, transport=Udp.
|
||||
// Should match because QUIC encrypts the ClientHello — SNI is unavailable
|
||||
// at accept time but verified per-request in H3ProxyService.
|
||||
let mut route = make_route(443, Some("example.com"), 0);
|
||||
route.route_match.transport = Some(TransportProtocol::Udp);
|
||||
let routes = vec![route];
|
||||
let manager = RouteManager::new(routes);
|
||||
|
||||
let ctx = MatchContext {
|
||||
port: 443,
|
||||
domain: None,
|
||||
path: None,
|
||||
client_ip: None,
|
||||
tls_version: None,
|
||||
headers: None,
|
||||
is_tls: true,
|
||||
protocol: Some("quic"),
|
||||
transport: Some(TransportProtocol::Udp),
|
||||
};
|
||||
|
||||
assert!(manager.find_route(&ctx).is_some(),
|
||||
"QUIC (UDP) with is_tls=true and domain=None should match domain-restricted routes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tcp_tls_no_sni_still_rejects_domain_restricted_route() {
|
||||
// TCP TLS without SNI must still be rejected (no QUIC exemption).
|
||||
let routes = vec![make_route(443, Some("example.com"), 0)];
|
||||
let manager = RouteManager::new(routes);
|
||||
|
||||
let ctx = MatchContext {
|
||||
port: 443,
|
||||
domain: None,
|
||||
path: None,
|
||||
client_ip: None,
|
||||
tls_version: None,
|
||||
headers: None,
|
||||
is_tls: true,
|
||||
protocol: None,
|
||||
transport: None, // TCP (default)
|
||||
};
|
||||
|
||||
assert!(manager.find_route(&ctx).is_none(),
|
||||
"TCP TLS without SNI should NOT match domain-restricted routes");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,3 +44,9 @@ mimalloc = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rcgen = { workspace = true }
|
||||
quinn = { workspace = true }
|
||||
h3 = { workspace = true }
|
||||
h3-quinn = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
http = "1"
|
||||
|
||||
195
rust/crates/rustproxy/tests/integration_h3_proxy.rs
Normal file
195
rust/crates/rustproxy/tests/integration_h3_proxy.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
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,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '25.17.0',
|
||||
version: '25.17.7',
|
||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user