From 631bbb33cc0b92da7a7eb232c874462caa8f1e44 Mon Sep 17 00:00:00 2001 From: chess Date: Wed, 12 Aug 2026 22:22:51 +0000 Subject: [PATCH] Use bounded fallback ports for Windows managed proxies (#38265) ## What changed - Try the explicitly configured Windows HTTP or SOCKS5 proxy port first, then scan the protocol's preferred port range when that port is unavailable. - Reserve HTTP and SOCKS5 listeners independently so a SOCKS5 collision does not replace an already available HTTP listener. - Retain ephemeral loopback fallback when all preferred ports are unavailable, and document the HTTP (`3128-3159`) and SOCKS5 (`8081-8112`) ranges. ## Testing - Cover bounded HTTP and SOCKS5 fallback, independent listener reservation, and ephemeral fallback after preferred-port exhaustion. GitOrigin-RevId: 002c641f5bd70fedf04cca96c65c5fee2c5ebcac --- codex-rs/network-proxy/README.md | 4 + codex-rs/network-proxy/src/proxy.rs | 151 +++++++++++++++++++++------- 2 files changed, 118 insertions(+), 37 deletions(-) diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 65929c8682..402585108d 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -5,6 +5,10 @@ - an HTTP proxy (default `127.0.0.1:3128`) - a SOCKS5 proxy (default `127.0.0.1:8081`, enabled by default) +On Windows, managed HTTP listeners prefer ports `3128-3159`, and SOCKS5 listeners prefer ports +`8081-8112`. An explicitly configured port is attempted first. If every preferred port is occupied, +the proxy preserves its existing ephemeral loopback fallback. + It enforces an allow/deny policy and a "limited" mode intended for read-only network access. ## Quickstart diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index dfdfc788b8..6577259405 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -30,6 +30,8 @@ use serde::Serialize; use std::collections::HashMap; use std::net::SocketAddr; use std::net::TcpListener as StdTcpListener; +#[cfg(target_os = "windows")] +use std::ops::RangeInclusive; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; @@ -38,6 +40,11 @@ use tracing::warn; use self::execution_scope::ExecutionScope; +#[cfg(target_os = "windows")] +const WINDOWS_MANAGED_HTTP_PROXY_PORTS: RangeInclusive = 3128..=3159; +#[cfg(target_os = "windows")] +const WINDOWS_MANAGED_SOCKS_PROXY_PORTS: RangeInclusive = 8081..=8112; + #[derive(Debug, Clone, Parser)] #[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")] pub struct Args {} @@ -317,49 +324,75 @@ pub(super) fn reserve_windows_managed_listeners( ) -> Result { let http_addr = windows_managed_loopback_addr(http_addr); let socks_addr = windows_managed_loopback_addr(socks_addr); - - match try_reserve_windows_managed_listeners(http_addr, socks_addr, reserve_socks_listener) { - Ok(listeners) => Ok(listeners), - Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => { - warn!("managed Windows proxy ports are busy; falling back to ephemeral loopback ports"); - reserve_loopback_ephemeral_listeners(reserve_socks_listener) - .context("reserve fallback loopback proxy listeners") - } - Err(err) => Err(err).context("reserve Windows managed proxy listeners"), - } + let http_listener = + reserve_windows_managed_listener(http_addr, WINDOWS_MANAGED_HTTP_PROXY_PORTS, "HTTP")?; + let socks_listener = if reserve_socks_listener { + Some(reserve_windows_managed_listener( + socks_addr, + WINDOWS_MANAGED_SOCKS_PROXY_PORTS, + "SOCKS5", + )?) + } else { + None + }; + Ok(ReservedListenerSet::new(http_listener, socks_listener)) } #[cfg(target_os = "windows")] pub(super) fn reserve_windows_managed_socks_listener( socks_addr: SocketAddr, ) -> Result { - let socks_addr = windows_managed_loopback_addr(socks_addr); - match StdTcpListener::bind(socks_addr) { - Ok(listener) => Ok(listener), - Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => { - warn!( - "managed Windows SOCKS5 proxy port is busy; falling back to an ephemeral loopback port" - ); - reserve_loopback_ephemeral_listener() - .context("reserve fallback loopback SOCKS5 proxy listener") - } - Err(err) => Err(err).context("reserve Windows managed SOCKS5 proxy listener"), - } + reserve_windows_managed_listener( + windows_managed_loopback_addr(socks_addr), + WINDOWS_MANAGED_SOCKS_PROXY_PORTS, + "SOCKS5", + ) } #[cfg(target_os = "windows")] -fn try_reserve_windows_managed_listeners( - http_addr: SocketAddr, - socks_addr: SocketAddr, - reserve_socks_listener: bool, -) -> std::io::Result { - let http_listener = StdTcpListener::bind(http_addr)?; - let socks_listener = if reserve_socks_listener { - Some(StdTcpListener::bind(socks_addr)?) - } else { - None - }; - Ok(ReservedListenerSet::new(http_listener, socks_listener)) +fn reserve_windows_managed_listener( + requested_addr: SocketAddr, + ports: RangeInclusive, + protocol: &str, +) -> Result { + let requested_port = requested_addr.port(); + anyhow::ensure!( + requested_port != 0, + "managed Windows {protocol} proxy must use a fixed non-zero port" + ); + let start = *ports.start(); + let end = *ports.end(); + + for port in std::iter::once(requested_port).chain(ports.filter(|port| *port != requested_port)) + { + let addr = SocketAddr::new(requested_addr.ip(), port); + match StdTcpListener::bind(addr) { + Ok(listener) => { + if port != requested_port { + warn!( + "managed Windows {protocol} proxy port {requested_port} is unavailable; using bounded fallback port {port}" + ); + } + return Ok(listener); + } + Err(err) + if matches!( + err.kind(), + std::io::ErrorKind::AddrInUse | std::io::ErrorKind::PermissionDenied + ) => {} + Err(err) => { + return Err(err).with_context(|| { + format!("reserve managed Windows {protocol} proxy listener on {addr}") + }); + } + } + } + + warn!( + "managed Windows {protocol} proxy port {requested_port} and preferred ports {start}-{end} are unavailable; falling back to an ephemeral loopback port" + ); + reserve_loopback_ephemeral_listener() + .with_context(|| format!("reserve fallback loopback Windows {protocol} proxy listener")) } #[cfg(target_os = "windows")] @@ -2167,6 +2200,7 @@ mod tests { assert_eq!(socks_proxy.http_addr(), proxy.http_addr()); assert!(actual_socks_addr.ip().is_loopback()); assert_ne!(actual_socks_addr, requested_socks_addr); + assert!(WINDOWS_MANAGED_SOCKS_PROXY_PORTS.contains(&actual_socks_addr.port())); assert_eq!(proxy.socks_addr(), actual_socks_addr); let socks_handle = socks_proxy .run() @@ -2279,12 +2313,55 @@ mod tests { .ip() .is_loopback() ); - assert_ne!( - reserved.http_listener.local_addr().unwrap().port(), - busy_port + let fallback_port = reserved.http_listener.local_addr().unwrap().port(); + assert_ne!(fallback_port, busy_port); + assert!(WINDOWS_MANAGED_HTTP_PROXY_PORTS.contains(&fallback_port)); + } + + #[cfg(target_os = "windows")] + #[test] + fn reserve_windows_managed_listeners_preserves_http_when_socks_port_is_busy() { + let http_listener = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); + let http_addr = http_listener.local_addr().unwrap(); + drop(http_listener); + let occupied_socks = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); + + let reserved = reserve_windows_managed_listeners( + http_addr, + occupied_socks.local_addr().unwrap(), + /*reserve_socks_listener*/ true, + ) + .unwrap(); + + assert_eq!(reserved.http_listener.local_addr().unwrap(), http_addr); + assert!( + WINDOWS_MANAGED_SOCKS_PROXY_PORTS.contains( + &reserved + .socks_listener + .unwrap() + .local_addr() + .unwrap() + .port() + ) ); } + #[cfg(target_os = "windows")] + #[test] + fn reserve_windows_managed_listener_uses_ephemeral_port_when_preferred_ports_are_busy() { + let occupied = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); + let occupied_addr = occupied.local_addr().unwrap(); + let occupied_port = occupied_addr.port(); + + let listener = + reserve_windows_managed_listener(occupied_addr, occupied_port..=occupied_port, "HTTP") + .unwrap(); + + let fallback_addr = listener.local_addr().unwrap(); + assert!(fallback_addr.ip().is_loopback()); + assert_ne!(fallback_addr.port(), occupied_port); + } + #[test] fn proxy_url_env_value_resolves_lowercase_aliases() { let mut env = HashMap::new();