use core::net::IpAddr;
use rama_core::error::{BoxError, ErrorExt as _};
#[cfg(any(
test,
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
use rama_core::error::BoxErrorExt as _;
#[cfg(any(
test,
target_vendor = "apple",
target_os = "android",
target_os = "windows",
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
use crate::address::Host;
use crate::{
Protocol,
address::{
HostPattern, HostRef,
ip::{IntoCanonicalIpAddr as _, ipnet::IpNet, parse_ip_net},
},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum BypassRuleDialect {
Rama,
NoProxy,
#[cfg(any(
test,
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
Glib,
#[cfg(any(
test,
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
Kde,
#[cfg(any(
test,
target_vendor = "apple",
target_os = "android",
target_os = "windows"
))]
FlatGlob,
}
impl BypassRuleDialect {
fn supports_standalone_wildcard(_dialect: Self) -> bool {
#[cfg(any(
test,
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
if _dialect == Self::Kde {
return false;
}
true
}
}
#[derive(Debug, Clone)]
pub(super) struct BypassRule {
raw: Box<str>,
scheme: Option<Box<str>>,
port: Option<u16>,
matcher: BypassMatcher,
}
#[derive(Debug, Clone)]
enum BypassMatcher {
All,
LocalName,
Network(IpNet),
Pattern(HostPattern),
}
impl BypassRule {
#[cfg(test)]
pub(super) fn compile(value: impl Into<Box<str>>) -> Result<Self, BoxError> {
Self::compile_with_dialect(value, BypassRuleDialect::Rama)
}
pub(super) fn compile_with_dialect(
value: impl Into<Box<str>>,
dialect: BypassRuleDialect,
) -> Result<Self, BoxError> {
let mut raw = value.into();
if raw.len() != raw.trim().len() {
raw = raw.trim().into();
}
let (scheme, pattern) = split_scheme(raw.trim());
let scheme = scheme.map(|scheme| scheme.to_ascii_lowercase().into_boxed_str());
let (pattern, port) = split_port(pattern);
let matcher = if pattern == "*" && BypassRuleDialect::supports_standalone_wildcard(dialect)
{
BypassMatcher::All
} else if pattern.eq_ignore_ascii_case("<local>") {
BypassMatcher::LocalName
} else if let Ok(network) = parse_ip_net(pattern) {
BypassMatcher::Network(network)
} else if let Ok(address) = pattern
.strip_prefix('[')
.and_then(|address| address.strip_suffix(']'))
.unwrap_or(pattern)
.parse::<IpAddr>()
{
BypassMatcher::Network(address.into_canonical_ip_addr().into())
} else {
BypassMatcher::Pattern(compile_host_pattern(pattern, dialect).map_err(|error| {
error
.context("parse system proxy bypass pattern")
.context_str_field("pattern", raw.as_ref())
})?)
};
Ok(Self {
raw,
scheme,
port,
matcher,
})
}
pub(super) fn raw(&self) -> &str {
&self.raw
}
#[cfg(test)]
pub(super) fn matches(
&self,
scheme: Option<&Protocol>,
host: HostRef<'_>,
port: Option<u16>,
) -> bool {
let host_text = self.requires_host_text().then(|| host.to_str());
self.matches_with_host_text(scheme, host, port, host_text.as_deref())
}
fn requires_host_text(&self) -> bool {
matches!(&self.matcher, BypassMatcher::Pattern(pattern) if pattern.is_glob())
}
fn matches_with_host_text(
&self,
scheme: Option<&Protocol>,
host: HostRef<'_>,
port: Option<u16>,
host_text: Option<&str>,
) -> bool {
if self.scheme.as_deref().is_some_and(|expected| {
!scheme.is_some_and(|actual| actual.as_str().eq_ignore_ascii_case(expected))
}) {
return false;
}
if self.port.is_some_and(|expected| port != Some(expected)) {
return false;
}
match &self.matcher {
BypassMatcher::All => true,
BypassMatcher::LocalName => is_simple_hostname(host),
BypassMatcher::Network(network) => host
.try_as_ip()
.is_ok_and(|ip| network.contains(&ip.into_canonical_ip_addr())),
BypassMatcher::Pattern(pattern) => pattern.matches_with_text(host, host_text),
}
}
}
pub(super) fn matches_any_rule(
rules: &[BypassRule],
scheme: Option<&Protocol>,
host: HostRef<'_>,
port: Option<u16>,
) -> bool {
let host_text = rules
.iter()
.any(BypassRule::requires_host_text)
.then(|| host.to_str());
rules
.iter()
.any(|rule| rule.matches_with_host_text(scheme, host, port, host_text.as_deref()))
}
fn compile_host_pattern(
pattern: &str,
dialect: BypassRuleDialect,
) -> Result<HostPattern, BoxError> {
match dialect {
BypassRuleDialect::Rama => pattern.parse(),
BypassRuleDialect::NoProxy => match Host::try_from(pattern) {
Ok(Host::Name(domain)) => Ok(HostPattern::sub(domain)),
Ok(_) | Err(_) if pattern.contains('*') => HostPattern::try_glob(pattern.to_owned()),
Ok(host) => Ok(HostPattern::exact(host)),
Err(error) => Err(error),
},
#[cfg(any(
test,
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
BypassRuleDialect::Glib => match Host::try_from(pattern) {
Ok(Host::Name(domain)) => Ok(HostPattern::sub(domain)),
Ok(_) | Err(_) if pattern.contains('*') => HostPattern::try_glob(pattern.to_owned()),
Ok(host) => Ok(HostPattern::exact(host)),
Err(error) => Err(error),
},
#[cfg(any(
test,
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
BypassRuleDialect::Kde => {
if pattern.contains(['*', '?']) {
return Err(BoxError::from_static_str(
"KDE proxy exceptions do not support wildcard characters",
));
}
match Host::try_from(pattern) {
Ok(Host::Name(domain)) => Ok(HostPattern::sub(domain)),
Ok(host) => Ok(HostPattern::exact(host)),
Err(error) => Err(error),
}
}
#[cfg(any(
test,
target_vendor = "apple",
target_os = "android",
target_os = "windows"
))]
BypassRuleDialect::FlatGlob => {
if pattern.contains('*') {
return HostPattern::try_glob(pattern.to_owned());
}
if pattern.starts_with('.') {
return HostPattern::try_glob(format!("*{pattern}"));
}
Host::try_from(pattern).map(HostPattern::exact)
}
}
}
fn split_scheme(pattern: &str) -> (Option<&str>, &str) {
let Some((scheme, remainder)) = pattern.split_once("://") else {
return (None, pattern);
};
if scheme.is_empty()
|| !scheme
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
{
(None, pattern)
} else {
(Some(scheme), remainder)
}
}
pub(super) fn is_simple_hostname(host: HostRef<'_>) -> bool {
if host.try_as_ip().is_ok() {
return false;
}
let text = host.to_str();
!text.contains(['.', ':'])
}
fn split_port(pattern: &str) -> (&str, Option<u16>) {
if let Some(bracketed) = pattern.strip_prefix('[')
&& let Some((candidate, suffix)) = bracketed.rsplit_once("]:")
&& let Ok(port) = suffix.parse::<u16>()
{
return (candidate, Some(port));
}
if pattern.bytes().filter(|byte| *byte == b':').count() == 1
&& let Some((candidate, suffix)) = pattern.rsplit_once(':')
&& let Ok(port) = suffix.parse::<u16>()
{
return (candidate, Some(port));
}
(pattern, None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::address::Host;
#[test]
fn boxed_rule_text_is_reused_as_snapshot_storage() {
let raw = Box::<str>::from("example.com");
let address = raw.as_ptr();
let rule = BypassRule::compile(raw).unwrap();
assert!(core::ptr::eq(address, rule.raw.as_ptr()));
}
#[test]
fn bypass_rule_text_is_trimmed_once_when_compiled() {
let rule = BypassRule::compile(" example.com\t").unwrap();
assert_eq!(rule.raw(), "example.com");
}
#[test]
fn general_windows_wildcards_match_without_allocating_per_rule() {
for (pattern, host, expected) in [
("192.168.*", "192.168.1.5", true),
("10.*", "10.20.30.40", true),
("*corp*", "api.CORP.example", true),
("*corp*", "example.test", false),
("ab*cd", "ab-123-cd", true),
("abc*def", "abcX", false),
] {
let parsed = host.parse::<crate::address::Host>().unwrap();
assert_eq!(
BypassRule::compile(pattern)
.unwrap()
.matches(None, (&parsed).into(), None,),
expected,
);
}
}
#[test]
fn scheme_prefixed_rules_keep_their_scheme_and_port_constraints() {
let host = "secure.example".parse::<crate::address::Host>().unwrap();
let rule = BypassRule::compile("HTTPS://secure.example:443").unwrap();
assert!(rule.matches(Some(&Protocol::HTTPS), (&host).into(), Some(443)));
assert!(!rule.matches(Some(&Protocol::HTTP), (&host).into(), Some(443)));
assert!(!rule.matches(Some(&Protocol::HTTPS), (&host).into(), Some(8443)));
}
#[test]
fn typed_exact_and_suffix_rules_use_canonical_host_semantics() {
let exact = Host::try_from("example.com").unwrap();
assert!(
BypassRule::compile("EXAMPLE.COM.")
.unwrap()
.matches(None, exact.view(), None,)
);
let ipv6 = Host::try_from("2001:db8::1").unwrap();
assert!(
BypassRule::compile("[2001:0db8::1]")
.unwrap()
.matches(None, ipv6.view(), None,)
);
let subdomain = Host::try_from("api.example.com").unwrap();
assert!(BypassRule::compile(".EXAMPLE.COM.").unwrap().matches(
None,
subdomain.view(),
None,
));
BypassRule::compile(".not a valid domain").unwrap_err();
}
#[test]
fn abbreviated_ipv4_networks_match_ip_hosts() {
let rule = BypassRule::compile("169.254/16").unwrap();
assert!(rule.matches(None, Host::try_from("169.254.42.7").unwrap().view(), None,));
assert!(!rule.matches(None, Host::try_from("169.253.42.7").unwrap().view(), None,));
}
#[test]
fn ipv4_networks_match_ipv4_mapped_ipv6_hosts() {
for (network, host, expected) in [
("10.0.0.0/8", "::ffff:10.42.1.9", true),
("10.0.0.0/8", "::ffff:11.42.1.9", false),
("0.0.0.0/0", "::ffff:203.0.113.9", true),
("192.0.2.9/32", "::ffff:192.0.2.9", true),
("192.0.2.9/32", "::ffff:192.0.2.10", false),
] {
let rule = BypassRule::compile(network).unwrap();
assert_eq!(
rule.matches(None, Host::try_from(host).unwrap().view(), None),
expected,
"network={network} host={host}"
);
}
let ipv6 = BypassRule::compile("2001:db8::/32").unwrap();
assert!(ipv6.matches(None, Host::try_from("2001:db8::1").unwrap().view(), None));
assert!(!ipv6.matches(
None,
Host::try_from("::ffff:192.0.2.9").unwrap().view(),
None
));
}
#[test]
fn exact_ip_rules_use_canonical_network_semantics() {
let exact = BypassRule::compile("192.0.2.9").unwrap();
assert!(exact.matches(
None,
Host::try_from("::ffff:192.0.2.9").unwrap().view(),
None,
));
assert!(!exact.matches(
None,
Host::try_from("::ffff:192.0.2.10").unwrap().view(),
None,
));
let bracketed = BypassRule::compile("[::1]").unwrap();
assert!(bracketed.matches(None, Host::try_from("::1").unwrap().view(), None));
assert!(!bracketed.matches(None, Host::try_from("::2").unwrap().view(), None));
}
#[test]
fn wildcard_prefixed_non_subtree_patterns_fall_back_to_globs() {
let rule = BypassRule::compile("*.corp*").unwrap();
assert!(rule.matches(None, Host::try_from("api.corporate").unwrap().view(), None,));
assert!(!rule.matches(None, Host::try_from("corp.example").unwrap().view(), None,));
}
#[test]
fn glib_domains_match_the_apex_and_descendants() {
for pattern in ["example.com", ".example.com", "*.example.com"] {
let rule = BypassRule::compile_with_dialect(pattern, BypassRuleDialect::Glib).unwrap();
assert!(rule.matches(None, Host::try_from("example.com").unwrap().view(), None));
assert!(rule.matches(
None,
Host::try_from("api.example.com").unwrap().view(),
None,
));
assert!(!rule.matches(None, Host::try_from("other.test").unwrap().view(), None,));
}
let glob = BypassRule::compile_with_dialect("*.corp*", BypassRuleDialect::Glib).unwrap();
assert!(glob.matches(None, Host::try_from("api.corporate").unwrap().view(), None,));
}
#[test]
fn flat_glob_platform_domains_do_not_add_the_apex() {
for pattern in [".example.com", "*.example.com"] {
let rule =
BypassRule::compile_with_dialect(pattern, BypassRuleDialect::FlatGlob).unwrap();
assert!(!rule.matches(None, Host::try_from("example.com").unwrap().view(), None));
assert!(rule.matches(
None,
Host::try_from("api.example.com").unwrap().view(),
None,
));
}
let exact =
BypassRule::compile_with_dialect("example.com", BypassRuleDialect::FlatGlob).unwrap();
assert!(exact.matches(None, Host::try_from("example.com").unwrap().view(), None));
assert!(!exact.matches(
None,
Host::try_from("api.example.com").unwrap().view(),
None,
));
}
#[test]
fn dialects_keep_their_distinct_apex_and_descendant_outcomes() {
let apex = Host::try_from("example.com").unwrap();
let child = Host::try_from("api.example.com").unwrap();
let grandchild = Host::try_from("v1.api.example.com").unwrap();
for (dialect, pattern, apex_matches, child_matches) in [
(BypassRuleDialect::Rama, "example.com", true, false),
(BypassRuleDialect::Rama, "*.example.com", true, true),
(BypassRuleDialect::Rama, ".example.com", true, true),
(BypassRuleDialect::NoProxy, "example.com", true, true),
(BypassRuleDialect::NoProxy, "*.example.com", true, true),
(BypassRuleDialect::NoProxy, ".example.com", true, true),
(BypassRuleDialect::Glib, "example.com", true, true),
(BypassRuleDialect::Glib, "*.example.com", true, true),
(BypassRuleDialect::Glib, ".example.com", true, true),
(BypassRuleDialect::Kde, "example.com", true, true),
(BypassRuleDialect::Kde, ".example.com", true, true),
(BypassRuleDialect::FlatGlob, "example.com", true, false),
(BypassRuleDialect::FlatGlob, "*.example.com", false, true),
(BypassRuleDialect::FlatGlob, ".example.com", false, true),
] {
let rule = BypassRule::compile_with_dialect(pattern, dialect).unwrap();
assert_eq!(
rule.matches(None, apex.view(), None),
apex_matches,
"dialect={dialect:?} pattern={pattern:?} apex"
);
assert_eq!(
rule.matches(None, child.view(), None),
child_matches,
"dialect={dialect:?} pattern={pattern:?} child"
);
assert_eq!(
rule.matches(None, grandchild.view(), None),
child_matches,
"dialect={dialect:?} pattern={pattern:?} grandchild"
);
}
for pattern in ["*", "*.example.com", "api-?.example.com"] {
BypassRule::compile_with_dialect(pattern, BypassRuleDialect::Kde).unwrap_err();
}
}
#[test]
fn standalone_wildcard_matches_every_host() {
let rule = BypassRule::compile("*").unwrap();
for host in ["example.com", "127.0.0.1", "2001:db8::1"] {
assert!(rule.matches(None, Host::try_from(host).unwrap().view(), None));
}
}
}