Skip to main content

pacenotes_server/reports/
cidr.rs

1/// An IP network written as `10.0.0.0/24`, or a single address.
2#[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    /// An IPv4-mapped IPv6 address, as a dual-stack listener reports it, counts as IPv4.
32    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}