Allow exec-server to proxy permitted private IPs upstream (#48568)

## Why

Private IP destinations always bypassed inherited upstream proxies, preventing their use for private networks reachable through an upstream VPN proxy.

## What changed

- Add `codex exec-server --proxy-private-ips-via-upstream`, also configurable with `CODEX_EXEC_SERVER_PROXY_PRIVATE_IPS_VIA_UPSTREAM=true`. The setting defaults to disabled.
- Allow permitted RFC 1918, carrier-grade NAT, and IPv6 unique-local destinations to use an applicable upstream proxy. Loopback and link-local destinations retain direct routing, and destination access policy still applies.
- Keep connections direct when no valid upstream proxy applies or `allow_upstream_proxy=false`. Errors after selecting an upstream proxy do not trigger a direct retry.
- Rename `ExecServerRuntimePaths` to `ExecServerRuntimeOptions` and carry the routing setting from executor startup into the managed network proxy.

## Testing

Add routing coverage for private address ranges, special-use addresses, and public targets with the option enabled and disabled. Verify that HTTP and CONNECT requests still enforce destination allowlists and denylists, and update the CLI help snapshot.

GitOrigin-RevId: b7c9cc7da0e0f545694a6521b74c9b36b7b92769
This commit is contained in:
open-matt
2026-09-26 23:17:41 +00:00
committed by copyberry
parent 228ae3da8d
commit b8d5e3f12e
65 changed files with 359 additions and 213 deletions
@@ -8,7 +8,7 @@ use crate::CapabilityRootsDiscoverParams;
use crate::CapabilityRootsDiscoverResponse;
use crate::CopyOptions;
use crate::CreateDirectoryOptions;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::ExecutorFileSystem;
use crate::GetMetadataOptions;
use crate::ReadFileOptions;
@@ -59,7 +59,7 @@ pub(crate) struct FileSystemHandler {
}
impl FileSystemHandler {
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
pub(crate) fn new(runtime_paths: ExecServerRuntimeOptions) -> Self {
Self {
file_system: LocalFileSystem::with_runtime_paths(runtime_paths),
file_reads: FileReadHandleManager::default(),
@@ -369,7 +369,7 @@ mod tests {
#[tokio::test]
async fn no_platform_sandbox_policies_do_not_require_configured_sandbox_helper() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let runtime_paths = ExecServerRuntimePaths::new(
let runtime_paths = ExecServerRuntimeOptions::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
+3 -3
View File
@@ -13,7 +13,7 @@ use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::client::http_client::PendingRouteAwareHttpBodyStream;
use crate::client::http_client::RouteAwareHttpClient;
use crate::client::http_client::RouteAwareHttpRequestRunner;
@@ -83,7 +83,7 @@ pub(crate) struct ExecServerHandler {
background_task_shutdown: CancellationToken,
background_tasks: TaskTracker,
file_system: FileSystemHandler,
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
http_client: RouteAwareHttpClient,
initialize_requested: AtomicBool,
initialized: AtomicBool,
@@ -93,7 +93,7 @@ impl ExecServerHandler {
pub(crate) fn new(
session_registry: Arc<SessionRegistry>,
notifications: RpcNotificationSender,
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
http_client_factory: HttpClientFactory,
) -> Self {
Self {
@@ -10,7 +10,7 @@ use tokio::sync::mpsc;
use uuid::Uuid;
use super::ExecServerHandler;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::ProcessId;
use crate::protocol::ExecParams;
use crate::protocol::InitializeParams;
@@ -77,8 +77,8 @@ fn windows_command_processor() -> String {
std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string())
}
fn test_runtime_paths() -> ExecServerRuntimePaths {
ExecServerRuntimePaths::new(
fn test_runtime_paths() -> ExecServerRuntimeOptions {
ExecServerRuntimeOptions::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
@@ -1,7 +1,7 @@
use crate::process_telemetry::ProcessTelemetry;
use codex_exec_server_protocol::JSONRPCErrorError;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::local_process::LocalProcess;
use crate::protocol::ExecParams;
use crate::protocol::ExecResponse;
@@ -25,7 +25,7 @@ impl ProcessHandler {
pub(crate) fn new(
notifications: RpcNotificationSender,
telemetry: ExecServerTelemetry,
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
) -> Self {
Self {
process: LocalProcess::new(notifications, telemetry, runtime_paths),
@@ -37,7 +37,7 @@ use wiremock::matchers::method;
use super::ExecServerHandler;
use super::registry::build_router;
use super::session_registry::SessionRegistry;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::ExecServerTelemetry;
use crate::connection::JsonRpcConnectionEvent;
use crate::protocol::EXEC_METHOD;
@@ -739,7 +739,7 @@ fn new_handler(
let mut handler = ExecServerHandler::new(
Arc::clone(sessions),
RpcNotificationSender::new(outgoing),
ExecServerRuntimePaths::new(
ExecServerRuntimeOptions::new(
std::env::current_exe().expect("test executable"),
/*codex_linux_sandbox_exe*/ None,
)
+9 -9
View File
@@ -9,7 +9,7 @@ use tokio_util::task::AbortOnDropHandle;
use tracing::debug;
use tracing::warn;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::LocalFileSystem;
use crate::connection::CHANNEL_CAPACITY;
use crate::connection::JsonRpcConnection;
@@ -38,7 +38,7 @@ pub(crate) struct ConnectionProcessor {
capability_manager: Arc<CapabilityManager>,
capability_prewarm: Arc<AbortOnDropHandle<()>>,
session_registry: Arc<SessionRegistry>,
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
request_dispatch_mode: RequestDispatchMode,
@@ -46,7 +46,7 @@ pub(crate) struct ConnectionProcessor {
impl ConnectionProcessor {
#[cfg(test)]
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
pub(crate) fn new(runtime_paths: ExecServerRuntimeOptions) -> Self {
Self::new_with_location_request(
runtime_paths,
ExecServerTelemetry::default(),
@@ -62,7 +62,7 @@ impl ConnectionProcessor {
}
pub(crate) fn new_with_telemetry(
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
request_dispatch_mode: RequestDispatchMode,
@@ -83,7 +83,7 @@ impl ConnectionProcessor {
}
fn new_with_location_request(
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
request_dispatch_mode: RequestDispatchMode,
@@ -342,7 +342,7 @@ mod tests {
use super::complete_queued_client_responses;
use super::run_connection;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::ProcessId;
use crate::connection::JsonRpcConnection;
use crate::connection::JsonRpcConnectionEvent;
@@ -372,7 +372,7 @@ mod tests {
#[tokio::test]
async fn startup_prewarms_without_a_client_connection() -> anyhow::Result<()> {
let paths = ExecServerRuntimePaths::new(
let paths = ExecServerRuntimeOptions::new(
std::env::current_exe()?,
/*codex_linux_sandbox_exe*/ None,
)?;
@@ -637,8 +637,8 @@ mod tests {
(client_writer, BufReader::new(client_reader).lines(), task)
}
fn test_runtime_paths() -> ExecServerRuntimePaths {
ExecServerRuntimePaths::new(
fn test_runtime_paths() -> ExecServerRuntimeOptions {
ExecServerRuntimeOptions::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
@@ -31,7 +31,7 @@ use super::ConcurrentRequestLimit;
use super::RequestDispatchMode;
use super::RequestDispatcher;
use super::RequestTaskResult;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::connection::JsonRpcConnectionEvent;
use crate::rpc::RpcNotificationSender;
use crate::rpc::RpcRouter;
@@ -178,7 +178,7 @@ async fn request_queue_waits_for_dispatcher_admission_before_recording_telemetry
let handler = Arc::new(ExecServerHandler::new(
SessionRegistry::new(telemetry.clone()),
notifications,
ExecServerRuntimePaths::new(
ExecServerRuntimeOptions::new(
std::env::current_exe().expect("current executable"),
/*codex_linux_sandbox_exe*/ None,
)
@@ -387,7 +387,7 @@ fn request_telemetry_fixture(router: RpcRouter<ExecServerHandler>) -> RequestTel
let handler = Arc::new(ExecServerHandler::new(
SessionRegistry::new(telemetry.clone()),
notifications,
ExecServerRuntimePaths::new(
ExecServerRuntimeOptions::new(
std::env::current_exe().expect("current executable"),
/*codex_linux_sandbox_exe*/ None,
)
@@ -7,7 +7,7 @@ use codex_exec_server_protocol::JSONRPCErrorError;
use tokio::sync::Mutex;
use uuid::Uuid;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::rpc::RpcNotificationSender;
use crate::rpc::invalid_request;
use crate::rpc::session_already_attached;
@@ -64,7 +64,7 @@ impl SessionRegistry {
self: &Arc<Self>,
resume_session_id: Option<String>,
notifications: RpcNotificationSender,
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
) -> Result<SessionHandle, JSONRPCErrorError> {
enum AttachOutcome {
Attached(Arc<SessionEntry>),
+5 -5
View File
@@ -30,7 +30,7 @@ use tokio::net::TcpListener;
use tracing::info;
use tracing::warn;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::ExecServerTelemetry;
use crate::connection::JsonRpcConnection;
use crate::server::RequestDispatchMode;
@@ -91,7 +91,7 @@ pub(crate) fn parse_listen_url(
pub(crate) async fn run_transport(
listen_url: &str,
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
request_dispatch_mode: RequestDispatchMode,
@@ -129,7 +129,7 @@ pub(crate) async fn run_transport(
}
async fn run_stdio_connection(
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
request_dispatch_mode: RequestDispatchMode,
@@ -148,7 +148,7 @@ async fn run_stdio_connection(
async fn run_stdio_connection_with_io<R, W>(
reader: R,
writer: W,
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
request_dispatch_mode: RequestDispatchMode,
@@ -177,7 +177,7 @@ where
async fn run_websocket_listener(
bind_address: SocketAddr,
runtime_paths: ExecServerRuntimePaths,
runtime_paths: ExecServerRuntimeOptions,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
request_dispatch_mode: RequestDispatchMode,
@@ -17,7 +17,7 @@ use super::DEFAULT_LISTEN_URL;
use super::ExecServerListenTransport;
use super::parse_listen_url;
use super::run_stdio_connection_with_io;
use crate::ExecServerRuntimePaths;
use crate::ExecServerRuntimeOptions;
use crate::protocol::INITIALIZE_METHOD;
use crate::protocol::INITIALIZED_METHOD;
use crate::protocol::InitializeParams;
@@ -163,8 +163,8 @@ async fn write_jsonrpc_line(writer: &mut tokio::io::DuplexStream, message: &JSON
.expect("JSON-RPC newline should write");
}
fn test_runtime_paths() -> ExecServerRuntimePaths {
ExecServerRuntimePaths::new(
fn test_runtime_paths() -> ExecServerRuntimeOptions {
ExecServerRuntimeOptions::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)