Skip to content

Commit 823633f

Browse files
committed
proxy: honor Windows proxy bypass macros and loopback defaults
Parse Windows `ProxyOverride` entries into explicit no-proxy rules while supporting the special `<local>` and `<-loopback>` semantics. By default, preserve the system loopback bypass behavior for `localhost`, `loopback`, `127.0.0.1`, and `::1`, and add tests covering local-name and loopback matching.
1 parent e1c5a6c commit 823633f

1 file changed

Lines changed: 146 additions & 11 deletions

File tree

src/client/proxy/matcher.rs

Lines changed: 146 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//! authentication to be used.
1414
1515
use std::fmt;
16-
use std::net::IpAddr;
16+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
1717

1818
use http::header::HeaderValue;
1919
use ipnet::IpNet;
@@ -50,6 +50,8 @@ pub struct Builder {
5050
http: String,
5151
https: String,
5252
no: String,
53+
no_local: bool,
54+
no_loopback: bool,
5355
}
5456

5557
#[derive(Clone)]
@@ -66,6 +68,8 @@ enum Auth {
6668
struct NoProxy {
6769
ips: IpMatcher,
6870
domains: DomainMatcher,
71+
local_names: bool,
72+
loopback_hosts: bool,
6973
}
7074

7175
#[derive(Clone, Debug, Default)]
@@ -232,6 +236,8 @@ impl Builder {
232236
http: get_first_env(&["HTTP_PROXY", "http_proxy"]),
233237
https: get_first_env(&["HTTPS_PROXY", "https_proxy"]),
234238
no: get_first_env(&["NO_PROXY", "no_proxy"]),
239+
no_local: false,
240+
no_loopback: false,
235241
}
236242
}
237243

@@ -315,7 +321,9 @@ impl Builder {
315321
Matcher {
316322
http: parse_env_uri(&self.http).or_else(|| all.clone()),
317323
https: parse_env_uri(&self.https).or(all),
318-
no: NoProxy::from_string(&self.no),
324+
no: NoProxy::from_string(&self.no)
325+
.with_local_names(self.no_local)
326+
.with_loopback_hosts(self.no_loopback),
319327
}
320328
}
321329
}
@@ -420,6 +428,8 @@ impl NoProxy {
420428
NoProxy {
421429
ips: IpMatcher(Vec::new()),
422430
domains: DomainMatcher(Vec::new()),
431+
local_names: false,
432+
loopback_hosts: false,
423433
}
424434
}
425435

@@ -463,6 +473,8 @@ impl NoProxy {
463473
NoProxy {
464474
ips: IpMatcher(ips),
465475
domains: DomainMatcher(domains),
476+
local_names: false,
477+
loopback_hosts: false,
466478
}
467479
}
468480

@@ -478,16 +490,41 @@ impl NoProxy {
478490
};
479491
match host.parse::<IpAddr>() {
480492
// If we can parse an IP addr, then use it, otherwise, assume it is a domain
481-
Ok(ip) => self.ips.contains(ip),
482-
Err(_) => self.domains.contains(host),
493+
Ok(ip) => self.ips.contains(ip) || self.loopback_hosts && is_loopback_ip(ip),
494+
Err(_) => {
495+
self.loopback_hosts && is_loopback_name(host)
496+
|| self.local_names && !host.contains('.')
497+
|| self.domains.contains(host)
498+
}
483499
}
484500
}
485501

486502
fn is_empty(&self) -> bool {
487-
self.ips.0.is_empty() && self.domains.0.is_empty()
503+
self.ips.0.is_empty()
504+
&& self.domains.0.is_empty()
505+
&& !self.local_names
506+
&& !self.loopback_hosts
507+
}
508+
509+
fn with_local_names(mut self, local_names: bool) -> Self {
510+
self.local_names = local_names;
511+
self
512+
}
513+
514+
fn with_loopback_hosts(mut self, loopback_hosts: bool) -> Self {
515+
self.loopback_hosts = loopback_hosts;
516+
self
488517
}
489518
}
490519

