Skip to main content

micromegas_auth/
ip_allowlist.rs

1//! A typed, allowlist-only IP allowlist: a set of CIDR ranges (or bare IPs, treated as a `/32` or
2//! `/128`) a credential may be presented from. An empty allowlist means "no restriction" -- the
3//! backward-compatible default for every key minted before this feature existed.
4
5use anyhow::{Context, Result};
6use ipnet::IpNet;
7use std::net::IpAddr;
8
9/// A set of CIDR ranges (or bare IPs) a credential may be used from.
10///
11/// Empty (the `Default`/`parse(&[])` result) means "no restriction": [`Self::allows`] always
12/// returns `true`. Once non-empty, an unresolved client IP (`None`) never satisfies the
13/// restriction -- "unknown" must never be treated as "allowed".
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct IpAllowlist(Vec<IpNet>);
16
17impl IpAllowlist {
18    /// Parses `entries` (CIDR notation, e.g. `"10.0.0.0/8"`, or a bare IP, e.g.
19    /// `"203.0.113.7"`) into an [`IpAllowlist`]. An empty slice parses to an empty (unrestricted)
20    /// allowlist. Fails on the first entry that is neither a valid CIDR nor a valid bare IP
21    /// address.
22    ///
23    /// `IpNet::from_str` doesn't accept a bare IP without a prefix, so this falls back to
24    /// parsing a bare `IpAddr` and widening it to a `/32`/`/128` via `IpNet::from`.
25    ///
26    /// Also rejects a CIDR whose address has host bits set (e.g. `10.0.0.5/8`): `ipnet` parses
27    /// it successfully but silently widens the range to the whole `/8`, which is the opposite of
28    /// what an operator narrowing an allowlist intends.
29    pub fn parse(entries: &[String]) -> Result<Self> {
30        let mut nets = Vec::with_capacity(entries.len());
31        for entry in entries {
32            let net = entry
33                .parse::<IpNet>()
34                .or_else(|_| entry.parse::<IpAddr>().map(IpNet::from))
35                .with_context(|| format!("invalid CIDR or IP address: {entry:?}"))?;
36            let net = canonicalize(net);
37            if net.addr() != net.network() {
38                anyhow::bail!(
39                    "CIDR entry {entry:?} has host bits set; did you mean {:?}?",
40                    net.trunc().to_string()
41                );
42            }
43            nets.push(net);
44        }
45        Ok(Self(nets))
46    }
47
48    /// `true` when `ip` is permitted by this allowlist: always `true` when the allowlist is
49    /// empty (no restriction); otherwise `true` only when `ip` is `Some` and falls within at
50    /// least one stored network. An unresolved client IP (`None`) never satisfies a non-empty
51    /// allowlist.
52    pub fn allows(&self, ip: Option<IpAddr>) -> bool {
53        if self.0.is_empty() {
54            return true;
55        }
56        match ip {
57            Some(ip) => self.0.iter().any(|net| net.contains(&ip)),
58            None => false,
59        }
60    }
61}
62
63/// Downgrades an IPv4-mapped IPv6 entry (e.g. `::ffff:203.0.113.7/128`, `::ffff:203.0.113.0/120`,
64/// or a bare `::ffff:203.0.113.7`) to its IPv4 form. `resolve_client_ip` always canonicalizes the
65/// client IP it checks against (`to_canonical()`), so an unnormalized V6-mapped entry here
66/// would never match: `ipnet`'s `contains` treats a V6 net and a V4 address as different
67/// families regardless of value. The mapped prefix `::ffff:0:0/96` is exactly the fixed 96 bits
68/// ahead of the embedded v4 address, so any prefix `>= 96` has a well-defined v4 equivalent at
69/// `prefix_len - 96`.
70fn canonicalize(net: IpNet) -> IpNet {
71    if net.prefix_len() >= 96 {
72        let canonical = net.addr().to_canonical();
73        if canonical.is_ipv4() {
74            return IpNet::new(canonical, net.prefix_len() - 96)
75                .expect("a v4 address always accepts a prefix in 0..=32");
76        }
77    }
78    net
79}
80
81impl Default for IpAllowlist {
82    /// The empty, unrestricted allowlist.
83    fn default() -> Self {
84        Self(Vec::new())
85    }
86}