From 81e0e6b4d88bc29bcded061f9e4530a98db580ce Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 12 Feb 2026 20:17:32 +0000 Subject: [PATCH] fix(rustproxy): install default rustls crypto provider early; detect and skip raw fast-path for HTTP connections and return proper HTTP 502 when no route matches --- changelog.md | 8 ++++ .../rustproxy-passthrough/src/tcp_listener.rs | 38 ++++++++++++++++++- rust/crates/rustproxy/src/main.rs | 5 +++ ts/00_commitinfo_data.ts | 2 +- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 2de1721..59d0a77 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-02-12 - 23.1.3 - fix(rustproxy) +install default rustls crypto provider early; detect and skip raw fast-path for HTTP connections and return proper HTTP 502 when no route matches + +- Install ring-based rustls crypto provider at startup to prevent panics from instant-acme/hyper-rustls calling ClientConfig::builder() before TLS listeners are initialized +- Add a non-blocking 10ms peek to detect HTTP traffic in the TCP passthrough fast-path to avoid misrouting HTTP and ensure HTTP proxy handles CORS, errors, and request-level routing +- Skip the fast-path and fall back to the HTTP proxy when HTTP is detected (with a debug log) +- When no route matches for detected HTTP connections, send an HTTP 502 Bad Gateway response and close the connection instead of silently dropping it + ## 2026-02-11 - 23.1.2 - fix(core) use node: scoped builtin imports and add route unit tests diff --git a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs index 8d2e7f3..43ef4c4 100644 --- a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs +++ b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs @@ -364,6 +364,10 @@ impl TcpListenerManager { // doesn't send initial data (e.g., SMTP, greeting-based protocols). // If a route matches by port alone and doesn't need domain/path/TLS info, // we can forward immediately without waiting for client data. + // + // IMPORTANT: HTTP connections must NOT use this path — they need the HTTP + // proxy for proper error responses, CORS handling, and request-level routing. + // We detect HTTP via a non-blocking peek before committing to raw forwarding. { let quick_ctx = rustproxy_routing::MatchContext { port, @@ -384,7 +388,28 @@ impl TcpListenerManager { // Only use fast path for simple port-only forward routes with no TLS if has_no_domain && has_no_path && is_forward && has_no_tls { - if let Some(target) = quick_match.target { + // Non-blocking peek: if client has already sent data that looks + // like HTTP, skip fast path and let the normal path handle it + // through the HTTP proxy (for CORS, error responses, path routing). + let is_likely_http = { + let mut probe = [0u8; 16]; + // Brief peek: HTTP clients send data immediately after connect. + // Server-speaks-first protocols (SMTP etc.) send nothing initially. + // 10ms is ample for any HTTP client while negligible for + // server-speaks-first protocols (which wait seconds for greeting). + match tokio::time::timeout( + std::time::Duration::from_millis(10), + stream.peek(&mut probe), + ).await { + Ok(Ok(n)) if n > 0 => sni_parser::is_http(&probe[..n]), + _ => false, + } + }; + + if is_likely_http { + debug!("Fast-path skipped: HTTP detected from {}, using HTTP proxy", peer_addr); + // Fall through to normal path for HTTP proxy handling + } else if let Some(target) = quick_match.target { let target_host = target.host.first().to_string(); let target_port = target.port.resolve(port); let route_id = quick_match.route.id.as_deref(); @@ -562,6 +587,17 @@ impl TcpListenerManager { Some(rm) => rm, None => { debug!("No route matched for port {} domain {:?}", port, domain); + if is_http { + // Send a proper HTTP error instead of dropping the connection + use tokio::io::AsyncWriteExt; + let body = "No route matched"; + let resp = format!( + "HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), body + ); + let _ = stream.write_all(resp.as_bytes()).await; + let _ = stream.shutdown().await; + } return Ok(()); } }; diff --git a/rust/crates/rustproxy/src/main.rs b/rust/crates/rustproxy/src/main.rs index bf6b8a3..0ebcc9a 100644 --- a/rust/crates/rustproxy/src/main.rs +++ b/rust/crates/rustproxy/src/main.rs @@ -29,6 +29,11 @@ struct Cli { #[tokio::main] async fn main() -> Result<()> { + // Install the default CryptoProvider early, before any TLS or ACME code runs. + // This prevents panics from instant-acme/hyper-rustls calling ClientConfig::builder() + // before TLS listeners have started. Idempotent — later calls harmlessly return Err. + let _ = rustls::crypto::ring::default_provider().install_default(); + let cli = Cli::parse(); // Initialize tracing - write to stderr so stdout is reserved for management IPC diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 599b32f..7a5e979 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: '23.1.2', + version: '23.1.3', 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.' }