use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[must_use]
pub fn is_public_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(ip) => is_public_v4(ip),
IpAddr::V6(ip) => is_public_v6(ip),
}
}
fn is_public_v4(ip: Ipv4Addr) -> bool {
let [a, b, c, _] = ip.octets();
!(a == 0
|| a == 10
|| a == 127
|| (a == 100 && (64..=127).contains(&b))
|| (a == 169 && b == 254)
|| (a == 172 && (16..=31).contains(&b))
|| (a == 192 && b == 0 && c == 0)
|| (a == 192 && b == 0 && c == 2)
|| (a == 192 && b == 88 && c == 99)
|| (a == 192 && b == 168)
|| (a == 198 && (b == 18 || b == 19))
|| (a == 198 && b == 51 && c == 100)
|| (a == 203 && b == 0 && c == 113)
|| a >= 224)
}
fn is_public_v6(ip: Ipv6Addr) -> bool {
let segments = ip.segments();
if let Some(mapped) = ip.to_ipv4_mapped() {
return is_public_v4(mapped);
}
!(ip.is_unspecified()
|| ip.is_loopback()
|| (segments[0] & 0xfe00) == 0xfc00
|| (segments[0] & 0xffc0) == 0xfe80
|| (segments[0] & 0xffc0) == 0xfec0
|| (segments[0] & 0xff00) == 0xff00
|| segments[0] == 0
|| (segments[0] == 0x0064 && segments[1] == 0xff9b)
|| (segments[0] == 0x0100 && segments[1] == 0)
|| (segments[0] == 0x2001 && segments[1] <= 0x01ff)
|| (segments[0] == 0x2001 && segments[1] == 0x0db8)
|| segments[0] == 0x2002
|| (segments[0] & 0xfff0) == 0x3ff0
|| segments[0] == 0x5f00)
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum NetGuardError {
#[error("DNS for '{host}' returned no addresses")]
NoAddresses { host: String },
#[error("'{host}' resolved to forbidden address {address}")]
Forbidden { host: String, address: IpAddr },
}
pub fn all_public<I>(host: &str, addresses: I) -> Result<Vec<std::net::SocketAddr>, NetGuardError>
where
I: IntoIterator<Item = std::net::SocketAddr>,
{
let addresses: Vec<_> = addresses.into_iter().collect();
if addresses.is_empty() {
return Err(NetGuardError::NoAddresses {
host: host.to_owned(),
});
}
for address in &addresses {
if !is_public_ip(address.ip()) {
return Err(NetGuardError::Forbidden {
host: host.to_owned(),
address: address.ip(),
});
}
}
let mut unique = std::collections::BTreeSet::new();
unique.extend(addresses);
Ok(unique.into_iter().collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_ranges_an_ssrf_payload_aims_at_are_refused() {
for addr in [
"127.0.0.1",
"10.0.0.1",
"172.16.0.1",
"192.168.1.1",
"169.254.169.254", "0.0.0.0",
"100.64.0.1",
"::1",
"fe80::1",
"fc00::1",
"::ffff:127.0.0.1",
] {
assert!(
!is_public_ip(addr.parse().unwrap()),
"{addr} was treated as publicly routable"
);
}
}
#[test]
fn ordinary_public_addresses_are_permitted() {
for addr in ["1.1.1.1", "93.184.216.34", "2606:4700:4700::1111"] {
assert!(
is_public_ip(addr.parse().unwrap()),
"{addr} was refused, so the guard refuses the internet"
);
}
}
#[test]
fn one_private_answer_refuses_the_whole_resolution() {
let addrs = [
"1.1.1.1:443".parse().unwrap(),
"127.0.0.1:443".parse().unwrap(),
];
assert!(
all_public("rebind.example", addrs).is_err(),
"a resolution containing a private address was accepted because \
another answer was public"
);
}
}