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