pacenotes_server/reports/
cidr.rs1#[derive(Clone, Debug, PartialEq, Eq)]
3pub struct Cidr {
4 network: std::net::IpAddr,
5 prefix: u8,
6}
7
8impl Cidr {
9 pub fn parse(text: &str) -> Result<Cidr, String> {
10 let text = text.trim();
11 let (address, prefix) = match text.split_once('/') {
12 Some((address, prefix)) => (address, Some(prefix)),
13 None => (text, None),
14 };
15 let network: std::net::IpAddr = address
16 .parse()
17 .map_err(|_| format!("{text:?} is not an IP address or CIDR"))?;
18 let network = network.to_canonical();
19 let max = if network.is_ipv4() { 32 } else { 128 };
20 let prefix = match prefix {
21 None => max,
22 Some(prefix) => prefix
23 .parse::<u8>()
24 .ok()
25 .filter(|prefix| *prefix <= max)
26 .ok_or_else(|| format!("{text:?}: prefix must be 0-{max}"))?,
27 };
28 Ok(Cidr { network, prefix })
29 }
30
31 pub fn contains(&self, ip: std::net::IpAddr) -> bool {
33 match (self.network, ip.to_canonical()) {
34 (std::net::IpAddr::V4(network), std::net::IpAddr::V4(ip)) => {
35 let mask = u32::MAX
36 .checked_shl(32 - u32::from(self.prefix))
37 .unwrap_or(0);
38 u32::from(network) & mask == u32::from(ip) & mask
39 }
40 (std::net::IpAddr::V6(network), std::net::IpAddr::V6(ip)) => {
41 let mask = u128::MAX
42 .checked_shl(128 - u32::from(self.prefix))
43 .unwrap_or(0);
44 u128::from(network) & mask == u128::from(ip) & mask
45 }
46 _ => false,
47 }
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 fn ip(text: &str) -> std::net::IpAddr {
54 text.parse().unwrap()
55 }
56
57 #[test]
58 fn a_network_matches_its_addresses_and_nothing_else() {
59 let network = super::Cidr::parse("10.0.0.0/24").unwrap();
60 assert!(network.contains(ip("10.0.0.90")));
61 assert!(!network.contains(ip("10.0.1.1")));
62 assert!(network.contains(ip("::ffff:10.0.0.90")));
63 assert!(!network.contains(ip("2001:db8::1")));
64
65 let v6 = super::Cidr::parse("fd00::/8").unwrap();
66 assert!(v6.contains(ip("fd12::1")));
67 }
68
69 #[test]
70 fn a_bare_address_matches_only_itself() {
71 let host = super::Cidr::parse("10.0.0.90").unwrap();
72 assert!(host.contains(ip("10.0.0.90")));
73 assert!(!host.contains(ip("10.0.0.91")));
74 }
75
76 #[test]
77 fn malformed_networks_are_refused() {
78 for bad in ["10.0.0.0/33", "caddy", "10.0.0.0/x"] {
79 assert!(super::Cidr::parse(bad).is_err(), "{bad}");
80 }
81 }
82}