use std::fmt;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use crate::Error;
pub fn is_public(ip: IpAddr) -> bool {
!denied(ip)
}
pub(crate) fn denied(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => v4_denied(v4),
IpAddr::V6(v6) => v6_denied(v6),
}
}
fn v4_denied(ip: Ipv4Addr) -> bool {
let o = ip.octets();
ip.is_private()
|| ip.is_loopback()
|| ip.is_link_local()
|| ip.is_unspecified()
|| ip.is_broadcast()
|| ip.is_multicast()
|| ip.is_documentation()
|| o[0] == 0
|| (o[0] == 100 && (o[1] & 0xc0) == 64)
|| (o[0] == 192 && o[1] == 0 && o[2] == 0)
|| (o[0] == 192 && o[1] == 88 && o[2] == 99)
|| (o[0] == 198 && (o[1] & 0xfe) == 18)
|| o[0] >= 240
}
fn v6_denied(ip: Ipv6Addr) -> bool {
if let Some(v4) = embedded_v4(ip) {
return v4_denied(v4);
}
let seg = ip.segments();
ip.is_loopback()
|| ip.is_unspecified()
|| ip.is_multicast()
|| seg[0] == 0x2002
|| (seg[0] == 0x2001 && seg[1] == 0)
|| seg[..3] == [0x64, 0xff9b, 1]
|| (seg[0] & 0xffc0) == 0xfe80
|| (seg[0] & 0xffc0) == 0xfec0
|| (seg[0] & 0xfe00) == 0xfc00
|| (seg[0] == 0x2001 && seg[1] == 0x0db8)
|| (seg[0] == 0x3fff && seg[1] < 0x1000)
|| seg[..3] == [0x2001, 2, 0]
|| seg[..4] == [0x100, 0, 0, 0]
|| seg[0] == 0x5f00
}
pub(crate) fn embedded_v4(ip: Ipv6Addr) -> Option<Ipv4Addr> {
if let Some(v4) = ip.to_ipv4_mapped() {
return Some(v4);
}
let seg = ip.segments();
let compatible = seg[..6] == [0; 6] && !ip.is_loopback() && !ip.is_unspecified();
let siit = seg[..6] == [0, 0, 0, 0, 0xffff, 0];
if compatible || siit || seg[..6] == [0x64, 0xff9b, 0, 0, 0, 0] {
let [.., a, b, c, d] = ip.octets();
return Some(Ipv4Addr::new(a, b, c, d));
}
None
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Cidr {
addr: IpAddr,
prefix: u8,
}
impl Cidr {
pub fn new(addr: IpAddr, prefix: u8) -> Result<Cidr, Error> {
let max = if addr.is_ipv4() { 32 } else { 128 };
if prefix > max {
return Err(Error::InvalidCidr(format!("{addr}/{prefix}")));
}
let addr = match addr {
IpAddr::V4(a) => IpAddr::V4(Ipv4Addr::from(u32::from(a) & mask32(prefix))),
IpAddr::V6(a) => IpAddr::V6(Ipv6Addr::from(u128::from(a) & mask128(prefix))),
};
Ok(Cidr { addr, prefix })
}
pub fn addr(&self) -> IpAddr {
self.addr
}
pub fn prefix(&self) -> u8 {
self.prefix
}
pub fn contains(&self, ip: IpAddr) -> bool {
match (self.addr, ip) {
(IpAddr::V4(net), IpAddr::V4(ip)) => self.holds_v4(net, ip),
(IpAddr::V4(net), IpAddr::V6(ip)) => {
embedded_v4(ip).is_some_and(|ip| self.holds_v4(net, ip))
}
(IpAddr::V6(net), IpAddr::V6(ip)) => {
u128::from(ip) & mask128(self.prefix) == u128::from(net)
}
(IpAddr::V6(_), IpAddr::V4(_)) => false,
}
}
fn holds_v4(&self, net: Ipv4Addr, ip: Ipv4Addr) -> bool {
u32::from(ip) & mask32(self.prefix) == u32::from(net)
}
}
fn mask32(prefix: u8) -> u32 {
u32::MAX.checked_shl(32 - u32::from(prefix)).unwrap_or(0)
}
fn mask128(prefix: u8) -> u128 {
u128::MAX.checked_shl(128 - u32::from(prefix)).unwrap_or(0)
}
impl From<IpAddr> for Cidr {
fn from(addr: IpAddr) -> Cidr {
let prefix = if addr.is_ipv4() { 32 } else { 128 };
Cidr { addr, prefix }
}
}
impl FromStr for Cidr {
type Err = Error;
fn from_str(s: &str) -> Result<Cidr, Error> {
let bad = || Error::InvalidCidr(s.to_string());
match s.split_once('/') {
None => s.parse::<IpAddr>().map(Cidr::from).map_err(|_| bad()),
Some((addr, prefix)) => {
let addr = addr.parse::<IpAddr>().map_err(|_| bad())?;
if !prefix.bytes().all(|b| b.is_ascii_digit()) {
return Err(bad());
}
let prefix = prefix.parse::<u8>().map_err(|_| bad())?;
Cidr::new(addr, prefix).map_err(|_| bad())
}
}
}
}
impl fmt::Display for Cidr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.addr, self.prefix)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deny_table() {
for bad in [
"10.1.2.3",
"10.255.255.254",
"172.16.5.4",
"192.168.1.1",
"127.0.0.1",
"169.254.169.254",
"100.64.0.1",
"100.127.255.254",
"0.0.0.0",
"0.1.2.3",
"224.0.0.1",
"255.255.255.255",
"240.0.0.1",
"198.18.0.1",
"198.19.255.255",
"192.0.0.8",
"192.88.99.1",
"192.0.2.1",
"198.51.100.7",
"203.0.113.9",
"::1",
"::",
"fe80::1",
"fd00::1",
"fc12::3",
"ff02::1",
"fec0::1",
"feff::1",
"3fff::1",
"3fff:fff::1",
"2001:2::1",
"2001:2:0:ffff::1",
"100::1",
"::ffff:10.1.2.3",
"::ffff:127.0.0.1",
"::ffff:169.254.169.254",
"::127.0.0.1",
"64:ff9b::a01:203",
"2001:db8::1",
"2002:808:808::1",
"2002:a01:203::1",
"2001:0:4136:e378:8000:63bf:3fff:fdd2",
"2001::1",
"64:ff9b:1::808:808",
"64:ff9b:1:ffff::1",
"::ffff:0:a00:4",
"::ffff:0:7f00:1",
"5f00::1",
"5f00:ffff::1",
] {
assert!(!is_public(bad.parse().unwrap()), "{bad} should be denied");
}
for good in [
"1.1.1.1",
"8.8.8.8",
"100.128.0.1",
"100.63.255.255",
"172.32.0.1",
"198.20.0.1",
"192.0.1.1",
"2606:4700::1111",
"::ffff:8.8.8.8",
"::8.8.8.8",
"64:ff9b::808:808",
"2001:4860:4860::8888",
"2003::1",
"192.88.98.1",
"192.88.100.1",
"3ffe::1",
"3fff:1000::1",
"4000::1",
"2001:2:1::1",
"2001:3::1",
"100:0:0:1::1",
"fe7f::1",
"::ffff:0:808:808",
"5eff::1",
"5f01::1",
"2001:20::1",
"2001:30::1",
] {
assert!(is_public(good.parse().unwrap()), "{good} should be allowed");
}
}
#[test]
fn cidr_parsing() {
let net: Cidr = "10.1.2.3/8".parse().unwrap();
assert_eq!(net.to_string(), "10.0.0.0/8");
assert_eq!(net.addr().to_string(), "10.0.0.0");
assert_eq!(net.prefix(), 8);
assert_eq!(
"2001:db8::1".parse::<Cidr>().unwrap().to_string(),
"2001:db8::1/128"
);
assert_eq!(
"0.0.0.0/0".parse::<Cidr>().unwrap().to_string(),
"0.0.0.0/0"
);
for bad in [
"10.0.0.0/33",
"::/129",
"10.0.0.0/",
"10.0.0.0/+8",
"example.com",
"10.0.0/8",
"",
] {
assert!(bad.parse::<Cidr>().is_err(), "{bad} should not parse");
}
}
#[test]
fn cidr_membership() {
let net: Cidr = "203.0.113.0/24".parse().unwrap();
assert!(net.contains("203.0.113.200".parse().unwrap()));
assert!(!net.contains("203.0.114.1".parse().unwrap()));
assert!(net.contains("::ffff:203.0.113.5".parse().unwrap()));
assert!(net.contains("64:ff9b::cb00:7105".parse().unwrap()));
assert!(!net.contains("2001:db8::1".parse().unwrap()));
let all: Cidr = "0.0.0.0/0".parse().unwrap();
assert!(all.contains("8.8.8.8".parse().unwrap()));
let v6: Cidr = "2001:db8::/32".parse().unwrap();
assert!(v6.contains("2001:db8:ffff::1".parse().unwrap()));
assert!(!v6.contains("2001:db9::1".parse().unwrap()));
assert!(!v6.contains("10.1.2.3".parse().unwrap()));
let ip = |s: &str| s.parse().unwrap();
let nat64: Cidr = "64:ff9b::/96".parse().unwrap();
assert!(nat64.contains(ip("64:ff9b::808:808")));
assert!(!nat64.contains(ip("8.8.8.8")));
let mapped: Cidr = "::ffff:0:0/96".parse().unwrap();
assert!(mapped.contains(ip("::ffff:8.8.8.8")));
let no_v6: Cidr = "::/0".parse().unwrap();
assert!(no_v6.contains(ip("::ffff:8.8.8.8")));
assert!(no_v6.contains(ip("::8.8.8.8")));
assert!(!no_v6.contains(ip("8.8.8.8")));
}
}