use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
pub fn is_blocked_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => is_blocked_ipv4(v4),
IpAddr::V6(v6) => match v6.to_ipv4_mapped() {
Some(v4) => is_blocked_ipv4(v4),
None => is_blocked_ipv6(v6),
},
}
}
fn is_blocked_ipv4(v4: Ipv4Addr) -> bool {
v4.is_loopback()
|| v4.is_private()
|| v4.is_link_local()
|| v4.is_broadcast()
|| v4.is_documentation()
|| v4.is_unspecified()
|| (v4.octets()[0] == 100 && (v4.octets()[1] & 0xc0) == 0x40)
|| v4.octets()[0] == 0
}
fn is_blocked_ipv6(v6: Ipv6Addr) -> bool {
let seg = v6.segments();
v6.is_loopback()
|| v6.is_unspecified()
|| (seg[0] & 0xfe00) == 0xfc00
|| (seg[0] & 0xffc0) == 0xfe80
|| (seg[0] == 0x0064 && seg[1] == 0xff9b && seg[2] == 0 && seg[3] == 0 && seg[4] == 0 && seg[5] == 0)
|| seg[0] == 0x2002
|| (seg[0] == 0 && seg[1] == 0 && seg[2] == 0 && seg[3] == 0 && seg[4] == 0 && seg[5] == 0)
}
pub fn blocked_ip_literal(host: &str) -> Option<bool> {
host.parse::<IpAddr>().ok().map(is_blocked_ip)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metadata_endpoint_is_blocked() {
assert!(is_blocked_ip("169.254.169.254".parse().unwrap()));
}
#[test]
fn loopback_is_blocked() {
assert!(is_blocked_ip("127.0.0.1".parse().unwrap()));
assert!(is_blocked_ip("::1".parse().unwrap()));
}
#[test]
fn rfc1918_is_blocked() {
assert!(is_blocked_ip("10.0.0.1".parse().unwrap()));
assert!(is_blocked_ip("172.16.5.4".parse().unwrap()));
assert!(is_blocked_ip("192.168.1.1".parse().unwrap()));
}
#[test]
fn link_local_and_cgnat_are_blocked() {
assert!(is_blocked_ip("169.254.1.1".parse().unwrap()));
assert!(is_blocked_ip("100.64.0.1".parse().unwrap()));
assert!(is_blocked_ip("fe80::1".parse().unwrap()));
assert!(is_blocked_ip("fc00::1".parse().unwrap()));
}
#[test]
fn ipv4_mapped_v6_cannot_smuggle_blocked_v4() {
assert!(is_blocked_ip("::ffff:127.0.0.1".parse().unwrap()));
assert!(is_blocked_ip("::ffff:169.254.169.254".parse().unwrap()));
}
#[test]
fn this_network_0_0_0_0_8_is_blocked() {
assert!(is_blocked_ip("0.0.0.0".parse().unwrap()));
assert!(is_blocked_ip("0.1.2.3".parse().unwrap()));
assert!(is_blocked_ip("0.255.255.255".parse().unwrap()));
}
#[test]
fn nat64_well_known_prefix_is_blocked() {
assert!(is_blocked_ip("64:ff9b::a00:1".parse().unwrap()));
assert!(is_blocked_ip("64:ff9b::a9fe:a9fe".parse().unwrap()));
assert!(is_blocked_ip("64:ff9b::".parse().unwrap()));
}
#[test]
fn six_to_four_2002_16_is_blocked() {
assert!(is_blocked_ip("2002:0a00:0001::1".parse().unwrap()));
assert!(is_blocked_ip("2002:a9fe:a9fe::1".parse().unwrap()));
}
#[test]
fn ipv4_compatible_ipv6_is_blocked() {
assert!(is_blocked_ip("::a00:1".parse().unwrap())); assert!(is_blocked_ip("::a9fe:a9fe".parse().unwrap())); assert!(is_blocked_ip("::808:808".parse().unwrap())); }
#[test]
fn public_ips_are_allowed() {
assert!(!is_blocked_ip("1.1.1.1".parse().unwrap()));
assert!(!is_blocked_ip("8.8.8.8".parse().unwrap()));
assert!(!is_blocked_ip("2606:4700:4700::1111".parse().unwrap()));
assert!(!is_blocked_ip("2001:4860:4860::8888".parse().unwrap()));
}
#[test]
fn blocked_ip_literal_distinguishes_names() {
assert_eq!(blocked_ip_literal("169.254.169.254"), Some(true));
assert_eq!(blocked_ip_literal("8.8.8.8"), Some(false));
assert_eq!(blocked_ip_literal("api.openai.com"), None);
}
}