From 1d47927aa06a4d4a0d15826dd04ceeee0f7c665a Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 10 Feb 2026 15:13:52 -0800 Subject: [PATCH] Enable SOCKS defaults for common local network proxy use cases (#11362) ## Summary - enable local-use defaults in network proxy settings: SOCKS5 on, SOCKS5 UDP on, upstream proxying on, and local binding on - add a regression test that asserts the full `NetworkProxySettings::default()` baseline - Fixed managed listener reservation behavior. Before: we always reserved a loopback SOCKS listener, even when enable_socks5 = false. Now: SOCKS listener is only reserved when SOCKS is enabled. - Fixed /debug-config env output for SOCKS-disabled sessions. ALL_PROXY now shows the HTTP proxy URL when SOCKS is disabled (instead of incorrectly showing socks5h://...). ## Validation - just fmt - cargo test -p codex-network-proxy - cargo clippy -p codex-network-proxy --all-targets --- .../core/src/config/network_proxy_spec.rs | 4 + codex-rs/network-proxy/README.md | 14 ++-- codex-rs/network-proxy/src/config.rs | 54 ++++++++++++-- codex-rs/network-proxy/src/proxy.rs | 74 +++++++++++++++---- codex-rs/tui/src/debug_config.rs | 35 ++++++++- 5 files changed, 152 insertions(+), 29 deletions(-) diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index e50a30d88..ee3e8ebed 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -68,6 +68,10 @@ impl NetworkProxySpec { host_and_port_from_network_addr(&self.config.network.proxy_url, 3128) } + pub fn socks_enabled(&self) -> bool { + self.config.network.enable_socks5 + } + pub(crate) fn from_constraints( _config_layer_stack: &config::ConfigLayerStack, requirements: NetworkConstraints, diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 3a8d20eb8..7ffed9008 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -3,7 +3,7 @@ `codex-network-proxy` is Codex's local network policy enforcement proxy. It runs: - an HTTP proxy (default `127.0.0.1:3128`) -- an optional SOCKS5 proxy (default `127.0.0.1:8081`, disabled by default) +- a SOCKS5 proxy (default `127.0.0.1:8081`, enabled by default) - an admin HTTP API (default `127.0.0.1:8080`) It enforces an allow/deny policy and a "limited" mode intended for read-only network access. @@ -21,14 +21,14 @@ Example config: enabled = true proxy_url = "http://127.0.0.1:3128" admin_url = "http://127.0.0.1:8080" -# Optional SOCKS5 listener (disabled by default). -enable_socks5 = false +# SOCKS5 listener (enabled by default). +enable_socks5 = true socks_url = "http://127.0.0.1:8081" -enable_socks5_udp = false +enable_socks5_udp = true # When `enabled` is false, the proxy no-ops and does not bind listeners. # When true, respect HTTP(S)_PROXY/ALL_PROXY for upstream requests (HTTP(S) proxies only), # including CONNECT tunnels in full mode. -allow_upstream_proxy = false +allow_upstream_proxy = true # By default, non-loopback binds are clamped to loopback for safety. # If you want to expose these listeners beyond localhost, you must opt in explicitly. dangerously_allow_non_loopback_proxy = false @@ -37,13 +37,13 @@ mode = "full" # default when unset; use "limited" for read-only mode # Hosts must match the allowlist (unless denied). # If `allowed_domains` is empty, the proxy blocks requests until an allowlist is configured. -allowed_domains = ["*.openai.com"] +allowed_domains = ["*.openai.com", "localhost", "127.0.0.1", "::1"] denied_domains = ["evil.example"] # If false, local/private networking is rejected. Explicit allowlisting of local IP literals # (or `localhost`) is required to permit them. # Hostnames that resolve to local/private IPs are still blocked even if allowlisted. -allow_local_binding = false +allow_local_binding = true # macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`. allow_unix_sockets = ["/tmp/example.sock"] diff --git a/codex-rs/network-proxy/src/config.rs b/codex-rs/network-proxy/src/config.rs index 505fa96d3..31197a1c9 100644 --- a/codex-rs/network-proxy/src/config.rs +++ b/codex-rs/network-proxy/src/config.rs @@ -15,6 +15,7 @@ pub struct NetworkProxyConfig { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] pub struct NetworkProxySettings { #[serde(default)] pub enabled: bool, @@ -22,13 +23,10 @@ pub struct NetworkProxySettings { pub proxy_url: String, #[serde(default = "default_admin_url")] pub admin_url: String, - #[serde(default)] pub enable_socks5: bool, #[serde(default = "default_socks_url")] pub socks_url: String, - #[serde(default)] pub enable_socks5_udp: bool, - #[serde(default)] pub allow_upstream_proxy: bool, #[serde(default)] pub dangerously_allow_non_loopback_proxy: bool, @@ -42,7 +40,6 @@ pub struct NetworkProxySettings { pub denied_domains: Vec, #[serde(default)] pub allow_unix_sockets: Vec, - #[serde(default)] pub allow_local_binding: bool, } @@ -52,17 +49,17 @@ impl Default for NetworkProxySettings { enabled: false, proxy_url: default_proxy_url(), admin_url: default_admin_url(), - enable_socks5: false, + enable_socks5: true, socks_url: default_socks_url(), - enable_socks5_udp: false, - allow_upstream_proxy: false, + enable_socks5_udp: true, + allow_upstream_proxy: true, dangerously_allow_non_loopback_proxy: false, dangerously_allow_non_loopback_admin: false, mode: NetworkMode::default(), allowed_domains: Vec::new(), denied_domains: Vec::new(), allow_unix_sockets: Vec::new(), - allow_local_binding: false, + allow_local_binding: true, } } } @@ -329,6 +326,47 @@ mod tests { use pretty_assertions::assert_eq; + #[test] + fn network_proxy_settings_default_matches_local_use_baseline() { + assert_eq!( + NetworkProxySettings::default(), + NetworkProxySettings { + enabled: false, + proxy_url: "http://127.0.0.1:3128".to_string(), + admin_url: "http://127.0.0.1:8080".to_string(), + enable_socks5: true, + socks_url: "http://127.0.0.1:8081".to_string(), + enable_socks5_udp: true, + allow_upstream_proxy: true, + dangerously_allow_non_loopback_proxy: false, + dangerously_allow_non_loopback_admin: false, + mode: NetworkMode::Full, + allowed_domains: Vec::new(), + denied_domains: Vec::new(), + allow_unix_sockets: Vec::new(), + allow_local_binding: true, + } + ); + } + + #[test] + fn partial_network_config_uses_struct_defaults_for_missing_fields() { + let config: NetworkProxyConfig = serde_json::from_str( + r#"{ + "network": { + "enabled": true + } + }"#, + ) + .unwrap(); + let expected = NetworkProxySettings { + enabled: true, + ..NetworkProxySettings::default() + }; + + assert_eq!(config.network, expected); + } + #[test] fn parse_host_port_defaults_for_empty_string() { assert!(parse_host_port("", 1234).is_err()); diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 0a95e4460..566ac0da2 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -29,10 +29,10 @@ struct ReservedListeners { } impl ReservedListeners { - fn new(http: StdTcpListener, socks: StdTcpListener, admin: StdTcpListener) -> Self { + fn new(http: StdTcpListener, socks: Option, admin: StdTcpListener) -> Self { Self { http: Mutex::new(Some(http)), - socks: Mutex::new(Some(socks)), + socks: Mutex::new(socks), admin: Mutex::new(Some(admin)), } } @@ -133,15 +133,20 @@ impl NetworkProxyBuilder { let current_cfg = state.current_cfg().await?; let (requested_http_addr, requested_socks_addr, requested_admin_addr, reserved_listeners) = if self.managed_by_codex { + let runtime = config::resolve_runtime(¤t_cfg)?; let (http_listener, socks_listener, admin_listener) = - reserve_loopback_ephemeral_listeners() + reserve_loopback_ephemeral_listeners(current_cfg.network.enable_socks5) .context("reserve managed loopback proxy listeners")?; let http_addr = http_listener .local_addr() .context("failed to read reserved HTTP proxy address")?; - let socks_addr = socks_listener - .local_addr() - .context("failed to read reserved SOCKS5 proxy address")?; + let socks_addr = if let Some(socks_listener) = socks_listener.as_ref() { + socks_listener + .local_addr() + .context("failed to read reserved SOCKS5 proxy address")? + } else { + runtime.socks_addr + }; let admin_addr = admin_listener .local_addr() .context("failed to read reserved admin API address")?; @@ -186,13 +191,19 @@ impl NetworkProxyBuilder { } } -fn reserve_loopback_ephemeral_listeners() -> Result<(StdTcpListener, StdTcpListener, StdTcpListener)> -{ - Ok(( - reserve_loopback_ephemeral_listener().context("reserve HTTP proxy listener")?, - reserve_loopback_ephemeral_listener().context("reserve SOCKS5 proxy listener")?, - reserve_loopback_ephemeral_listener().context("reserve admin API listener")?, - )) +fn reserve_loopback_ephemeral_listeners( + reserve_socks_listener: bool, +) -> Result<(StdTcpListener, Option, StdTcpListener)> { + let http_listener = + reserve_loopback_ephemeral_listener().context("reserve HTTP proxy listener")?; + let socks_listener = if reserve_socks_listener { + Some(reserve_loopback_ephemeral_listener().context("reserve SOCKS5 proxy listener")?) + } else { + None + }; + let admin_listener = + reserve_loopback_ephemeral_listener().context("reserve admin API listener")?; + Ok((http_listener, socks_listener, admin_listener)) } fn reserve_loopback_ephemeral_listener() -> Result { @@ -612,6 +623,43 @@ mod tests { ); } + #[tokio::test] + async fn managed_proxy_builder_does_not_reserve_socks_listener_when_disabled() { + let settings = NetworkProxySettings { + enable_socks5: false, + socks_url: "http://127.0.0.1:43129".to_string(), + ..NetworkProxySettings::default() + }; + let state = Arc::new(network_proxy_state_for_policy(settings)); + let proxy = match NetworkProxy::builder().state(state).build().await { + Ok(proxy) => proxy, + Err(err) => { + if err + .chain() + .any(|cause| cause.to_string().contains("Operation not permitted")) + { + return; + } + panic!("failed to build managed proxy: {err:#}"); + } + }; + + assert!(proxy.http_addr.ip().is_loopback()); + assert!(proxy.admin_addr.ip().is_loopback()); + assert_eq!( + proxy.socks_addr, + "127.0.0.1:43129".parse::().unwrap() + ); + assert!( + proxy + .reserved_listeners + .as_ref() + .expect("managed builder should reserve listeners") + .take_socks() + .is_none() + ); + } + #[test] fn proxy_url_env_value_resolves_lowercase_aliases() { let mut env = HashMap::new(); diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index b5edeeb23..681d9c3a9 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -27,14 +27,30 @@ pub(crate) fn new_debug_config_output( socks_addr, admin_addr, } = proxy; + let all_proxy = session_all_proxy_url( + http_addr, + socks_addr, + config + .network + .as_ref() + .is_some_and(codex_core::config::NetworkProxySpec::socks_enabled), + ); lines.push(format!(" - HTTP_PROXY = http://{http_addr}").into()); - lines.push(format!(" - ALL_PROXY = socks5h://{socks_addr}").into()); + lines.push(format!(" - ALL_PROXY = {all_proxy}").into()); lines.push(format!(" - ADMIN_PROXY = http://{admin_addr}").into()); } PlainHistoryCell::new(lines) } +fn session_all_proxy_url(http_addr: &str, socks_addr: &str, socks_enabled: bool) -> String { + if socks_enabled { + format!("socks5h://{socks_addr}") + } else { + format!("http://{http_addr}") + } +} + fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec> { let mut lines = vec!["/debug-config".magenta().into(), "".into()]; @@ -288,6 +304,7 @@ fn format_network_constraints(network: &NetworkConstraints) -> String { #[cfg(test)] mod tests { use super::render_debug_config_lines; + use super::session_all_proxy_url; use codex_app_server_protocol::ConfigLayerSource; use codex_core::config::Constrained; use codex_core::config_loader::ConfigLayerEntry; @@ -503,4 +520,20 @@ mod tests { rendered.contains("allowed_web_search_modes: disabled (source: cloud requirements)") ); } + + #[test] + fn session_all_proxy_url_uses_socks_when_enabled() { + assert_eq!( + session_all_proxy_url("127.0.0.1:3128", "127.0.0.1:8081", true), + "socks5h://127.0.0.1:8081".to_string() + ); + } + + #[test] + fn session_all_proxy_url_uses_http_when_socks_disabled() { + assert_eq!( + session_all_proxy_url("127.0.0.1:3128", "127.0.0.1:8081", false), + "http://127.0.0.1:3128".to_string() + ); + } }