use std::net::{IpAddr, Ipv4Addr};
use std::sync::LazyLock;
use ipnet::IpNet;
use crate::config::FetchConfig;
pub(crate) const BLOCKED_CIDRS: &[&str] = &[
"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", "192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", "255.255.255.255/32", "::/128", "::1/128", "::/96", "::ffff:0:0/96", "64:ff9b::/96", "64:ff9b:1::/48", "100::/64", "2001:db8::/32", "2002::/16", "3fff::/20", "fc00::/7", "fe80::/10", "fec0::/10", "ff00::/8", ];
#[expect(
clippy::expect_used,
reason = "every entry in BLOCKED_CIDRS is a compile-time CIDR literal, so parsing cannot fail at runtime"
)]
static BLOCKED_NETS: LazyLock<Vec<IpNet>> = LazyLock::new(|| {
BLOCKED_CIDRS
.iter()
.map(|cidr| {
cidr.parse::<IpNet>()
.expect("every entry in BLOCKED_CIDRS is a valid CIDR literal")
})
.collect()
});
fn embedded_ipv4(ip: IpAddr) -> Option<Ipv4Addr> {
match ip {
IpAddr::V6(v6) => v6.to_ipv4(),
IpAddr::V4(_) => None,
}
}
fn builtin_blocked(ip: IpAddr) -> Option<String> {
if let Some(net) = BLOCKED_NETS.iter().find(|net| net.contains(&ip)) {
return Some(net.to_string());
}
if let Some(v4) = embedded_ipv4(ip) {
let v4 = IpAddr::V4(v4);
if let Some(net) = BLOCKED_NETS.iter().find(|net| net.contains(&v4)) {
return Some(format!("{net} (embedded ipv4 {v4})"));
}
}
None
}
#[must_use]
pub(crate) fn blocked_range(ip: IpAddr, config: &FetchConfig) -> Option<String> {
if let Some(range) = builtin_blocked(ip) {
return Some(range);
}
if let Some(net) = config.deny_extra().iter().find(|net| net.contains(&ip)) {
return Some(format!("deny_extra {net}"));
}
if let Some(v4) = embedded_ipv4(ip) {
let v4 = IpAddr::V4(v4);
if let Some(net) = config.deny_extra().iter().find(|net| net.contains(&v4)) {
return Some(format!("deny_extra {net} (embedded ipv4 {v4})"));
}
}
None
}
#[must_use]
pub(crate) fn addr_allowed(ip: IpAddr, config: &FetchConfig) -> bool {
blocked_range(ip, config).is_none()
}
#[must_use]
pub(crate) fn addr_allowed_for_host(host: &str, ip: IpAddr, config: &FetchConfig) -> bool {
if config
.allow_exact()
.iter()
.any(|entry| entry.matches(host, ip))
{
return true;
}
addr_allowed(ip, config)
}
#[cfg(test)]
mod tests {
use std::net::IpAddr;
use super::{BLOCKED_CIDRS, BLOCKED_NETS, addr_allowed, addr_allowed_for_host};
use crate::config::FetchConfig;
fn ip(s: &str) -> IpAddr {
s.parse().expect("test address literal must parse")
}
#[test]
fn every_blocked_cidr_parses() {
assert_eq!(
BLOCKED_NETS.len(),
BLOCKED_CIDRS.len(),
"all blocked CIDR literals must parse"
);
}
#[test]
fn classification_matrix() {
let cfg = FetchConfig::default();
let boundaries: &[(&str, &str, &str)] = &[
("0.0.0.0/8", "0.0.0.0", "0.255.255.255"),
("10.0.0.0/8", "10.0.0.0", "10.255.255.255"),
("100.64.0.0/10", "100.64.0.0", "100.127.255.255"),
("127.0.0.0/8", "127.0.0.0", "127.255.255.255"),
("169.254.0.0/16", "169.254.0.0", "169.254.255.255"),
("172.16.0.0/12", "172.16.0.0", "172.31.255.255"),
("192.0.0.0/24", "192.0.0.0", "192.0.0.255"),
("192.0.2.0/24", "192.0.2.0", "192.0.2.255"),
("192.88.99.0/24", "192.88.99.0", "192.88.99.255"),
("192.168.0.0/16", "192.168.0.0", "192.168.255.255"),
("198.18.0.0/15", "198.18.0.0", "198.19.255.255"),
("198.51.100.0/24", "198.51.100.0", "198.51.100.255"),
("203.0.113.0/24", "203.0.113.0", "203.0.113.255"),
("224.0.0.0/4", "224.0.0.0", "239.255.255.255"),
("240.0.0.0/4", "240.0.0.0", "255.255.255.254"),
("255.255.255.255/32", "255.255.255.255", "255.255.255.255"),
("::/128", "::", "::"),
("::1/128", "::1", "::1"),
("::/96", "::0.0.0.1", "::255.255.255.255"),
("::ffff:0:0/96", "::ffff:0.0.0.0", "::ffff:255.255.255.255"),
("64:ff9b::/96", "64:ff9b::", "64:ff9b::ffff:ffff"),
(
"64:ff9b:1::/48",
"64:ff9b:1::",
"64:ff9b:1:ffff:ffff:ffff:ffff:ffff",
),
("100::/64", "100::", "100::ffff:ffff:ffff:ffff"),
(
"2001:db8::/32",
"2001:db8::",
"2001:db8:ffff:ffff:ffff:ffff:ffff:ffff",
),
(
"2002::/16",
"2002::",
"2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
),
(
"3fff::/20",
"3fff::",
"3fff:fff:ffff:ffff:ffff:ffff:ffff:ffff",
),
(
"fc00::/7",
"fc00::",
"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
),
(
"fe80::/10",
"fe80::",
"febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
),
(
"fec0::/10",
"fec0::",
"feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
),
(
"ff00::/8",
"ff00::",
"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
),
];
for (range, first, last) in boundaries {
assert!(
!addr_allowed(ip(first), &cfg),
"{first} (first of {range}) must be denied"
);
assert!(
!addr_allowed(ip(last), &cfg),
"{last} (last of {range}) must be denied"
);
}
}
#[test]
fn classification_embedded_and_allowed() {
let cfg = FetchConfig::default();
let embedded_blocked = [
"::127.0.0.1", "::10.0.0.1", "::169.254.169.254", "::1.1.1.1", "::ffff:127.0.0.1", "::ffff:10.0.0.1", "::ffff:169.254.169.254", "::ffff:1.1.1.1", "64:ff9b::7f00:1", ];
for addr in embedded_blocked {
assert!(!addr_allowed(ip(addr), &cfg), "{addr} must be denied");
}
let allowed = [
"1.1.1.1",
"8.8.8.8",
"93.184.216.34",
"11.0.0.0", "172.32.0.0", "223.255.255.255", "2606:4700:4700::1111",
"2001:4860:4860::8888",
"2001:db9::1", "3fff:1000::1", ];
for addr in allowed {
assert!(addr_allowed(ip(addr), &cfg), "{addr} must be allowed");
}
}
#[test]
fn allow_exact_admits_only_the_named_host() {
let target = ip("127.0.0.1");
let cfg = FetchConfig::builder()
.allow_host_address("localhost", target)
.build()
.expect("valid config");
assert!(
addr_allowed_for_host("localhost", target, &cfg),
"the exact (host, addr) pair must be admitted"
);
assert!(
!addr_allowed_for_host("evil.com", target, &cfg),
"a different host must not inherit the allow_exact exception"
);
assert!(
!addr_allowed(target, &cfg),
"addr_allowed alone must grant no allow_exact bypass"
);
assert!(
!addr_allowed_for_host("localhost", ip("127.0.0.2"), &cfg),
"allow_exact must not widen to the rest of the range"
);
}
#[test]
fn deny_extra_blocks_an_otherwise_public_address() {
let cfg = FetchConfig::builder()
.deny_cidr("203.0.114.0/24")
.build()
.expect("valid config");
assert!(
!addr_allowed(ip("203.0.114.5"), &cfg),
"a deny_extra range must block an otherwise-public address"
);
}
}