From fb1c59ac9a06d40f7eb6224676468b1ea2e9561d Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 20 Mar 2026 08:57:18 +0000 Subject: [PATCH] fix(rustproxy-http): reuse the shared HTTP proxy service for HTTP/3 request handling --- changelog.md | 8 + rust/crates/rustproxy-http/src/h3_service.rs | 298 +++--------------- .../rustproxy-http/src/proxy_service.rs | 40 ++- .../rustproxy-http/src/request_filter.rs | 9 +- .../rustproxy-passthrough/src/tcp_listener.rs | 5 + rust/crates/rustproxy/src/lib.rs | 13 +- .../rustproxy/tests/integration_h3_proxy.rs | 2 +- ts/00_commitinfo_data.ts | 2 +- 8 files changed, 101 insertions(+), 276 deletions(-) diff --git a/changelog.md b/changelog.md index 382978c..4a5d881 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-03-20 - 25.17.10 - fix(rustproxy-http) +reuse the shared HTTP proxy service for HTTP/3 request handling + +- Refactors H3ProxyService to delegate requests to the shared HttpProxyService instead of maintaining separate routing and backend forwarding logic. +- Aligns HTTP/3 with the TCP/HTTP path for route matching, connection pooling, and ALPN-based upstream protocol detection. +- Generalizes request handling and filters to accept boxed/generic HTTP bodies so both HTTP/3 and existing HTTP paths share the same proxy pipeline. +- Updates the HTTP/3 integration route matcher to allow transport matching across shared HTTP and QUIC handling. + ## 2026-03-20 - 25.17.9 - fix(rustproxy-http) correct HTTP/3 host extraction and avoid protocol filtering during UDP route lookup diff --git a/rust/crates/rustproxy-http/src/h3_service.rs b/rust/crates/rustproxy-http/src/h3_service.rs index 1c1d054..b63b200 100644 --- a/rust/crates/rustproxy-http/src/h3_service.rs +++ b/rust/crates/rustproxy-http/src/h3_service.rs @@ -1,63 +1,36 @@ //! HTTP/3 proxy service. //! //! Accepts QUIC connections via quinn, runs h3 server to handle HTTP/3 requests, -//! and forwards them to backends using the same routing and pool infrastructure -//! as the HTTP/1+2 proxy. +//! and delegates backend forwarding to the shared `HttpProxyService` — same +//! route matching, connection pool, and protocol auto-detection as TCP/HTTP. use std::net::SocketAddr; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; -use std::time::Duration; -use arc_swap::ArcSwap; use bytes::{Buf, Bytes}; use http_body::Frame; +use http_body_util::BodyExt; +use http_body_util::combinators::BoxBody; use tracing::{debug, warn}; -use rustproxy_config::{RouteConfig, TransportProtocol}; -use rustproxy_metrics::MetricsCollector; -use rustproxy_routing::{MatchContext, RouteManager}; +use rustproxy_config::RouteConfig; +use tokio_util::sync::CancellationToken; -use crate::connection_pool::ConnectionPool; -use crate::protocol_cache::ProtocolCache; -use crate::upstream_selector::UpstreamSelector; +use crate::proxy_service::{ConnActivity, HttpProxyService}; /// HTTP/3 proxy service. /// -/// Handles QUIC connections with the h3 crate, parses HTTP/3 requests, -/// and forwards them to backends using per-request route matching and -/// shared connection pooling. +/// Accepts QUIC connections, parses HTTP/3 requests, and delegates backend +/// forwarding to the shared `HttpProxyService`. pub struct H3ProxyService { - route_manager: Arc>, - metrics: Arc, - connection_pool: Arc, - #[allow(dead_code)] - protocol_cache: Arc, - #[allow(dead_code)] - upstream_selector: UpstreamSelector, - backend_tls_config: Arc, - connect_timeout: Duration, + http_proxy: Arc, } impl H3ProxyService { - pub fn new( - route_manager: Arc>, - metrics: Arc, - connection_pool: Arc, - protocol_cache: Arc, - backend_tls_config: Arc, - connect_timeout: Duration, - ) -> Self { - Self { - route_manager: Arc::clone(&route_manager), - metrics: Arc::clone(&metrics), - connection_pool, - protocol_cache, - upstream_selector: UpstreamSelector::new(), - backend_tls_config, - connect_timeout, - } + pub fn new(http_proxy: Arc) -> Self { + Self { http_proxy } } /// Handle an accepted QUIC connection as HTTP/3. @@ -81,8 +54,6 @@ impl H3ProxyService { .await .map_err(|e| anyhow::anyhow!("H3 connection setup failed: {}", e))?; - let client_ip = remote_addr.ip().to_string(); - loop { match h3_conn.accept().await { Ok(Some(resolver)) => { @@ -94,21 +65,13 @@ impl H3ProxyService { } }; - self.metrics.record_http_request(); - - 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(); + let http_proxy = Arc::clone(&self.http_proxy); tokio::spawn(async move { if let Err(e) = handle_h3_request( - request, stream, port, &client_ip, &rm, &pool, &metrics, - &backend_tls, connect_timeout, + request, stream, port, remote_addr, &http_proxy, ).await { - debug!("HTTP/3 request error from {}: {}", client_ip, e); + debug!("HTTP/3 request error from {}: {}", remote_addr, e); } }); } @@ -127,109 +90,27 @@ impl H3ProxyService { } } -/// Handle a single HTTP/3 request with per-request route matching. +/// Handle a single HTTP/3 request by delegating to HttpProxyService. +/// +/// 1. Read the H3 request body via an mpsc channel (streaming, not buffered) +/// 2. Build a `hyper::Request` that HttpProxyService can handle +/// 3. Call `HttpProxyService::handle_request` — same route matching, connection +/// pool, ALPN protocol detection (H1/H2/H3) as the TCP/HTTP path +/// 4. Stream the response back over the H3 stream async fn handle_h3_request( request: hyper::Request<()>, mut stream: h3::server::RequestStream, Bytes>, port: u16, - client_ip: &str, - route_manager: &RouteManager, - _connection_pool: &ConnectionPool, - metrics: &MetricsCollector, - backend_tls_config: &Arc, - connect_timeout: Duration, + peer_addr: SocketAddr, + http_proxy: &HttpProxyService, ) -> anyhow::Result<()> { - let method = request.method().clone(); - let uri = request.uri().clone(); - let path = uri.path().to_string(); - - // Extract host from :authority or Host header (strip port to match TCP/HTTP path) - let host = request.uri().host() - .map(|h| h.to_string()) - .or_else(|| request.headers().get("host").and_then(|v| v.to_str().ok()) - .map(|h| h.split(':').next().unwrap_or(h).to_string())) - .unwrap_or_default(); - - debug!("HTTP/3 {} {} (host: {}, client: {})", method, path, host, client_ip); - - // Per-request route matching - let ctx = MatchContext { - port, - domain: if host.is_empty() { None } else { Some(&host) }, - path: Some(&path), - client_ip: Some(client_ip), - tls_version: Some("TLSv1.3"), - headers: None, - is_tls: true, - protocol: None, // Don't filter on protocol — transport: Udp already excludes TCP routes, - // and the route was already protocol-validated at the QUIC accept level. - transport: Some(TransportProtocol::Udp), - }; - - let route_match = route_manager.find_route(&ctx) - .ok_or_else(|| anyhow::anyhow!("No route matched for HTTP/3 request to {}{}", host, path))?; - let route = route_match.route; - - // Resolve backend target (use matched target or first target) - let target = route_match.target - .or_else(|| route.action.targets.as_ref().and_then(|t| t.first())) - .ok_or_else(|| anyhow::anyhow!("No target for HTTP/3 route"))?; - - let backend_host = target.host.first(); - let backend_port = target.port.resolve(port); - let backend_addr = format!("{}:{}", backend_host, backend_port); - - // 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), - ).await - .map_err(|_| anyhow::anyhow!("Backend connect timeout to {}", backend_addr))? - .map_err(|e| anyhow::anyhow!("Backend connect to {} failed: {}", backend_addr, e))?; - - let _ = tcp_stream.set_nodelay(true); - - // Branch: wrap in TLS if backend requires it, then HTTP/1.1 handshake. - // hyper's SendRequest 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. + // Stream request body from H3 client via an mpsc channel. let (body_tx, body_rx) = tokio::sync::mpsc::channel::(4); - let total_bytes_in = Arc::new(std::sync::atomic::AtomicU64::new(0)); - let total_bytes_in_writer = Arc::clone(&total_bytes_in); // Spawn the H3 body reader task let body_reader = tokio::spawn(async move { while let Ok(Some(mut chunk)) = stream.recv_data().await { let data = Bytes::copy_from_slice(chunk.chunk()); - total_bytes_in_writer.fetch_add(data.len() as u64, std::sync::atomic::Ordering::Relaxed); chunk.advance(chunk.remaining()); if body_tx.send(data).await.is_err() { break; @@ -238,145 +119,64 @@ async fn handle_h3_request( stream }); - // Create a body that polls from the mpsc receiver + // Build a hyper::Request from the H3 request + streaming body. + // The URI already has scheme + authority + path set by the h3 crate. let body = H3RequestBody { receiver: body_rx }; - let backend_req = build_backend_request(&method, &backend_addr, &path, &host, &request, body, use_tls)?; + let (parts, _) = request.into_parts(); + let boxed_body: BoxBody = BoxBody::new(body); + let req = hyper::Request::from_parts(parts, boxed_body); - let response = sender.send_request(backend_req).await + // Delegate to HttpProxyService — same backend path as TCP/HTTP: + // route matching, ALPN protocol detection, connection pool, H1/H2/H3 auto. + let cancel = CancellationToken::new(); + let conn_activity = ConnActivity::new_standalone(); + let response = http_proxy.handle_request(req, peer_addr, port, cancel, conn_activity).await .map_err(|e| anyhow::anyhow!("Backend request failed: {}", e))?; - // Await the body reader to get the stream back + // Await the body reader to get the H3 stream back let mut stream = body_reader.await .map_err(|e| anyhow::anyhow!("Body reader task failed: {}", e))?; - let total_bytes_in = total_bytes_in.load(std::sync::atomic::Ordering::Relaxed); - // Build H3 response - let status = response.status(); - let mut h3_response = hyper::Response::builder().status(status); - - // Copy response headers (skip hop-by-hop) - for (name, value) in response.headers() { - let n = name.as_str().to_lowercase(); + // Send response headers over H3 (skip hop-by-hop headers) + let (resp_parts, resp_body) = response.into_parts(); + let mut h3_response = hyper::Response::builder().status(resp_parts.status); + for (name, value) in &resp_parts.headers { + let n = name.as_str(); if n == "transfer-encoding" || n == "connection" || n == "keep-alive" || n == "upgrade" { continue; } h3_response = h3_response.header(name, value); } - - // Extract content-length for body loop termination (must be before into_body()) - let content_length: Option = 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()) - .map(|q| { - let p = q.alt_svc_port.unwrap_or(port); - let ma = q.alt_svc_max_age.unwrap_or(86400); - format!("h3=\":{}\"; ma={}", p, ma) - }) - .unwrap_or_else(|| format!("h3=\":{}\"; ma=86400", port)); - h3_response = h3_response.header("alt-svc", alt_svc); - let h3_response = h3_response.body(()) .map_err(|e| anyhow::anyhow!("Failed to build H3 response: {}", e))?; - // Send response headers stream.send_response(h3_response).await .map_err(|e| anyhow::anyhow!("Failed to send H3 response: {}", e))?; - // 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; - - // 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))) => { + // Stream response body back over H3 + let mut resp_body = resp_body; + while let Some(frame) = resp_body.frame().await { + match frame { + 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; - } - } } } - 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 - ); + Err(e) => { + warn!("Response body read error: {}", e); break; } } } - // Record metrics - let route_id = route.name.as_deref().or(route.id.as_deref()); - metrics.record_bytes(total_bytes_in, total_bytes_out, route_id, Some(client_ip)); - - // Finish the stream + // Finish the H3 stream (send QUIC FIN) stream.finish().await .map_err(|e| anyhow::anyhow!("Failed to finish H3 stream: {}", e))?; Ok(()) } -/// Build an HTTP/1.1 backend request from the H3 frontend request. -fn build_backend_request( - method: &hyper::Method, - backend_addr: &str, - path: &str, - host: &str, - original_request: &hyper::Request<()>, - body: B, - use_tls: bool, -) -> anyhow::Result> { - let scheme = if use_tls { "https" } else { "http" }; - let mut req = hyper::Request::builder() - .method(method) - .uri(format!("{}://{}{}", scheme, backend_addr, path)) - .header("host", host); - - // Forward non-pseudo headers - for (name, value) in original_request.headers() { - let n = name.as_str(); - if !n.starts_with(':') && n != "host" { - req = req.header(name, value); - } - } - - req.body(body) - .map_err(|e| anyhow::anyhow!("Failed to build backend request: {}", e)) -} - /// A streaming request body backed by an mpsc channel receiver. /// /// Implements `http_body::Body` so hyper can poll chunks as they arrive diff --git a/rust/crates/rustproxy-http/src/proxy_service.rs b/rust/crates/rustproxy-http/src/proxy_service.rs index 3ceb18e..5d493b7 100644 --- a/rust/crates/rustproxy-http/src/proxy_service.rs +++ b/rust/crates/rustproxy-http/src/proxy_service.rs @@ -36,7 +36,7 @@ use crate::upstream_selector::UpstreamSelector; /// Per-connection context for keeping the idle watchdog alive during body streaming. /// Passed through the forwarding chain so CountingBody can update the timestamp. #[derive(Clone)] -struct ConnActivity { +pub struct ConnActivity { last_activity: Arc, start: std::time::Instant, /// Active-request counter from handle_io's idle watchdog. When set, CountingBody @@ -49,6 +49,19 @@ struct ConnActivity { alt_svc_cache_key: Option, } +impl ConnActivity { + /// Create a minimal ConnActivity (no idle watchdog, no Alt-Svc cache). + /// Used by H3ProxyService where the TCP idle watchdog doesn't apply. + pub fn new_standalone() -> Self { + Self { + last_activity: Arc::new(AtomicU64::new(0)), + start: std::time::Instant::now(), + active_requests: None, + alt_svc_cache_key: None, + } + } +} + /// Default upstream connect timeout (30 seconds). const DEFAULT_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); @@ -347,6 +360,7 @@ impl HttpProxyService { let st = start; let ca = ConnActivity { last_activity: Arc::clone(&la_inner), start, active_requests: Some(Arc::clone(&ar_inner)), alt_svc_cache_key: None }; async move { + let req = req.map(|body| BoxBody::new(body)); let result = svc.handle_request(req, peer, port, cn, ca).await; // Mark request end — update activity timestamp before guard drops la.store(st.elapsed().as_millis() as u64, Ordering::Relaxed); @@ -416,9 +430,13 @@ impl HttpProxyService { } /// Handle a single HTTP request. - async fn handle_request( + /// + /// Accepts a generic body (`BoxBody`) so both the TCP/HTTP path (which boxes + /// `Incoming`) and the H3 path (which boxes the H3 request body stream) can + /// share the same backend forwarding logic. + pub async fn handle_request( &self, - req: Request, + req: Request>, peer_addr: std::net::SocketAddr, port: u16, cancel: CancellationToken, @@ -965,7 +983,7 @@ impl HttpProxyService { &self, io: TokioIo, parts: hyper::http::request::Parts, - body: Incoming, + body: BoxBody, upstream_headers: hyper::HeaderMap, upstream_path: &str, _upstream: &crate::upstream_selector::UpstreamSelection, @@ -1013,7 +1031,7 @@ impl HttpProxyService { &self, mut sender: hyper::client::conn::http1::SendRequest>, parts: hyper::http::request::Parts, - body: Incoming, + body: BoxBody, upstream_headers: hyper::HeaderMap, upstream_path: &str, route: &rustproxy_config::RouteConfig, @@ -1077,7 +1095,7 @@ impl HttpProxyService { &self, io: TokioIo, parts: hyper::http::request::Parts, - body: Incoming, + body: BoxBody, upstream_headers: hyper::HeaderMap, upstream_path: &str, _upstream: &crate::upstream_selector::UpstreamSelection, @@ -1151,7 +1169,7 @@ impl HttpProxyService { &self, sender: hyper::client::conn::http2::SendRequest>, parts: hyper::http::request::Parts, - body: Incoming, + body: BoxBody, upstream_headers: hyper::HeaderMap, upstream_path: &str, route: &rustproxy_config::RouteConfig, @@ -1344,7 +1362,7 @@ impl HttpProxyService { &self, io: TokioIo, parts: hyper::http::request::Parts, - body: Incoming, + body: BoxBody, mut upstream_headers: hyper::HeaderMap, upstream_path: &str, upstream: &crate::upstream_selector::UpstreamSelection, @@ -1675,7 +1693,7 @@ impl HttpProxyService { &self, mut sender: hyper::client::conn::http2::SendRequest>, parts: hyper::http::request::Parts, - body: Incoming, + body: BoxBody, upstream_headers: hyper::HeaderMap, upstream_path: &str, route: &rustproxy_config::RouteConfig, @@ -1816,7 +1834,7 @@ impl HttpProxyService { /// Handle a WebSocket upgrade request (H1 Upgrade or H2 Extended CONNECT per RFC 8441). async fn handle_websocket_upgrade( &self, - req: Request, + req: Request>, peer_addr: std::net::SocketAddr, upstream: &crate::upstream_selector::UpstreamSelection, route: &rustproxy_config::RouteConfig, @@ -2538,7 +2556,7 @@ impl HttpProxyService { &self, quic_conn: quinn::Connection, parts: hyper::http::request::Parts, - body: Incoming, + body: BoxBody, upstream_headers: hyper::HeaderMap, upstream_path: &str, route: &rustproxy_config::RouteConfig, diff --git a/rust/crates/rustproxy-http/src/request_filter.rs b/rust/crates/rustproxy-http/src/request_filter.rs index 7bfa777..92fc2a2 100644 --- a/rust/crates/rustproxy-http/src/request_filter.rs +++ b/rust/crates/rustproxy-http/src/request_filter.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use bytes::Bytes; use http_body_util::Full; use http_body_util::BodyExt; -use hyper::body::Incoming; use hyper::{Request, Response, StatusCode}; use http_body_util::combinators::BoxBody; @@ -19,7 +18,7 @@ impl RequestFilter { /// Apply security filters. Returns Some(response) if the request should be blocked. pub fn apply( security: &RouteSecurity, - req: &Request, + req: &Request, peer_addr: &SocketAddr, ) -> Option>> { Self::apply_with_rate_limiter(security, req, peer_addr, None) @@ -29,7 +28,7 @@ impl RequestFilter { /// Returns Some(response) if the request should be blocked. pub fn apply_with_rate_limiter( security: &RouteSecurity, - req: &Request, + req: &Request, peer_addr: &SocketAddr, rate_limiter: Option<&Arc>, ) -> Option>> { @@ -182,7 +181,7 @@ impl RequestFilter { /// Determine the rate limit key based on configuration. fn rate_limit_key( config: &rustproxy_config::RouteRateLimit, - req: &Request, + req: &Request, peer_addr: &SocketAddr, ) -> String { use rustproxy_config::RateLimitKeyBy; @@ -220,7 +219,7 @@ impl RequestFilter { /// Handle CORS preflight (OPTIONS) requests. /// Returns Some(response) if this is a CORS preflight that should be handled. pub fn handle_cors_preflight( - req: &Request, + req: &Request, ) -> Option>> { if req.method() != hyper::Method::OPTIONS { return None; diff --git a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs index c8e0b94..6a67a1a 100644 --- a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs +++ b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs @@ -428,6 +428,11 @@ impl TcpListenerManager { self.http_proxy.prune_stale_routes(active_route_ids); } + /// Get a reference to the HTTP proxy service (shared with H3). + pub fn http_proxy(&self) -> &Arc { + &self.http_proxy + } + /// Get a reference to the connection tracker. pub fn conn_tracker(&self) -> &Arc { &self.conn_tracker diff --git a/rust/crates/rustproxy/src/lib.rs b/rust/crates/rustproxy/src/lib.rs index fb56f26..93ad9e7 100644 --- a/rust/crates/rustproxy/src/lib.rs +++ b/rust/crates/rustproxy/src/lib.rs @@ -343,15 +343,10 @@ impl RustProxy { ); udp_mgr.set_proxy_ips(udp_proxy_ips.clone()); - // Construct H3ProxyService for HTTP/3 request handling - let h3_svc = rustproxy_http::h3_service::H3ProxyService::new( - Arc::new(ArcSwap::from(Arc::clone(&*self.route_table.load()))), - Arc::clone(&self.metrics), - Arc::new(rustproxy_http::connection_pool::ConnectionPool::new()), - Arc::new(rustproxy_http::protocol_cache::ProtocolCache::new()), - rustproxy_passthrough::tls_handler::shared_backend_tls_config(), - std::time::Duration::from_secs(30), - ); + // Share HttpProxyService with H3 — same route matching, connection + // pool, and ALPN protocol detection as the TCP/HTTP path. + let http_proxy = self.listener_manager.as_ref().unwrap().http_proxy().clone(); + let h3_svc = rustproxy_http::h3_service::H3ProxyService::new(http_proxy); udp_mgr.set_h3_service(Arc::new(h3_svc)); for port in &udp_ports { diff --git a/rust/crates/rustproxy/tests/integration_h3_proxy.rs b/rust/crates/rustproxy/tests/integration_h3_proxy.rs index e708b9f..a834986 100644 --- a/rust/crates/rustproxy/tests/integration_h3_proxy.rs +++ b/rust/crates/rustproxy/tests/integration_h3_proxy.rs @@ -15,7 +15,7 @@ fn make_h3_route( 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); + route.route_match.transport = Some(TransportProtocol::All); // Keep domain="localhost" from make_tls_terminate_route — needed for TLS cert extraction route.action.udp = Some(RouteUdp { session_timeout: None, diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 353fc52..3add3c9 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '25.17.9', + version: '25.17.10', 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.' }