Skip to main content

biscuit_ssh_core/
net.rs

1//! IP networks in canonical form.
2//!
3//! Biscuit Datalog has no IP type, so network membership is expressed with
4//! strings: the verifier provides one `source_net` fact per network enclosing
5//! the client address, and tokens check for the networks they allow. This only
6//! works if both sides spell a network the same way, hence [`Network`] always
7//! clears host bits and uses the standard textual form of the address.
8
9use std::fmt;
10use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
11use std::str::FromStr;
12
13use crate::Error;
14
15/// An IP network with its host bits cleared, displayed as `addr/prefix`
16/// (e.g. `10.0.0.0/8`, `2001:db8::/32`).
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct Network {
19    addr: IpAddr,
20    prefix: u8,
21}
22
23impl Network {
24    /// Builds the network of length `prefix` containing `addr`.
25    ///
26    /// IPv4-mapped IPv6 addresses (`::ffff:a.b.c.d`) are treated as IPv4, so
27    /// a client reaching a dual-stack listener is matched against IPv4
28    /// networks: `::ffff:192.0.2.0/120` is `192.0.2.0/24`. A mapped network
29    /// shorter than `/96` spans more than IPv4 and is refused.
30    pub fn new(addr: IpAddr, prefix: u8) -> Result<Self, Error> {
31        let (addr, prefix) = match addr {
32            IpAddr::V6(a) => match a.to_ipv4_mapped() {
33                Some(_) if prefix < 96 => {
34                    return Err(Error::invalid(
35                        "network",
36                        "IPv4-mapped networks need a prefix of at least 96",
37                    ));
38                }
39                Some(v4) => (IpAddr::V4(v4), prefix - 96),
40                None => (addr, prefix),
41            },
42            IpAddr::V4(_) => (addr, prefix),
43        };
44        if prefix > max_prefix(addr) {
45            return Err(Error::invalid("network", "prefix is longer than the address"));
46        }
47        Ok(Self::masked(addr, prefix))
48    }
49
50    /// Returns every network containing `addr`, from `/0` to the host itself
51    /// (`/32` or `/128`).
52    pub fn enclosing(addr: IpAddr) -> impl Iterator<Item = Self> {
53        let addr = addr.to_canonical();
54        (0..=max_prefix(addr)).map(move |prefix| Self::masked(addr, prefix))
55    }
56
57    /// The host network (`/32` or `/128`) of `addr`.
58    pub fn host(addr: IpAddr) -> Self {
59        let addr = addr.to_canonical();
60        Self::masked(addr, max_prefix(addr))
61    }
62
63    /// Clears the host bits of `addr`, which must already be canonical and
64    /// have at least `prefix` bits.
65    fn masked(addr: IpAddr, prefix: u8) -> Self {
66        let addr = match addr {
67            // The mask of a 32-bit width fits in a `u32`.
68            IpAddr::V4(a) => IpAddr::V4(Ipv4Addr::from_bits(a.to_bits() & mask(prefix, 32) as u32)),
69            IpAddr::V6(a) => IpAddr::V6(Ipv6Addr::from_bits(a.to_bits() & mask(prefix, 128))),
70        };
71        Self { addr, prefix }
72    }
73}
74
75impl fmt::Display for Network {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        write!(f, "{}/{}", self.addr, self.prefix)
78    }
79}
80
81impl FromStr for Network {
82    type Err = Error;
83
84    /// Parses `addr/prefix`; a bare address is read as a host network.
85    fn from_str(s: &str) -> Result<Self, Error> {
86        let invalid = || Error::invalid("network", "expected an address or addr/prefix");
87        match s.split_once('/') {
88            Some((addr, prefix)) => Self::new(
89                addr.parse().map_err(|_| invalid())?,
90                prefix.parse().map_err(|_| invalid())?,
91            ),
92            None => Ok(Self::host(s.parse().map_err(|_| invalid())?)),
93        }
94    }
95}
96
97fn max_prefix(addr: IpAddr) -> u8 {
98    if addr.is_ipv4() { 32 } else { 128 }
99}
100
101/// A mask with the `prefix` high bits set, over a `width`-bit address.
102fn mask(prefix: u8, width: u32) -> u128 {
103    match u32::from(prefix) {
104        0 => 0,
105        p => (u128::MAX << (128 - p)) >> (128 - width),
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    fn net(s: &str) -> String {
114        s.parse::<Network>().unwrap().to_string()
115    }
116
117    #[test]
118    fn canonicalizes_host_bits() {
119        assert_eq!(net("10.1.2.3/8"), "10.0.0.0/8");
120        assert_eq!(net("2001:db8:1::1/32"), "2001:db8::/32");
121        assert_eq!(net("192.0.2.7"), "192.0.2.7/32");
122        assert_eq!(net("0.0.0.0/0"), "0.0.0.0/0");
123        assert_eq!(net("::ffff:192.0.2.7/120"), "192.0.2.0/24");
124        assert_eq!(net("::ffff:192.0.2.7"), "192.0.2.7/32");
125        assert_eq!(net("2001:db8::1"), "2001:db8::1/128");
126    }
127
128    #[test]
129    fn rejects_invalid_networks() {
130        for s in [
131            "10.0.0.0/33",
132            "::/129",
133            "::ffff:192.0.2.7/24",
134            "::ffff:192.0.2.7/129",
135            "10.0.0/8",
136            "10.0.0.0/",
137            "host",
138            "",
139        ] {
140            assert!(s.parse::<Network>().is_err(), "{s}");
141        }
142    }
143
144    #[test]
145    fn enumerates_enclosing_networks() {
146        let v4: Vec<_> = Network::enclosing("192.0.2.7".parse().unwrap())
147            .map(|n| n.to_string())
148            .collect();
149        assert_eq!(v4.len(), 33);
150        assert_eq!(v4[0], "0.0.0.0/0");
151        assert_eq!(v4[24], "192.0.2.0/24");
152        assert_eq!(v4[32], "192.0.2.7/32");
153
154        let mapped = Network::enclosing("::ffff:192.0.2.7".parse().unwrap()).count();
155        assert_eq!(mapped, 33);
156        assert_eq!(Network::enclosing("2001:db8::1".parse().unwrap()).count(), 129);
157    }
158}