From 8cdb95a853c3403c4e3e11e2634a04a5631f36c3 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 16 Mar 2026 03:01:16 +0000 Subject: [PATCH] fix(rustproxy): prevent TLS route reload certificate mismatches and tighten passthrough connection handling --- changelog.md | 7 ++++ .../rustproxy-passthrough/src/tcp_listener.rs | 40 ++++++++++++------- rust/crates/rustproxy/src/lib.rs | 21 ++++++---- ts/00_commitinfo_data.ts | 2 +- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/changelog.md b/changelog.md index e5f8a3e..034b3f9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-16 - 25.11.7 - fix(rustproxy) +prevent TLS route reload certificate mismatches and tighten passthrough connection handling + +- Load updated TLS configs before swapping the route manager so newly visible routes always have their certificates available. +- Add timeouts when peeking initial decrypted data after TLS handshake to avoid leaked idle connections. +- Raise dropped, blocked, unmatched, and errored passthrough connection events from debug to warn for better operational visibility. + ## 2026-03-16 - 25.11.6 - fix(rustproxy-http,rustproxy-passthrough) improve upstream connection cleanup and graceful tunnel shutdown diff --git a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs index 094b63e..c06ae22 100644 --- a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs +++ b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs @@ -472,12 +472,12 @@ impl TcpListenerManager { let permit = match conn_semaphore.clone().try_acquire_owned() { Ok(permit) => permit, Err(tokio::sync::TryAcquireError::NoPermits) => { - debug!("Global connection limit reached, dropping connection from {}", peer_addr); + warn!("Global connection limit reached, dropping connection from {}", peer_addr); drop(stream); continue; } Err(tokio::sync::TryAcquireError::Closed) => { - debug!("Connection semaphore closed, dropping connection from {}", peer_addr); + warn!("Connection semaphore closed, dropping connection from {}", peer_addr); drop(stream); continue; } @@ -485,7 +485,7 @@ impl TcpListenerManager { // Check per-IP limits and rate limiting if !conn_tracker.try_accept(&ip) { - debug!("Rejected connection from {} (per-IP limit or rate limit)", peer_addr); + warn!("Rejected connection from {} (per-IP limit or rate limit)", peer_addr); drop(stream); drop(permit); continue; @@ -517,7 +517,7 @@ impl TcpListenerManager { stream, port, peer_addr, rm, m, tc, sa, hp, cc, cn, sr, rc, ).await; if let Err(e) = result { - debug!("Connection error from {}: {}", peer_addr, e); + warn!("Connection error from {}: {}", peer_addr, e); } }); } @@ -662,7 +662,7 @@ impl TcpListenerManager { if !rustproxy_http::request_filter::RequestFilter::check_ip_security( security, &peer_addr.ip(), ) { - debug!("Connection from {} blocked by route security", peer_addr); + warn!("Connection from {} blocked by route security", peer_addr); return Ok(()); } } @@ -808,7 +808,7 @@ impl TcpListenerManager { let route_match = match route_match { Some(rm) => rm, None => { - debug!("No route matched for port {} domain {:?}", port, domain); + warn!("No route matched for port {} domain {:?} from {}", port, domain, peer_addr); if is_http { // Send a proper HTTP error instead of dropping the connection use tokio::io::AsyncWriteExt; @@ -842,7 +842,7 @@ impl TcpListenerManager { security, &peer_addr.ip(), ) { - debug!("Connection from {} blocked by route security", peer_addr); + warn!("Connection from {} blocked by route security", peer_addr); return Ok(()); } } @@ -985,13 +985,18 @@ impl TcpListenerManager { Err(_) => return Err("TLS handshake timeout".into()), }; - // Peek at decrypted data to determine if HTTP + // Peek at decrypted data to determine if HTTP. + // Timeout prevents connection leak if client completes TLS + // but never sends application data (scanners, health probes, slow-loris). let mut buf_stream = tokio::io::BufReader::new(tls_stream); let peeked = { use tokio::io::AsyncBufReadExt; - match buf_stream.fill_buf().await { - Ok(data) => sni_parser::is_http(data), - Err(_) => false, + match tokio::time::timeout( + std::time::Duration::from_millis(conn_config.initial_data_timeout_ms), + buf_stream.fill_buf(), + ).await { + Ok(Ok(data)) => sni_parser::is_http(data), + Ok(Err(_)) | Err(_) => false, } }; @@ -1060,13 +1065,18 @@ impl TcpListenerManager { Err(_) => return Err("TLS handshake timeout".into()), }; - // Peek at decrypted data to detect protocol + // Peek at decrypted data to detect protocol. + // Timeout prevents connection leak if client completes TLS + // but never sends application data (scanners, health probes, slow-loris). let mut buf_stream = tokio::io::BufReader::new(tls_stream); let is_http_data = { use tokio::io::AsyncBufReadExt; - match buf_stream.fill_buf().await { - Ok(data) => sni_parser::is_http(data), - Err(_) => false, + match tokio::time::timeout( + std::time::Duration::from_millis(conn_config.initial_data_timeout_ms), + buf_stream.fill_buf(), + ).await { + Ok(Ok(data)) => sni_parser::is_http(data), + Ok(Err(_)) | Err(_) => false, } }; diff --git a/rust/crates/rustproxy/src/lib.rs b/rust/crates/rustproxy/src/lib.rs index fcdb226..e66b90b 100644 --- a/rust/crates/rustproxy/src/lib.rs +++ b/rust/crates/rustproxy/src/lib.rs @@ -632,15 +632,13 @@ impl RustProxy { let new_manager = Arc::new(new_manager); self.route_table.store(Arc::clone(&new_manager)); - // Update listener manager + // Update listener manager. + // IMPORTANT: TLS configs must be swapped BEFORE the route manager so that + // new routes only become visible after their certs are loaded. The reverse + // order (routes first) creates a window where connections match new routes + // but get the old TLS acceptor, causing cert mismatches. if let Some(ref mut listener) = self.listener_manager { - listener.update_route_manager(Arc::clone(&new_manager)); - // Cancel connections on routes that were removed or disabled - listener.invalidate_removed_routes(&active_route_ids); - // Prune HTTP proxy caches (rate limiters, regex cache, round-robin counters) - listener.prune_http_proxy_caches(&active_route_ids); - - // Update TLS configs + // 1. Update TLS configs first (so new certs are available before new routes) let mut tls_configs = Self::extract_tls_configs(&routes); if let Some(ref cm_arc) = self.cert_manager { let cm = cm_arc.lock().await; @@ -661,6 +659,13 @@ impl RustProxy { } listener.set_tls_configs(tls_configs); + // 2. Now swap the route manager (new routes become visible with certs already loaded) + listener.update_route_manager(Arc::clone(&new_manager)); + // Cancel connections on routes that were removed or disabled + listener.invalidate_removed_routes(&active_route_ids); + // Prune HTTP proxy caches (rate limiters, regex cache, round-robin counters) + listener.prune_http_proxy_caches(&active_route_ids); + // Add new ports for port in &new_ports { if !old_ports.contains(port) { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 27cf000..7a8f475 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.11.6', + version: '25.11.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.' }