use crate::config;use crate::http_proxy;use crate::network_policy::NetworkPolicyDecider;use crate::runtime::BlockedRequestObserver;use crate::runtime::ConfigState;use crate::runtime::unix_socket_permissions_supported;use crate::socks5;use crate::state::NetworkProxyState;use anyhow::Context;use anyhow::Result;use clap::Parser;use codex_utils_absolute_path::AbsolutePathBuf;use std::collections::HashMap;use std::net::SocketAddr;use std::net::TcpListener as StdTcpListener;use std::sync::Arc;use std::sync::Mutex;use std::sync::RwLock;use tokio::task::JoinHandle;use tracing::warn;#[derive(Debug, Clone, Parser)]#[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")]pub struct Args {}#[derive(Debug)]struct ReservedListeners { http: Mutex<Option<StdTcpListener>>, socks: Mutex<Option<StdTcpListener>>,}impl ReservedListeners { fn new(http: StdTcpListener, socks: Option<StdTcpListener>) -> Self { Self { http: Mutex::new(Some(http)), socks: Mutex::new(socks), } } fn take_http(&self) -> Option<StdTcpListener> { let mut guard = self .http .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); guard.take() } fn take_socks(&self) -> Option<StdTcpListener> { let mut guard = self .socks .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); guard.take() }}struct ReservedListenerSet { http_listener: StdTcpListener, socks_listener: Option<StdTcpListener>,}impl ReservedListenerSet { fn new(http_listener: StdTcpListener, socks_listener: Option<StdTcpListener>) -> Self { Self { http_listener, socks_listener, } } fn http_addr(&self) -> Result<SocketAddr> { self.http_listener .local_addr() .context("failed to read reserved HTTP proxy address") } fn socks_addr(&self, default_addr: SocketAddr) -> Result<SocketAddr> { self.socks_listener .as_ref() .map_or(Ok(default_addr), |listener| { listener .local_addr() .context("failed to read reserved SOCKS5 proxy address") }) } fn into_reserved_listeners(self) -> Arc<ReservedListeners> { Arc::new(ReservedListeners::new( self.http_listener, self.socks_listener, )) }}#[derive(Clone)]pub struct NetworkProxyBuilder { state: Option<Arc<NetworkProxyState>>, http_addr: Option<SocketAddr>, socks_addr: Option<SocketAddr>, managed_by_codex: bool, policy_decider: Option<Arc<dyn NetworkPolicyDecider>>, blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,}impl Default for NetworkProxyBuilder { fn default() -> Self { Self { state: None, http_addr: None, socks_addr: None, managed_by_codex: true, policy_decider: None, blocked_request_observer: None, } }}impl NetworkProxyBuilder { pub fn state(mut self, state: Arc<NetworkProxyState>) -> Self { self.state = Some(state); self } pub fn http_addr(mut self, addr: SocketAddr) -> Self { self.http_addr = Some(addr); self } pub fn socks_addr(mut self, addr: SocketAddr) -> Self { self.socks_addr = Some(addr); self } pub fn managed_by_codex(mut self, managed_by_codex: bool) -> Self { self.managed_by_codex = managed_by_codex; self } pub fn policy_decider<D>(mut self, decider: D) -> Self where D: NetworkPolicyDecider, { self.policy_decider = Some(Arc::new(decider)); self } pub fn policy_decider_arc(mut self, decider: Arc<dyn NetworkPolicyDecider>) -> Self { self.policy_decider = Some(decider); self } pub fn blocked_request_observer<O>(mut self, observer: O) -> Self where O: BlockedRequestObserver, { self.blocked_request_observer = Some(Arc::new(observer)); self } pub fn blocked_request_observer_arc( mut self, observer: Arc<dyn BlockedRequestObserver>, ) -> Self { self.blocked_request_observer = Some(observer); self } pub async fn build(self) -> Result<NetworkProxy> { let state = self.state.ok_or_else(|| { anyhow::anyhow!( "NetworkProxyBuilder requires a state; supply one via builder.state(...)" ) })?; state .set_blocked_request_observer(self.blocked_request_observer.clone()) .await; let current_cfg = state.current_cfg().await?; let (requested_http_addr, requested_socks_addr, reserved_listeners) = if self .managed_by_codex { let runtime = config::resolve_runtime(¤t_cfg)?; #[cfg(target_os = "windows")] let (managed_http_addr, managed_socks_addr) = config::clamp_bind_addrs( runtime.http_addr, runtime.socks_addr, ¤t_cfg.network, ); #[cfg(target_os = "windows")] let reserved = reserve_windows_managed_listeners( managed_http_addr, managed_socks_addr, current_cfg.network.enable_socks5, ) .context("reserve managed loopback proxy listeners")?; #[cfg(not(target_os = "windows"))] let reserved = reserve_loopback_ephemeral_listeners(current_cfg.network.enable_socks5) .context("reserve managed loopback proxy listeners")?; let http_addr = reserved.http_addr()?; let socks_addr = reserved.socks_addr(runtime.socks_addr)?; ( http_addr, socks_addr, Some(reserved.into_reserved_listeners()), ) } else { let runtime = config::resolve_runtime(¤t_cfg)?; ( self.http_addr.unwrap_or(runtime.http_addr), self.socks_addr.unwrap_or(runtime.socks_addr), None, ) }; // Reapply bind clamping for caller overrides so unix-socket proxying stays loopback-only. let (http_addr, socks_addr) = config::clamp_bind_addrs( requested_http_addr, requested_socks_addr, ¤t_cfg.network, ); Ok(NetworkProxy { state, http_addr, socks_addr, socks_enabled: current_cfg.network.enable_socks5, runtime_settings: Arc::new(RwLock::new(NetworkProxyRuntimeSettings::from_config( ¤t_cfg, )?)), reserved_listeners, policy_decider: self.policy_decider, }) }}fn reserve_loopback_ephemeral_listeners( reserve_socks_listener: bool,) -> Result<ReservedListenerSet> { 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 }; Ok(ReservedListenerSet::new(http_listener, socks_listener))}#[cfg(target_os = "windows")]fn reserve_windows_managed_listeners( http_addr: SocketAddr, socks_addr: SocketAddr, reserve_socks_listener: bool,) -> Result<ReservedListenerSet> { let http_addr = windows_managed_loopback_addr(http_addr); let socks_addr = windows_managed_loopback_addr(socks_addr); match try_reserve_windows_managed_listeners(http_addr, socks_addr, reserve_socks_listener) { Ok(listeners) => Ok(listeners), Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => { warn!("managed Windows proxy ports are busy; falling back to ephemeral loopback ports"); reserve_loopback_ephemeral_listeners(reserve_socks_listener) .context("reserve fallback loopback proxy listeners") } Err(err) => Err(err).context("reserve Windows managed proxy listeners"), }}#[cfg(target_os = "windows")]fn try_reserve_windows_managed_listeners( http_addr: SocketAddr, socks_addr: SocketAddr, reserve_socks_listener: bool,) -> std::io::Result<ReservedListenerSet> { let http_listener = StdTcpListener::bind(http_addr)?; let socks_listener = if reserve_socks_listener { Some(StdTcpListener::bind(socks_addr)?) } else { None }; Ok(ReservedListenerSet::new(http_listener, socks_listener))}#[cfg(target_os = "windows")]fn windows_managed_loopback_addr(addr: SocketAddr) -> SocketAddr { if !addr.ip().is_loopback() { warn!( "managed Windows proxies must bind to loopback; clamping {addr} to 127.0.0.1:{}", addr.port() ); } SocketAddr::from(([127, 0, 0, 1], addr.port()))}fn reserve_loopback_ephemeral_listener() -> Result<StdTcpListener> { StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))) .context("bind loopback ephemeral port")}#[derive(Debug, Clone, PartialEq, Eq)]struct NetworkProxyRuntimeSettings { allow_local_binding: bool, allow_unix_sockets: Arc<[String]>, dangerously_allow_all_unix_sockets: bool, mitm_ca_trust_bundle: Option<crate::certs::ManagedMitmCaTrustBundle>,}impl NetworkProxyRuntimeSettings { fn from_config(config: &config::NetworkProxyConfig) -> Result<Self> { let mitm_ca_trust_bundle = if config.network.mitm { let env = crate::certs::CUSTOM_CA_ENV_KEYS .into_iter() .filter_map(|key| std::env::var(key).ok().map(|value| (key, value))) .collect(); Some(crate::certs::managed_ca_trust_bundle(&env)?) } else { None }; Ok(Self { allow_local_binding: config.network.allow_local_binding, allow_unix_sockets: config.network.allow_unix_sockets().into(), dangerously_allow_all_unix_sockets: config.network.dangerously_allow_all_unix_sockets, mitm_ca_trust_bundle, }) }}#[derive(Clone)]pub struct NetworkProxy { state: Arc<NetworkProxyState>, http_addr: SocketAddr, socks_addr: SocketAddr, socks_enabled: bool, runtime_settings: Arc<RwLock<NetworkProxyRuntimeSettings>>, reserved_listeners: Option<Arc<ReservedListeners>>, policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,}impl std::fmt::Debug for NetworkProxy { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Avoid logging internal state (config contents, derived globsets, etc.) which can be noisy // and may contain sensitive paths. f.debug_struct("NetworkProxy") .field("http_addr", &self.http_addr) .field("socks_addr", &self.socks_addr) .finish_non_exhaustive() }}impl PartialEq for NetworkProxy { fn eq(&self, other: &Self) -> bool { self.http_addr == other.http_addr && self.socks_addr == other.socks_addr && self.runtime_settings() == other.runtime_settings() }}impl Eq for NetworkProxy {}pub const PROXY_URL_ENV_KEYS: &[&str] = &[ "HTTP_PROXY", "HTTPS_PROXY", "WS_PROXY", "WSS_PROXY", "ALL_PROXY", "FTP_PROXY", "YARN_HTTP_PROXY", "YARN_HTTPS_PROXY", "NPM_CONFIG_HTTP_PROXY", "NPM_CONFIG_HTTPS_PROXY", "NPM_CONFIG_PROXY", "BUNDLE_HTTP_PROXY", "BUNDLE_HTTPS_PROXY", "PIP_PROXY", "DOCKER_HTTP_PROXY", "DOCKER_HTTPS_PROXY",];pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"];pub const PROXY_ACTIVE_ENV_KEY: &str = "CODEX_NETWORK_PROXY_ACTIVE";pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING";const ELECTRON_GET_USE_PROXY_ENV_KEY: &str = "ELECTRON_GET_USE_PROXY";const NODE_USE_ENV_PROXY_ENV_KEY: &str = "NODE_USE_ENV_PROXY";#[cfg(any(target_os = "macos", test))]const GIT_SSH_COMMAND_ENV_KEY: &str = "GIT_SSH_COMMAND";pub const PROXY_ENV_KEYS: &[&str] = &[ PROXY_ACTIVE_ENV_KEY, ALLOW_LOCAL_BINDING_ENV_KEY, ELECTRON_GET_USE_PROXY_ENV_KEY, NODE_USE_ENV_PROXY_ENV_KEY, "HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "YARN_HTTP_PROXY", "YARN_HTTPS_PROXY", "npm_config_http_proxy", "npm_config_https_proxy", "npm_config_proxy", "NPM_CONFIG_HTTP_PROXY", "NPM_CONFIG_HTTPS_PROXY", "NPM_CONFIG_PROXY", "BUNDLE_HTTP_PROXY", "BUNDLE_HTTPS_PROXY", "PIP_PROXY", "DOCKER_HTTP_PROXY", "DOCKER_HTTPS_PROXY", "WS_PROXY", "WSS_PROXY", "ws_proxy", "wss_proxy", "NO_PROXY", "no_proxy", "npm_config_noproxy", "NPM_CONFIG_NOPROXY", "YARN_NO_PROXY", "BUNDLE_NO_PROXY", "ALL_PROXY", "all_proxy", "FTP_PROXY", "ftp_proxy",];#[cfg(target_os = "macos")]pub const PROXY_GIT_SSH_COMMAND_ENV_KEY: &str = GIT_SSH_COMMAND_ENV_KEY;const FTP_PROXY_ENV_KEYS: &[&str] = &["FTP_PROXY", "ftp_proxy"];const WEBSOCKET_PROXY_ENV_KEYS: &[&str] = &["WS_PROXY", "WSS_PROXY", "ws_proxy", "wss_proxy"];pub const NO_PROXY_ENV_KEYS: &[&str] = &[ "NO_PROXY", "no_proxy", "npm_config_noproxy", "NPM_CONFIG_NOPROXY", "YARN_NO_PROXY", "BUNDLE_NO_PROXY",];pub const DEFAULT_NO_PROXY_VALUE: &str = concat!( "localhost,127.0.0.1,::1,", "10.0.0.0/8,", "172.16.0.0/12,", "192.168.0.0/16");#[cfg(target_os = "macos")]pub const CODEX_PROXY_GIT_SSH_COMMAND_MARKER: &str = "CODEX_PROXY_GIT_SSH_COMMAND=1 ";#[cfg(target_os = "macos")]const CODEX_PROXY_GIT_SSH_COMMAND_PREFIX: &str = "CODEX_PROXY_GIT_SSH_COMMAND=1 ssh -o ProxyCommand='nc -X 5 -x ";#[cfg(target_os = "macos")]const CODEX_PROXY_GIT_SSH_COMMAND_SUFFIX: &str = " %h %p'";pub fn proxy_url_env_value<'a>( env: &'a HashMap<String, String>, canonical_key: &str,) -> Option<&'a str> { if let Some(value) = env.get(canonical_key) { return Some(value.as_str()); } let lower_key = canonical_key.to_ascii_lowercase(); env.get(lower_key.as_str()).map(String::as_str)}pub fn has_proxy_url_env_vars(env: &HashMap<String, String>) -> bool { PROXY_URL_ENV_KEYS .iter() .any(|key| proxy_url_env_value(env, key).is_some_and(|value| !value.trim().is_empty()))}fn set_env_keys(env: &mut HashMap<String, String>, keys: &[&str], value: &str) { for key in keys { env.insert((*key).to_string(), value.to_string()); }}#[cfg(target_os = "macos")]fn codex_proxy_git_ssh_command(socks_addr: SocketAddr) -> String { format!("{CODEX_PROXY_GIT_SSH_COMMAND_PREFIX}{socks_addr}{CODEX_PROXY_GIT_SSH_COMMAND_SUFFIX}")}#[cfg(target_os = "macos")]fn is_codex_proxy_git_ssh_command(command: &str) -> bool { command.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_PREFIX) && command.ends_with(CODEX_PROXY_GIT_SSH_COMMAND_SUFFIX)}fn apply_proxy_env_overrides( env: &mut HashMap<String, String>, http_addr: SocketAddr, socks_addr: SocketAddr, socks_enabled: bool, allow_local_binding: bool, mitm_ca_trust_bundle: Option<&crate::certs::ManagedMitmCaTrustBundle>,) { let http_proxy_url = format!("http://{http_addr}"); let socks_proxy_url = format!("socks5h://{socks_addr}"); env.insert(PROXY_ACTIVE_ENV_KEY.to_string(), "1".to_string()); env.insert( ALLOW_LOCAL_BINDING_ENV_KEY.to_string(), if allow_local_binding { "1".to_string() } else { "0".to_string() }, ); // HTTP-based clients are best served by explicit HTTP proxy URLs. set_env_keys( env, &[ "HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "YARN_HTTP_PROXY", "YARN_HTTPS_PROXY", "npm_config_http_proxy", "npm_config_https_proxy", "npm_config_proxy", "NPM_CONFIG_HTTP_PROXY", "NPM_CONFIG_HTTPS_PROXY", "NPM_CONFIG_PROXY", "BUNDLE_HTTP_PROXY", "BUNDLE_HTTPS_PROXY", "PIP_PROXY", "DOCKER_HTTP_PROXY", "DOCKER_HTTPS_PROXY", ], &http_proxy_url, ); // Some websocket clients look for dedicated WS/WSS proxy environment variables instead of // HTTP(S)_PROXY. Keep them aligned with the managed HTTP proxy endpoint. set_env_keys(env, WEBSOCKET_PROXY_ENV_KEYS, &http_proxy_url); // Keep loopback and IP-literal private targets direct so local IPC/LAN access avoids the proxy. // Do not include hostname suffixes here: those can force clients to resolve internal names // locally instead of letting the proxy resolve them. set_env_keys(env, NO_PROXY_ENV_KEYS, DEFAULT_NO_PROXY_VALUE); env.insert( ELECTRON_GET_USE_PROXY_ENV_KEY.to_string(), "true".to_string(), ); // Node.js built-in HTTP clients only honor proxy environment variables when this is enabled. env.insert(NODE_USE_ENV_PROXY_ENV_KEY.to_string(), "1".to_string()); // Keep HTTP_PROXY/HTTPS_PROXY as HTTP endpoints. A lot of clients break if // those vars contain SOCKS URLs. We only switch ALL_PROXY here. // if socks_enabled { set_env_keys(env, ALL_PROXY_ENV_KEYS, &socks_proxy_url); set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url); } else { set_env_keys(env, ALL_PROXY_ENV_KEYS, &http_proxy_url); set_env_keys(env, FTP_PROXY_ENV_KEYS, &http_proxy_url); } #[cfg(target_os = "macos")] if socks_enabled { // Preserve existing SSH wrappers (for example: Secretive/Teleport setups) // but refresh a previously injected Codex fallback so it cannot point // at a stale proxy port after the proxy is restarted. match env.get(GIT_SSH_COMMAND_ENV_KEY) { Some(command) if !is_codex_proxy_git_ssh_command(command) => {} _ => { env.insert( GIT_SSH_COMMAND_ENV_KEY.to_string(), codex_proxy_git_ssh_command(socks_addr), ); } } } if let Some(mitm_ca_trust_bundle) = mitm_ca_trust_bundle { let managed_path = mitm_ca_trust_bundle.path.to_string_lossy().into_owned(); for key in crate::certs::CUSTOM_CA_ENV_KEYS { if env .get(key) .filter(|value| !value.is_empty()) .is_some_and(|value| { value != &managed_path && mitm_ca_trust_bundle.startup_env_values.get(key) != Some(value) }) { // TODO(winston): Materialize policy-checked per-child bundles for readable // startup and command-scoped CA overrides. For now startup overrides are // replaced with the default bundle and later command-scoped overrides are // preserved, either of which can make intercepted TLS fail. continue; } env.insert(key.to_string(), managed_path.clone()); } }}impl NetworkProxy { pub fn builder() -> NetworkProxyBuilder { NetworkProxyBuilder::default() } pub fn http_addr(&self) -> SocketAddr { self.http_addr } pub fn socks_addr(&self) -> SocketAddr { self.socks_addr } pub async fn current_cfg(&self) -> Result<config::NetworkProxyConfig> { self.state.current_cfg().await } pub async fn add_allowed_domain(&self, host: &str) -> Result<()> { self.state.add_allowed_domain(host).await } pub async fn add_denied_domain(&self, host: &str) -> Result<()> { self.state.add_denied_domain(host).await } pub fn allow_local_binding(&self) -> bool { self.runtime_settings().allow_local_binding } pub fn allow_unix_sockets(&self) -> Arc<[String]> { self.runtime_settings().allow_unix_sockets } pub fn dangerously_allow_all_unix_sockets(&self) -> bool { self.runtime_settings().dangerously_allow_all_unix_sockets } /// Returns the generated MITM CA bundle path child sandboxes should expose to TLS clients. pub fn managed_mitm_ca_trust_bundle_path(&self) -> Option<AbsolutePathBuf> { self.runtime_settings() .mitm_ca_trust_bundle .and_then(|bundle| { AbsolutePathBuf::from_absolute_path(bundle.path) .map_err(|err| warn!("managed MITM CA trust bundle path is invalid: {err}")) .ok() }) } pub fn apply_to_env(&self, env: &mut HashMap<String, String>) { let runtime_settings = self.runtime_settings(); // Enforce proxying for child processes. Proxy endpoint values are always rewritten; // managed MITM CA vars preserve child-scoped overrides after proxy startup. apply_proxy_env_overrides( env, self.http_addr, self.socks_addr, self.socks_enabled, runtime_settings.allow_local_binding, runtime_settings.mitm_ca_trust_bundle.as_ref(), ); } pub async fn replace_config_state(&self, new_state: ConfigState) -> Result<()> { let current_cfg = self.state.current_cfg().await?; anyhow::ensure!( new_state.config.network.enabled == current_cfg.network.enabled, "cannot update network.enabled on a running proxy" ); anyhow::ensure!( new_state.config.network.proxy_url == current_cfg.network.proxy_url, "cannot update network.proxy_url on a running proxy" ); anyhow::ensure!( new_state.config.network.socks_url == current_cfg.network.socks_url, "cannot update network.socks_url on a running proxy" ); anyhow::ensure!( new_state.config.network.enable_socks5 == current_cfg.network.enable_socks5, "cannot update network.enable_socks5 on a running proxy" ); anyhow::ensure!( new_state.config.network.enable_socks5_udp == current_cfg.network.enable_socks5_udp, "cannot update network.enable_socks5_udp on a running proxy" ); let settings = NetworkProxyRuntimeSettings::from_config(&new_state.config)?; self.state.replace_config_state(new_state).await?; let mut guard = self .runtime_settings .write() .unwrap_or_else(std::sync::PoisonError::into_inner); *guard = settings; Ok(()) } fn runtime_settings(&self) -> NetworkProxyRuntimeSettings { self.runtime_settings .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub async fn run(&self) -> Result<NetworkProxyHandle> { let current_cfg = self.state.current_cfg().await?; if !current_cfg.network.enabled { warn!("network.enabled is false; skipping proxy listeners"); return Ok(NetworkProxyHandle::noop()); } if !unix_socket_permissions_supported() { warn!( "allowUnixSockets and dangerouslyAllowAllUnixSockets are macOS-only; requests will be rejected on this platform" ); } let reserved_listeners = self.reserved_listeners.as_ref(); let http_listener = reserved_listeners.and_then(|listeners| listeners.take_http()); let socks_listener = reserved_listeners.and_then(|listeners| listeners.take_socks()); let http_state = self.state.clone(); let http_decider = self.policy_decider.clone(); let http_addr = self.http_addr; let http_task = tokio::spawn(async move { match http_listener { Some(listener) => { http_proxy::run_http_proxy_with_std_listener(http_state, listener, http_decider) .await } None => http_proxy::run_http_proxy(http_state, http_addr, http_decider).await, } }); let socks_task = if current_cfg.network.enable_socks5 { let socks_state = self.state.clone(); let socks_decider = self.policy_decider.clone(); let socks_addr = self.socks_addr; let enable_socks5_udp = current_cfg.network.enable_socks5_udp; Some(tokio::spawn(async move { match socks_listener { Some(listener) => { socks5::run_socks5_with_std_listener( socks_state, listener, socks_decider, enable_socks5_udp, ) .await } None => { socks5::run_socks5( socks_state, socks_addr, socks_decider, enable_socks5_udp, ) .await } } })) } else { None }; Ok(NetworkProxyHandle { http_task: Some(http_task), socks_task, completed: false, }) }}pub struct NetworkProxyHandle { http_task: Option<JoinHandle<Result<()>>>, socks_task: Option<JoinHandle<Result<()>>>, completed: bool,}impl NetworkProxyHandle { fn noop() -> Self { Self { http_task: Some(tokio::spawn(async { Ok(()) })), socks_task: None, completed: true, } } pub async fn wait(mut self) -> Result<()> { let http_task = self.http_task.take().context("missing http proxy task")?; let socks_task = self.socks_task.take(); let http_result = http_task.await; let socks_result = match socks_task { Some(task) => Some(task.await), None => None, }; self.completed = true; http_result??; if let Some(socks_result) = socks_result { socks_result??; } Ok(()) } pub async fn shutdown(mut self) -> Result<()> { abort_tasks(self.http_task.take(), self.socks_task.take()).await; self.completed = true; Ok(()) }}async fn abort_task(task: Option<JoinHandle<Result<()>>>) { if let Some(task) = task { task.abort(); let _ = task.await; }}async fn abort_tasks( http_task: Option<JoinHandle<Result<()>>>, socks_task: Option<JoinHandle<Result<()>>>,) { abort_task(http_task).await; abort_task(socks_task).await;}impl Drop for NetworkProxyHandle { fn drop(&mut self) { if self.completed { return; } let http_task = self.http_task.take(); let socks_task = self.socks_task.take(); tokio::spawn(async move { abort_tasks(http_task, socks_task).await; }); }}#[cfg(test)]mod tests { use super::*; use crate::config::NetworkProxySettings; use crate::state::network_proxy_state_for_policy; use pretty_assertions::assert_eq; use std::net::IpAddr; use std::net::Ipv4Addr; use std::path::Path; #[tokio::test] async fn managed_proxy_builder_uses_loopback_ports() { let http_listener = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); let http_addr = http_listener.local_addr().unwrap(); let socks_listener = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); let socks_addr = socks_listener.local_addr().unwrap(); drop(http_listener); drop(socks_listener); let state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings { proxy_url: format!("http://{http_addr}"), socks_url: format!("http://{socks_addr}"), ..NetworkProxySettings::default() })); 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.socks_addr.ip().is_loopback()); #[cfg(target_os = "windows")] { assert_eq!(proxy.http_addr, http_addr); assert_eq!(proxy.socks_addr, socks_addr); } #[cfg(not(target_os = "windows"))] { assert_ne!(proxy.http_addr.port(), 0); assert_ne!(proxy.socks_addr.port(), 0); } } #[tokio::test] async fn non_codex_managed_proxy_builder_uses_configured_ports() { let settings = NetworkProxySettings { proxy_url: "http://127.0.0.1:43128".to_string(), socks_url: "http://127.0.0.1:48081".to_string(), ..NetworkProxySettings::default() }; let state = Arc::new(network_proxy_state_for_policy(settings)); let proxy = NetworkProxy::builder() .state(state) .managed_by_codex(/*managed_by_codex*/ false) .build() .await .unwrap(); assert_eq!( proxy.http_addr, "127.0.0.1:43128".parse::<SocketAddr>().unwrap() ); assert_eq!( proxy.socks_addr, "127.0.0.1:48081".parse::<SocketAddr>().unwrap() ); } #[tokio::test] async fn managed_proxy_builder_does_not_reserve_socks_listener_when_disabled() { let settings = NetworkProxySettings { enable_socks5: false, proxy_url: "http://127.0.0.1:43128".to_string(), 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_ne!(proxy.http_addr.port(), 0); 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() ); } #[cfg(target_os = "windows")] #[test] fn windows_managed_loopback_addr_clamps_non_loopback_inputs() { assert_eq!( windows_managed_loopback_addr("0.0.0.0:3128".parse::<SocketAddr>().unwrap()), "127.0.0.1:3128".parse::<SocketAddr>().unwrap() ); assert_eq!( windows_managed_loopback_addr("[::]:8081".parse::<SocketAddr>().unwrap()), "127.0.0.1:8081".parse::<SocketAddr>().unwrap() ); } #[cfg(target_os = "windows")] #[test] fn reserve_windows_managed_listeners_falls_back_when_http_port_is_busy() { let occupied = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); let busy_port = occupied.local_addr().unwrap().port(); let reserved = reserve_windows_managed_listeners( SocketAddr::from(([127, 0, 0, 1], busy_port)), SocketAddr::from(([127, 0, 0, 1], 48081)), /*reserve_socks_listener*/ false, ) .unwrap(); assert!(reserved.socks_listener.is_none()); assert!( reserved .http_listener .local_addr() .unwrap() .ip() .is_loopback() ); assert_ne!( reserved.http_listener.local_addr().unwrap().port(), busy_port ); } #[test] fn proxy_url_env_value_resolves_lowercase_aliases() { let mut env = HashMap::new(); env.insert( "http_proxy".to_string(), "http://127.0.0.1:3128".to_string(), ); assert_eq!( proxy_url_env_value(&env, "HTTP_PROXY"), Some("http://127.0.0.1:3128") ); } #[test] fn has_proxy_url_env_vars_detects_lowercase_aliases() { let mut env = HashMap::new(); env.insert( "all_proxy".to_string(), "socks5h://127.0.0.1:8081".to_string(), ); assert_eq!(has_proxy_url_env_vars(&env), true); } #[test] fn has_proxy_url_env_vars_detects_websocket_proxy_keys() { let mut env = HashMap::new(); env.insert("wss_proxy".to_string(), "http://127.0.0.1:3128".to_string()); assert_eq!(has_proxy_url_env_vars(&env), true); } #[test] fn apply_proxy_env_overrides_sets_common_tool_vars() { let mut env = HashMap::new(); apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), /*socks_enabled*/ true, /*allow_local_binding*/ false, /*mitm_ca_trust_bundle*/ None, ); assert_eq!( env.get("HTTP_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!( env.get("WS_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!( env.get("WSS_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!( env.get("npm_config_proxy"), Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!( env.get("ALL_PROXY"), Some(&"socks5h://127.0.0.1:8081".to_string()) ); assert_eq!( env.get("FTP_PROXY"), Some(&"socks5h://127.0.0.1:8081".to_string()) ); assert_eq!( env.get("NO_PROXY"), Some(&DEFAULT_NO_PROXY_VALUE.to_string()) ); let no_proxy = env.get("NO_PROXY").expect("NO_PROXY should be set"); assert!(no_proxy.contains("10.0.0.0/8")); assert!(no_proxy.contains("172.16.0.0/12")); assert!(no_proxy.contains("192.168.0.0/16")); assert!(!no_proxy.contains("169.254.0.0/16")); assert_eq!(env.get(PROXY_ACTIVE_ENV_KEY), Some(&"1".to_string())); assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"0".to_string())); assert_eq!( env.get(ELECTRON_GET_USE_PROXY_ENV_KEY), Some(&"true".to_string()) ); assert_eq!(env.get(NODE_USE_ENV_PROXY_ENV_KEY), Some(&"1".to_string())); #[cfg(target_os = "macos")] assert_eq!( env.get(GIT_SSH_COMMAND_ENV_KEY), Some( &"CODEX_PROXY_GIT_SSH_COMMAND=1 ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'" .to_string() ) ); #[cfg(not(target_os = "macos"))] assert_eq!(env.get(GIT_SSH_COMMAND_ENV_KEY), None); } #[test] fn apply_proxy_env_overrides_sets_only_expected_env_keys() { let mut env = HashMap::new(); apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), /*socks_enabled*/ true, /*allow_local_binding*/ false, /*mitm_ca_trust_bundle*/ None, ); for key in env.keys() { let is_managed_git_ssh_key = cfg!(target_os = "macos") && key == GIT_SSH_COMMAND_ENV_KEY; assert!( PROXY_ENV_KEYS.contains(&key.as_str()) || is_managed_git_ssh_key, "proxy env writer set unexpected key: {key}" ); } } #[test] fn apply_proxy_env_overrides_sets_mitm_ca_trust_bundle_vars() { let mut env = HashMap::new(); let mitm_ca_trust_bundle_path = Path::new("/tmp/codex-proxy/ca-bundle.pem"); let mitm_ca_trust_bundle = crate::certs::ManagedMitmCaTrustBundle { path: mitm_ca_trust_bundle_path.to_path_buf(), startup_env_values: HashMap::new(), }; apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), /*socks_enabled*/ true, /*allow_local_binding*/ false, Some(&mitm_ca_trust_bundle), ); for key in crate::certs::CUSTOM_CA_ENV_KEYS { assert_eq!( env.get(key), Some(&mitm_ca_trust_bundle_path.display().to_string()) ); } } #[test] fn apply_proxy_env_overrides_preserves_command_scoped_mitm_ca_override() { let command_ca_bundle_path = "/tmp/command-ca.pem".to_string(); let mut env = HashMap::from([( "REQUESTS_CA_BUNDLE".to_string(), command_ca_bundle_path.clone(), )]); let mitm_ca_trust_bundle_path = Path::new("/tmp/codex-proxy/ca-bundle.pem"); let mitm_ca_trust_bundle = crate::certs::ManagedMitmCaTrustBundle { path: mitm_ca_trust_bundle_path.to_path_buf(), startup_env_values: HashMap::new(), }; apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), /*socks_enabled*/ true, /*allow_local_binding*/ false, Some(&mitm_ca_trust_bundle), ); assert_eq!(env.get("REQUESTS_CA_BUNDLE"), Some(&command_ca_bundle_path)); assert_eq!( env.get("SSL_CERT_FILE"), Some(&mitm_ca_trust_bundle_path.display().to_string()) ); } #[test] fn apply_proxy_env_overrides_uses_http_for_all_proxy_without_socks() { let mut env = HashMap::new(); apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), /*socks_enabled*/ false, /*allow_local_binding*/ true, /*mitm_ca_trust_bundle*/ None, ); assert_eq!( env.get("ALL_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"1".to_string())); } #[test] fn apply_proxy_env_overrides_uses_plain_http_proxy_url() { let mut env = HashMap::new(); apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), /*socks_enabled*/ true, /*allow_local_binding*/ false, /*mitm_ca_trust_bundle*/ None, ); assert_eq!( env.get("HTTP_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!( env.get("HTTPS_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!( env.get("WS_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!( env.get("WSS_PROXY"), Some(&"http://127.0.0.1:3128".to_string()) ); assert_eq!( env.get("ALL_PROXY"), Some(&"socks5h://127.0.0.1:8081".to_string()) ); #[cfg(target_os = "macos")] assert_eq!( env.get(GIT_SSH_COMMAND_ENV_KEY), Some( &"CODEX_PROXY_GIT_SSH_COMMAND=1 ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'" .to_string() ) ); #[cfg(not(target_os = "macos"))] assert_eq!(env.get(GIT_SSH_COMMAND_ENV_KEY), None); } #[cfg(target_os = "macos")] #[test] fn apply_proxy_env_overrides_preserves_existing_git_ssh_command() { let mut env = HashMap::new(); env.insert( GIT_SSH_COMMAND_ENV_KEY.to_string(), "ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string(), ); apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), /*socks_enabled*/ true, /*allow_local_binding*/ false, /*mitm_ca_trust_bundle*/ None, ); assert_eq!( env.get(GIT_SSH_COMMAND_ENV_KEY), Some(&"ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string()) ); } #[cfg(target_os = "macos")] #[test] fn apply_proxy_env_overrides_preserves_unmarked_git_ssh_command_with_proxy_shape() { let mut env = HashMap::new(); env.insert( GIT_SSH_COMMAND_ENV_KEY.to_string(), "ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'".to_string(), ); apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 48081), /*socks_enabled*/ true, /*allow_local_binding*/ false, /*mitm_ca_trust_bundle*/ None, ); assert_eq!( env.get(GIT_SSH_COMMAND_ENV_KEY), Some(&"ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'".to_string()) ); } #[cfg(target_os = "macos")] #[test] fn apply_proxy_env_overrides_refreshes_previous_codex_proxy_git_ssh_command() { let mut env = HashMap::new(); env.insert( GIT_SSH_COMMAND_ENV_KEY.to_string(), codex_proxy_git_ssh_command(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081)), ); apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 43128), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 48081), /*socks_enabled*/ true, /*allow_local_binding*/ false, /*mitm_ca_trust_bundle*/ None, ); assert_eq!( env.get(GIT_SSH_COMMAND_ENV_KEY), Some(&codex_proxy_git_ssh_command(SocketAddr::new( IpAddr::V4(Ipv4Addr::LOCALHOST), 48081, ))) ); }}