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
This commit is contained in:
viyatb-oai
2026-02-10 15:13:52 -08:00
committed by GitHub
parent 623d3f4071
commit 1d47927aa0
5 changed files with 152 additions and 29 deletions

View File

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

View File

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

View File

@@ -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<String>,
#[serde(default)]
pub allow_unix_sockets: Vec<String>,
#[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());

View File

@@ -29,10 +29,10 @@ struct ReservedListeners {
}
impl ReservedListeners {
fn new(http: StdTcpListener, socks: StdTcpListener, admin: StdTcpListener) -> Self {
fn new(http: StdTcpListener, socks: Option<StdTcpListener>, 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(&current_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>, 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<StdTcpListener> {
@@ -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::<SocketAddr>().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();

View File

@@ -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<Line<'static>> {
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()
);
}
}