feat: reserve loopback ephemeral listeners for managed proxy (#11269)

Codex may run many per-thread proxy instances, so hardcoded proxy ports
are brittle and conflict-prone. The previous "ephemeral" approach still
had a race: `build()` read `local_addr()` from temporary listeners and
dropped them before `run()` rebound the ports. That left a
[TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)
window where the OS (or another process) could reuse the same port,
causing intermittent `EADDRINUSE` and partial proxy startup.

Change the managed proxy path to reserve real listener sockets up front
and keep them alive until startup:

- add `ReservedListeners` on `NetworkProxy` to hold HTTP/SOCKS/admin std
listeners allocated during `build()`
- in managed mode, bind `127.0.0.1:0` for each listener and carry those
bound sockets into `run()` instead of rebinding by address later
- add `run_*_with_std_listener` entry points for HTTP, SOCKS5, and admin
servers so `run()` can start services from already-reserved sockets
- keep static/configured ports only when `managed_by_codex(false)`,
including explicit `socks_addr` override support
- remove fallback synthetic port allocation and add tests for managed
ephemeral loopback binding and unmanaged configured-port behavior

This makes managed startup deterministic, avoids port collisions, and
preserves the intended distinction between Codex-managed ephemeral ports
and externally managed fixed ports.
This commit is contained in:
Michael Bolin
2026-02-09 22:11:02 -08:00
committed by GitHub
parent bb974c78de
commit 503186b31f
4 changed files with 308 additions and 17 deletions

View File

@@ -64,6 +64,7 @@ use rama_tls_rustls::client::TlsConnectorLayer;
use serde::Serialize;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::net::TcpListener as StdTcpListener;
use std::sync::Arc;
use tracing::error;
use tracing::info;
@@ -85,6 +86,28 @@ pub async fn run_http_proxy(
.map_err(anyhow::Error::from)
.with_context(|| format!("bind HTTP proxy: {addr}"))?;
run_http_proxy_with_listener(state, listener, policy_decider).await
}
pub async fn run_http_proxy_with_std_listener(
state: Arc<NetworkProxyState>,
listener: StdTcpListener,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
) -> Result<()> {
let listener =
TcpListener::try_from(listener).context("convert std listener to HTTP proxy listener")?;
run_http_proxy_with_listener(state, listener, policy_decider).await
}
async fn run_http_proxy_with_listener(
state: Arc<NetworkProxyState>,
listener: TcpListener,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
) -> Result<()> {
let addr = listener
.local_addr()
.context("read HTTP proxy listener local addr")?;
let http_service = HttpServer::auto(Executor::new()).service(
(
UpgradeLayer::new(