520+
fn is_loopback_ip(ip: IpAddr) -> bool {
521+
ip == IpAddr::V4(Ipv4Addr::LOCALHOST) || ip == IpAddr::V6(Ipv6Addr::LOCALHOST)
522+
}
523+
524+
fn is_loopback_name(host: &str) -> bool {
525+
host.eq_ignore_ascii_case("localhost") || host.eq_ignore_ascii_case("loopback")
526+
}
527+
491528
impl IpMatcher {
492529
fn contains(&self, addr: IpAddr) -> bool {
493530
for ip in &self.0 {
@@ -662,6 +699,12 @@ mod mac {
662699
#[cfg(feature = "client-proxy-system")]
663700
#[cfg(windows)]
664701
mod win {
702+
struct ProxyOverride {
703+
no: String,
704+
no_local: bool,
705+
no_loopback: bool,
706+
}
707+
665708
pub(super) fn with_system(builder: &mut super::Builder) {
666709
let settings = if let Ok(settings) = windows_registry::CURRENT_USER
667710
.open("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
@@ -685,16 +728,59 @@ mod win {
685728
}
686729

687730
if builder.no.is_empty() {
731+
builder.no_loopback = true;
732+
688733
if let Ok(val) = settings.get_string("ProxyOverride") {
689-
builder.no = val
690-
.split(';')
691-
.map(|s| s.trim())
692-
.collect::<Vec<&str>>()
693-
.join(",")
694-
.replace("*.", "");
734+
let proxy_override = parse_proxy_override(&val);
735+
builder.no = proxy_override.no;
736+
builder.no_local = proxy_override.no_local;
737+
builder.no_loopback = proxy_override.no_loopback;
695738
}
696739
}
697740
}
741+
742+
fn parse_proxy_override(val: &str) -> ProxyOverride {
743+
let mut no_local = false;
744+
let mut no_loopback = true;
745+
let no = val
746+
.split(';')
747+
.map(str::trim)
748+
.filter_map(|part| {
749+
if part.is_empty() {
750+
None
751+
} else if part.eq_ignore_ascii_case("<local>") {
752+
no_local = true;
753+
None
754+
} else if part.eq_ignore_ascii_case("<-loopback>") {
755+
no_loopback = false;
756+
None
757+
} else {
758+
Some(part)
759+
}
760+
})
761+
.collect::<Vec<&str>>()
762+
.join(",")
763+
.replace("*.", "");
764+
765+
ProxyOverride {
766+
no,
767+
no_local,
768+
no_loopback,
769+
}
770+
}
771+
772+
#[cfg(test)]
773+
mod tests {
774+
#[test]
775+
fn test_parse_proxy_override_macros() {
776+
let rules =
777+
super::parse_proxy_override("*.example.com; <local>; <-loopback>; 10.0.0.1 ;");
778+
779+
assert_eq!(rules.no, "example.com,10.0.0.1");
780+
assert!(rules.no_local);
781+
assert!(!rules.no_loopback);
782+
}
783+
}
698784
}
699785

700786
#[cfg(test)]
@@ -926,4 +1012,53 @@ mod tests {
9261012
.intercept(&"http://Www.Example.Com".parse().unwrap())
9271013
.is_none());
9281014
}
1015+
1016+
#[test]
1017+
fn test_no_proxy_local_names() {
1018+
let mut builder = Matcher::builder();
1019+
builder.all = "http://proxy.local".into();
1020+
builder.no_local = true;
1021+
let p = builder.build();
1022+
1023+
assert!(p.intercept(&"http://webserver".parse().unwrap()).is_none());
1024+
assert!(p.intercept(&"http://INTRANET".parse().unwrap()).is_none());
1025+
1026+
assert!(p
1027+
.intercept(&"http://webserver.example.com".parse().unwrap())
1028+
.is_some());
1029+
assert!(p.intercept(&"http://10.0.0.1".parse().unwrap()).is_some());
1030+
assert!(p.intercept(&"http://[::1]".parse().unwrap()).is_some());
1031+
}
1032+
1033+
#[test]
1034+
fn test_no_proxy_loopback_hosts() {
1035+
let mut builder = Matcher::builder();
1036+
builder.all = "http://proxy.local".into();
1037+
builder.no_loopback = true;
1038+
let p = builder.build();
1039+
1040+
assert!(p.intercept(&"http://127.0.0.1".parse().unwrap()).is_none());
1041+
assert!(p.intercept(&"http://[::1]".parse().unwrap()).is_none());
1042+
assert!(p.intercept(&"http://localhost".parse().unwrap()).is_none());
1043+
assert!(p.intercept(&"http://LOCALHOST".parse().unwrap()).is_none());
1044+
assert!(p.intercept(&"http://loopback".parse().unwrap()).is_none());
1045+
assert!(p.intercept(&"http://LOOPBACK".parse().unwrap()).is_none());
1046+
1047+
assert!(p.intercept(&"http://webserver".parse().unwrap()).is_some());
1048+
assert!(p.intercept(&"http://10.0.0.1".parse().unwrap()).is_some());
1049+
}
1050+
1051+
#[test]
1052+
fn test_no_proxy_loopback_hosts_disabled() {
1053+
let mut builder = Matcher::builder();
1054+
builder.all = "http://proxy.local".into();
1055+
let p = builder.build();
1056+
1057+
assert!(p.intercept(&"http://127.0.0.1".parse().unwrap()).is_some());
1058+
assert!(p.intercept(&"http://[::1]".parse().unwrap()).is_some());
1059+
assert!(p.intercept(&"http://localhost".parse().unwrap()).is_some());
1060+
assert!(p.intercept(&"http://LOCALHOST".parse().unwrap()).is_some());
1061+
assert!(p.intercept(&"http://loopback".parse().unwrap()).is_some());
1062+
assert!(p.intercept(&"http://LOOPBACK".parse().unwrap()).is_some());
1063+
}
9291064
}

0 commit comments

Comments
 (0)