1mod execution_scope;
2
3use crate::attribution::PROXY_ATTRIBUTION_TOKEN_ENV_KEY;
4use crate::config;
5use crate::credential_broker::BROKERED_CREDENTIALS_ENV_KEY;
6use crate::credential_broker::CREDENTIAL_BROKER_ACTIVE_ENV_KEY;
7use crate::http_proxy;
8use crate::network_policy::NetworkPolicyDecider;
9use crate::runtime::BlockedRequestObserver;
10use crate::runtime::ConfigState;
11use crate::runtime::unix_socket_permissions_supported;
12use crate::socks5;
13use crate::state::NetworkProxyState;
14#[cfg(target_os = "windows")]
15use crate::windows_proxy_ingress::WindowsProxyIngress;
16#[cfg(target_os = "windows")]
17use crate::windows_proxy_ingress::WindowsProxyRoute;
18#[cfg(target_os = "windows")]
19use crate::windows_proxy_ingress::WindowsRouteService;
20use anyhow::Context;
21use anyhow::Result;
22use clap::Parser;
23use codex_utils_absolute_path::AbsolutePathBuf;
24use serde::Deserialize;
25use serde::Serialize;
26use std::collections::HashMap;
27use std::net::SocketAddr;
28use std::net::TcpListener as StdTcpListener;
29use std::sync::Arc;
30use std::sync::Mutex;
31use std::sync::RwLock;
32use tokio::task::JoinHandle;
33use tracing::warn;
34
35use self::execution_scope::ExecutionScope;
36
37#[derive(Debug, Clone, Parser)]
38#[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")]
39pub struct Args {}
40
41#[derive(Debug)]
42struct ReservedListeners {
43 http: Mutex<Option<StdTcpListener>>,
44 socks: Mutex<Option<StdTcpListener>>,
45}
46
47impl ReservedListeners {
48 #[cfg(not(target_os = "windows"))]
49 fn new(http: StdTcpListener, socks: Option<StdTcpListener>) -> Self {
50 Self {
51 http: Mutex::new(Some(http)),
52 socks: Mutex::new(socks),
53 }
54 }
55
56 fn take_http(&self) -> Option<StdTcpListener> {
57 let mut guard = self
58 .http
59 .lock()
60 .unwrap_or_else(std::sync::PoisonError::into_inner);
61 guard.take()
62 }
63
64 fn take_socks(&self) -> Option<StdTcpListener> {
65 let mut guard = self
66 .socks
67 .lock()
68 .unwrap_or_else(std::sync::PoisonError::into_inner);
69 guard.take()
70 }
71}
72
73pub(super) struct ReservedListenerSet {
74 http_listener: StdTcpListener,
75 socks_listener: Option<StdTcpListener>,
76}
77
78impl ReservedListenerSet {
79 fn new(http_listener: StdTcpListener, socks_listener: Option<StdTcpListener>) -> Self {
80 Self {
81 http_listener,
82 socks_listener,
83 }
84 }
85
86 pub(super) fn http_addr(&self) -> Result<SocketAddr> {
87 self.http_listener
88 .local_addr()
89 .context("failed to read reserved HTTP proxy address")
90 }
91
92 pub(super) fn socks_addr(&self, default_addr: SocketAddr) -> Result<SocketAddr> {
93 self.socks_listener
94 .as_ref()
95 .map_or(Ok(default_addr), |listener| {
96 listener
97 .local_addr()
98 .context("failed to read reserved SOCKS5 proxy address")
99 })
100 }
101
102 #[cfg(not(target_os = "windows"))]
103 fn into_reserved_listeners(self) -> Arc<ReservedListeners> {
104 Arc::new(ReservedListeners::new(
105 self.http_listener,
106 self.socks_listener,
107 ))
108 }
109
110 #[cfg(target_os = "windows")]
111 pub(super) fn into_listeners(self) -> (StdTcpListener, Option<StdTcpListener>) {
112 (self.http_listener, self.socks_listener)
113 }
114}
115
116#[derive(Clone)]
117pub struct NetworkProxyBuilder {
118 state: Option<Arc<NetworkProxyState>>,
119 http_addr: Option<SocketAddr>,
120 socks_addr: Option<SocketAddr>,
121 managed_by_codex: bool,
122 policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
123 blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,
124}
125
126impl Default for NetworkProxyBuilder {
127 fn default() -> Self {
128 Self {
129 state: None,
130 http_addr: None,
131 socks_addr: None,
132 managed_by_codex: true,
133 policy_decider: None,
134 blocked_request_observer: None,
135 }
136 }
137}
138
139impl NetworkProxyBuilder {
140 pub fn state(mut self, state: Arc<NetworkProxyState>) -> Self {
141 self.state = Some(state);
142 self
143 }
144
145 pub fn http_addr(mut self, addr: SocketAddr) -> Self {
146 self.http_addr = Some(addr);
147 self
148 }
149
150 pub fn socks_addr(mut self, addr: SocketAddr) -> Self {
151 self.socks_addr = Some(addr);
152 self
153 }
154
155 pub fn managed_by_codex(mut self, managed_by_codex: bool) -> Self {
156 self.managed_by_codex = managed_by_codex;
157 self
158 }
159
160 pub fn policy_decider<D>(mut self, decider: D) -> Self
161 where
162 D: NetworkPolicyDecider,
163 {
164 self.policy_decider = Some(Arc::new(decider));
165 self
166 }
167
168 pub fn policy_decider_arc(mut self, decider: Arc<dyn NetworkPolicyDecider>) -> Self {
169 self.policy_decider = Some(decider);
170 self
171 }
172
173 pub fn blocked_request_observer<O>(mut self, observer: O) -> Self
174 where
175 O: BlockedRequestObserver,
176 {
177 self.blocked_request_observer = Some(Arc::new(observer));
178 self
179 }
180
181 pub fn blocked_request_observer_arc(
182 mut self,
183 observer: Arc<dyn BlockedRequestObserver>,
184 ) -> Self {
185 self.blocked_request_observer = Some(observer);
186 self
187 }
188
189 pub async fn build(self) -> Result<NetworkProxy> {
190 let state = self.state.ok_or_else(|| {
191 anyhow::anyhow!(
192 "NetworkProxyBuilder requires a state; supply one via builder.state(...)"
193 )
194 })?;
195 state
196 .set_blocked_request_observer(self.blocked_request_observer.clone())
197 .await;
198 let current_cfg = state.current_cfg().await?;
199 #[cfg(target_os = "windows")]
200 let runtime_settings = NetworkProxyRuntimeSettings::from_config(¤t_cfg)?;
201 #[cfg(target_os = "windows")]
202 let mut windows_ingress = None;
203 let (requested_http_addr, requested_socks_addr, reserved_listeners) = if self
204 .managed_by_codex
205 {
206 let runtime = config::resolve_runtime(¤t_cfg)?;
207 #[cfg(target_os = "windows")]
208 {
209 let (managed_http_addr, managed_socks_addr) =
210 config::clamp_bind_addrs(runtime.http_addr, runtime.socks_addr, ¤t_cfg);
211 let ingress = WindowsProxyIngress::shared(
212 managed_http_addr,
213 managed_socks_addr,
214 current_cfg.enable_socks5,
215 )?;
216 let http_addr = ingress.http_addr();
217 let socks_addr = ingress.socks_addr();
218 windows_ingress = Some(ingress);
219 (http_addr, socks_addr, None)
220 }
221 #[cfg(not(target_os = "windows"))]
222 {
223 let reserved = reserve_loopback_ephemeral_listeners(current_cfg.enable_socks5)
224 .context("reserve managed loopback proxy listeners")?;
225 let http_addr = reserved.http_addr()?;
226 let socks_addr = reserved.socks_addr(runtime.socks_addr)?;
227 (
228 http_addr,
229 socks_addr,
230 Some(reserved.into_reserved_listeners()),
231 )
232 }
233 } else {
234 let runtime = config::resolve_runtime(¤t_cfg)?;
235 (
236 self.http_addr.unwrap_or(runtime.http_addr),
237 self.socks_addr.unwrap_or(runtime.socks_addr),
238 None,
239 )
240 };
241
242 let (http_addr, socks_addr) =
244 config::clamp_bind_addrs(requested_http_addr, requested_socks_addr, ¤t_cfg);
245
246 #[cfg(target_os = "windows")]
247 let windows_runtime = windows_ingress.map(|ingress| {
248 let http = http_proxy::http_proxy_service(
249 Arc::clone(&state),
250 self.policy_decider.clone(),
251 None,
252 );
253 let socks = current_cfg.enable_socks5.then(|| {
254 socks5::socks5_proxy_service(
255 Arc::clone(&state),
256 self.policy_decider.clone(),
257 None,
258 current_cfg.enable_socks5_udp,
259 )
260 });
261 Arc::new(WindowsSharedProxyRuntime {
262 ingress,
263 http_service: http,
264 socks_service: socks,
265 active_route: Arc::new(Mutex::new(None)),
266 })
267 });
268
269 #[cfg(not(target_os = "windows"))]
270 let runtime_settings = NetworkProxyRuntimeSettings::from_config(¤t_cfg)?;
271
272 Ok(NetworkProxy {
273 state,
274 http_addr,
275 socks_addr,
276 socks_enabled: current_cfg.enable_socks5,
277 socks5_udp_enabled: current_cfg.enable_socks5_udp,
278 runtime_settings: Arc::new(RwLock::new(runtime_settings)),
279 reserved_listeners,
280 policy_decider: self.policy_decider,
281 environment_proxies: Arc::new(Mutex::new(HashMap::new())),
282 execution_scope: None,
283 #[cfg(target_os = "windows")]
284 windows_runtime,
285 })
286 }
287}
288
289fn reserve_loopback_ephemeral_listeners(
290 reserve_socks_listener: bool,
291) -> Result<ReservedListenerSet> {
292 let http_listener =
293 reserve_loopback_ephemeral_listener().context("reserve HTTP proxy listener")?;
294 let socks_listener = if reserve_socks_listener {
295 Some(reserve_loopback_ephemeral_listener().context("reserve SOCKS5 proxy listener")?)
296 } else {
297 None
298 };
299 Ok(ReservedListenerSet::new(http_listener, socks_listener))
300}
301
302#[cfg(target_os = "windows")]
303pub(super) fn reserve_windows_managed_listeners(
304 http_addr: SocketAddr,
305 socks_addr: SocketAddr,
306 reserve_socks_listener: bool,
307) -> Result<ReservedListenerSet> {
308 let http_addr = windows_managed_loopback_addr(http_addr);
309 let socks_addr = windows_managed_loopback_addr(socks_addr);
310
311 match try_reserve_windows_managed_listeners(http_addr, socks_addr, reserve_socks_listener) {
312 Ok(listeners) => Ok(listeners),
313 Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => {
314 warn!("managed Windows proxy ports are busy; falling back to ephemeral loopback ports");
315 reserve_loopback_ephemeral_listeners(reserve_socks_listener)
316 .context("reserve fallback loopback proxy listeners")
317 }
318 Err(err) => Err(err).context("reserve Windows managed proxy listeners"),
319 }
320}
321
322#[cfg(target_os = "windows")]
323pub(super) fn reserve_windows_managed_socks_listener(
324 socks_addr: SocketAddr,
325) -> Result<StdTcpListener> {
326 let socks_addr = windows_managed_loopback_addr(socks_addr);
327 match StdTcpListener::bind(socks_addr) {
328 Ok(listener) => Ok(listener),
329 Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => {
330 warn!(
331 "managed Windows SOCKS5 proxy port is busy; falling back to an ephemeral loopback port"
332 );
333 reserve_loopback_ephemeral_listener()
334 .context("reserve fallback loopback SOCKS5 proxy listener")
335 }
336 Err(err) => Err(err).context("reserve Windows managed SOCKS5 proxy listener"),
337 }
338}
339
340#[cfg(target_os = "windows")]
341fn try_reserve_windows_managed_listeners(
342 http_addr: SocketAddr,
343 socks_addr: SocketAddr,
344 reserve_socks_listener: bool,
345) -> std::io::Result<ReservedListenerSet> {
346 let http_listener = StdTcpListener::bind(http_addr)?;
347 let socks_listener = if reserve_socks_listener {
348 Some(StdTcpListener::bind(socks_addr)?)
349 } else {
350 None
351 };
352 Ok(ReservedListenerSet::new(http_listener, socks_listener))
353}
354
355#[cfg(target_os = "windows")]
356pub(super) fn windows_managed_loopback_addr(addr: SocketAddr) -> SocketAddr {
357 if !addr.ip().is_loopback() {
358 warn!(
359 "managed Windows proxies must bind to loopback; clamping {addr} to 127.0.0.1:{}",
360 addr.port()
361 );
362 }
363 SocketAddr::from(([127, 0, 0, 1], addr.port()))
364}
365
366fn reserve_loopback_ephemeral_listener() -> Result<StdTcpListener> {
367 StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
368 .context("bind loopback ephemeral port")
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372struct NetworkProxyRuntimeSettings {
373 allow_local_binding: bool,
374 allow_unix_sockets: Arc<[String]>,
375 dangerously_allow_all_unix_sockets: bool,
376 mitm_ca_trust_bundle: Option<crate::certs::ManagedMitmCaTrustBundle>,
377}
378
379impl NetworkProxyRuntimeSettings {
380 fn from_config(config: &config::NetworkProxyConfig) -> Result<Self> {
381 let mitm_ca_trust_bundle = if config.mitm {
382 let env = crate::certs::ca_env_from_process();
383 Some(crate::certs::managed_ca_trust_bundle(&env)?)
384 } else {
385 None
386 };
387 Ok(Self {
388 allow_local_binding: config.allow_local_binding,
389 allow_unix_sockets: config.allow_unix_sockets().into(),
390 dangerously_allow_all_unix_sockets: config.dangerously_allow_all_unix_sockets,
391 mitm_ca_trust_bundle,
392 })
393 }
394}
395
396#[derive(Clone, Copy, Debug, PartialEq, Eq)]
397struct EnvironmentProxyAddrs {
398 http_addr: SocketAddr,
399 socks_addr: SocketAddr,
400}
401
402#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct ManagedNetworkSandboxContext {
406 #[serde(default)]
408 pub loopback_ports: Vec<u16>,
409 #[serde(default)]
411 pub allow_local_binding: bool,
412}
413
414#[derive(Clone, Debug, Eq, PartialEq)]
416pub struct PreparedManagedNetwork {
417 pub env: HashMap<String, String>,
419 pub sandbox_context: ManagedNetworkSandboxContext,
421}
422
423struct EnvironmentProxy {
424 addrs: EnvironmentProxyAddrs,
425 runtime: EnvironmentProxyRuntime,
426}
427
428#[derive(Clone, Copy, Debug, Eq, PartialEq)]
429enum EnvironmentProxyClient {
430 SandboxedProcess,
431 TrustedBridge,
432}
433
434enum EnvironmentProxyRuntime {
435 ListenerTasks {
436 http_task: JoinHandle<Result<()>>,
437 socks_task: Option<JoinHandle<Result<()>>>,
438 },
439 #[cfg(target_os = "windows")]
440 SharedIngress { _route: Arc<WindowsProxyRoute> },
441}
442
443impl EnvironmentProxyRuntime {
444 #[cfg(target_os = "windows")]
445 fn network_proxy_restricting_sid(&self) -> Option<String> {
446 match self {
447 Self::ListenerTasks { .. } => None,
448 Self::SharedIngress { _route: route } => Some(route.sid().to_string()),
449 }
450 }
451}
452
453#[cfg(target_os = "windows")]
454struct WindowsSharedProxyRuntime {
455 ingress: Arc<WindowsProxyIngress>,
456 http_service: WindowsRouteService,
457 socks_service: Option<WindowsRouteService>,
458 active_route: Arc<Mutex<Option<Arc<WindowsProxyRoute>>>>,
459}
460
461#[derive(Clone)]
462pub struct NetworkProxy {
463 state: Arc<NetworkProxyState>,
464 http_addr: SocketAddr,
465 socks_addr: SocketAddr,
466 socks_enabled: bool,
467 socks5_udp_enabled: bool,
468 runtime_settings: Arc<RwLock<NetworkProxyRuntimeSettings>>,
469 reserved_listeners: Option<Arc<ReservedListeners>>,
470 policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
471 environment_proxies: Arc<Mutex<HashMap<String, EnvironmentProxy>>>,
472 execution_scope: Option<Arc<ExecutionScope>>,
473 #[cfg(target_os = "windows")]
474 windows_runtime: Option<Arc<WindowsSharedProxyRuntime>>,
475}
476
477impl std::fmt::Debug for NetworkProxy {
478 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479 f.debug_struct("NetworkProxy")
482 .field("http_addr", &self.http_addr)
483 .field("socks_addr", &self.socks_addr())
484 .finish_non_exhaustive()
485 }
486}
487
488impl PartialEq for NetworkProxy {
489 fn eq(&self, other: &Self) -> bool {
490 self.http_addr == other.http_addr
491 && self.socks_addr() == other.socks_addr()
492 && self.runtime_settings() == other.runtime_settings()
493 }
494}
495
496impl Eq for NetworkProxy {}
497
498pub const PROXY_URL_ENV_KEYS: &[&str] = &[
499 "HTTP_PROXY",
500 "HTTPS_PROXY",
501 "WS_PROXY",
502 "WSS_PROXY",
503 "ALL_PROXY",
504 "FTP_PROXY",
505 "YARN_HTTP_PROXY",
506 "YARN_HTTPS_PROXY",
507 "NPM_CONFIG_HTTP_PROXY",
508 "NPM_CONFIG_HTTPS_PROXY",
509 "NPM_CONFIG_PROXY",
510 "BUNDLE_HTTP_PROXY",
511 "BUNDLE_HTTPS_PROXY",
512 "PIP_PROXY",
513 "DOCKER_HTTP_PROXY",
514 "DOCKER_HTTPS_PROXY",
515];
516
517pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"];
518pub const PROXY_ACTIVE_ENV_KEY: &str = "CODEX_NETWORK_PROXY_ACTIVE";
519pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING";
520#[cfg(target_os = "windows")]
524const WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY: &str = "CODEX_WINDOWS_SANDBOX_PROXY_PORTS";
525const ELECTRON_GET_USE_PROXY_ENV_KEY: &str = "ELECTRON_GET_USE_PROXY";
526const NODE_USE_ENV_PROXY_ENV_KEY: &str = "NODE_USE_ENV_PROXY";
527#[cfg(any(target_os = "macos", test))]
528const GIT_SSH_COMMAND_ENV_KEY: &str = "GIT_SSH_COMMAND";
529pub const PROXY_ENV_KEYS: &[&str] = &[
530 PROXY_ACTIVE_ENV_KEY,
531 CREDENTIAL_BROKER_ACTIVE_ENV_KEY,
532 BROKERED_CREDENTIALS_ENV_KEY,
533 ALLOW_LOCAL_BINDING_ENV_KEY,
534 #[cfg(target_os = "windows")]
535 WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY,
536 PROXY_ATTRIBUTION_TOKEN_ENV_KEY,
537 ELECTRON_GET_USE_PROXY_ENV_KEY,
538 NODE_USE_ENV_PROXY_ENV_KEY,
539 "HTTP_PROXY",
540 "HTTPS_PROXY",
541 "http_proxy",
542 "https_proxy",
543 "YARN_HTTP_PROXY",
544 "YARN_HTTPS_PROXY",
545 "npm_config_http_proxy",
546 "npm_config_https_proxy",
547 "npm_config_proxy",
548 "NPM_CONFIG_HTTP_PROXY",
549 "NPM_CONFIG_HTTPS_PROXY",
550 "NPM_CONFIG_PROXY",
551 "BUNDLE_HTTP_PROXY",
552 "BUNDLE_HTTPS_PROXY",
553 "PIP_PROXY",
554 "DOCKER_HTTP_PROXY",
555 "DOCKER_HTTPS_PROXY",
556 "WS_PROXY",
557 "WSS_PROXY",
558 "ws_proxy",
559 "wss_proxy",
560 "NO_PROXY",
561 "no_proxy",
562 "npm_config_noproxy",
563 "NPM_CONFIG_NOPROXY",
564 "YARN_NO_PROXY",
565 "BUNDLE_NO_PROXY",
566 "ALL_PROXY",
567 "all_proxy",
568 "FTP_PROXY",
569 "ftp_proxy",
570];
571
572pub fn is_managed_proxy_env_var(key: &str, value: &str) -> bool {
573 if PROXY_ENV_KEYS.contains(&key) {
574 return true;
575 }
576 if crate::certs::CUSTOM_CA_ENV_KEYS.contains(&key) {
577 return crate::certs::is_managed_mitm_ca_trust_bundle_path(value);
578 }
579 #[cfg(target_os = "macos")]
580 {
581 key == PROXY_GIT_SSH_COMMAND_ENV_KEY
582 && value.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER)
583 }
584 #[cfg(not(target_os = "macos"))]
585 {
586 false
587 }
588}
589
590pub fn strip_managed_proxy_env(env: &mut HashMap<String, String>) {
591 env.retain(|key, value| !is_managed_proxy_env_var(key, value));
592}
593
594#[cfg(target_os = "macos")]
595pub const PROXY_GIT_SSH_COMMAND_ENV_KEY: &str = GIT_SSH_COMMAND_ENV_KEY;
596
597const FTP_PROXY_ENV_KEYS: &[&str] = &["FTP_PROXY", "ftp_proxy"];
598const WEBSOCKET_PROXY_ENV_KEYS: &[&str] = &["WS_PROXY", "WSS_PROXY", "ws_proxy", "wss_proxy"];
599
600pub const NO_PROXY_ENV_KEYS: &[&str] = &[
601 "NO_PROXY",
602 "no_proxy",
603 "npm_config_noproxy",
604 "NPM_CONFIG_NOPROXY",
605 "YARN_NO_PROXY",
606 "BUNDLE_NO_PROXY",
607];
608
609pub const DEFAULT_NO_PROXY_VALUE: &str = concat!(
610 "localhost,127.0.0.1,::1,",
611 "10.0.0.0/8,",
612 "172.16.0.0/12,",
613 "192.168.0.0/16"
614);
615
616#[cfg(target_os = "macos")]
617pub const CODEX_PROXY_GIT_SSH_COMMAND_MARKER: &str = "CODEX_PROXY_GIT_SSH_COMMAND=1 ";
618#[cfg(target_os = "macos")]
619const CODEX_PROXY_GIT_SSH_COMMAND_PREFIX: &str =
620 "CODEX_PROXY_GIT_SSH_COMMAND=1 ssh -o ProxyCommand='nc -X 5 -x ";
621#[cfg(target_os = "macos")]
622const CODEX_PROXY_GIT_SSH_COMMAND_SUFFIX: &str = " %h %p'";
623
624pub fn proxy_url_env_value<'a>(
625 env: &'a HashMap<String, String>,
626 canonical_key: &str,
627) -> Option<&'a str> {
628 if let Some(value) = env.get(canonical_key) {
629 return Some(value.as_str());
630 }
631 let lower_key = canonical_key.to_ascii_lowercase();
632 env.get(lower_key.as_str()).map(String::as_str)
633}
634
635pub fn has_proxy_url_env_vars(env: &HashMap<String, String>) -> bool {
636 PROXY_URL_ENV_KEYS
637 .iter()
638 .any(|key| proxy_url_env_value(env, key).is_some_and(|value| !value.trim().is_empty()))
639}
640
641fn set_env_keys(env: &mut HashMap<String, String>, keys: &[&str], value: &str) {
642 for key in keys {
643 env.insert((*key).to_string(), value.to_string());
644 }
645}
646
647#[cfg(target_os = "macos")]
648fn codex_proxy_git_ssh_command(socks_addr: SocketAddr) -> String {
649 format!("{CODEX_PROXY_GIT_SSH_COMMAND_PREFIX}{socks_addr}{CODEX_PROXY_GIT_SSH_COMMAND_SUFFIX}")
650}
651
652#[cfg(target_os = "macos")]
653fn is_codex_proxy_git_ssh_command(command: &str) -> bool {
654 command.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_PREFIX)
655 && command.ends_with(CODEX_PROXY_GIT_SSH_COMMAND_SUFFIX)
656}
657
658fn apply_proxy_env_overrides(
659 env: &mut HashMap<String, String>,
660 http_addr: SocketAddr,
661 socks_addr: SocketAddr,
662 socks_enabled: bool,
663 allow_local_binding: bool,
664 mitm_ca_trust_bundle: Option<&crate::certs::ManagedMitmCaTrustBundle>,
665) {
666 let http_proxy_url = format!("http://{http_addr}");
667 let socks_proxy_url = format!("socks5h://{socks_addr}");
668 env.insert(PROXY_ACTIVE_ENV_KEY.to_string(), "1".to_string());
669 env.insert(
670 ALLOW_LOCAL_BINDING_ENV_KEY.to_string(),
671 if allow_local_binding {
672 "1".to_string()
673 } else {
674 "0".to_string()
675 },
676 );
677
678 set_env_keys(
680 env,
681 &[
682 "HTTP_PROXY",
683 "HTTPS_PROXY",
684 "http_proxy",
685 "https_proxy",
686 "YARN_HTTP_PROXY",
687 "YARN_HTTPS_PROXY",
688 "npm_config_http_proxy",
689 "npm_config_https_proxy",
690 "npm_config_proxy",
691 "NPM_CONFIG_HTTP_PROXY",
692 "NPM_CONFIG_HTTPS_PROXY",
693 "NPM_CONFIG_PROXY",
694 "BUNDLE_HTTP_PROXY",
695 "BUNDLE_HTTPS_PROXY",
696 "PIP_PROXY",
697 "DOCKER_HTTP_PROXY",
698 "DOCKER_HTTPS_PROXY",
699 ],
700 &http_proxy_url,
701 );
702 set_env_keys(env, WEBSOCKET_PROXY_ENV_KEYS, &http_proxy_url);
705
706 let no_proxy = if allow_local_binding {
709 DEFAULT_NO_PROXY_VALUE
710 } else {
711 ""
712 };
713 set_env_keys(env, NO_PROXY_ENV_KEYS, no_proxy);
714
715 env.insert(
716 ELECTRON_GET_USE_PROXY_ENV_KEY.to_string(),
717 "true".to_string(),
718 );
719 env.insert(NODE_USE_ENV_PROXY_ENV_KEY.to_string(), "1".to_string());
721
722 if socks_enabled {
726 set_env_keys(env, ALL_PROXY_ENV_KEYS, &socks_proxy_url);
727 set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url);
728 } else {
729 set_env_keys(env, ALL_PROXY_ENV_KEYS, &http_proxy_url);
730 set_env_keys(env, FTP_PROXY_ENV_KEYS, &http_proxy_url);
731 }
732
733 #[cfg(target_os = "macos")]
734 if socks_enabled {
735 match env.get(GIT_SSH_COMMAND_ENV_KEY) {
739 Some(command) if !is_codex_proxy_git_ssh_command(command) => {}
740 _ => {
741 env.insert(
742 GIT_SSH_COMMAND_ENV_KEY.to_string(),
743 codex_proxy_git_ssh_command(socks_addr),
744 );
745 }
746 }
747 }
748
749 if let Some(mitm_ca_trust_bundle) = mitm_ca_trust_bundle {
750 let managed_path = mitm_ca_trust_bundle.path.to_string_lossy().into_owned();
751 for key in crate::certs::CUSTOM_CA_ENV_KEYS {
752 if env
753 .get(key)
754 .filter(|value| !value.is_empty())
755 .is_some_and(|value| {
756 value != &managed_path
757 && mitm_ca_trust_bundle.startup_env_values.get(key) != Some(value)
758 })
759 {
760 continue;
765 }
766 env.insert(key.to_string(), managed_path.clone());
767 }
768 }
769}
770
771impl NetworkProxy {
772 pub fn builder() -> NetworkProxyBuilder {
773 NetworkProxyBuilder::default()
774 }
775
776 pub fn http_addr(&self) -> SocketAddr {
777 self.http_addr
778 }
779
780 pub fn socks_addr(&self) -> SocketAddr {
781 #[cfg(target_os = "windows")]
782 if let Some(runtime) = self.windows_runtime.as_ref() {
783 return runtime.ingress.socks_addr();
784 }
785 self.socks_addr
786 }
787
788 #[cfg(target_os = "windows")]
791 pub fn network_proxy_restricting_sid(&self, environment_id: Option<&str>) -> Option<String> {
792 match environment_id {
793 Some(environment_id) => self
794 .environment_proxies
795 .lock()
796 .unwrap_or_else(std::sync::PoisonError::into_inner)
797 .get(environment_id)
798 .and_then(|proxy| proxy.runtime.network_proxy_restricting_sid()),
799 None => self.windows_runtime.as_ref().and_then(|runtime| {
800 runtime
801 .active_route
802 .lock()
803 .unwrap_or_else(std::sync::PoisonError::into_inner)
804 .as_ref()
805 .map(|route| route.sid().to_string())
806 }),
807 }
808 }
809
810 pub async fn current_cfg(&self) -> Result<config::NetworkProxyConfig> {
811 self.state.current_cfg().await
812 }
813
814 pub async fn remote_launch_config(&self) -> Result<crate::RemoteNetworkProxyLaunchConfig> {
816 let proxy = crate::RemoteNetworkProxyConfig::from_effective_config(
817 &self.state.current_cfg().await?,
818 )?;
819 let (environment_id, execution_id) = self
820 .execution_scope
821 .as_ref()
822 .map(|scope| {
823 (
824 Some(scope.environment_id.clone()),
825 Some(scope.execution_id.clone()),
826 )
827 })
828 .unwrap_or_default();
829 Ok(crate::RemoteNetworkProxyLaunchConfig {
830 proxy,
831 audit_metadata: self.state.audit_metadata().clone(),
832 environment_id,
833 execution_id,
834 })
835 }
836
837 pub async fn add_allowed_domain(&self, host: &str) -> Result<()> {
838 self.state.add_allowed_domain(host).await
839 }
840
841 pub async fn add_denied_domain(&self, host: &str) -> Result<()> {
842 self.state.add_denied_domain(host).await
843 }
844
845 pub fn allow_local_binding(&self) -> bool {
846 self.runtime_settings().allow_local_binding
847 }
848
849 pub fn allow_unix_sockets(&self) -> Arc<[String]> {
850 self.runtime_settings().allow_unix_sockets
851 }
852
853 pub fn dangerously_allow_all_unix_sockets(&self) -> bool {
854 self.runtime_settings().dangerously_allow_all_unix_sockets
855 }
856
857 pub fn managed_mitm_ca_trust_bundle_path(&self) -> Option<AbsolutePathBuf> {
859 self.runtime_settings()
860 .mitm_ca_trust_bundle
861 .and_then(|bundle| {
862 AbsolutePathBuf::from_absolute_path(bundle.path)
863 .map_err(|err| warn!("managed MITM CA trust bundle path is invalid: {err}"))
864 .ok()
865 })
866 }
867
868 fn prepare_for_addrs(
869 &self,
870 mut env: HashMap<String, String>,
871 addrs: EnvironmentProxyAddrs,
872 #[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
873 client: EnvironmentProxyClient,
874 ) -> PreparedManagedNetwork {
875 #[cfg(target_os = "windows")]
876 let shared_socks_addr = (client == EnvironmentProxyClient::SandboxedProcess)
877 .then(|| {
878 self.windows_runtime
879 .as_ref()
880 .and_then(|runtime| runtime.ingress.active_socks_addr())
881 })
882 .flatten();
883 #[cfg(target_os = "windows")]
884 let addrs = EnvironmentProxyAddrs {
885 socks_addr: shared_socks_addr.unwrap_or(addrs.socks_addr),
886 ..addrs
887 };
888 let runtime_settings = self.runtime_settings();
889 apply_proxy_env_overrides(
892 &mut env,
893 addrs.http_addr,
894 addrs.socks_addr,
895 self.socks_enabled,
896 runtime_settings.allow_local_binding,
897 runtime_settings.mitm_ca_trust_bundle.as_ref(),
898 );
899 self.state.virtualize_child_credentials(&mut env);
900 if let Some(execution_scope) = self.execution_scope.as_ref() {
901 env.insert(
902 PROXY_ATTRIBUTION_TOKEN_ENV_KEY.to_string(),
903 execution_scope.attribution_token.clone(),
904 );
905 } else {
906 env.remove(PROXY_ATTRIBUTION_TOKEN_ENV_KEY);
907 }
908 let expose_socks_port = self.socks_enabled;
909 #[cfg(target_os = "windows")]
910 let expose_socks_port = expose_socks_port || shared_socks_addr.is_some();
911 let mut loopback_ports = [
912 Some(addrs.http_addr),
913 expose_socks_port.then_some(addrs.socks_addr),
914 ]
915 .into_iter()
916 .flatten()
917 .filter(|addr| addr.ip().is_loopback())
918 .map(|addr| addr.port())
919 .collect::<Vec<_>>();
920 loopback_ports.sort_unstable();
921 loopback_ports.dedup();
922 #[cfg(target_os = "windows")]
923 if client == EnvironmentProxyClient::SandboxedProcess && self.windows_runtime.is_some() {
924 env.insert(
925 WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY.to_string(),
926 loopback_ports
927 .iter()
928 .map(u16::to_string)
929 .collect::<Vec<_>>()
930 .join(","),
931 );
932 } else {
933 env.remove(WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY);
934 }
935 PreparedManagedNetwork {
936 env,
937 sandbox_context: ManagedNetworkSandboxContext {
938 loopback_ports,
939 allow_local_binding: runtime_settings.allow_local_binding,
940 },
941 }
942 }
943
944 fn apply_to_env_for_addrs(
945 &self,
946 env: &mut HashMap<String, String>,
947 addrs: EnvironmentProxyAddrs,
948 ) {
949 let prepared = self.prepare_for_addrs(
950 std::mem::take(env),
951 addrs,
952 EnvironmentProxyClient::SandboxedProcess,
953 );
954 *env = prepared.env;
955 }
956
957 pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
958 self.apply_to_env_for_addrs(
959 env,
960 EnvironmentProxyAddrs {
961 http_addr: self.http_addr,
962 socks_addr: self.socks_addr,
963 },
964 );
965 }
966
967 pub fn apply_to_env_for_environment(
968 &self,
969 env: &mut HashMap<String, String>,
970 environment_id: &str,
971 ) -> Result<()> {
972 let addrs =
973 self.environment_proxy_addrs(environment_id, EnvironmentProxyClient::SandboxedProcess)?;
974 self.apply_to_env_for_addrs(env, addrs);
975 Ok(())
976 }
977
978 pub fn apply_to_env_for_optional_environment(
979 &self,
980 env: &mut HashMap<String, String>,
981 environment_id: Option<&str>,
982 ) -> Result<()> {
983 match environment_id {
984 Some(environment_id) => self.apply_to_env_for_environment(env, environment_id),
985 None => {
986 self.apply_to_env(env);
987 Ok(())
988 }
989 }
990 }
991
992 pub fn prepare_for_optional_environment(
995 &self,
996 env: HashMap<String, String>,
997 environment_id: Option<&str>,
998 ) -> Result<PreparedManagedNetwork> {
999 let addrs = match environment_id {
1000 Some(environment_id) => self.environment_proxy_addrs(
1001 environment_id,
1002 EnvironmentProxyClient::SandboxedProcess,
1003 )?,
1004 None => EnvironmentProxyAddrs {
1005 http_addr: self.http_addr,
1006 socks_addr: self.socks_addr,
1007 },
1008 };
1009 Ok(self.prepare_for_addrs(env, addrs, EnvironmentProxyClient::SandboxedProcess))
1010 }
1011
1012 pub fn prepare_for_remote_environment(
1015 &self,
1016 env: HashMap<String, String>,
1017 environment_id: &str,
1018 ) -> Result<PreparedManagedNetwork> {
1019 let addrs =
1020 self.environment_proxy_addrs(environment_id, EnvironmentProxyClient::TrustedBridge)?;
1021 Ok(self.prepare_for_addrs(env, addrs, EnvironmentProxyClient::TrustedBridge))
1022 }
1023
1024 fn environment_proxy_addrs(
1025 &self,
1026 environment_id: &str,
1027 #[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
1028 client: EnvironmentProxyClient,
1029 ) -> Result<EnvironmentProxyAddrs> {
1030 if let Some(execution_scope) = self.execution_scope.as_ref() {
1031 anyhow::ensure!(
1032 execution_scope.environment_id == environment_id,
1033 "execution-scoped network proxy belongs to environment `{}`, not `{environment_id}`",
1034 execution_scope.environment_id
1035 );
1036 }
1037
1038 let mut proxies = self
1039 .environment_proxies
1040 .lock()
1041 .unwrap_or_else(std::sync::PoisonError::into_inner);
1042 if let Some(proxy) = proxies.get(environment_id) {
1043 #[cfg(target_os = "windows")]
1044 let uses_shared_ingress = client == EnvironmentProxyClient::SandboxedProcess
1045 && self.windows_runtime.is_some();
1046 #[cfg(target_os = "windows")]
1047 anyhow::ensure!(
1048 matches!(
1049 (&proxy.runtime, uses_shared_ingress),
1050 (EnvironmentProxyRuntime::SharedIngress { .. }, true)
1051 | (EnvironmentProxyRuntime::ListenerTasks { .. }, false)
1052 ),
1053 "network proxy for environment `{environment_id}` was prepared for a different client type"
1054 );
1055 return Ok(proxy.addrs);
1056 }
1057
1058 #[cfg(target_os = "windows")]
1059 if client == EnvironmentProxyClient::SandboxedProcess
1060 && let Some(windows_runtime) = self.windows_runtime.as_ref()
1061 {
1062 let active_route = windows_runtime
1063 .active_route
1064 .lock()
1065 .unwrap_or_else(std::sync::PoisonError::into_inner);
1066 anyhow::ensure!(
1067 active_route.is_some(),
1068 "shared managed Windows proxy route is not running"
1069 );
1070 let environment_id = environment_id.to_string();
1071 let http = http_proxy::http_proxy_service(
1072 Arc::clone(&self.state),
1073 self.policy_decider.clone(),
1074 Some(environment_id.clone()),
1075 );
1076 let socks = self.socks_enabled.then(|| {
1077 socks5::socks5_proxy_service(
1078 Arc::clone(&self.state),
1079 self.policy_decider.clone(),
1080 Some(environment_id.clone()),
1081 self.socks5_udp_enabled,
1082 )
1083 });
1084 let route = Arc::new(windows_runtime.ingress.register_route(http, socks));
1085 let addrs = EnvironmentProxyAddrs {
1086 http_addr: self.http_addr,
1087 socks_addr: self.socks_addr,
1088 };
1089 proxies.insert(
1090 environment_id,
1091 EnvironmentProxy {
1092 addrs,
1093 runtime: EnvironmentProxyRuntime::SharedIngress { _route: route },
1094 },
1095 );
1096 return Ok(addrs);
1097 }
1098
1099 let runtime = tokio::runtime::Handle::try_current().with_context(|| {
1100 format!("failed to create network proxy for environment `{environment_id}`")
1101 })?;
1102 let listeners =
1103 reserve_loopback_ephemeral_listeners(self.socks_enabled).with_context(|| {
1104 format!("failed to reserve network proxy for environment `{environment_id}`")
1105 })?;
1106 let http_addr = listeners.http_addr().with_context(|| {
1107 format!("failed to read HTTP proxy address for environment `{environment_id}`")
1108 })?;
1109 let socks_addr = listeners.socks_addr(self.socks_addr).with_context(|| {
1110 format!("failed to read SOCKS proxy address for environment `{environment_id}`")
1111 })?;
1112 let addrs = EnvironmentProxyAddrs {
1113 http_addr,
1114 socks_addr,
1115 };
1116 let ReservedListenerSet {
1117 http_listener,
1118 socks_listener,
1119 } = listeners;
1120
1121 let environment_id = environment_id.to_string();
1122 let http_state = self.state.clone();
1123 let http_decider = self.policy_decider.clone();
1124 let http_environment_id = Some(environment_id.clone());
1125 let http_task = runtime.spawn(async move {
1126 http_proxy::run_http_proxy_with_std_listener(
1127 http_state,
1128 http_listener,
1129 http_decider,
1130 http_environment_id,
1131 )
1132 .await
1133 });
1134
1135 let socks_task = if self.socks_enabled {
1136 let socks_state = self.state.clone();
1137 let socks_decider = self.policy_decider.clone();
1138 let socks_environment_id = Some(environment_id.clone());
1139 let socks5_udp_enabled = self.socks5_udp_enabled;
1140 socks_listener.map(|listener| {
1141 runtime.spawn(async move {
1142 socks5::run_socks5_with_std_listener(
1143 socks_state,
1144 listener,
1145 socks_decider,
1146 socks_environment_id,
1147 socks5_udp_enabled,
1148 )
1149 .await
1150 })
1151 })
1152 } else {
1153 None
1154 };
1155
1156 proxies.insert(
1157 environment_id,
1158 EnvironmentProxy {
1159 addrs,
1160 runtime: EnvironmentProxyRuntime::ListenerTasks {
1161 http_task,
1162 socks_task,
1163 },
1164 },
1165 );
1166 Ok(addrs)
1167 }
1168
1169 pub async fn replace_config_state(&self, new_state: ConfigState) -> Result<()> {
1170 let current_cfg = self.state.current_cfg().await?;
1171 anyhow::ensure!(
1172 new_state.config.enabled == current_cfg.enabled,
1173 "cannot update network.enabled on a running proxy"
1174 );
1175 anyhow::ensure!(
1176 new_state.config.proxy_url == current_cfg.proxy_url,
1177 "cannot update network.proxy_url on a running proxy"
1178 );
1179 anyhow::ensure!(
1180 new_state.config.socks_url == current_cfg.socks_url,
1181 "cannot update network.socks_url on a running proxy"
1182 );
1183 anyhow::ensure!(
1184 new_state.config.enable_socks5 == current_cfg.enable_socks5,
1185 "cannot update network.enable_socks5 on a running proxy"
1186 );
1187 anyhow::ensure!(
1188 new_state.config.enable_socks5_udp == current_cfg.enable_socks5_udp,
1189 "cannot update network.enable_socks5_udp on a running proxy"
1190 );
1191 let settings = NetworkProxyRuntimeSettings::from_config(&new_state.config)?;
1192 self.state.replace_config_state(new_state).await?;
1193 let mut guard = self
1194 .runtime_settings
1195 .write()
1196 .unwrap_or_else(std::sync::PoisonError::into_inner);
1197 *guard = settings;
1198 Ok(())
1199 }
1200
1201 fn runtime_settings(&self) -> NetworkProxyRuntimeSettings {
1202 self.runtime_settings
1203 .read()
1204 .unwrap_or_else(std::sync::PoisonError::into_inner)
1205 .clone()
1206 }
1207
1208 pub async fn run(&self) -> Result<NetworkProxyHandle> {
1209 anyhow::ensure!(
1210 self.execution_scope.is_none(),
1211 "execution-scoped network proxy is already running"
1212 );
1213 let current_cfg = self.state.current_cfg().await?;
1214 if !current_cfg.enabled {
1215 warn!("network.enabled is false; skipping proxy listeners");
1216 return Ok(NetworkProxyHandle::noop());
1217 }
1218
1219 if !unix_socket_permissions_supported() {
1220 warn!(
1221 "allowUnixSockets and dangerouslyAllowAllUnixSockets are macOS-only; requests will be rejected on this platform"
1222 );
1223 }
1224
1225 #[cfg(target_os = "windows")]
1226 if let Some(windows_runtime) = self.windows_runtime.as_ref() {
1227 let mut active_route = windows_runtime
1228 .active_route
1229 .lock()
1230 .unwrap_or_else(std::sync::PoisonError::into_inner);
1231 anyhow::ensure!(
1232 active_route.is_none(),
1233 "shared managed Windows proxy route is already running"
1234 );
1235 *active_route = Some(Arc::new(windows_runtime.ingress.register_route(
1236 windows_runtime.http_service.clone(),
1237 windows_runtime.socks_service.clone(),
1238 )));
1239 drop(active_route);
1240 return Ok(NetworkProxyHandle::windows_shared(
1241 Arc::clone(&windows_runtime.active_route),
1242 Arc::clone(&self.environment_proxies),
1243 ));
1244 }
1245
1246 let reserved_listeners = self.reserved_listeners.as_ref();
1247 let http_listener = reserved_listeners.and_then(|listeners| listeners.take_http());
1248 let socks_listener = reserved_listeners.and_then(|listeners| listeners.take_socks());
1249
1250 let http_state = self.state.clone();
1251 let http_decider = self.policy_decider.clone();
1252 let http_addr = self.http_addr;
1253 let http_task = tokio::spawn(async move {
1254 match http_listener {
1255 Some(listener) => {
1256 http_proxy::run_http_proxy_with_std_listener(
1257 http_state,
1258 listener,
1259 http_decider,
1260 None,
1261 )
1262 .await
1263 }
1264 None => {
1265 http_proxy::run_http_proxy(
1266 http_state,
1267 http_addr,
1268 http_decider,
1269 None,
1270 )
1271 .await
1272 }
1273 }
1274 });
1275
1276 let socks_task = if current_cfg.enable_socks5 {
1277 let socks_state = self.state.clone();
1278 let socks_decider = self.policy_decider.clone();
1279 let socks_addr = self.socks_addr;
1280 let enable_socks5_udp = current_cfg.enable_socks5_udp;
1281 Some(tokio::spawn(async move {
1282 match socks_listener {
1283 Some(listener) => {
1284 socks5::run_socks5_with_std_listener(
1285 socks_state,
1286 listener,
1287 socks_decider,
1288 None,
1289 enable_socks5_udp,
1290 )
1291 .await
1292 }
1293 None => {
1294 socks5::run_socks5(
1295 socks_state,
1296 socks_addr,
1297 socks_decider,
1298 None,
1299 enable_socks5_udp,
1300 )
1301 .await
1302 }
1303 }
1304 }))
1305 } else {
1306 None
1307 };
1308
1309 Ok(NetworkProxyHandle {
1310 http_task: Some(http_task),
1311 socks_task,
1312 environment_proxies: self.environment_proxies.clone(),
1313 completed: false,
1314 #[cfg(target_os = "windows")]
1315 windows_active_route: None,
1316 })
1317 }
1318}
1319
1320pub struct NetworkProxyHandle {
1321 http_task: Option<JoinHandle<Result<()>>>,
1322 socks_task: Option<JoinHandle<Result<()>>>,
1323 environment_proxies: Arc<Mutex<HashMap<String, EnvironmentProxy>>>,
1324 completed: bool,
1325 #[cfg(target_os = "windows")]
1326 windows_active_route: Option<Arc<Mutex<Option<Arc<WindowsProxyRoute>>>>>,
1327}
1328
1329impl NetworkProxyHandle {
1330 fn noop() -> Self {
1331 Self {
1332 http_task: Some(tokio::spawn(async { Ok(()) })),
1333 socks_task: None,
1334 environment_proxies: Arc::new(Mutex::new(HashMap::new())),
1335 completed: true,
1336 #[cfg(target_os = "windows")]
1337 windows_active_route: None,
1338 }
1339 }
1340
1341 #[cfg(target_os = "windows")]
1342 fn windows_shared(
1343 active_route: Arc<Mutex<Option<Arc<WindowsProxyRoute>>>>,
1344 environment_proxies: Arc<Mutex<HashMap<String, EnvironmentProxy>>>,
1345 ) -> Self {
1346 Self {
1347 http_task: Some(tokio::spawn(async {
1348 std::future::pending::<()>().await;
1349 Ok(())
1350 })),
1351 socks_task: None,
1352 environment_proxies,
1353 completed: false,
1354 windows_active_route: Some(active_route),
1355 }
1356 }
1357
1358 #[cfg(target_os = "windows")]
1359 fn deactivate_windows_route(&mut self) {
1360 if let Some(active_route) = self.windows_active_route.take() {
1361 active_route
1362 .lock()
1363 .unwrap_or_else(std::sync::PoisonError::into_inner)
1364 .take();
1365 }
1366 }
1367
1368 pub async fn wait(mut self) -> Result<()> {
1369 let http_task = self.http_task.take().context("missing http proxy task")?;
1370 let socks_task = self.socks_task.take();
1371 let http_result = http_task.await;
1372 let socks_result = match socks_task {
1373 Some(task) => Some(task.await),
1374 None => None,
1375 };
1376 #[cfg(target_os = "windows")]
1377 self.deactivate_windows_route();
1378 self.completed = true;
1379 abort_environment_proxies(self.environment_proxies.clone()).await;
1380 http_result??;
1381 if let Some(socks_result) = socks_result {
1382 socks_result??;
1383 }
1384 Ok(())
1385 }
1386
1387 pub async fn shutdown(mut self) -> Result<()> {
1388 #[cfg(target_os = "windows")]
1389 self.deactivate_windows_route();
1390 abort_tasks(self.http_task.take(), self.socks_task.take()).await;
1391 abort_environment_proxies(self.environment_proxies.clone()).await;
1392 self.completed = true;
1393 Ok(())
1394 }
1395}
1396
1397async fn abort_task(task: Option<JoinHandle<Result<()>>>) {
1398 if let Some(task) = task {
1399 task.abort();
1400 let _ = task.await;
1401 }
1402}
1403
1404async fn abort_tasks(
1405 http_task: Option<JoinHandle<Result<()>>>,
1406 socks_task: Option<JoinHandle<Result<()>>>,
1407) {
1408 abort_task(http_task).await;
1409 abort_task(socks_task).await;
1410}
1411
1412async fn abort_environment_proxies(
1413 environment_proxies: Arc<Mutex<HashMap<String, EnvironmentProxy>>>,
1414) {
1415 let proxies = {
1416 let mut guard = environment_proxies
1417 .lock()
1418 .unwrap_or_else(std::sync::PoisonError::into_inner);
1419 guard.drain().map(|(_, proxy)| proxy).collect::<Vec<_>>()
1420 };
1421 for proxy in proxies {
1422 match proxy.runtime {
1423 EnvironmentProxyRuntime::ListenerTasks {
1424 http_task,
1425 socks_task,
1426 } => {
1427 abort_task(Some(http_task)).await;
1428 abort_task(socks_task).await;
1429 }
1430 #[cfg(target_os = "windows")]
1431 EnvironmentProxyRuntime::SharedIngress { .. } => {}
1432 }
1433 }
1434}
1435
1436#[cfg(target_os = "windows")]
1437fn unregister_windows_ingress_environment_routes(
1438 environment_proxies: &Arc<Mutex<HashMap<String, EnvironmentProxy>>>,
1439) {
1440 environment_proxies
1441 .lock()
1442 .unwrap_or_else(std::sync::PoisonError::into_inner)
1443 .retain(|_, proxy| {
1444 matches!(
1445 &proxy.runtime,
1446 EnvironmentProxyRuntime::ListenerTasks { .. }
1447 )
1448 });
1449}
1450
1451impl Drop for NetworkProxyHandle {
1452 fn drop(&mut self) {
1453 if self.completed {
1454 return;
1455 }
1456 let http_task = self.http_task.take();
1457 let socks_task = self.socks_task.take();
1458 let environment_proxies = self.environment_proxies.clone();
1459 #[cfg(target_os = "windows")]
1460 {
1461 self.deactivate_windows_route();
1462 unregister_windows_ingress_environment_routes(&environment_proxies);
1463 }
1464 tokio::spawn(async move {
1465 abort_tasks(http_task, socks_task).await;
1466 abort_environment_proxies(environment_proxies).await;
1467 });
1468 }
1469}
1470
1471#[cfg(test)]
1472mod tests {
1473 use super::*;
1474 use crate::config::NetworkProxyConfig;
1475 use crate::state::network_proxy_state_for_policy;
1476 use pretty_assertions::assert_eq;
1477 use std::net::IpAddr;
1478 use std::net::Ipv4Addr;
1479 use std::path::Path;
1480
1481 #[cfg(target_os = "windows")]
1482 static WINDOWS_INGRESS_TEST_LOCK: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(1);
1483
1484 #[tokio::test]
1485 async fn managed_proxy_builder_uses_loopback_ports() {
1486 #[cfg(target_os = "windows")]
1487 let _permit = WINDOWS_INGRESS_TEST_LOCK.acquire().await.unwrap();
1488 let http_listener = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
1489 let http_addr = http_listener.local_addr().unwrap();
1490 let socks_listener = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
1491 let socks_addr = socks_listener.local_addr().unwrap();
1492 drop(http_listener);
1493 drop(socks_listener);
1494
1495 let state = Arc::new(network_proxy_state_for_policy(NetworkProxyConfig {
1496 enabled: true,
1497 proxy_url: format!("http://{http_addr}"),
1498 socks_url: format!("http://{socks_addr}"),
1499 ..NetworkProxyConfig::default()
1500 }));
1501 let proxy = match NetworkProxy::builder().state(state).build().await {
1502 Ok(proxy) => proxy,
1503 Err(err) => {
1504 if err
1505 .chain()
1506 .any(|cause| cause.to_string().contains("Operation not permitted"))
1507 {
1508 return;
1509 }
1510 panic!("failed to build managed proxy: {err:#}");
1511 }
1512 };
1513
1514 assert!(proxy.http_addr.ip().is_loopback());
1515 assert!(proxy.socks_addr.ip().is_loopback());
1516 #[cfg(target_os = "windows")]
1517 {
1518 assert_eq!(proxy.http_addr, http_addr);
1519 assert_eq!(proxy.socks_addr, socks_addr);
1520 assert_eq!(proxy.network_proxy_restricting_sid(None), None);
1521 let handle = proxy.run().await.expect("start stable ingress route");
1522 let second_state = Arc::new(network_proxy_state_for_policy(NetworkProxyConfig {
1523 enabled: true,
1524 proxy_url: format!("http://{http_addr}"),
1525 socks_url: format!("http://{socks_addr}"),
1526 ..NetworkProxyConfig::default()
1527 }));
1528 let second = NetworkProxy::builder()
1529 .state(second_state)
1530 .build()
1531 .await
1532 .expect("second proxy should share the stable ingress");
1533 let second_handle = second
1534 .run()
1535 .await
1536 .expect("start second stable ingress route");
1537 assert_eq!(second.http_addr, proxy.http_addr);
1538 assert_eq!(second.socks_addr, proxy.socks_addr);
1539 assert_ne!(
1540 second.network_proxy_restricting_sid(None),
1541 proxy.network_proxy_restricting_sid(None)
1542 );
1543 let differently_configured =
1544 Arc::new(network_proxy_state_for_policy(NetworkProxyConfig {
1545 enabled: true,
1546 proxy_url: "http://127.0.0.1:1".to_string(),
1547 socks_url: "http://127.0.0.1:2".to_string(),
1548 allow_local_binding: true,
1549 ..NetworkProxyConfig::default()
1550 }));
1551 let third = NetworkProxy::builder()
1552 .state(differently_configured)
1553 .build()
1554 .await
1555 .expect("different route config should share the stable ingress");
1556 let third_handle = third
1557 .run()
1558 .await
1559 .expect("start differently configured stable route");
1560 assert_eq!(third.http_addr, proxy.http_addr);
1561 assert_eq!(third.socks_addr, proxy.socks_addr);
1562 assert!(third.allow_local_binding());
1563 let replacement = crate::state::build_config_state(
1564 NetworkProxyConfig {
1565 enabled: true,
1566 proxy_url: format!("http://{http_addr}"),
1567 socks_url: format!("http://{socks_addr}"),
1568 allow_local_binding: true,
1569 ..NetworkProxyConfig::default()
1570 },
1571 Default::default(),
1572 )
1573 .expect("replacement config state");
1574 proxy
1575 .replace_config_state(replacement)
1576 .await
1577 .expect("live route should accept a local-binding policy change");
1578 assert!(proxy.allow_local_binding());
1579 third_handle
1580 .shutdown()
1581 .await
1582 .expect("stop differently configured stable route");
1583 second_handle
1584 .shutdown()
1585 .await
1586 .expect("stop second stable ingress route");
1587 handle.shutdown().await.expect("stop stable ingress route");
1588 assert_eq!(proxy.network_proxy_restricting_sid(None), None);
1589 }
1590 #[cfg(not(target_os = "windows"))]
1591 {
1592 assert_ne!(proxy.http_addr.port(), 0);
1593 assert_ne!(proxy.socks_addr.port(), 0);
1594 }
1595 }
1596
1597 #[tokio::test]
1598 async fn non_codex_managed_proxy_builder_uses_configured_ports() {
1599 let settings = NetworkProxyConfig {
1600 proxy_url: "http://127.0.0.1:43128".to_string(),
1601 socks_url: "http://127.0.0.1:48081".to_string(),
1602 ..NetworkProxyConfig::default()
1603 };
1604 let state = Arc::new(network_proxy_state_for_policy(settings));
1605 let proxy = NetworkProxy::builder()
1606 .state(state)
1607 .managed_by_codex(false)
1608 .build()
1609 .await
1610 .unwrap();
1611
1612 assert_eq!(
1613 proxy.http_addr,
1614 "127.0.0.1:43128".parse::<SocketAddr>().unwrap()
1615 );
1616 assert_eq!(
1617 proxy.socks_addr,
1618 "127.0.0.1:48081".parse::<SocketAddr>().unwrap()
1619 );
1620 }
1621
1622 #[tokio::test]
1623 async fn prepare_for_environment_keeps_env_and_sandbox_ports_in_sync() -> Result<()> {
1624 #[cfg(target_os = "windows")]
1625 let _permit = WINDOWS_INGRESS_TEST_LOCK.acquire().await.unwrap();
1626 let state = Arc::new(network_proxy_state_for_policy(NetworkProxyConfig {
1627 enabled: true,
1628 ..NetworkProxyConfig::default()
1629 }));
1630 let proxy = NetworkProxy::builder().state(state).build().await?;
1631 let handle = proxy.run().await?;
1632
1633 let base_env = HashMap::from([("PRESERVED".to_string(), "value".to_string())]);
1634 let local = proxy.prepare_for_optional_environment(base_env.clone(), Some("local"))?;
1635 let remote = proxy.prepare_for_remote_environment(HashMap::new(), "remote")?;
1636
1637 assert_eq!(
1638 local.env.get("PRESERVED").map(String::as_str),
1639 Some("value")
1640 );
1641 #[cfg(target_os = "windows")]
1642 {
1643 assert_eq!(
1644 local.env.get("HTTP_PROXY"),
1645 Some(&format!("http://{}", proxy.http_addr()))
1646 );
1647 assert_ne!(local.env.get("HTTP_PROXY"), remote.env.get("HTTP_PROXY"));
1648 assert!(proxy.network_proxy_restricting_sid(Some("local")).is_some());
1649 assert_eq!(proxy.network_proxy_restricting_sid(Some("remote")), None);
1650 }
1651 #[cfg(not(target_os = "windows"))]
1652 {
1653 assert_ne!(local.env.get("HTTP_PROXY"), remote.env.get("HTTP_PROXY"));
1654 assert_ne!(
1655 local.env.get("HTTP_PROXY"),
1656 Some(&format!("http://{}", proxy.http_addr()))
1657 );
1658 assert_ne!(
1659 remote.env.get("HTTP_PROXY"),
1660 Some(&format!("http://{}", proxy.http_addr()))
1661 );
1662 }
1663 for prepared in [&local, &remote] {
1664 let http_port = prepared
1665 .env
1666 .get("HTTP_PROXY")
1667 .and_then(|value| value.strip_prefix("http://"))
1668 .and_then(|value| value.parse::<SocketAddr>().ok())
1669 .map(|addr| addr.port())
1670 .expect("managed HTTP proxy address");
1671 let socks_port = prepared
1672 .env
1673 .get("ALL_PROXY")
1674 .and_then(|value| value.strip_prefix("socks5h://"))
1675 .and_then(|value| value.parse::<SocketAddr>().ok())
1676 .map(|addr| addr.port())
1677 .expect("managed SOCKS proxy address");
1678 let mut expected_ports = vec![http_port, socks_port];
1679 expected_ports.sort_unstable();
1680 expected_ports.dedup();
1681 assert_eq!(
1682 prepared.sandbox_context,
1683 ManagedNetworkSandboxContext {
1684 loopback_ports: expected_ports,
1685 allow_local_binding: false,
1686 }
1687 );
1688 }
1689 let mut legacy_env = base_env;
1690 proxy.apply_to_env_for_environment(&mut legacy_env, "local")?;
1691 assert_eq!(legacy_env, local.env);
1692
1693 handle.shutdown().await?;
1694 Ok(())
1695 }
1696
1697 #[tokio::test]
1698 async fn remote_launch_config_carries_execution_scope() -> Result<()> {
1699 #[cfg(target_os = "windows")]
1700 let _permit = WINDOWS_INGRESS_TEST_LOCK.acquire().await.unwrap();
1701 let state = Arc::new(network_proxy_state_for_policy(NetworkProxyConfig::default()));
1702 let proxy = match NetworkProxy::builder().state(state).build().await {
1703 Ok(proxy) => proxy,
1704 Err(err) => {
1705 if err
1706 .chain()
1707 .any(|cause| cause.to_string().contains("Operation not permitted"))
1708 {
1709 return Ok(());
1710 }
1711 return Err(err);
1712 }
1713 };
1714
1715 let scoped = proxy.for_execution("remote-env", "execution-1", "token-1".to_string())?;
1716 let launch = scoped.remote_launch_config().await?;
1717 let prepared = scoped.prepare_for_optional_environment(
1718 HashMap::from([(
1719 PROXY_ATTRIBUTION_TOKEN_ENV_KEY.to_string(),
1720 "foreign-token".to_string(),
1721 )]),
1722 None,
1723 )?;
1724
1725 assert_eq!(launch.environment_id.as_deref(), Some("remote-env"));
1726 assert_eq!(launch.execution_id.as_deref(), Some("execution-1"));
1727 assert_eq!(
1728 prepared
1729 .env
1730 .get(PROXY_ATTRIBUTION_TOKEN_ENV_KEY)
1731 .map(String::as_str),
1732 Some("token-1")
1733 );
1734 Ok(())
1735 }
1736
1737 #[tokio::test]
1738 async fn managed_proxy_builder_lazily_upgrades_disabled_socks() {
1739 #[cfg(target_os = "windows")]
1740 let _permit = WINDOWS_INGRESS_TEST_LOCK.acquire().await.unwrap();
1741 let http_listener = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
1742 let http_addr = http_listener.local_addr().unwrap();
1743 drop(http_listener);
1744 let occupied_socks = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
1745 let socks_addr = occupied_socks.local_addr().unwrap();
1746 let settings = NetworkProxyConfig {
1747 enabled: true,
1748 enable_socks5: false,
1749 proxy_url: format!("http://{http_addr}"),
1750 socks_url: format!("http://{socks_addr}"),
1751 ..NetworkProxyConfig::default()
1752 };
1753 let state = Arc::new(network_proxy_state_for_policy(settings));
1754 let proxy = match NetworkProxy::builder().state(state).build().await {
1755 Ok(proxy) => proxy,
1756 Err(err) => {
1757 if err
1758 .chain()
1759 .any(|cause| cause.to_string().contains("Operation not permitted"))
1760 {
1761 return;
1762 }
1763 panic!("failed to build managed proxy: {err:#}");
1764 }
1765 };
1766
1767 assert!(proxy.http_addr.ip().is_loopback());
1768 assert_ne!(proxy.http_addr.port(), 0);
1769 assert_eq!(proxy.socks_addr, socks_addr);
1770 #[cfg(target_os = "windows")]
1771 {
1772 assert_eq!(proxy.http_addr, http_addr);
1773 assert!(proxy.reserved_listeners.is_none());
1774 assert!(proxy.windows_runtime.is_some());
1775 assert_eq!(proxy.network_proxy_restricting_sid(None), None);
1776 let handle = proxy.run().await.expect("start HTTP-only stable route");
1777 assert!(proxy.network_proxy_restricting_sid(None).is_some());
1778 let prepared_before_upgrade = proxy
1779 .prepare_for_optional_environment(
1780 HashMap::from([(
1781 WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY.to_string(),
1782 "1,2".to_string(),
1783 )]),
1784 None,
1785 )
1786 .expect("prepare stable Windows proxy");
1787 assert_eq!(
1788 prepared_before_upgrade.sandbox_context.loopback_ports,
1789 vec![proxy.http_addr.port()]
1790 );
1791 assert_eq!(
1792 prepared_before_upgrade
1793 .env
1794 .get(WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY),
1795 Some(&proxy.http_addr.port().to_string())
1796 );
1797 assert_eq!(
1798 prepared_before_upgrade.env.get("ALL_PROXY"),
1799 Some(&format!("http://{}", proxy.http_addr))
1800 );
1801 let environment_id = "cached-before-socks-upgrade";
1802 let environment_before_upgrade = proxy
1803 .prepare_for_optional_environment(HashMap::new(), Some(environment_id))
1804 .expect("prepare cached HTTP-only environment");
1805 assert_eq!(
1806 environment_before_upgrade.sandbox_context.loopback_ports,
1807 vec![proxy.http_addr.port()]
1808 );
1809
1810 let requested_socks =
1811 StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
1812 let requested_socks_addr = requested_socks.local_addr().unwrap();
1813 assert_ne!(requested_socks_addr, socks_addr);
1814 let socks_state = Arc::new(network_proxy_state_for_policy(NetworkProxyConfig {
1815 enabled: true,
1816 enable_socks5: true,
1817 proxy_url: format!("http://{http_addr}"),
1818 socks_url: format!("socks5://{requested_socks_addr}"),
1819 ..NetworkProxyConfig::default()
1820 }));
1821 let socks_proxy = NetworkProxy::builder()
1822 .state(socks_state)
1823 .build()
1824 .await
1825 .expect("upgrade stable ingress to SOCKS5");
1826 let actual_socks_addr = socks_proxy.socks_addr();
1827 assert_eq!(socks_proxy.http_addr(), proxy.http_addr());
1828 assert!(actual_socks_addr.ip().is_loopback());
1829 assert_ne!(actual_socks_addr, requested_socks_addr);
1830 assert_eq!(proxy.socks_addr(), actual_socks_addr);
1831 let socks_handle = socks_proxy
1832 .run()
1833 .await
1834 .expect("start SOCKS-enabled stable route");
1835
1836 let mut expected_ports = vec![proxy.http_addr.port(), actual_socks_addr.port()];
1837 expected_ports.sort_unstable();
1838 let prepared_after_upgrade = proxy
1839 .prepare_for_optional_environment(HashMap::new(), None)
1840 .expect("re-prepare HTTP-only route after SOCKS5 upgrade");
1841 let environment_after_upgrade = proxy
1842 .prepare_for_optional_environment(HashMap::new(), Some(environment_id))
1843 .expect("re-prepare cached environment after SOCKS5 upgrade");
1844 for prepared in [&prepared_after_upgrade, &environment_after_upgrade] {
1845 assert_eq!(prepared.sandbox_context.loopback_ports, expected_ports);
1846 assert_eq!(
1847 prepared.env.get(WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY),
1848 Some(
1849 &expected_ports
1850 .iter()
1851 .map(u16::to_string)
1852 .collect::<Vec<_>>()
1853 .join(",")
1854 )
1855 );
1856 assert_eq!(
1857 prepared.env.get("ALL_PROXY"),
1858 Some(&format!("http://{}", proxy.http_addr))
1859 );
1860 }
1861 let socks_prepared = socks_proxy
1862 .prepare_for_optional_environment(HashMap::new(), None)
1863 .expect("prepare SOCKS-enabled route");
1864 assert_eq!(
1865 socks_prepared.sandbox_context.loopback_ports,
1866 expected_ports
1867 );
1868 assert_eq!(
1869 socks_prepared.env.get("ALL_PROXY"),
1870 Some(&format!("socks5h://{actual_socks_addr}"))
1871 );
1872 let remote = proxy
1873 .prepare_for_remote_environment(
1874 HashMap::from([(
1875 WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY.to_string(),
1876 "1,2".to_string(),
1877 )]),
1878 "remote",
1879 )
1880 .expect("prepare HTTP-only trusted bridge proxy");
1881 assert_eq!(remote.sandbox_context.loopback_ports.len(), 1);
1882 assert_eq!(remote.env.get(WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY), None);
1883 assert_eq!(proxy.network_proxy_restricting_sid(Some("remote")), None);
1884 socks_handle
1885 .shutdown()
1886 .await
1887 .expect("stop SOCKS-enabled stable route");
1888 handle
1889 .shutdown()
1890 .await
1891 .expect("stop HTTP-only stable route");
1892 assert_eq!(proxy.network_proxy_restricting_sid(None), None);
1893 }
1894 #[cfg(not(target_os = "windows"))]
1895 assert!(
1896 proxy
1897 .reserved_listeners
1898 .as_ref()
1899 .expect("managed builder should reserve listeners")
1900 .take_socks()
1901 .is_none()
1902 );
1903 drop(proxy);
1904 drop(occupied_socks);
1905 }
1906
1907 #[cfg(target_os = "windows")]
1908 #[test]
1909 fn windows_managed_loopback_addr_clamps_non_loopback_inputs() {
1910 assert_eq!(
1911 windows_managed_loopback_addr("0.0.0.0:3128".parse::<SocketAddr>().unwrap()),
1912 "127.0.0.1:3128".parse::<SocketAddr>().unwrap()
1913 );
1914 assert_eq!(
1915 windows_managed_loopback_addr("[::]:8081".parse::<SocketAddr>().unwrap()),
1916 "127.0.0.1:8081".parse::<SocketAddr>().unwrap()
1917 );
1918 }
1919
1920 #[cfg(target_os = "windows")]
1921 #[test]
1922 fn reserve_windows_managed_listeners_falls_back_when_http_port_is_busy() {
1923 let occupied = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
1924 let busy_port = occupied.local_addr().unwrap().port();
1925
1926 let reserved = reserve_windows_managed_listeners(
1927 SocketAddr::from(([127, 0, 0, 1], busy_port)),
1928 SocketAddr::from(([127, 0, 0, 1], 48081)),
1929 false,
1930 )
1931 .unwrap();
1932
1933 assert!(reserved.socks_listener.is_none());
1934 assert!(
1935 reserved
1936 .http_listener
1937 .local_addr()
1938 .unwrap()
1939 .ip()
1940 .is_loopback()
1941 );
1942 assert_ne!(
1943 reserved.http_listener.local_addr().unwrap().port(),
1944 busy_port
1945 );
1946 }
1947
1948 #[test]
1949 fn proxy_url_env_value_resolves_lowercase_aliases() {
1950 let mut env = HashMap::new();
1951 env.insert(
1952 "http_proxy".to_string(),
1953 "http://127.0.0.1:3128".to_string(),
1954 );
1955
1956 assert_eq!(
1957 proxy_url_env_value(&env, "HTTP_PROXY"),
1958 Some("http://127.0.0.1:3128")
1959 );
1960 }
1961
1962 #[test]
1963 fn has_proxy_url_env_vars_detects_lowercase_aliases() {
1964 let mut env = HashMap::new();
1965 env.insert(
1966 "all_proxy".to_string(),
1967 "socks5h://127.0.0.1:8081".to_string(),
1968 );
1969
1970 assert_eq!(has_proxy_url_env_vars(&env), true);
1971 }
1972
1973 #[test]
1974 fn has_proxy_url_env_vars_detects_websocket_proxy_keys() {
1975 let mut env = HashMap::new();
1976 env.insert("wss_proxy".to_string(), "http://127.0.0.1:3128".to_string());
1977
1978 assert_eq!(has_proxy_url_env_vars(&env), true);
1979 }
1980
1981 #[test]
1982 fn apply_proxy_env_overrides_sets_common_tool_vars() {
1983 let mut env = HashMap::new();
1984 apply_proxy_env_overrides(
1985 &mut env,
1986 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
1987 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
1988 true,
1989 false,
1990 None,
1991 );
1992
1993 assert_eq!(
1994 env.get("HTTP_PROXY"),
1995 Some(&"http://127.0.0.1:3128".to_string())
1996 );
1997 assert_eq!(
1998 env.get("WS_PROXY"),
1999 Some(&"http://127.0.0.1:3128".to_string())
2000 );
2001 assert_eq!(
2002 env.get("WSS_PROXY"),
2003 Some(&"http://127.0.0.1:3128".to_string())
2004 );
2005 assert_eq!(
2006 env.get("npm_config_proxy"),
2007 Some(&"http://127.0.0.1:3128".to_string())
2008 );
2009 assert_eq!(
2010 env.get("ALL_PROXY"),
2011 Some(&"socks5h://127.0.0.1:8081".to_string())
2012 );
2013 assert_eq!(
2014 env.get("FTP_PROXY"),
2015 Some(&"socks5h://127.0.0.1:8081".to_string())
2016 );
2017 assert_eq!(env.get("NO_PROXY"), Some(&String::new()));
2018 assert_eq!(env.get(PROXY_ACTIVE_ENV_KEY), Some(&"1".to_string()));
2019 assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"0".to_string()));
2020 assert_eq!(
2021 env.get(ELECTRON_GET_USE_PROXY_ENV_KEY),
2022 Some(&"true".to_string())
2023 );
2024 assert_eq!(env.get(NODE_USE_ENV_PROXY_ENV_KEY), Some(&"1".to_string()));
2025 #[cfg(target_os = "macos")]
2026 assert_eq!(
2027 env.get(GIT_SSH_COMMAND_ENV_KEY),
2028 Some(
2029 &"CODEX_PROXY_GIT_SSH_COMMAND=1 ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'"
2030 .to_string()
2031 )
2032 );
2033 #[cfg(not(target_os = "macos"))]
2034 assert_eq!(env.get(GIT_SSH_COMMAND_ENV_KEY), None);
2035 }
2036
2037 #[test]
2038 fn apply_proxy_env_overrides_keeps_local_targets_direct_when_local_binding_enabled() {
2039 let mut env = HashMap::new();
2040 apply_proxy_env_overrides(
2041 &mut env,
2042 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
2043 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
2044 true,
2045 true,
2046 None,
2047 );
2048
2049 assert_eq!(
2050 env.get("NO_PROXY"),
2051 Some(&DEFAULT_NO_PROXY_VALUE.to_string())
2052 );
2053 }
2054
2055 #[test]
2056 fn apply_proxy_env_overrides_sets_only_expected_env_keys() {
2057 let mut env = HashMap::new();
2058 apply_proxy_env_overrides(
2059 &mut env,
2060 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
2061 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
2062 true,
2063 false,
2064 None,
2065 );
2066
2067 for key in env.keys() {
2068 let is_managed_git_ssh_key =
2069 cfg!(target_os = "macos") && key == GIT_SSH_COMMAND_ENV_KEY;
2070 assert!(
2071 PROXY_ENV_KEYS.contains(&key.as_str()) || is_managed_git_ssh_key,
2072 "proxy env writer set unexpected key: {key}"
2073 );
2074 }
2075 }
2076
2077 #[test]
2078 fn apply_proxy_env_overrides_sets_mitm_ca_trust_bundle_vars() {
2079 let mut env = HashMap::new();
2080 let mitm_ca_trust_bundle_path = Path::new("/tmp/codex-proxy/ca-bundle.pem");
2081 let mitm_ca_trust_bundle = crate::certs::ManagedMitmCaTrustBundle {
2082 path: mitm_ca_trust_bundle_path.to_path_buf(),
2083 startup_env_values: HashMap::new(),
2084 };
2085 apply_proxy_env_overrides(
2086 &mut env,
2087 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
2088 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
2089 true,
2090 false,
2091 Some(&mitm_ca_trust_bundle),
2092 );
2093
2094 for key in crate::certs::CUSTOM_CA_ENV_KEYS {
2095 assert_eq!(
2096 env.get(key),
2097 Some(&mitm_ca_trust_bundle_path.display().to_string())
2098 );
2099 }
2100 }
2101
2102 #[test]
2103 fn apply_proxy_env_overrides_preserves_command_scoped_mitm_ca_override() {
2104 let command_ca_bundle_path = "/tmp/command-ca.pem".to_string();
2105 let mut env = HashMap::from([(
2106 "REQUESTS_CA_BUNDLE".to_string(),
2107 command_ca_bundle_path.clone(),
2108 )]);
2109 let mitm_ca_trust_bundle_path = Path::new("/tmp/codex-proxy/ca-bundle.pem");
2110 let mitm_ca_trust_bundle = crate::certs::ManagedMitmCaTrustBundle {
2111 path: mitm_ca_trust_bundle_path.to_path_buf(),
2112 startup_env_values: HashMap::new(),
2113 };
2114
2115 apply_proxy_env_overrides(
2116 &mut env,
2117 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
2118 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
2119 true,
2120 false,
2121 Some(&mitm_ca_trust_bundle),
2122 );
2123
2124 assert_eq!(env.get("REQUESTS_CA_BUNDLE"), Some(&command_ca_bundle_path));
2125 assert_eq!(
2126 env.get("SSL_CERT_FILE"),
2127 Some(&mitm_ca_trust_bundle_path.display().to_string())
2128 );
2129 }
2130
2131 #[test]
2132 fn apply_proxy_env_overrides_uses_http_for_all_proxy_without_socks() {
2133 let mut env = HashMap::new();
2134 apply_proxy_env_overrides(
2135 &mut env,
2136 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
2137 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
2138 false,
2139 true,
2140 None,
2141 );
2142
2143 assert_eq!(
2144 env.get("ALL_PROXY"),
2145 Some(&"http://127.0.0.1:3128".to_string())
2146 );
2147 assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"1".to_string()));
2148 }
2149
2150 #[test]
2151 fn apply_proxy_env_overrides_uses_plain_http_proxy_url() {
2152 let mut env = HashMap::new();
2153 apply_proxy_env_overrides(
2154 &mut env,
2155 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
2156 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
2157 true,
2158 false,
2159 None,
2160 );
2161
2162 assert_eq!(
2163 env.get("HTTP_PROXY"),
2164 Some(&"http://127.0.0.1:3128".to_string())
2165 );
2166 assert_eq!(
2167 env.get("HTTPS_PROXY"),
2168 Some(&"http://127.0.0.1:3128".to_string())
2169 );
2170 assert_eq!(
2171 env.get("WS_PROXY"),
2172 Some(&"http://127.0.0.1:3128".to_string())
2173 );
2174 assert_eq!(
2175 env.get("WSS_PROXY"),
2176 Some(&"http://127.0.0.1:3128".to_string())
2177 );
2178 assert_eq!(
2179 env.get("ALL_PROXY"),
2180 Some(&"socks5h://127.0.0.1:8081".to_string())
2181 );
2182 #[cfg(target_os = "macos")]
2183 assert_eq!(
2184 env.get(GIT_SSH_COMMAND_ENV_KEY),
2185 Some(
2186 &"CODEX_PROXY_GIT_SSH_COMMAND=1 ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'"
2187 .to_string()
2188 )
2189 );
2190 #[cfg(not(target_os = "macos"))]
2191 assert_eq!(env.get(GIT_SSH_COMMAND_ENV_KEY), None);
2192 }
2193
2194 #[cfg(target_os = "macos")]
2195 #[test]
2196 fn apply_proxy_env_overrides_preserves_existing_git_ssh_command() {
2197 let mut env = HashMap::new();
2198 env.insert(
2199 GIT_SSH_COMMAND_ENV_KEY.to_string(),
2200 "ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string(),
2201 );
2202 apply_proxy_env_overrides(
2203 &mut env,
2204 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
2205 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
2206 true,
2207 false,
2208 None,
2209 );
2210
2211 assert_eq!(
2212 env.get(GIT_SSH_COMMAND_ENV_KEY),
2213 Some(&"ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string())
2214 );
2215 }
2216
2217 #[cfg(target_os = "macos")]
2218 #[test]
2219 fn apply_proxy_env_overrides_preserves_unmarked_git_ssh_command_with_proxy_shape() {
2220 let mut env = HashMap::new();
2221 env.insert(
2222 GIT_SSH_COMMAND_ENV_KEY.to_string(),
2223 "ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'".to_string(),
2224 );
2225 apply_proxy_env_overrides(
2226 &mut env,
2227 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
2228 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 48081),
2229 true,
2230 false,
2231 None,
2232 );
2233
2234 assert_eq!(
2235 env.get(GIT_SSH_COMMAND_ENV_KEY),
2236 Some(&"ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'".to_string())
2237 );
2238 }
2239
2240 #[cfg(target_os = "macos")]
2241 #[test]
2242 fn apply_proxy_env_overrides_refreshes_previous_codex_proxy_git_ssh_command() {
2243 let mut env = HashMap::new();
2244 env.insert(
2245 GIT_SSH_COMMAND_ENV_KEY.to_string(),
2246 codex_proxy_git_ssh_command(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081)),
2247 );
2248
2249 apply_proxy_env_overrides(
2250 &mut env,
2251 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 43128),
2252 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 48081),
2253 true,
2254 false,
2255 None,
2256 );
2257
2258 assert_eq!(
2259 env.get(GIT_SSH_COMMAND_ENV_KEY),
2260 Some(&codex_proxy_git_ssh_command(SocketAddr::new(
2261 IpAddr::V4(Ipv4Addr::LOCALHOST),
2262 48081,
2263 )))
2264 );
2265 }
2266}