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