1use std::fmt;
5use std::net::Ipv4Addr;
6use std::str::FromStr;
7
8use crate::Error;
9use crate::model::Spec;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Cidr {
14 pub base: u32,
16 pub len: u8,
18}
19
20impl Cidr {
21 #[must_use]
24 pub fn parse(s: &str) -> Option<Cidr> {
25 let (ip, len) = s.split_once('/')?;
26 let ip: Ipv4Addr = ip.parse().ok()?;
27 let len: u8 = len.parse().ok()?;
28 if len > 32 {
29 return None;
30 }
31 let mask = Self::mask(len);
32 let base = u32::from(ip);
33 (base & mask == base).then_some(Cidr { base, len })
35 }
36
37 fn mask(len: u8) -> u32 {
38 if len == 0 { 0 } else { u32::MAX << (32 - len) }
39 }
40
41 #[must_use]
43 pub fn contains(self, other: Cidr) -> bool {
44 self.len <= other.len && other.base & Self::mask(self.len) == self.base
45 }
46
47 #[must_use]
49 pub fn overlaps(self, other: Cidr) -> bool {
50 self.contains(other) || other.contains(self)
51 }
52
53 #[must_use]
56 pub fn host(self, last_octet: u8) -> Option<Ipv4Addr> {
57 if self.len < 24 || self.len > 29 {
58 return None;
59 }
60 let size = 1u32 << (32 - self.len);
61 let offset = u32::from(last_octet) & 0xff;
62 let addr = (self.base & !0xff) | offset;
63 let first = self.base + 2; let last = self.base + size - 4; (addr >= first && addr <= last).then(|| Ipv4Addr::from(addr))
66 }
67
68 #[must_use]
70 pub fn gateway(self) -> Ipv4Addr {
71 Ipv4Addr::from(self.base + 1)
72 }
73
74 #[must_use]
76 pub fn gateway_octet(self) -> u8 {
77 let [.., last] = self.gateway().octets();
78 last
79 }
80
81 #[must_use]
84 pub fn controller(self) -> Ipv4Addr {
85 Ipv4Addr::from(self.base + (1u32 << (32 - self.len)) - 3)
86 }
87
88 #[must_use]
91 pub fn tool(self, i: usize) -> Ipv4Addr {
92 let below = u32::try_from(i).unwrap_or(u32::MAX);
93 Ipv4Addr::from((self.base + (1u32 << (32 - self.len)) - 4).saturating_sub(below))
94 }
95
96 #[must_use]
98 pub fn router(self) -> Ipv4Addr {
99 Ipv4Addr::from(self.base + (1u32 << (32 - self.len)) - 2)
100 }
101}
102
103pub const PRIVATE: [Cidr; 3] = [
105 Cidr { base: 0x0a00_0000, len: 8 },
106 Cidr { base: 0xac10_0000, len: 12 },
107 Cidr { base: 0xc0a8_0000, len: 16 },
108];
109
110pub const DOCKER_BLOCK: Cidr = Cidr { base: 0x0a00_0000, len: 8 };
113
114#[must_use]
120pub fn docker_cidrs(spec: &Spec) -> Vec<(String, Cidr)> {
121 let mut fixed: Vec<(String, Cidr)> = Vec::new();
122 let mut moved: Vec<(String, Cidr)> = Vec::new();
123 for (name, n) in &spec.networks {
124 let Some(c) = Cidr::parse(&n.cidr) else { continue };
125 match n.docker.as_ref().and_then(|d| Cidr::parse(&d.cidr)) {
126 Some(d) => fixed.push((name.clone(), d)),
127 None if DOCKER_BLOCK.contains(c) || n.is_outside() => fixed.push((name.clone(), c)),
129 None => moved.push((name.clone(), c)),
130 }
131 }
132 let mut taken: Vec<Cidr> = fixed.iter().map(|(_, c)| *c).collect();
133 let mut out: indexmap::IndexMap<String, Cidr> = fixed.into_iter().collect();
134 for (name, c) in moved {
135 let pick = moved_block(c, &taken);
136 taken.push(pick);
137 out.insert(name, pick);
138 }
139 spec.networks.keys().filter_map(|n| Some((n.clone(), *out.get(n)?))).collect()
140}
141
142fn moved_block(block: Cidr, taken: &[Cidr]) -> Cidr {
145 let [first_octet, second, third, fourth] = Ipv4Addr::from(block.base).octets();
146 let first = if first_octet == 192 {
147 u32::from_be_bytes([10, 192, third, fourth])
148 } else {
149 u32::from_be_bytes([10, second, third, fourth])
150 };
151 let size = usize::try_from(1u32 << (32 - block.len)).unwrap_or(usize::MAX);
152 let scan = (0x0af0_0000u32..0x0b00_0000).step_by(size);
157 std::iter::once(first)
158 .chain(scan)
159 .map(|base| Cidr { base, len: block.len })
160 .find(|cand| DOCKER_BLOCK.contains(*cand) && !taken.iter().any(|t| t.overlaps(*cand)))
161 .unwrap_or(Cidr { base: first, len: block.len })
162}
163
164impl fmt::Display for Cidr {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 write!(f, "{}/{}", Ipv4Addr::from(self.base), self.len)
167 }
168}
169
170impl FromStr for Cidr {
171 type Err = Error;
172
173 fn from_str(s: &str) -> Result<Cidr, Error> {
174 Cidr::parse(s).ok_or_else(|| Error::Cidr(s.to_owned()))
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn block(text: &str) -> Cidr {
183 Cidr::parse(text).unwrap()
184 }
185
186 fn ip(text: &str) -> Ipv4Addr {
187 text.parse().unwrap()
188 }
189
190 #[test]
191 fn a_moved_block_steps_by_its_own_size_past_the_blocks_taken() {
192 let taken = [block("10.16.0.0/28"), block("10.240.0.0/29")];
193 assert_eq!(moved_block(block("172.16.0.0/28"), &taken), block("10.240.0.16/28"));
194 }
195
196 #[test]
197 fn a_block_reads_and_writes_as_its_text() {
198 let lan: Cidr = "10.20.0.0/24".parse().unwrap();
199 assert_eq!(lan, Cidr { base: 0x0a14_0000, len: 24 });
200 assert_eq!(lan.to_string(), "10.20.0.0/24");
201 assert_eq!(Cidr { base: 0, len: 0 }.to_string(), "0.0.0.0/0");
202 let host = "10.20.0.5/24".parse::<Cidr>().unwrap_err();
203 assert_eq!(
204 host.to_string(),
205 "`10.20.0.5/24` isn't an IPv4 block: write the network's own address, `/` and the prefix length (`10.20.0.0/24`)"
206 );
207 }
208
209 #[test]
210 fn a_24_reserves_its_first_and_last_addresses() {
211 let lan = block("10.9.0.0/24");
212 assert_eq!((lan.gateway(), lan.gateway_octet()), (ip("10.9.0.1"), 1));
213 assert_eq!((lan.controller(), lan.router()), (ip("10.9.0.253"), ip("10.9.0.254")));
214 assert_eq!((lan.tool(0), lan.tool(2)), (ip("10.9.0.252"), ip("10.9.0.250")));
215 assert_eq!(
216 (lan.host(2), lan.host(100), lan.host(252)),
217 (Some(ip("10.9.0.2")), Some(ip("10.9.0.100")), Some(ip("10.9.0.252")))
218 );
219 for reserved in [0, 1, 253, 254, 255] {
220 assert_eq!(lan.host(reserved), None, "{reserved}");
221 }
222 }
223
224 #[test]
225 fn a_29_inside_a_24_counts_from_its_own_base() {
226 let small = block("10.0.0.8/29");
227 assert_eq!((small.gateway(), small.gateway_octet()), (ip("10.0.0.9"), 9));
228 assert_eq!(
229 (small.controller(), small.router(), small.tool(0)),
230 (ip("10.0.0.13"), ip("10.0.0.14"), ip("10.0.0.12"))
231 );
232 assert_eq!((small.host(10), small.host(12)), (Some(ip("10.0.0.10")), Some(ip("10.0.0.12"))));
233 for outside in [8, 9, 13, 14, 15, 16, 7] {
234 assert_eq!(small.host(outside), None, "{outside}");
235 }
236 }
237
238 #[test]
239 fn a_tool_below_the_block_saturates_at_its_base() {
240 assert_eq!(block("10.0.0.8/29").tool(usize::MAX), ip("0.0.0.0"));
241 }
242
243 #[test]
244 fn only_a_24_to_a_29_has_host_addresses() {
245 for size in ["10.0.0.0/23", "10.0.0.0/16", "10.0.0.0/30", "10.0.0.0/32"] {
246 assert_eq!(block(size).host(2), None, "{size}");
247 }
248 assert_eq!(block("10.0.0.0/25").host(2), Some(ip("10.0.0.2")));
249 }
250
251 fn docker(networks: &str) -> Vec<(String, String)> {
253 let spec = crate::testing::model(&format!("version: 1\nname: t\nnetworks:\n{networks}")).unwrap();
254 docker_cidrs(&spec)
255 .into_iter()
256 .map(|(n, c)| (n, format!("{}/{}", Ipv4Addr::from(c.base), c.len)))
257 .collect()
258 }
259
260 fn pairs(list: &[(&str, &str)]) -> Vec<(String, String)> {
261 list.iter().map(|(a, b)| ((*a).to_owned(), (*b).to_owned())).collect()
262 }
263
264 #[test]
265 fn a_block_outside_10_moves_into_it_keeping_its_octets() {
266 assert_eq!(
267 docker(" home: { cidr: 192.168.5.0/24 }\n dmz: { cidr: 172.20.3.0/24 }\n lab: { cidr: 10.7.0.0/24 }\n"),
268 pairs(&[("home", "10.192.5.0/24"), ("dmz", "10.20.3.0/24"), ("lab", "10.7.0.0/24")])
269 );
270 }
271
272 #[test]
273 fn a_networks_own_docker_block_and_an_outside_network_stay_as_written() {
274 assert_eq!(
275 docker(" home: { cidr: 192.168.5.0/24, docker: { cidr: 10.50.0.0/24 } }\n corp: { cidr: 192.168.50.0/24, external: corp-net }\n"),
276 pairs(&[("home", "10.50.0.0/24"), ("corp", "192.168.50.0/24")])
277 );
278 }
279
280 #[test]
281 fn a_taken_block_moves_to_the_next_free_one_from_10_240() {
282 assert_eq!(
283 docker(" lab: { cidr: 10.192.5.0/24 }\n home: { cidr: 192.168.5.0/24 }\n also: { cidr: 192.168.5.0/24 }\n"),
284 pairs(&[("lab", "10.192.5.0/24"), ("home", "10.240.0.0/24"), ("also", "10.240.1.0/24")])
285 );
286 assert_eq!(
287 docker(" lab: { cidr: 10.240.0.0/23 }\n dmz: { cidr: 172.240.0.0/24 }\n"),
288 pairs(&[("lab", "10.240.0.0/23"), ("dmz", "10.240.2.0/24")])
289 );
290 }
291}