1use crate::config;
2use crate::http_proxy;
3use crate::network_policy::NetworkPolicyDecider;
4use crate::runtime::BlockedRequestObserver;
5use crate::runtime::ConfigState;
6use crate::runtime::unix_socket_permissions_supported;
7use crate::socks5;
8use crate::state::NetworkProxyState;
9use anyhow::Context;
10use anyhow::Result;
11use clap::Parser;
12use std::collections::HashMap;
13use std::net::SocketAddr;
14use std::net::TcpListener as StdTcpListener;
15use std::sync::Arc;
16use std::sync::Mutex;
17use std::sync::RwLock;
18use tokio::task::JoinHandle;
19use tracing::warn;
20
21#[derive(Debug, Clone, Parser)]
22#[command(name = "zerobox-network-proxy", about = "Codex network sandbox proxy")]
23pub struct Args {}
24
25#[derive(Debug)]
26struct ReservedListeners {
27 http: Mutex<Option<StdTcpListener>>,
28 socks: Mutex<Option<StdTcpListener>>,
29}
30
31impl ReservedListeners {
32 fn new(http: StdTcpListener, socks: Option<StdTcpListener>) -> Self {
33 Self {
34 http: Mutex::new(Some(http)),
35 socks: Mutex::new(socks),
36 }
37 }
38
39 fn take_http(&self) -> Option<StdTcpListener> {
40 let mut guard = self
41 .http
42 .lock()
43 .unwrap_or_else(std::sync::PoisonError::into_inner);
44 guard.take()
45 }
46
47 fn take_socks(&self) -> Option<StdTcpListener> {
48 let mut guard = self
49 .socks
50 .lock()
51 .unwrap_or_else(std::sync::PoisonError::into_inner);
52 guard.take()
53 }
54}
55
56struct ReservedListenerSet {
57 http_listener: StdTcpListener,
58 socks_listener: Option<StdTcpListener>,
59}
60
61impl ReservedListenerSet {
62 fn new(http_listener: StdTcpListener, socks_listener: Option<StdTcpListener>) -> Self {
63 Self {
64 http_listener,
65 socks_listener,
66 }
67 }
68
69 fn http_addr(&self) -> Result<SocketAddr> {
70 self.http_listener
71 .local_addr()
72 .context("failed to read reserved HTTP proxy address")
73 }
74
75 fn socks_addr(&self, default_addr: SocketAddr) -> Result<SocketAddr> {
76 self.socks_listener
77 .as_ref()
78 .map_or(Ok(default_addr), |listener| {
79 listener
80 .local_addr()
81 .context("failed to read reserved SOCKS5 proxy address")
82 })
83 }
84
85 fn into_reserved_listeners(self) -> Arc<ReservedListeners> {
86 Arc::new(ReservedListeners::new(
87 self.http_listener,
88 self.socks_listener,
89 ))
90 }
91}
92
93#[derive(Clone)]
94pub struct NetworkProxyBuilder {
95 state: Option<Arc<NetworkProxyState>>,
96 http_addr: Option<SocketAddr>,
97 socks_addr: Option<SocketAddr>,
98 managed_by_codex: bool,
99 policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
100 blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,
101}
102
103impl Default for NetworkProxyBuilder {
104 fn default() -> Self {
105 Self {
106 state: None,
107 http_addr: None,
108 socks_addr: None,
109 managed_by_codex: true,
110 policy_decider: None,
111 blocked_request_observer: None,
112 }
113 }
114}
115
116impl NetworkProxyBuilder {
117 pub fn state(mut self, state: Arc<NetworkProxyState>) -> Self {
118 self.state = Some(state);
119 self
120 }
121
122 pub fn http_addr(mut self, addr: SocketAddr) -> Self {
123 self.http_addr = Some(addr);
124 self
125 }
126
127 pub fn socks_addr(mut self, addr: SocketAddr) -> Self {
128 self.socks_addr = Some(addr);
129 self
130 }
131
132 pub fn managed_by_codex(mut self, managed_by_codex: bool) -> Self {
133 self.managed_by_codex = managed_by_codex;
134 self
135 }
136
137 pub fn policy_decider<D>(mut self, decider: D) -> Self
138 where
139 D: NetworkPolicyDecider,
140 {
141 self.policy_decider = Some(Arc::new(decider));
142 self
143 }
144
145 pub fn policy_decider_arc(mut self, decider: Arc<dyn NetworkPolicyDecider>) -> Self {
146 self.policy_decider = Some(decider);
147 self
148 }
149
150 pub fn blocked_request_observer<O>(mut self, observer: O) -> Self
151 where
152 O: BlockedRequestObserver,
153 {
154 self.blocked_request_observer = Some(Arc::new(observer));
155 self
156 }
157
158 pub fn blocked_request_observer_arc(
159 mut self,
160 observer: Arc<dyn BlockedRequestObserver>,
161 ) -> Self {
162 self.blocked_request_observer = Some(observer);
163 self
164 }
165
166 pub async fn build(self) -> Result<NetworkProxy> {
167 let state = self.state.ok_or_else(|| {
168 anyhow::anyhow!(
169 "NetworkProxyBuilder requires a state; supply one via builder.state(...)"
170 )
171 })?;
172 state
173 .set_blocked_request_observer(self.blocked_request_observer.clone())
174 .await;
175 let current_cfg = state.current_cfg().await?;
176 let (requested_http_addr, requested_socks_addr, reserved_listeners) = if self
177 .managed_by_codex
178 {
179 let runtime = config::resolve_runtime(¤t_cfg)?;
180 #[cfg(target_os = "windows")]
181 let (managed_http_addr, managed_socks_addr) = config::clamp_bind_addrs(
182 runtime.http_addr,
183 runtime.socks_addr,
184 ¤t_cfg.network,
185 );
186 #[cfg(target_os = "windows")]
187 let reserved = reserve_windows_managed_listeners(
188 managed_http_addr,
189 managed_socks_addr,
190 current_cfg.network.enable_socks5,
191 )
192 .context("reserve managed loopback proxy listeners")?;
193 #[cfg(not(target_os = "windows"))]
194 let reserved = reserve_loopback_ephemeral_listeners(current_cfg.network.enable_socks5)
195 .context("reserve managed loopback proxy listeners")?;
196 let http_addr = reserved.http_addr()?;
197 let socks_addr = reserved.socks_addr(runtime.socks_addr)?;
198 (
199 http_addr,
200 socks_addr,
201 Some(reserved.into_reserved_listeners()),
202 )
203 } else {
204 let runtime = config::resolve_runtime(¤t_cfg)?;
205 (
206 self.http_addr.unwrap_or(runtime.http_addr),
207 self.socks_addr.unwrap_or(runtime.socks_addr),
208 None,
209 )
210 };
211
212 let (http_addr, socks_addr) = config::clamp_bind_addrs(
214 requested_http_addr,
215 requested_socks_addr,
216 ¤t_cfg.network,
217 );
218
219 Ok(NetworkProxy {
220 state,
221 http_addr,
222 socks_addr,
223 socks_enabled: current_cfg.network.enable_socks5,
224 runtime_settings: Arc::new(RwLock::new(NetworkProxyRuntimeSettings::from_config(
225 ¤t_cfg,
226 ))),
227 reserved_listeners,
228 policy_decider: self.policy_decider,
229 })
230 }
231}
232
233fn reserve_loopback_ephemeral_listeners(
234 reserve_socks_listener: bool,
235) -> Result<ReservedListenerSet> {
236 let http_listener =
237 reserve_loopback_ephemeral_listener().context("reserve HTTP proxy listener")?;
238 let socks_listener = if reserve_socks_listener {
239 Some(reserve_loopback_ephemeral_listener().context("reserve SOCKS5 proxy listener")?)
240 } else {
241 None
242 };
243 Ok(ReservedListenerSet::new(http_listener, socks_listener))
244}
245
246#[cfg(target_os = "windows")]
247fn reserve_windows_managed_listeners(
248 http_addr: SocketAddr,
249 socks_addr: SocketAddr,
250 reserve_socks_listener: bool,
251) -> Result<ReservedListenerSet> {
252 let http_addr = windows_managed_loopback_addr(http_addr);
253 let socks_addr = windows_managed_loopback_addr(socks_addr);
254
255 match try_reserve_windows_managed_listeners(http_addr, socks_addr, reserve_socks_listener) {
256 Ok(listeners) => Ok(listeners),
257 Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => {
258 warn!("managed Windows proxy ports are busy; falling back to ephemeral loopback ports");
259 reserve_loopback_ephemeral_listeners(reserve_socks_listener)
260 .context("reserve fallback loopback proxy listeners")
261 }
262 Err(err) => Err(err).context("reserve Windows managed proxy listeners"),
263 }
264}
265
266#[cfg(target_os = "windows")]
267fn try_reserve_windows_managed_listeners(
268 http_addr: SocketAddr,
269 socks_addr: SocketAddr,
270 reserve_socks_listener: bool,
271) -> std::io::Result<ReservedListenerSet> {
272 let http_listener = StdTcpListener::bind(http_addr)?;
273 let socks_listener = if reserve_socks_listener {
274 Some(StdTcpListener::bind(socks_addr)?)
275 } else {
276 None
277 };
278 Ok(ReservedListenerSet::new(http_listener, socks_listener))
279}
280
281#[cfg(target_os = "windows")]
282fn windows_managed_loopback_addr(addr: SocketAddr) -> SocketAddr {
283 if !addr.ip().is_loopback() {
284 warn!(
285 "managed Windows proxies must bind to loopback; clamping {addr} to 127.0.0.1:{}",
286 addr.port()
287 );
288 }
289 SocketAddr::from(([127, 0, 0, 1], addr.port()))
290}
291
292fn reserve_loopback_ephemeral_listener() -> Result<StdTcpListener> {
293 StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
294 .context("bind loopback ephemeral port")
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
298struct NetworkProxyRuntimeSettings {
299 allow_local_binding: bool,
300 allow_unix_sockets: Arc<[String]>,
301 dangerously_allow_all_unix_sockets: bool,
302}
303
304impl NetworkProxyRuntimeSettings {
305 fn from_config(config: &config::NetworkProxyConfig) -> Self {
306 Self {
307 allow_local_binding: config.network.allow_local_binding,
308 allow_unix_sockets: config.network.allow_unix_sockets().into(),
309 dangerously_allow_all_unix_sockets: config.network.dangerously_allow_all_unix_sockets,
310 }
311 }
312}
313
314#[derive(Clone)]
315pub struct NetworkProxy {
316 state: Arc<NetworkProxyState>,
317 http_addr: SocketAddr,
318 socks_addr: SocketAddr,
319 socks_enabled: bool,
320 runtime_settings: Arc<RwLock<NetworkProxyRuntimeSettings>>,
321 reserved_listeners: Option<Arc<ReservedListeners>>,
322 policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
323}
324
325impl std::fmt::Debug for NetworkProxy {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 f.debug_struct("NetworkProxy")
330 .field("http_addr", &self.http_addr)
331 .field("socks_addr", &self.socks_addr)
332 .finish_non_exhaustive()
333 }
334}
335
336impl PartialEq for NetworkProxy {
337 fn eq(&self, other: &Self) -> bool {
338 self.http_addr == other.http_addr
339 && self.socks_addr == other.socks_addr
340 && self.runtime_settings() == other.runtime_settings()
341 }
342}
343
344impl Eq for NetworkProxy {}
345
346pub const PROXY_URL_ENV_KEYS: &[&str] = &[
347 "HTTP_PROXY",
348 "HTTPS_PROXY",
349 "WS_PROXY",
350 "WSS_PROXY",
351 "ALL_PROXY",
352 "FTP_PROXY",
353 "YARN_HTTP_PROXY",
354 "YARN_HTTPS_PROXY",
355 "NPM_CONFIG_HTTP_PROXY",
356 "NPM_CONFIG_HTTPS_PROXY",
357 "NPM_CONFIG_PROXY",
358 "BUNDLE_HTTP_PROXY",
359 "BUNDLE_HTTPS_PROXY",
360 "PIP_PROXY",
361 "DOCKER_HTTP_PROXY",
362 "DOCKER_HTTPS_PROXY",
363];
364
365pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"];
366pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING";
367
368const FTP_PROXY_ENV_KEYS: &[&str] = &["FTP_PROXY", "ftp_proxy"];
369const WEBSOCKET_PROXY_ENV_KEYS: &[&str] = &["WS_PROXY", "WSS_PROXY", "ws_proxy", "wss_proxy"];
370
371pub const NO_PROXY_ENV_KEYS: &[&str] = &[
372 "NO_PROXY",
373 "no_proxy",
374 "npm_config_noproxy",
375 "NPM_CONFIG_NOPROXY",
376 "YARN_NO_PROXY",
377 "BUNDLE_NO_PROXY",
378];
379
380pub const DEFAULT_NO_PROXY_VALUE: &str = concat!(
381 "localhost,127.0.0.1,::1,",
382 "169.254.0.0/16,",
383 "10.0.0.0/8,",
384 "172.16.0.0/12,",
385 "192.168.0.0/16"
386);
387
388pub fn proxy_url_env_value<'a>(
389 env: &'a HashMap<String, String>,
390 canonical_key: &str,
391) -> Option<&'a str> {
392 if let Some(value) = env.get(canonical_key) {
393 return Some(value.as_str());
394 }
395 let lower_key = canonical_key.to_ascii_lowercase();
396 env.get(lower_key.as_str()).map(String::as_str)
397}
398
399pub fn has_proxy_url_env_vars(env: &HashMap<String, String>) -> bool {
400 PROXY_URL_ENV_KEYS
401 .iter()
402 .any(|key| proxy_url_env_value(env, key).is_some_and(|value| !value.trim().is_empty()))
403}
404
405fn set_env_keys(env: &mut HashMap<String, String>, keys: &[&str], value: &str) {
406 for key in keys {
407 env.insert((*key).to_string(), value.to_string());
408 }
409}
410
411fn apply_proxy_env_overrides(
412 env: &mut HashMap<String, String>,
413 http_addr: SocketAddr,
414 socks_addr: SocketAddr,
415 socks_enabled: bool,
416 allow_local_binding: bool,
417) {
418 let http_proxy_url = format!("http://{http_addr}");
419 let socks_proxy_url = format!("socks5h://{socks_addr}");
420 env.insert(
421 ALLOW_LOCAL_BINDING_ENV_KEY.to_string(),
422 if allow_local_binding {
423 "1".to_string()
424 } else {
425 "0".to_string()
426 },
427 );
428
429 set_env_keys(
431 env,
432 &[
433 "HTTP_PROXY",
434 "HTTPS_PROXY",
435 "http_proxy",
436 "https_proxy",
437 "YARN_HTTP_PROXY",
438 "YARN_HTTPS_PROXY",
439 "npm_config_http_proxy",
440 "npm_config_https_proxy",
441 "npm_config_proxy",
442 "NPM_CONFIG_HTTP_PROXY",
443 "NPM_CONFIG_HTTPS_PROXY",
444 "NPM_CONFIG_PROXY",
445 "BUNDLE_HTTP_PROXY",
446 "BUNDLE_HTTPS_PROXY",
447 "PIP_PROXY",
448 "DOCKER_HTTP_PROXY",
449 "DOCKER_HTTPS_PROXY",
450 ],
451 &http_proxy_url,
452 );
453 set_env_keys(env, WEBSOCKET_PROXY_ENV_KEYS, &http_proxy_url);
456
457 set_env_keys(env, NO_PROXY_ENV_KEYS, DEFAULT_NO_PROXY_VALUE);
461
462 env.insert("ELECTRON_GET_USE_PROXY".to_string(), "true".to_string());
463
464 env.insert("NODE_USE_ENV_PROXY".to_string(), "1".to_string());
466
467 if socks_enabled {
471 set_env_keys(env, ALL_PROXY_ENV_KEYS, &socks_proxy_url);
472 set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url);
473 } else {
474 set_env_keys(env, ALL_PROXY_ENV_KEYS, &http_proxy_url);
475 set_env_keys(env, FTP_PROXY_ENV_KEYS, &http_proxy_url);
476 }
477
478 #[cfg(target_os = "macos")]
479 if socks_enabled {
480 env.entry("GIT_SSH_COMMAND".to_string())
483 .or_insert_with(|| format!("ssh -o ProxyCommand='nc -X 5 -x {socks_addr} %h %p'"));
484 }
485}
486
487impl NetworkProxy {
488 pub fn builder() -> NetworkProxyBuilder {
489 NetworkProxyBuilder::default()
490 }
491
492 pub fn http_addr(&self) -> SocketAddr {
493 self.http_addr
494 }
495
496 pub fn socks_addr(&self) -> SocketAddr {
497 self.socks_addr
498 }
499
500 pub async fn current_cfg(&self) -> Result<config::NetworkProxyConfig> {
501 self.state.current_cfg().await
502 }
503
504 pub async fn add_allowed_domain(&self, host: &str) -> Result<()> {
505 self.state.add_allowed_domain(host).await
506 }
507
508 pub async fn add_denied_domain(&self, host: &str) -> Result<()> {
509 self.state.add_denied_domain(host).await
510 }
511
512 pub fn allow_local_binding(&self) -> bool {
513 self.runtime_settings().allow_local_binding
514 }
515
516 pub fn allow_unix_sockets(&self) -> Arc<[String]> {
517 self.runtime_settings().allow_unix_sockets
518 }
519
520 pub fn dangerously_allow_all_unix_sockets(&self) -> bool {
521 self.runtime_settings().dangerously_allow_all_unix_sockets
522 }
523
524 pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
525 let allow_local_binding = self.allow_local_binding();
526 apply_proxy_env_overrides(
529 env,
530 self.http_addr,
531 self.socks_addr,
532 self.socks_enabled,
533 allow_local_binding,
534 );
535 }
536
537 pub async fn replace_config_state(&self, new_state: ConfigState) -> Result<()> {
538 let current_cfg = self.state.current_cfg().await?;
539 anyhow::ensure!(
540 new_state.config.network.enabled == current_cfg.network.enabled,
541 "cannot update network.enabled on a running proxy"
542 );
543 anyhow::ensure!(
544 new_state.config.network.proxy_url == current_cfg.network.proxy_url,
545 "cannot update network.proxy_url on a running proxy"
546 );
547 anyhow::ensure!(
548 new_state.config.network.socks_url == current_cfg.network.socks_url,
549 "cannot update network.socks_url on a running proxy"
550 );
551 anyhow::ensure!(
552 new_state.config.network.enable_socks5 == current_cfg.network.enable_socks5,
553 "cannot update network.enable_socks5 on a running proxy"
554 );
555 anyhow::ensure!(
556 new_state.config.network.enable_socks5_udp == current_cfg.network.enable_socks5_udp,
557 "cannot update network.enable_socks5_udp on a running proxy"
558 );
559
560 let settings = NetworkProxyRuntimeSettings::from_config(&new_state.config);
561 self.state.replace_config_state(new_state).await?;
562 let mut guard = self
563 .runtime_settings
564 .write()
565 .unwrap_or_else(std::sync::PoisonError::into_inner);
566 *guard = settings;
567 Ok(())
568 }
569
570 fn runtime_settings(&self) -> NetworkProxyRuntimeSettings {
571 self.runtime_settings
572 .read()
573 .unwrap_or_else(std::sync::PoisonError::into_inner)
574 .clone()
575 }
576
577 pub async fn run(&self) -> Result<NetworkProxyHandle> {
578 let current_cfg = self.state.current_cfg().await?;
579 if !current_cfg.network.enabled {
580 warn!("network.enabled is false; skipping proxy listeners");
581 return Ok(NetworkProxyHandle::noop());
582 }
583
584 if !unix_socket_permissions_supported() {
585 warn!(
586 "allowUnixSockets and dangerouslyAllowAllUnixSockets are macOS-only; requests will be rejected on this platform"
587 );
588 }
589
590 let reserved_listeners = self.reserved_listeners.as_ref();
591 let http_listener = reserved_listeners.and_then(|listeners| listeners.take_http());
592 let socks_listener = reserved_listeners.and_then(|listeners| listeners.take_socks());
593
594 let http_state = self.state.clone();
595 let http_decider = self.policy_decider.clone();
596 let http_addr = self.http_addr;
597 let http_task = tokio::spawn(async move {
598 match http_listener {
599 Some(listener) => {
600 http_proxy::run_http_proxy_with_std_listener(http_state, listener, http_decider)
601 .await
602 }
603 None => http_proxy::run_http_proxy(http_state, http_addr, http_decider).await,
604 }
605 });
606
607 let socks_task = if current_cfg.network.enable_socks5 {
608 let socks_state = self.state.clone();
609 let socks_decider = self.policy_decider.clone();
610 let socks_addr = self.socks_addr;
611 let enable_socks5_udp = current_cfg.network.enable_socks5_udp;
612 Some(tokio::spawn(async move {
613 match socks_listener {
614 Some(listener) => {
615 socks5::run_socks5_with_std_listener(
616 socks_state,
617 listener,
618 socks_decider,
619 enable_socks5_udp,
620 )
621 .await
622 }
623 None => {
624 socks5::run_socks5(
625 socks_state,
626 socks_addr,
627 socks_decider,
628 enable_socks5_udp,
629 )
630 .await
631 }
632 }
633 }))
634 } else {
635 None
636 };
637
638 Ok(NetworkProxyHandle {
639 http_task: Some(http_task),
640 socks_task,
641 completed: false,
642 })
643 }
644}
645
646pub struct NetworkProxyHandle {
647 http_task: Option<JoinHandle<Result<()>>>,
648 socks_task: Option<JoinHandle<Result<()>>>,
649 completed: bool,
650}
651
652impl NetworkProxyHandle {
653 fn noop() -> Self {
654 Self {
655 http_task: Some(tokio::spawn(async { Ok(()) })),
656 socks_task: None,
657 completed: true,
658 }
659 }
660
661 pub async fn wait(mut self) -> Result<()> {
662 let http_task = self.http_task.take().context("missing http proxy task")?;
663 let socks_task = self.socks_task.take();
664 let http_result = http_task.await;
665 let socks_result = match socks_task {
666 Some(task) => Some(task.await),
667 None => None,
668 };
669 self.completed = true;
670 http_result??;
671 if let Some(socks_result) = socks_result {
672 socks_result??;
673 }
674 Ok(())
675 }
676
677 pub async fn shutdown(mut self) -> Result<()> {
678 abort_tasks(self.http_task.take(), self.socks_task.take()).await;
679 self.completed = true;
680 Ok(())
681 }
682}
683
684async fn abort_task(task: Option<JoinHandle<Result<()>>>) {
685 if let Some(task) = task {
686 task.abort();
687 let _ = task.await;
688 }
689}
690
691async fn abort_tasks(
692 http_task: Option<JoinHandle<Result<()>>>,
693 socks_task: Option<JoinHandle<Result<()>>>,
694) {
695 abort_task(http_task).await;
696 abort_task(socks_task).await;
697}
698
699impl Drop for NetworkProxyHandle {
700 fn drop(&mut self) {
701 if self.completed {
702 return;
703 }
704 let http_task = self.http_task.take();
705 let socks_task = self.socks_task.take();
706 tokio::spawn(async move {
707 abort_tasks(http_task, socks_task).await;
708 });
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::config::NetworkProxySettings;
716 use crate::state::network_proxy_state_for_policy;
717 use pretty_assertions::assert_eq;
718 use std::net::IpAddr;
719 use std::net::Ipv4Addr;
720
721 #[tokio::test]
722 async fn managed_proxy_builder_uses_loopback_ports() {
723 let state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings {
724 proxy_url: "http://127.0.0.1:43128".to_string(),
725 socks_url: "http://127.0.0.1:48081".to_string(),
726 ..NetworkProxySettings::default()
727 }));
728 let proxy = match NetworkProxy::builder().state(state).build().await {
729 Ok(proxy) => proxy,
730 Err(err) => {
731 if err
732 .chain()
733 .any(|cause| cause.to_string().contains("Operation not permitted"))
734 {
735 return;
736 }
737 panic!("failed to build managed proxy: {err:#}");
738 }
739 };
740
741 assert!(proxy.http_addr.ip().is_loopback());
742 assert!(proxy.socks_addr.ip().is_loopback());
743 #[cfg(target_os = "windows")]
744 {
745 assert_eq!(
746 proxy.http_addr,
747 "127.0.0.1:43128".parse::<SocketAddr>().unwrap()
748 );
749 assert_eq!(
750 proxy.socks_addr,
751 "127.0.0.1:48081".parse::<SocketAddr>().unwrap()
752 );
753 }
754 #[cfg(not(target_os = "windows"))]
755 {
756 assert_ne!(proxy.http_addr.port(), 0);
757 assert_ne!(proxy.socks_addr.port(), 0);
758 }
759 }
760
761 #[tokio::test]
762 async fn non_codex_managed_proxy_builder_uses_configured_ports() {
763 let settings = NetworkProxySettings {
764 proxy_url: "http://127.0.0.1:43128".to_string(),
765 socks_url: "http://127.0.0.1:48081".to_string(),
766 ..NetworkProxySettings::default()
767 };
768 let state = Arc::new(network_proxy_state_for_policy(settings));
769 let proxy = NetworkProxy::builder()
770 .state(state)
771 .managed_by_codex(false)
772 .build()
773 .await
774 .unwrap();
775
776 assert_eq!(
777 proxy.http_addr,
778 "127.0.0.1:43128".parse::<SocketAddr>().unwrap()
779 );
780 assert_eq!(
781 proxy.socks_addr,
782 "127.0.0.1:48081".parse::<SocketAddr>().unwrap()
783 );
784 }
785
786 #[tokio::test]
787 async fn managed_proxy_builder_does_not_reserve_socks_listener_when_disabled() {
788 let settings = NetworkProxySettings {
789 enable_socks5: false,
790 proxy_url: "http://127.0.0.1:43128".to_string(),
791 socks_url: "http://127.0.0.1:43129".to_string(),
792 ..NetworkProxySettings::default()
793 };
794 let state = Arc::new(network_proxy_state_for_policy(settings));
795 let proxy = match NetworkProxy::builder().state(state).build().await {
796 Ok(proxy) => proxy,
797 Err(err) => {
798 if err
799 .chain()
800 .any(|cause| cause.to_string().contains("Operation not permitted"))
801 {
802 return;
803 }
804 panic!("failed to build managed proxy: {err:#}");
805 }
806 };
807
808 assert!(proxy.http_addr.ip().is_loopback());
809 assert_ne!(proxy.http_addr.port(), 0);
810 assert_eq!(
811 proxy.socks_addr,
812 "127.0.0.1:43129".parse::<SocketAddr>().unwrap()
813 );
814 assert!(
815 proxy
816 .reserved_listeners
817 .as_ref()
818 .expect("managed builder should reserve listeners")
819 .take_socks()
820 .is_none()
821 );
822 }
823
824 #[cfg(target_os = "windows")]
825 #[test]
826 fn windows_managed_loopback_addr_clamps_non_loopback_inputs() {
827 assert_eq!(
828 windows_managed_loopback_addr("0.0.0.0:3128".parse::<SocketAddr>().unwrap()),
829 "127.0.0.1:3128".parse::<SocketAddr>().unwrap()
830 );
831 assert_eq!(
832 windows_managed_loopback_addr("[::]:8081".parse::<SocketAddr>().unwrap()),
833 "127.0.0.1:8081".parse::<SocketAddr>().unwrap()
834 );
835 }
836
837 #[cfg(target_os = "windows")]
838 #[test]
839 fn reserve_windows_managed_listeners_falls_back_when_http_port_is_busy() {
840 let occupied = StdTcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap();
841 let busy_port = occupied.local_addr().unwrap().port();
842
843 let reserved = reserve_windows_managed_listeners(
844 SocketAddr::from(([127, 0, 0, 1], busy_port)),
845 SocketAddr::from(([127, 0, 0, 1], 48081)),
846 false,
847 )
848 .unwrap();
849
850 assert!(reserved.socks_listener.is_none());
851 assert!(
852 reserved
853 .http_listener
854 .local_addr()
855 .unwrap()
856 .ip()
857 .is_loopback()
858 );
859 assert_ne!(
860 reserved.http_listener.local_addr().unwrap().port(),
861 busy_port
862 );
863 }
864
865 #[test]
866 fn proxy_url_env_value_resolves_lowercase_aliases() {
867 let mut env = HashMap::new();
868 env.insert(
869 "http_proxy".to_string(),
870 "http://127.0.0.1:3128".to_string(),
871 );
872
873 assert_eq!(
874 proxy_url_env_value(&env, "HTTP_PROXY"),
875 Some("http://127.0.0.1:3128")
876 );
877 }
878
879 #[test]
880 fn has_proxy_url_env_vars_detects_lowercase_aliases() {
881 let mut env = HashMap::new();
882 env.insert(
883 "all_proxy".to_string(),
884 "socks5h://127.0.0.1:8081".to_string(),
885 );
886
887 assert_eq!(has_proxy_url_env_vars(&env), true);
888 }
889
890 #[test]
891 fn has_proxy_url_env_vars_detects_websocket_proxy_keys() {
892 let mut env = HashMap::new();
893 env.insert("wss_proxy".to_string(), "http://127.0.0.1:3128".to_string());
894
895 assert_eq!(has_proxy_url_env_vars(&env), true);
896 }
897
898 #[test]
899 fn apply_proxy_env_overrides_sets_common_tool_vars() {
900 let mut env = HashMap::new();
901 apply_proxy_env_overrides(
902 &mut env,
903 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
904 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
905 true,
906 false,
907 );
908
909 assert_eq!(
910 env.get("HTTP_PROXY"),
911 Some(&"http://127.0.0.1:3128".to_string())
912 );
913 assert_eq!(
914 env.get("WS_PROXY"),
915 Some(&"http://127.0.0.1:3128".to_string())
916 );
917 assert_eq!(
918 env.get("WSS_PROXY"),
919 Some(&"http://127.0.0.1:3128".to_string())
920 );
921 assert_eq!(
922 env.get("npm_config_proxy"),
923 Some(&"http://127.0.0.1:3128".to_string())
924 );
925 assert_eq!(
926 env.get("ALL_PROXY"),
927 Some(&"socks5h://127.0.0.1:8081".to_string())
928 );
929 assert_eq!(
930 env.get("FTP_PROXY"),
931 Some(&"socks5h://127.0.0.1:8081".to_string())
932 );
933 assert_eq!(
934 env.get("NO_PROXY"),
935 Some(&DEFAULT_NO_PROXY_VALUE.to_string())
936 );
937 let no_proxy = env.get("NO_PROXY").expect("NO_PROXY should be set");
938 assert!(no_proxy.contains("10.0.0.0/8"));
939 assert!(no_proxy.contains("172.16.0.0/12"));
940 assert!(no_proxy.contains("192.168.0.0/16"));
941 assert!(no_proxy.contains("169.254.0.0/16"));
942 assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"0".to_string()));
943 assert_eq!(env.get("ELECTRON_GET_USE_PROXY"), Some(&"true".to_string()));
944 #[cfg(target_os = "macos")]
945 assert_eq!(
946 env.get("GIT_SSH_COMMAND"),
947 Some(&"ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'".to_string())
948 );
949 #[cfg(not(target_os = "macos"))]
950 assert_eq!(env.get("GIT_SSH_COMMAND"), None);
951 }
952
953 #[test]
954 fn apply_proxy_env_overrides_uses_http_for_all_proxy_without_socks() {
955 let mut env = HashMap::new();
956 apply_proxy_env_overrides(
957 &mut env,
958 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
959 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
960 false,
961 true,
962 );
963
964 assert_eq!(
965 env.get("ALL_PROXY"),
966 Some(&"http://127.0.0.1:3128".to_string())
967 );
968 assert_eq!(env.get(ALLOW_LOCAL_BINDING_ENV_KEY), Some(&"1".to_string()));
969 }
970
971 #[test]
972 fn apply_proxy_env_overrides_uses_plain_http_proxy_url() {
973 let mut env = HashMap::new();
974 apply_proxy_env_overrides(
975 &mut env,
976 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
977 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
978 true,
979 false,
980 );
981
982 assert_eq!(
983 env.get("HTTP_PROXY"),
984 Some(&"http://127.0.0.1:3128".to_string())
985 );
986 assert_eq!(
987 env.get("HTTPS_PROXY"),
988 Some(&"http://127.0.0.1:3128".to_string())
989 );
990 assert_eq!(
991 env.get("WS_PROXY"),
992 Some(&"http://127.0.0.1:3128".to_string())
993 );
994 assert_eq!(
995 env.get("WSS_PROXY"),
996 Some(&"http://127.0.0.1:3128".to_string())
997 );
998 assert_eq!(
999 env.get("ALL_PROXY"),
1000 Some(&"socks5h://127.0.0.1:8081".to_string())
1001 );
1002 #[cfg(target_os = "macos")]
1003 assert_eq!(
1004 env.get("GIT_SSH_COMMAND"),
1005 Some(&"ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:8081 %h %p'".to_string())
1006 );
1007 #[cfg(not(target_os = "macos"))]
1008 assert_eq!(env.get("GIT_SSH_COMMAND"), None);
1009 }
1010
1011 #[cfg(target_os = "macos")]
1012 #[test]
1013 fn apply_proxy_env_overrides_preserves_existing_git_ssh_command() {
1014 let mut env = HashMap::new();
1015 env.insert(
1016 "GIT_SSH_COMMAND".to_string(),
1017 "ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string(),
1018 );
1019 apply_proxy_env_overrides(
1020 &mut env,
1021 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
1022 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
1023 true,
1024 false,
1025 );
1026
1027 assert_eq!(
1028 env.get("GIT_SSH_COMMAND"),
1029 Some(&"ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string())
1030 );
1031 }
1032}