Skip to main content

isoloom_spec/validate/
cidr6.rs

1//! IPv6 blocks: parsing `prefix::/64`, the address a machine's octet gives it, and the
2//! addresses Isoloom keeps for itself (the gateway, its router).
3
4use std::fmt;
5use std::net::Ipv6Addr;
6use std::str::FromStr;
7
8use crate::Error;
9
10/// The prefix length of every IPv6 block: a LAN's, the one SLAAC and the clouds' subnets use.
11pub const PREFIX: u8 = 64;
12
13/// An IPv6 /64 block, parsed from `prefix::/64` and written back the same way (`Display`,
14/// `FromStr`).
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct Cidr6 {
17    /// The network address, as a number.
18    pub base: u128,
19}
20
21impl Cidr6 {
22    /// The block written `prefix::/64`, when it is one: the address must be the network
23    /// address itself, unique local (`fc00::/7`) or global unicast (`2000::/3`).
24    #[must_use]
25    pub fn parse(s: &str) -> Option<Cidr6> {
26        let (ip, len) = s.split_once('/')?;
27        let ip: Ipv6Addr = ip.parse().ok()?;
28        if len.parse::<u8>().ok()? != PREFIX {
29            return None;
30        }
31        let base = u128::from(ip);
32        let host_bits = (1u128 << (128 - PREFIX)) - 1;
33        let unique_local = base >> 121 == 0x7e;
34        let global = base >> 125 == 1;
35        (base & host_bits == 0 && (unique_local || global)).then_some(Cidr6 { base })
36    }
37
38    /// Whether the two blocks are the same: two /64 blocks share an address only then.
39    #[must_use]
40    pub fn overlaps(self, other: Cidr6) -> bool {
41        self.base == other.base
42    }
43
44    /// The machine at IPv4 octet `octet`: the octet's decimal digits as the last group (10 is
45    /// `::10`, 254 is `::254`), so the two addresses of a dual-stack machine read alike.
46    #[must_use]
47    pub fn host(self, octet: u8) -> Ipv6Addr {
48        Ipv6Addr::from(self.base | interface_id(octet))
49    }
50
51    /// The gateway (the host's bridge, the first address): `::1`.
52    #[must_use]
53    pub fn gateway(self) -> Ipv6Addr {
54        Ipv6Addr::from(self.base | 1)
55    }
56}
57
58impl fmt::Display for Cidr6 {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(f, "{}/{PREFIX}", Ipv6Addr::from(self.base))
61    }
62}
63
64impl FromStr for Cidr6 {
65    type Err = Error;
66
67    fn from_str(s: &str) -> Result<Cidr6, Error> {
68        Cidr6::parse(s).ok_or_else(|| Error::Cidr6(s.to_owned()))
69    }
70}
71
72/// An octet's decimal digits read as hexadecimal: 10 is 0x10, 254 is 0x254.
73fn interface_id(octet: u8) -> u128 {
74    u128::from(octet / 100) << 8 | u128::from(octet / 10 % 10) << 4 | u128::from(octet % 10)
75}
76
77#[cfg(test)]
78mod tests {
79    use proptest::prelude::*;
80
81    use super::*;
82
83    #[test]
84    fn a_machine_reads_like_its_ipv4_address() {
85        let c = Cidr6::parse("fd00:20::/64").unwrap();
86        assert_eq!(c.host(10).to_string(), "fd00:20::10");
87        assert_eq!(c.host(254).to_string(), "fd00:20::254");
88        assert_eq!(c.gateway().to_string(), "fd00:20::1");
89        assert_eq!(c.to_string(), "fd00:20::/64");
90    }
91
92    #[test]
93    fn a_block_reads_from_its_text_or_says_why_not() {
94        assert_eq!("fd00:20::/64".parse::<Cidr6>().map(|c| c.to_string()).ok().as_deref(), Some("fd00:20::/64"));
95        let short = "fd00:20::/48".parse::<Cidr6>().unwrap_err();
96        assert_eq!(
97            short.to_string(),
98            "`fd00:20::/48` isn't an IPv6 block: write a unique local or global network address and `/64` (`fd00:20::/64`)"
99        );
100    }
101
102    #[test]
103    fn two_64s_overlap_only_when_they_are_the_same_block() {
104        let a = Cidr6::parse("fd00:20::/64").unwrap();
105        let b = Cidr6::parse("fd00:21::/64").unwrap();
106        assert!(a.overlaps(a) && !a.overlaps(b) && !b.overlaps(a));
107    }
108
109    #[test]
110    fn only_unicast_network_addresses_of_a_64() {
111        assert!(Cidr6::parse("2001:db8:1:2::/64").is_some());
112        for bad in [
113            "fd00::1/64",
114            "fd00::/48",
115            "fe80::/64",
116            "ff02::/64",
117            "::/64",
118            "10.0.0.0/24",
119            "fd00::",
120            "fd00::/x",
121        ] {
122            assert_eq!(Cidr6::parse(bad), None, "{bad}");
123        }
124    }
125
126    proptest! {
127        /// Two octets never share an address, and each stays in its block, off the gateway.
128        #[test]
129        fn hosts_are_distinct_and_inside(prefix in any::<u64>(), a in 2u8.., b in 2u8..) {
130            let c = Cidr6 { base: u128::from(prefix) << 64 };
131            let (ha, hb) = (u128::from(c.host(a)), u128::from(c.host(b)));
132            prop_assert_eq!(ha == hb, a == b);
133            prop_assert_eq!(ha >> 64, u128::from(prefix));
134            prop_assert_ne!(c.host(a), c.gateway());
135        }
136
137        /// A parsed block is written back as it was read.
138        #[test]
139        fn parse_round_trips(groups in proptest::array::uniform4(any::<u16>())) {
140            let [a, b, c, d] = groups;
141            let ip = Ipv6Addr::new(0xfd00 | (a & 0xff), b, c, d, 0, 0, 0, 0);
142            let text = format!("{ip}/64");
143            let parsed = Cidr6::parse(&text).unwrap();
144            prop_assert_eq!(parsed.to_string(), text);
145        }
146
147        /// Any address with host bits set is refused: only the network address names a block.
148        #[test]
149        fn host_bits_are_refused(prefix in any::<u16>(), host in 1u64..) {
150            let ip = Ipv6Addr::from(u128::from(0xfd00u16 | (prefix & 0xff)) << 112 | u128::from(host));
151            prop_assert_eq!(Cidr6::parse(&format!("{ip}/64")), None);
152        }
153    }
154}