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

@@ -16,6 +16,7 @@ use serde::Deserialize;
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;
@@ -31,6 +32,26 @@ pub async fn run_admin_api(state: Arc<NetworkProxyState>, addr: SocketAddr) -> R
.map_err(anyhow::Error::from)
.with_context(|| format!("bind admin API: {addr}"))?;
run_admin_api_with_listener(state, listener).await
}
pub async fn run_admin_api_with_std_listener(
state: Arc<NetworkProxyState>,
listener: StdTcpListener,
) -> Result<()> {
let listener =
TcpListener::try_from(listener).context("convert std listener to admin API listener")?;
run_admin_api_with_listener(state, listener).await
}
async fn run_admin_api_with_listener(
state: Arc<NetworkProxyState>,
listener: TcpListener,
) -> Result<()> {
let addr = listener
.local_addr()
.context("read admin API listener local addr")?;
let server_state = state.clone();
let server = HttpServer::auto(Executor::new()).service(service_fn(move |req| {
let state = server_state.clone();