diff --git a/changelog.md b/changelog.md index 14ad64f..96a8a69 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-02-19 - 25.7.7 - fix(proxy) +restrict PROXY protocol parsing to configured trusted proxy IPs and parse PROXY headers before metrics/fast-path so client IPs reflect the real source + +- Add proxy_ips: Vec to ConnectionConfig with a default empty Vec +- Populate proxy_ips from options.proxy_ips strings in rust/crates/rustproxy/src/lib.rs, parsing each to IpAddr +- Only peek for and parse PROXY v1 headers when the remote IP is contained in proxy_ips (prevents untrusted clients from injecting PROXY headers) +- Move PROXY protocol parsing earlier so metrics and fast-path logic use the effective (real client) IP after PROXY parsing +- If proxy_ips is empty, behavior remains unchanged (no PROXY parsing) + ## 2026-02-19 - 25.7.6 - fix(throughput) add tests for per-IP connection tracking and throughput history; assert per-IP eviction after connection close to prevent memory leak diff --git a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs index 7b98ad1..26ea027 100644 --- a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs +++ b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs @@ -84,6 +84,9 @@ pub struct ConnectionConfig { pub accept_proxy_protocol: bool, /// Whether to send PROXY protocol pub send_proxy_protocol: bool, + /// Trusted IPs that may send PROXY protocol headers. + /// When non-empty, only connections from these IPs will have PROXY headers parsed. + pub proxy_ips: Vec, } impl Default for ConnectionConfig { @@ -101,6 +104,7 @@ impl Default for ConnectionConfig { extended_keep_alive_lifetime_ms: None, accept_proxy_protocol: false, send_proxy_protocol: false, + proxy_ips: Vec::new(), } } } @@ -415,7 +419,41 @@ impl TcpListenerManager { stream.set_nodelay(true)?; - // Extract source IP once for all metric calls + // --- PROXY protocol: must happen BEFORE ip_str and fast path --- + // Only parse PROXY headers from trusted proxy IPs (security). + // Non-proxy connections skip the peek entirely (no latency cost). + let mut effective_peer_addr = peer_addr; + if !conn_config.proxy_ips.is_empty() && conn_config.proxy_ips.contains(&peer_addr.ip()) { + // Trusted proxy IP — peek for PROXY protocol header + let mut proxy_peek = vec![0u8; 256]; + let pn = match tokio::time::timeout( + std::time::Duration::from_millis(conn_config.initial_data_timeout_ms), + stream.peek(&mut proxy_peek), + ).await { + Ok(Ok(n)) => n, + Ok(Err(e)) => return Err(e.into()), + Err(_) => return Err("Initial data timeout (proxy protocol peek)".into()), + }; + + if pn > 0 && crate::proxy_protocol::is_proxy_protocol_v1(&proxy_peek[..pn]) { + match crate::proxy_protocol::parse_v1(&proxy_peek[..pn]) { + Ok((header, consumed)) => { + debug!("PROXY protocol: real client {} -> {}", header.source_addr, header.dest_addr); + effective_peer_addr = header.source_addr; + // Consume the proxy protocol header bytes + let mut discard = vec![0u8; consumed]; + stream.read_exact(&mut discard).await?; + } + Err(e) => { + debug!("Failed to parse PROXY protocol header: {}", e); + // Not a PROXY protocol header, continue normally + } + } + } + } + let peer_addr = effective_peer_addr; + + // Extract source IP once for all metric calls (reflects real client IP after PROXY parsing) let ip_str = peer_addr.ip().to_string(); // === Fast path: try port-only matching before peeking at data === @@ -548,37 +586,6 @@ impl TcpListenerManager { } // === End fast path === - // Handle PROXY protocol if configured - let mut effective_peer_addr = peer_addr; - if conn_config.accept_proxy_protocol { - let mut proxy_peek = vec![0u8; 256]; - let pn = match tokio::time::timeout( - std::time::Duration::from_millis(conn_config.initial_data_timeout_ms), - stream.peek(&mut proxy_peek), - ).await { - Ok(Ok(n)) => n, - Ok(Err(e)) => return Err(e.into()), - Err(_) => return Err("Initial data timeout (proxy protocol peek)".into()), - }; - - if pn > 0 && crate::proxy_protocol::is_proxy_protocol_v1(&proxy_peek[..pn]) { - match crate::proxy_protocol::parse_v1(&proxy_peek[..pn]) { - Ok((header, consumed)) => { - debug!("PROXY protocol: real client {} -> {}", header.source_addr, header.dest_addr); - effective_peer_addr = header.source_addr; - // Consume the proxy protocol header bytes - let mut discard = vec![0u8; consumed]; - stream.read_exact(&mut discard).await?; - } - Err(e) => { - debug!("Failed to parse PROXY protocol header: {}", e); - // Not a PROXY protocol header, continue normally - } - } - } - } - let peer_addr = effective_peer_addr; - // Peek at initial bytes with timeout let mut peek_buf = vec![0u8; 4096]; let n = match tokio::time::timeout( diff --git a/rust/crates/rustproxy/src/lib.rs b/rust/crates/rustproxy/src/lib.rs index 4c29a83..3a1286b 100644 --- a/rust/crates/rustproxy/src/lib.rs +++ b/rust/crates/rustproxy/src/lib.rs @@ -217,6 +217,10 @@ impl RustProxy { extended_keep_alive_lifetime_ms: options.extended_keep_alive_lifetime, accept_proxy_protocol: options.accept_proxy_protocol.unwrap_or(false), send_proxy_protocol: options.send_proxy_protocol.unwrap_or(false), + proxy_ips: options.proxy_ips.as_deref().unwrap_or(&[]) + .iter() + .filter_map(|s| s.parse::().ok()) + .collect(), } } diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index bb4d9c3..38077bc 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.7.6', + version: '25.7.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.' }