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
This commit is contained in:
chess
2026-08-12 22:22:51 +00:00
committed by copyberry
parent 130c7c93a9
commit 631bbb33cc
2 changed files with 118 additions and 37 deletions

View File

@@ -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

View File

@@ -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<u16> = 3128..=3159;
#[cfg(target_os = "windows")]
const WINDOWS_MANAGED_SOCKS_PROXY_PORTS: RangeInclusive<u16> = 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<ReservedListenerSet> {
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<StdTcpListener> {
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<ReservedListenerSet> {
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<u16>,
protocol: &str,
) -> Result<StdTcpListener> {
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();