1use anyhow::Context;
2use anyhow::Result;
3use anyhow::bail;
4use codex_utils_absolute_path::AbsolutePathBuf;
5use serde::Deserialize;
6use serde::Deserializer;
7use serde::Serialize;
8use serde::Serializer;
9use std::collections::BTreeMap;
10use std::net::IpAddr;
11use std::net::SocketAddr;
12use std::path::Path;
13use tracing::warn;
14use url::Url;
15
16use crate::mitm_hook::MitmHookConfig;
17
18#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
21#[serde(rename_all = "lowercase")]
22pub enum NetworkDomainPermission {
23 None,
24 Allow,
25 Deny,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct NetworkDomainPermissionEntry {
30 pub pattern: String,
31 pub permission: NetworkDomainPermission,
32}
33
34#[derive(Debug, Clone, Default, PartialEq, Eq)]
35pub struct NetworkDomainPermissions {
36 pub entries: Vec<NetworkDomainPermissionEntry>,
37}
38
39impl Serialize for NetworkDomainPermissions {
40 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
41 where
42 S: Serializer,
43 {
44 self.effective_entries()
45 .into_iter()
46 .map(|entry| (entry.pattern, entry.permission))
47 .collect::<BTreeMap<_, _>>()
48 .serialize(serializer)
49 }
50}
51
52impl<'de> Deserialize<'de> for NetworkDomainPermissions {
53 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
54 where
55 D: Deserializer<'de>,
56 {
57 let entries = BTreeMap::<String, NetworkDomainPermission>::deserialize(deserializer)?
58 .into_iter()
59 .map(|(pattern, permission)| NetworkDomainPermissionEntry {
60 pattern,
61 permission,
62 })
63 .collect();
64 Ok(Self { entries })
65 }
66}
67
68impl NetworkDomainPermissions {
69 fn effective_entries(&self) -> Vec<NetworkDomainPermissionEntry> {
70 let mut order = Vec::new();
71 let mut effective_permissions = BTreeMap::new();
72
73 for entry in &self.entries {
74 if !effective_permissions.contains_key(&entry.pattern) {
75 order.push(entry.pattern.clone());
76 }
77
78 let permission = effective_permissions
79 .entry(entry.pattern.clone())
80 .or_insert(entry.permission);
81 if entry.permission > *permission {
82 *permission = entry.permission;
83 }
84 }
85
86 order
87 .into_iter()
88 .filter_map(|pattern| {
89 effective_permissions.remove(&pattern).map(|permission| {
90 NetworkDomainPermissionEntry {
91 pattern,
92 permission,
93 }
94 })
95 })
96 .collect()
97 }
98}
99
100#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
101#[serde(rename_all = "lowercase")]
102pub enum NetworkUnixSocketPermission {
103 Allow,
104 Deny,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
108pub struct NetworkUnixSocketPermissions {
109 #[serde(flatten)]
110 pub entries: BTreeMap<String, NetworkUnixSocketPermission>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114#[serde(default)]
115pub struct NetworkProxyConfig {
116 #[serde(default)]
117 pub enabled: bool,
118 #[serde(default = "default_proxy_url")]
119 pub proxy_url: String,
120 pub enable_socks5: bool,
121 #[serde(default = "default_socks_url")]
122 pub socks_url: String,
123 pub enable_socks5_udp: bool,
124 pub allow_upstream_proxy: bool,
125 #[serde(default)]
126 pub dangerously_allow_non_loopback_proxy: bool,
127 #[serde(default)]
128 pub dangerously_allow_all_unix_sockets: bool,
129 #[serde(default)]
130 pub mode: NetworkMode,
131 #[serde(default)]
132 pub domains: Option<NetworkDomainPermissions>,
133 #[serde(default)]
134 pub unix_sockets: Option<NetworkUnixSocketPermissions>,
135 pub allow_local_binding: bool,
136 #[serde(default)]
137 pub mitm: bool,
138 #[serde(default)]
139 pub credential_broker: bool,
140 #[serde(default)]
141 pub dangerously_allow_plaintext_credential_injection: bool,
142 #[serde(default)]
143 pub mitm_hooks: Vec<MitmHookConfig>,
144}
145
146impl Default for NetworkProxyConfig {
147 fn default() -> Self {
148 Self {
149 enabled: false,
150 proxy_url: default_proxy_url(),
151 enable_socks5: true,
152 socks_url: default_socks_url(),
153 enable_socks5_udp: true,
154 allow_upstream_proxy: true,
155 dangerously_allow_non_loopback_proxy: false,
156 dangerously_allow_all_unix_sockets: false,
157 mode: NetworkMode::default(),
158 domains: None,
159 unix_sockets: None,
160 allow_local_binding: false,
161 mitm: false,
162 credential_broker: false,
163 dangerously_allow_plaintext_credential_injection: false,
164 mitm_hooks: Vec::new(),
165 }
166 }
167}
168
169impl NetworkProxyConfig {
170 pub fn set_credential_broker_enabled(&mut self, enabled: bool) {
171 self.credential_broker = enabled;
172 self.mitm |= enabled;
173 }
174
175 pub fn allowed_domains(&self) -> Option<Vec<String>> {
176 self.domain_entries(NetworkDomainPermission::Allow)
177 }
178
179 pub fn denied_domains(&self) -> Option<Vec<String>> {
180 self.domain_entries(NetworkDomainPermission::Deny)
181 }
182
183 fn domain_entries(&self, permission: NetworkDomainPermission) -> Option<Vec<String>> {
184 self.domains
185 .as_ref()
186 .map(|domains| {
187 domains
188 .effective_entries()
189 .iter()
190 .filter(|entry| entry.permission == permission)
191 .map(|entry| entry.pattern.clone())
192 .collect()
193 })
194 .filter(|entries: &Vec<String>| !entries.is_empty())
195 }
196
197 pub fn allow_unix_sockets(&self) -> Vec<String> {
198 self.unix_sockets
199 .as_ref()
200 .map(|unix_sockets| {
201 unix_sockets
202 .entries
203 .iter()
204 .filter(|(_, permission)| {
205 matches!(permission, NetworkUnixSocketPermission::Allow)
206 })
207 .map(|(path, _)| path.clone())
208 .collect()
209 })
210 .unwrap_or_default()
211 }
212
213 pub fn set_allowed_domains(&mut self, allowed_domains: Vec<String>) {
214 self.set_domain_entries(allowed_domains, NetworkDomainPermission::Allow);
215 }
216
217 pub fn set_denied_domains(&mut self, denied_domains: Vec<String>) {
218 self.set_domain_entries(denied_domains, NetworkDomainPermission::Deny);
219 }
220
221 pub fn upsert_domain_permission(
222 &mut self,
223 host: String,
224 permission: NetworkDomainPermission,
225 normalize: impl Fn(&str) -> String,
226 ) {
227 let mut domains = self.domains.take().unwrap_or_default();
228 let normalized_host = normalize(&host);
229 domains
230 .entries
231 .retain(|entry| normalize(&entry.pattern) != normalized_host);
232 domains.entries.push(NetworkDomainPermissionEntry {
233 pattern: host,
234 permission,
235 });
236 self.domains = (!domains.entries.is_empty()).then_some(domains);
237 }
238
239 pub fn set_allow_unix_sockets(&mut self, allow_unix_sockets: Vec<String>) {
240 self.set_unix_socket_entries(allow_unix_sockets, NetworkUnixSocketPermission::Allow);
241 }
242
243 fn set_domain_entries(&mut self, entries: Vec<String>, permission: NetworkDomainPermission) {
244 let mut domains = self.domains.take().unwrap_or_default();
245 domains
246 .entries
247 .retain(|entry| entry.permission != permission);
248 for entry in entries {
249 if !domains
250 .entries
251 .iter()
252 .any(|existing| existing.pattern == entry && existing.permission == permission)
253 {
254 domains.entries.push(NetworkDomainPermissionEntry {
255 pattern: entry,
256 permission,
257 });
258 }
259 }
260 self.domains = (!domains.entries.is_empty()).then_some(domains);
261 }
262
263 fn set_unix_socket_entries(
264 &mut self,
265 entries: Vec<String>,
266 permission: NetworkUnixSocketPermission,
267 ) {
268 let mut unix_sockets = self.unix_sockets.take().unwrap_or_default();
269 unix_sockets
270 .entries
271 .retain(|_, existing| *existing != permission);
272 for entry in entries {
273 unix_sockets.entries.insert(entry, permission);
274 }
275 self.unix_sockets = (!unix_sockets.entries.is_empty()).then_some(unix_sockets);
276 }
277}
278
279#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
280#[serde(rename_all = "lowercase")]
281pub enum NetworkMode {
282 Limited,
286 #[default]
289 Full,
290}
291
292impl NetworkMode {
293 pub fn allows_method(self, method: &str) -> bool {
294 match self {
295 Self::Full => true,
296 Self::Limited => matches!(method, "GET" | "HEAD" | "OPTIONS"),
297 }
298 }
299}
300
301fn default_proxy_url() -> String {
302 "http://127.0.0.1:3128".to_string()
303}
304
305fn default_socks_url() -> String {
306 "http://127.0.0.1:8081".to_string()
307}
308
309fn clamp_non_loopback(
311 addr: SocketAddr,
312 allow_non_loopback: bool,
313 name: &str,
314 override_setting_name: &str,
315) -> SocketAddr {
316 if addr.ip().is_loopback() {
317 return addr;
318 }
319
320 if allow_non_loopback {
321 warn!("DANGEROUS: {name} listening on non-loopback address {addr}");
322 return addr;
323 }
324
325 warn!(
326 "{name} requested non-loopback bind ({addr}); clamping to 127.0.0.1:{port} (set {override_setting_name} to override)",
327 port = addr.port()
328 );
329 SocketAddr::from(([127, 0, 0, 1], addr.port()))
330}
331
332pub(crate) fn clamp_bind_addrs(
333 http_addr: SocketAddr,
334 socks_addr: SocketAddr,
335 cfg: &NetworkProxyConfig,
336) -> (SocketAddr, SocketAddr) {
337 let http_addr = clamp_non_loopback(
338 http_addr,
339 cfg.dangerously_allow_non_loopback_proxy,
340 "HTTP proxy",
341 "dangerously_allow_non_loopback_proxy",
342 );
343 let socks_addr = clamp_non_loopback(
344 socks_addr,
345 cfg.dangerously_allow_non_loopback_proxy,
346 "SOCKS5 proxy",
347 "dangerously_allow_non_loopback_proxy",
348 );
349 if cfg.allow_unix_sockets().is_empty() && !cfg.dangerously_allow_all_unix_sockets {
350 return (http_addr, socks_addr);
351 }
352
353 if cfg.dangerously_allow_non_loopback_proxy && !http_addr.ip().is_loopback() {
358 warn!(
359 "unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping HTTP proxy to loopback"
360 );
361 }
362 if cfg.dangerously_allow_non_loopback_proxy && !socks_addr.ip().is_loopback() {
363 warn!(
364 "unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping SOCKS5 proxy to loopback"
365 );
366 }
367 (
368 SocketAddr::from(([127, 0, 0, 1], http_addr.port())),
369 SocketAddr::from(([127, 0, 0, 1], socks_addr.port())),
370 )
371}
372
373pub struct RuntimeConfig {
374 pub http_addr: SocketAddr,
375 pub socks_addr: SocketAddr,
376}
377
378#[derive(Debug, Clone, PartialEq, Eq)]
379pub(crate) struct UnixStyleAbsolutePath(String);
380
381impl UnixStyleAbsolutePath {
382 fn parse(value: &str) -> Option<Self> {
383 value.starts_with('/').then(|| Self(value.to_string()))
384 }
385}
386
387#[derive(Debug, Clone, PartialEq, Eq)]
388pub(crate) enum ValidatedUnixSocketPath {
389 Native(AbsolutePathBuf),
390 UnixStyleAbsolute(UnixStyleAbsolutePath),
391}
392
393impl ValidatedUnixSocketPath {
394 pub(crate) fn parse(socket_path: &str) -> Result<Self> {
395 let path = Path::new(socket_path);
396 if path.is_absolute() {
397 let path = AbsolutePathBuf::from_absolute_path(path)
398 .with_context(|| format!("failed to normalize unix socket path {socket_path:?}"))?;
399 return Ok(Self::Native(path));
400 }
401
402 if let Some(path) = UnixStyleAbsolutePath::parse(socket_path) {
403 return Ok(Self::UnixStyleAbsolute(path));
404 }
405
406 bail!("expected an absolute path, got {socket_path:?}");
407 }
408}
409
410pub(crate) fn validate_unix_socket_allowlist_paths(cfg: &NetworkProxyConfig) -> Result<()> {
411 for (index, socket_path) in cfg.allow_unix_sockets().iter().enumerate() {
412 ValidatedUnixSocketPath::parse(socket_path)
413 .with_context(|| format!("invalid network.allow_unix_sockets[{index}]"))?;
414 }
415 Ok(())
416}
417
418pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result<RuntimeConfig> {
419 validate_unix_socket_allowlist_paths(cfg)?;
420
421 let http_addr = resolve_addr(&cfg.proxy_url, 3128)
422 .with_context(|| format!("invalid network.proxy_url: {}", cfg.proxy_url))?;
423 let socks_addr = resolve_addr(&cfg.socks_url, 8081)
424 .with_context(|| format!("invalid network.socks_url: {}", cfg.socks_url))?;
425 let (http_addr, socks_addr) = clamp_bind_addrs(http_addr, socks_addr, cfg);
426
427 Ok(RuntimeConfig {
428 http_addr,
429 socks_addr,
430 })
431}
432
433pub fn managed_proxy_ports(cfg: &NetworkProxyConfig) -> Result<Vec<u16>> {
435 let runtime = resolve_runtime(cfg)?;
436 if runtime.http_addr.port() == 0 {
437 bail!("network.proxy_url must use a fixed non-zero port for managed proxy provisioning");
438 }
439 let mut ports = vec![runtime.http_addr.port()];
440 if cfg.enable_socks5 {
441 if runtime.socks_addr.port() == 0 {
442 bail!(
443 "network.socks_url must use a fixed non-zero port for managed proxy provisioning"
444 );
445 }
446 ports.push(runtime.socks_addr.port());
447 }
448 ports.sort_unstable();
449 ports.dedup();
450 Ok(ports)
451}
452
453fn resolve_addr(url: &str, default_port: u16) -> Result<SocketAddr> {
454 let addr_parts = parse_host_port(url, default_port)?;
455 let host = if addr_parts.host.eq_ignore_ascii_case("localhost") {
456 "127.0.0.1".to_string()
457 } else {
458 addr_parts.host
459 };
460 match host.parse::<IpAddr>() {
461 Ok(ip) => Ok(SocketAddr::new(ip, addr_parts.port)),
462 Err(_) => Ok(SocketAddr::from(([127, 0, 0, 1], addr_parts.port))),
463 }
464}
465
466pub fn host_and_port_from_network_addr(value: &str, default_port: u16) -> String {
467 let trimmed = value.trim();
468 if trimmed.is_empty() {
469 return "<missing>".to_string();
470 }
471
472 let parts = match parse_host_port(trimmed, default_port) {
473 Ok(parts) => parts,
474 Err(_) => {
475 return format_host_and_port(trimmed, default_port);
476 }
477 };
478
479 format_host_and_port(&parts.host, parts.port)
480}
481
482fn format_host_and_port(host: &str, port: u16) -> String {
483 if host.contains(':') {
484 format!("[{host}]:{port}")
485 } else {
486 format!("{host}:{port}")
487 }
488}
489
490#[derive(Debug, Clone, PartialEq, Eq)]
491struct SocketAddressParts {
492 host: String,
493 port: u16,
494}
495
496fn parse_host_port(url: &str, default_port: u16) -> Result<SocketAddressParts> {
497 let trimmed = url.trim();
498 if trimmed.is_empty() {
499 bail!("missing host in network proxy address: {url}");
500 }
501
502 if matches!(trimmed.parse::<IpAddr>(), Ok(IpAddr::V6(_))) && !trimmed.starts_with('[') {
504 return Ok(SocketAddressParts {
505 host: trimmed.to_string(),
506 port: default_port,
507 });
508 }
509
510 let candidate = if trimmed.contains("://") {
513 trimmed.to_string()
514 } else {
515 format!("http://{trimmed}")
516 };
517 if let Ok(parsed) = Url::parse(&candidate)
518 && let Some(host) = parsed.host_str()
519 {
520 let host = host.trim_matches(|c| c == '[' || c == ']');
521 if host.is_empty() {
522 bail!("missing host in network proxy address: {url}");
523 }
524 return Ok(SocketAddressParts {
525 host: host.to_string(),
526 port: parsed.port().unwrap_or(default_port),
527 });
528 }
529
530 parse_host_port_fallback(trimmed, default_port)
531}
532
533fn parse_host_port_fallback(input: &str, default_port: u16) -> Result<SocketAddressParts> {
534 let without_scheme = input
535 .split_once("://")
536 .map(|(_, rest)| rest)
537 .unwrap_or(input);
538 let host_port = without_scheme.split('/').next().unwrap_or(without_scheme);
539 let host_port = host_port
540 .rsplit_once('@')
541 .map(|(_, rest)| rest)
542 .unwrap_or(host_port);
543
544 if host_port.starts_with('[')
545 && let Some(end) = host_port.find(']')
546 {
547 let host = &host_port[1..end];
548 let port = host_port[end + 1..]
549 .strip_prefix(':')
550 .and_then(|port| port.parse::<u16>().ok())
551 .unwrap_or(default_port);
552 if host.is_empty() {
553 bail!("missing host in network proxy address: {input}");
554 }
555 return Ok(SocketAddressParts {
556 host: host.to_string(),
557 port,
558 });
559 }
560
561 if host_port.bytes().filter(|b| *b == b':').count() == 1
564 && let Some((host, port)) = host_port.rsplit_once(':')
565 {
566 if host.is_empty() {
567 bail!("missing host in network proxy address: {input}");
568 }
569 return Ok(SocketAddressParts {
570 host: host.to_string(),
571 port: port.parse::<u16>().ok().unwrap_or(default_port),
572 });
573 }
574
575 if host_port.is_empty() {
576 bail!("missing host in network proxy address: {input}");
577 }
578 Ok(SocketAddressParts {
579 host: host_port.to_string(),
580 port: default_port,
581 })
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587
588 use pretty_assertions::assert_eq;
589
590 fn settings_with_unix_sockets(unix_sockets: &[&str]) -> NetworkProxyConfig {
591 let mut settings = NetworkProxyConfig::default();
592 if !unix_sockets.is_empty() {
593 settings.set_allow_unix_sockets(
594 unix_sockets
595 .iter()
596 .map(|path| (*path).to_string())
597 .collect(),
598 );
599 }
600 settings
601 }
602
603 #[test]
604 fn network_proxy_settings_default_matches_local_use_baseline() {
605 assert_eq!(
606 NetworkProxyConfig::default(),
607 NetworkProxyConfig {
608 enabled: false,
609 proxy_url: "http://127.0.0.1:3128".to_string(),
610 enable_socks5: true,
611 socks_url: "http://127.0.0.1:8081".to_string(),
612 enable_socks5_udp: true,
613 allow_upstream_proxy: true,
614 dangerously_allow_non_loopback_proxy: false,
615 dangerously_allow_all_unix_sockets: false,
616 mode: NetworkMode::Full,
617 domains: None,
618 unix_sockets: None,
619 allow_local_binding: false,
620 mitm: false,
621 credential_broker: false,
622 dangerously_allow_plaintext_credential_injection: false,
623 mitm_hooks: Vec::new(),
624 }
625 );
626 }
627
628 #[test]
629 fn managed_proxy_ports_reject_ephemeral_ports() {
630 let mut config = NetworkProxyConfig {
631 proxy_url: "http://127.0.0.1:0".to_string(),
632 ..Default::default()
633 };
634
635 assert_eq!(
636 managed_proxy_ports(&config).unwrap_err().to_string(),
637 "network.proxy_url must use a fixed non-zero port for managed proxy provisioning"
638 );
639
640 config.proxy_url = "http://127.0.0.1:3128".to_string();
641 config.socks_url = "socks5h://127.0.0.1:48081".to_string();
642 assert_eq!(managed_proxy_ports(&config).unwrap(), vec![3128, 48081]);
643
644 config.socks_url = "socks5h://127.0.0.1:0".to_string();
645 assert_eq!(
646 managed_proxy_ports(&config).unwrap_err().to_string(),
647 "network.socks_url must use a fixed non-zero port for managed proxy provisioning"
648 );
649
650 config.enable_socks5 = false;
651 assert_eq!(managed_proxy_ports(&config).unwrap(), vec![3128]);
652 }
653
654 #[test]
655 fn network_proxy_config_uses_struct_defaults_for_missing_fields() {
656 let config: NetworkProxyConfig = serde_json::from_str(r#"{ "enabled": true }"#).unwrap();
657 let expected = NetworkProxyConfig {
658 enabled: true,
659 ..NetworkProxyConfig::default()
660 };
661
662 assert_eq!(config, expected);
663 }
664
665 #[test]
666 fn set_allowed_domains_preserves_existing_deny_for_same_pattern() {
667 let mut settings = NetworkProxyConfig::default();
668 settings.set_denied_domains(vec!["example.com".to_string()]);
669
670 settings.set_allowed_domains(vec!["example.com".to_string()]);
671
672 assert_eq!(settings.allowed_domains(), None);
673 assert_eq!(
674 settings.denied_domains(),
675 Some(vec!["example.com".to_string()])
676 );
677 }
678
679 #[test]
680 fn network_domain_permissions_serialize_to_effective_map_shape() {
681 let mut settings = NetworkProxyConfig::default();
682 settings.set_denied_domains(vec!["example.com".to_string()]);
683 settings.set_allowed_domains(vec!["example.com".to_string()]);
684 let config = settings;
685
686 let value = serde_json::to_value(&config).unwrap();
687
688 assert_eq!(
689 value,
690 serde_json::json!({
691 "enabled": false,
692 "proxy_url": "http://127.0.0.1:3128",
693 "enable_socks5": true,
694 "socks_url": "http://127.0.0.1:8081",
695 "enable_socks5_udp": true,
696 "allow_upstream_proxy": true,
697 "dangerously_allow_non_loopback_proxy": false,
698 "dangerously_allow_all_unix_sockets": false,
699 "mode": "full",
700 "domains": {
701 "example.com": "deny",
702 },
703 "unix_sockets": null,
704 "allow_local_binding": false,
705 "mitm": false,
706 "credential_broker": false,
707 "dangerously_allow_plaintext_credential_injection": false,
708 "mitm_hooks": [],
709 })
710 );
711 }
712
713 #[test]
714 fn parse_host_port_defaults_for_empty_string() {
715 assert!(parse_host_port("", 1234).is_err());
716 }
717
718 #[test]
719 fn parse_host_port_defaults_for_whitespace() {
720 assert!(parse_host_port(" ", 5555).is_err());
721 }
722
723 #[test]
724 fn parse_host_port_parses_host_port_without_scheme() {
725 assert_eq!(
726 parse_host_port("127.0.0.1:8080", 3128).unwrap(),
727 SocketAddressParts {
728 host: "127.0.0.1".to_string(),
729 port: 8080,
730 }
731 );
732 }
733
734 #[test]
735 fn parse_host_port_parses_host_port_with_scheme_and_path() {
736 assert_eq!(
737 parse_host_port(
738 "http://example.com:8080/some/path",
739 3128
740 )
741 .unwrap(),
742 SocketAddressParts {
743 host: "example.com".to_string(),
744 port: 8080,
745 }
746 );
747 }
748
749 #[test]
750 fn parse_host_port_strips_userinfo() {
751 assert_eq!(
752 parse_host_port(
753 "http://user:pass@host.example:5555",
754 3128
755 )
756 .unwrap(),
757 SocketAddressParts {
758 host: "host.example".to_string(),
759 port: 5555,
760 }
761 );
762 }
763
764 #[test]
765 fn parse_host_port_parses_ipv6_with_brackets() {
766 assert_eq!(
767 parse_host_port("http://[::1]:9999", 3128).unwrap(),
768 SocketAddressParts {
769 host: "::1".to_string(),
770 port: 9999,
771 }
772 );
773 }
774
775 #[test]
776 fn parse_host_port_does_not_treat_unbracketed_ipv6_as_host_port() {
777 assert_eq!(
778 parse_host_port("2001:db8::1", 3128).unwrap(),
779 SocketAddressParts {
780 host: "2001:db8::1".to_string(),
781 port: 3128,
782 }
783 );
784 }
785
786 #[test]
787 fn parse_host_port_falls_back_to_default_port_when_port_is_invalid() {
788 assert_eq!(
789 parse_host_port("example.com:notaport", 3128).unwrap(),
790 SocketAddressParts {
791 host: "example.com".to_string(),
792 port: 3128,
793 }
794 );
795 }
796
797 #[test]
798 fn host_and_port_from_network_addr_defaults_for_empty_string() {
799 assert_eq!(
800 host_and_port_from_network_addr("", 1234),
801 "<missing>"
802 );
803 }
804
805 #[test]
806 fn host_and_port_from_network_addr_formats_ipv6() {
807 assert_eq!(
808 host_and_port_from_network_addr("http://[::1]:8080", 3128),
809 "[::1]:8080"
810 );
811 }
812
813 #[test]
814 fn resolve_addr_maps_localhost_to_loopback() {
815 assert_eq!(
816 resolve_addr("localhost", 3128).unwrap(),
817 "127.0.0.1:3128".parse::<SocketAddr>().unwrap()
818 );
819 }
820
821 #[test]
822 fn resolve_addr_parses_ip_literals() {
823 assert_eq!(
824 resolve_addr("1.2.3.4", 80).unwrap(),
825 "1.2.3.4:80".parse::<SocketAddr>().unwrap()
826 );
827 }
828
829 #[test]
830 fn resolve_addr_parses_ipv6_literals() {
831 assert_eq!(
832 resolve_addr("http://[::1]:8080", 3128).unwrap(),
833 "[::1]:8080".parse::<SocketAddr>().unwrap()
834 );
835 }
836
837 #[test]
838 fn resolve_addr_falls_back_to_loopback_for_hostnames() {
839 assert_eq!(
840 resolve_addr("http://example.com:5555", 3128).unwrap(),
841 "127.0.0.1:5555".parse::<SocketAddr>().unwrap()
842 );
843 }
844
845 #[test]
846 fn clamp_bind_addrs_allows_non_loopback_when_enabled() {
847 let cfg = NetworkProxyConfig {
848 dangerously_allow_non_loopback_proxy: true,
849 ..Default::default()
850 };
851 let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
852 let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
853
854 let (http_addr, socks_addr) = clamp_bind_addrs(http_addr, socks_addr, &cfg);
855
856 assert_eq!(http_addr, "0.0.0.0:3128".parse::<SocketAddr>().unwrap());
857 assert_eq!(socks_addr, "0.0.0.0:8081".parse::<SocketAddr>().unwrap());
858 }
859
860 #[test]
861 fn clamp_bind_addrs_forces_loopback_when_unix_sockets_enabled() {
862 let cfg = {
863 let mut settings = settings_with_unix_sockets(&["/tmp/docker.sock"]);
864 settings.dangerously_allow_non_loopback_proxy = true;
865 settings
866 };
867 let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
868 let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
869
870 let (http_addr, socks_addr) = clamp_bind_addrs(http_addr, socks_addr, &cfg);
871
872 assert_eq!(http_addr, "127.0.0.1:3128".parse::<SocketAddr>().unwrap());
873 assert_eq!(socks_addr, "127.0.0.1:8081".parse::<SocketAddr>().unwrap());
874 }
875
876 #[test]
877 fn clamp_bind_addrs_forces_loopback_when_all_unix_sockets_enabled() {
878 let cfg = NetworkProxyConfig {
879 dangerously_allow_non_loopback_proxy: true,
880 dangerously_allow_all_unix_sockets: true,
881 ..Default::default()
882 };
883 let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
884 let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
885
886 let (http_addr, socks_addr) = clamp_bind_addrs(http_addr, socks_addr, &cfg);
887
888 assert_eq!(http_addr, "127.0.0.1:3128".parse::<SocketAddr>().unwrap());
889 assert_eq!(socks_addr, "127.0.0.1:8081".parse::<SocketAddr>().unwrap());
890 }
891
892 #[test]
893 fn resolve_runtime_rejects_relative_allow_unix_sockets_entries() {
894 let cfg = settings_with_unix_sockets(&["relative.sock"]);
895
896 let err = match resolve_runtime(&cfg) {
897 Ok(runtime) => panic!(
898 "relative allow_unix_sockets should fail, but resolve_runtime succeeded: {:?}",
899 runtime.http_addr
900 ),
901 Err(err) => err,
902 };
903 assert!(
904 err.to_string().contains("network.allow_unix_sockets[0]"),
905 "error should point at the invalid allow_unix_sockets entry: {err:#}"
906 );
907 }
908
909 #[test]
910 fn resolve_runtime_accepts_unix_style_absolute_allow_unix_sockets_entries() {
911 let cfg = settings_with_unix_sockets(&["/private/tmp/example.sock"]);
912
913 assert!(
914 resolve_runtime(&cfg).is_ok(),
915 "unix-style absolute allow_unix_sockets entry should be accepted"
916 );
917 }
918}