use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
pub fn split_host_port(addr: &str) -> (&str, Option<u16>) {
if let Some(rest) = addr.strip_prefix('[') {
if let Some(end) = rest.find(']') {
let host = &rest[..end];
let port = rest[end + 1..].strip_prefix(':').and_then(|p| p.parse().ok());
return (host, port);
}
return (addr, None); }
if addr.matches(':').count() > 1 {
return (addr, None);
}
match addr.rsplit_once(':') {
Some((host, port)) => match port.parse() {
Ok(p) => (host, Some(p)),
Err(_) => (addr, None),
},
None => (addr, None),
}
}
fn restricted_v4(v4: Ipv4Addr) -> bool {
v4.is_private()
|| v4.is_loopback()
|| v4.is_link_local()
|| v4.is_multicast()
|| v4.is_broadcast()
|| v4.is_unspecified()
|| v4.is_documentation()
|| v4.octets()[0] == 0 || (v4.octets()[0] == 100 && (v4.octets()[1] & 0xc0) == 64) }
fn restricted_v6(v6: Ipv6Addr) -> bool {
if let Some(mapped) = v6.to_ipv4_mapped() {
return restricted_v4(mapped);
}
v6.is_loopback()
|| v6.is_multicast()
|| v6.is_unspecified()
|| (v6.segments()[0] & 0xfe00) == 0xfc00 || (v6.segments()[0] & 0xffc0) == 0xfe80 }
pub fn is_restricted(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => restricted_v4(v4),
IpAddr::V6(v6) => restricted_v6(v6),
}
}
pub fn ip_allowed(ip: IpAddr, allowed_networks: &[ipnetwork::IpNetwork]) -> bool {
if !is_restricted(ip) {
return true;
}
let canonical = match ip {
IpAddr::V6(v6) => v6.to_ipv4_mapped().map(IpAddr::V4).unwrap_or(ip),
v4 => v4,
};
allowed_networks
.iter()
.any(|net| net.contains(ip) || net.contains(canonical))
}
pub fn parse_networks(allowed: Option<&[String]>) -> Vec<ipnetwork::IpNetwork> {
allowed
.unwrap_or(&[])
.iter()
.filter_map(|c| c.parse().ok())
.collect()
}
pub fn is_upstream_allowed(addr: &str, allowed_networks: Option<&[String]>) -> anyhow::Result<()> {
let (host, _) = split_host_port(addr);
if let Ok(ip) = host.parse::<IpAddr>() {
let nets = parse_networks(allowed_networks);
if !ip_allowed(ip, &nets) {
anyhow::bail!(
"Upstream {} is in a restricted range and not in upstream_allowed_networks",
addr
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loopback_rejected_when_no_allowlist() {
assert!(is_upstream_allowed("127.0.0.1:9081", None).is_err());
}
#[test]
fn loopback_allowed_when_in_allowlist() {
let nets = vec!["127.0.0.0/8".to_string()];
assert!(is_upstream_allowed("127.0.0.1:9081", Some(&nets)).is_ok());
}
#[test]
fn loopback_rejected_when_not_in_allowlist() {
let nets = vec!["10.0.0.0/8".to_string()];
assert!(is_upstream_allowed("127.0.0.1:9081", Some(&nets)).is_err());
}
#[test]
fn private_range_rejected_when_no_allowlist() {
assert!(is_upstream_allowed("192.168.1.10:80", None).is_err());
}
#[test]
fn public_ip_allowed() {
assert!(is_upstream_allowed("93.184.216.34:80", None).is_ok());
}
#[test]
fn hostname_allowed() {
assert!(is_upstream_allowed("backend.internal:8080", None).is_ok());
}
#[test]
fn ipv4_mapped_loopback_rejected() {
assert!(is_upstream_allowed("[::ffff:127.0.0.1]:80", None).is_err());
assert!(is_upstream_allowed("::ffff:127.0.0.1", None).is_err());
}
#[test]
fn ipv4_mapped_loopback_allowed_via_v4_allowlist() {
let nets = vec!["127.0.0.0/8".to_string()];
assert!(is_upstream_allowed("[::ffff:127.0.0.1]:80", Some(&nets)).is_ok());
}
#[test]
fn bracketed_v6_loopback_rejected() {
assert!(is_upstream_allowed("[::1]:80", None).is_err());
assert!(is_upstream_allowed("::1", None).is_err());
}
#[test]
fn v6_unique_local_and_link_local_rejected() {
assert!(is_upstream_allowed("[fc00::1]:80", None).is_err());
assert!(is_upstream_allowed("[fd12:3456::1]:80", None).is_err());
assert!(is_upstream_allowed("[fe80::1]:80", None).is_err());
}
#[test]
fn cgnat_and_zero_net_rejected() {
assert!(is_upstream_allowed("100.64.0.1:80", None).is_err());
assert!(is_upstream_allowed("0.0.0.0:80", None).is_err());
}
#[test]
fn split_host_port_forms() {
assert_eq!(split_host_port("1.2.3.4:80"), ("1.2.3.4", Some(80)));
assert_eq!(split_host_port("1.2.3.4"), ("1.2.3.4", None));
assert_eq!(split_host_port("[::1]:80"), ("::1", Some(80)));
assert_eq!(split_host_port("[::1]"), ("::1", None));
assert_eq!(split_host_port("::1"), ("::1", None));
assert_eq!(split_host_port("host.example:8080"), ("host.example", Some(8080)));
assert_eq!(split_host_port("host.example"), ("host.example", None));
}
}
#[cfg(all(test, not(miri)))]
mod prop_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn loopback_gated_by_allowlist(a in 0u8..=255, b in 0u8..=255, c in 1u8..=254, port in 1u16..=65535) {
let addr = format!("127.{}.{}.{}:{}", a, b, c, port);
prop_assert!(is_upstream_allowed(&addr, None).is_err());
let nets = vec!["127.0.0.0/8".to_string()];
prop_assert!(is_upstream_allowed(&addr, Some(&nets)).is_ok());
}
#[test]
fn mapped_loopback_gated_by_allowlist(c in 1u8..=254, port in 1u16..=65535) {
let addr = format!("[::ffff:127.0.0.{}]:{}", c, port);
prop_assert!(is_upstream_allowed(&addr, None).is_err());
let nets = vec!["127.0.0.0/8".to_string()];
prop_assert!(is_upstream_allowed(&addr, Some(&nets)).is_ok());
}
}
}