From 2b58615d2427ae8ca7ec247a293a80b5094387fc Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 15 Mar 2026 16:00:26 +0000 Subject: [PATCH] feat(rustproxy-http): add HTTP/2 Extended CONNECT WebSocket proxy support --- changelog.md | 7 +++ .../rustproxy-http/src/proxy_service.rs | 63 ++++++++++++++++--- ts/00_commitinfo_data.ts | 2 +- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/changelog.md b/changelog.md index fbc57da..33362e8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-15 - 25.11.0 - feat(rustproxy-http) +add HTTP/2 Extended CONNECT WebSocket proxy support + +- Enable HTTP/2 CONNECT protocol support on the Hyper auto connection builder +- Detect WebSocket requests for both HTTP/1 Upgrade and HTTP/2 Extended CONNECT flows +- Translate HTTP/2 WebSocket requests to an HTTP/1.1 backend handshake and return RFC-compliant client responses + ## 2026-03-12 - 25.10.7 - fix(rustproxy-http) remove Host header from HTTP/2 upstream requests while preserving it for HTTP/1 retries diff --git a/rust/crates/rustproxy-http/src/proxy_service.rs b/rust/crates/rustproxy-http/src/proxy_service.rs index a4b95d5..cda6fca 100644 --- a/rust/crates/rustproxy-http/src/proxy_service.rs +++ b/rust/crates/rustproxy-http/src/proxy_service.rs @@ -304,8 +304,10 @@ impl HttpProxyService { }); // Auto-detect h1 vs h2 based on ALPN / connection preface. - // serve_connection_with_upgrades supports h1 Upgrade (WebSocket) and h2 CONNECT. - let builder = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new()); + // serve_connection_with_upgrades supports h1 Upgrade (WebSocket) and h2 Extended CONNECT (RFC 8441). + let mut builder = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new()); + // Advertise Extended CONNECT support so H2 clients can initiate WebSocket connections + builder.http2().enable_connect_protocol(); let conn = builder.serve_connection_with_upgrades(io, service); // Pin on the heap — auto::UpgradeableConnection is !Unpin let mut conn = Box::pin(conn); @@ -482,16 +484,22 @@ impl HttpProxyService { let domain_str = host.as_deref().unwrap_or("-"); self.upstream_selector.connection_started(&upstream_key); - // Check for WebSocket upgrade - let is_websocket = req.headers() + // Check for WebSocket upgrade: H1 (Upgrade header) or H2 Extended CONNECT (RFC 8441) + let is_h1_websocket = req.headers() .get("upgrade") .and_then(|v| v.to_str().ok()) .map(|v| v.eq_ignore_ascii_case("websocket")) .unwrap_or(false); - if is_websocket { + let is_h2_websocket = req.method() == hyper::Method::CONNECT + && req.extensions() + .get::() + .map(|p| p.as_str().eq_ignore_ascii_case("websocket")) + .unwrap_or(false); + + if is_h1_websocket || is_h2_websocket { let result = self.handle_websocket_upgrade( - req, peer_addr, &upstream, route_match.route, route_id, &upstream_key, cancel, &ip_str, + req, peer_addr, &upstream, route_match.route, route_id, &upstream_key, cancel, &ip_str, is_h2_websocket, ).await; // Note: for WebSocket, connection_ended is called inside // the spawned tunnel task when the connection closes. @@ -1559,7 +1567,7 @@ impl HttpProxyService { Ok(response.body(body).unwrap()) } - /// Handle a WebSocket upgrade request. + /// Handle a WebSocket upgrade request (H1 Upgrade or H2 Extended CONNECT per RFC 8441). async fn handle_websocket_upgrade( &self, req: Request, @@ -1570,6 +1578,7 @@ impl HttpProxyService { upstream_key: &str, cancel: CancellationToken, source_ip: &str, + is_h2: bool, ) -> Result>, hyper::Error> { use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -1655,9 +1664,11 @@ impl HttpProxyService { let (parts, _body) = req.into_parts(); + // H2 Extended CONNECT uses method=CONNECT, but the H1.1 backend expects GET + let backend_method = if is_h2 { "GET" } else { parts.method.as_str() }; let mut raw_request = format!( "{} {} HTTP/1.1\r\n", - parts.method, upstream_path + backend_method, upstream_path ); // Copy all original headers (preserving the client's Host header). @@ -1685,6 +1696,23 @@ impl HttpProxyService { } } + // H2 Extended CONNECT doesn't carry H1 WebSocket handshake headers; + // inject them so the H1.1 backend can complete the upgrade. + if is_h2 { + if !parts.headers.contains_key("upgrade") { + raw_request.push_str("upgrade: websocket\r\n"); + } + if !parts.headers.contains_key("connection") { + raw_request.push_str("connection: Upgrade\r\n"); + } + if !parts.headers.contains_key("sec-websocket-version") { + raw_request.push_str("sec-websocket-version: 13\r\n"); + } + if !parts.headers.contains_key("sec-websocket-key") { + raw_request.push_str("sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==\r\n"); + } + } + // Add standard reverse-proxy headers (X-Forwarded-*) { let original_host = parts.headers.get("host") @@ -1787,8 +1815,12 @@ impl HttpProxyService { )); } - let mut client_resp = Response::builder() - .status(StatusCode::SWITCHING_PROTOCOLS); + // H1: 101 Switching Protocols; H2: 200 OK (RFC 8441 — hyper requires 2xx for Extended CONNECT upgrade) + let mut client_resp = if is_h2 { + Response::builder().status(StatusCode::OK) + } else { + Response::builder().status(StatusCode::SWITCHING_PROTOCOLS) + }; if let Some(resp_headers) = client_resp.headers_mut() { for line in response_str.lines().skip(1) { @@ -1799,6 +1831,17 @@ impl HttpProxyService { if let Some((name, value)) = line.split_once(':') { let name = name.trim(); let value = value.trim(); + // Skip hop-by-hop headers for H2 (forbidden by RFC 9113 §8.2.2) + if is_h2 { + let name_lower = name.to_lowercase(); + if name_lower == "upgrade" || name_lower == "connection" + || name_lower == "sec-websocket-accept" + || name_lower == "transfer-encoding" + || name_lower == "keep-alive" + { + continue; + } + } if let Ok(header_name) = hyper::header::HeaderName::from_bytes(name.as_bytes()) { if let Ok(header_value) = hyper::header::HeaderValue::from_str(value) { resp_headers.insert(header_name, header_value); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 2fbc6c3..dff3502 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.10.7', + version: '25.11.0', 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.' }