use std::fmt;
use std::net::Ipv4Addr;
use std::str::FromStr;
use crate::Error;
use crate::model::Spec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cidr {
pub base: u32,
pub len: u8,
}
impl Cidr {
#[must_use]
pub fn parse(s: &str) -> Option<Cidr> {
let (ip, len) = s.split_once('/')?;
let ip: Ipv4Addr = ip.parse().ok()?;
let len: u8 = len.parse().ok()?;
if len > 32 {
return None;
}
let mask = Self::mask(len);
let base = u32::from(ip);
(base & mask == base).then_some(Cidr { base, len })
}
fn mask(len: u8) -> u32 {
if len == 0 { 0 } else { u32::MAX << (32 - len) }
}
#[must_use]
pub fn contains(self, other: Cidr) -> bool {
self.len <= other.len && other.base & Self::mask(self.len) == self.base
}
#[must_use]
pub fn overlaps(self, other: Cidr) -> bool {
self.contains(other) || other.contains(self)
}
#[must_use]
pub fn host(self, last_octet: u8) -> Option<Ipv4Addr> {
if self.len < 24 || self.len > 29 {
return None;
}
let size = 1u32 << (32 - self.len);
let offset = u32::from(last_octet) & 0xff;
let addr = (self.base & !0xff) | offset;
let first = self.base + 2; let last = self.base + size - 4; (addr >= first && addr <= last).then(|| Ipv4Addr::from(addr))
}
#[must_use]
pub fn gateway(self) -> Ipv4Addr {
Ipv4Addr::from(self.base + 1)
}
#[must_use]
pub fn gateway_octet(self) -> u8 {
let [.., last] = self.gateway().octets();
last
}
#[must_use]
pub fn controller(self) -> Ipv4Addr {
Ipv4Addr::from(self.base + (1u32 << (32 - self.len)) - 3)
}
#[must_use]
pub fn tool(self, i: usize) -> Ipv4Addr {
let below = u32::try_from(i).unwrap_or(u32::MAX);
Ipv4Addr::from((self.base + (1u32 << (32 - self.len)) - 4).saturating_sub(below))
}
#[must_use]
pub fn router(self) -> Ipv4Addr {
Ipv4Addr::from(self.base + (1u32 << (32 - self.len)) - 2)
}
}
pub const PRIVATE: [Cidr; 3] = [
Cidr { base: 0x0a00_0000, len: 8 },
Cidr { base: 0xac10_0000, len: 12 },
Cidr { base: 0xc0a8_0000, len: 16 },
];
pub const DOCKER_BLOCK: Cidr = Cidr { base: 0x0a00_0000, len: 8 };
#[must_use]
pub fn docker_cidrs(spec: &Spec) -> Vec<(String, Cidr)> {
let mut fixed: Vec<(String, Cidr)> = Vec::new();
let mut moved: Vec<(String, Cidr)> = Vec::new();
for (name, n) in &spec.networks {
let Some(c) = Cidr::parse(&n.cidr) else { continue };
match n.docker.as_ref().and_then(|d| Cidr::parse(&d.cidr)) {
Some(d) => fixed.push((name.clone(), d)),
None if DOCKER_BLOCK.contains(c) || n.is_outside() => fixed.push((name.clone(), c)),
None => moved.push((name.clone(), c)),
}
}
let mut taken: Vec<Cidr> = fixed.iter().map(|(_, c)| *c).collect();
let mut out: indexmap::IndexMap<String, Cidr> = fixed.into_iter().collect();
for (name, c) in moved {
let pick = moved_block(c, &taken);
taken.push(pick);
out.insert(name, pick);
}
spec.networks.keys().filter_map(|n| Some((n.clone(), *out.get(n)?))).collect()
}
fn moved_block(block: Cidr, taken: &[Cidr]) -> Cidr {
let [first_octet, second, third, fourth] = Ipv4Addr::from(block.base).octets();
let first = if first_octet == 192 {
u32::from_be_bytes([10, 192, third, fourth])
} else {
u32::from_be_bytes([10, second, third, fourth])
};
let size = usize::try_from(1u32 << (32 - block.len)).unwrap_or(usize::MAX);
let scan = (0x0af0_0000u32..0x0b00_0000).step_by(size);
std::iter::once(first)
.chain(scan)
.map(|base| Cidr { base, len: block.len })
.find(|cand| DOCKER_BLOCK.contains(*cand) && !taken.iter().any(|t| t.overlaps(*cand)))
.unwrap_or(Cidr { base: first, len: block.len })
}
impl fmt::Display for Cidr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", Ipv4Addr::from(self.base), self.len)
}
}
impl FromStr for Cidr {
type Err = Error;
fn from_str(s: &str) -> Result<Cidr, Error> {
Cidr::parse(s).ok_or_else(|| Error::Cidr(s.to_owned()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn block(text: &str) -> Cidr {
Cidr::parse(text).unwrap()
}
fn ip(text: &str) -> Ipv4Addr {
text.parse().unwrap()
}
#[test]
fn a_moved_block_steps_by_its_own_size_past_the_blocks_taken() {
let taken = [block("10.16.0.0/28"), block("10.240.0.0/29")];
assert_eq!(moved_block(block("172.16.0.0/28"), &taken), block("10.240.0.16/28"));
}
#[test]
fn a_block_reads_and_writes_as_its_text() {
let lan: Cidr = "10.20.0.0/24".parse().unwrap();
assert_eq!(lan, Cidr { base: 0x0a14_0000, len: 24 });
assert_eq!(lan.to_string(), "10.20.0.0/24");
assert_eq!(Cidr { base: 0, len: 0 }.to_string(), "0.0.0.0/0");
let host = "10.20.0.5/24".parse::<Cidr>().unwrap_err();
assert_eq!(
host.to_string(),
"`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`)"
);
}
#[test]
fn a_24_reserves_its_first_and_last_addresses() {
let lan = block("10.9.0.0/24");
assert_eq!((lan.gateway(), lan.gateway_octet()), (ip("10.9.0.1"), 1));
assert_eq!((lan.controller(), lan.router()), (ip("10.9.0.253"), ip("10.9.0.254")));
assert_eq!((lan.tool(0), lan.tool(2)), (ip("10.9.0.252"), ip("10.9.0.250")));
assert_eq!(
(lan.host(2), lan.host(100), lan.host(252)),
(Some(ip("10.9.0.2")), Some(ip("10.9.0.100")), Some(ip("10.9.0.252")))
);
for reserved in [0, 1, 253, 254, 255] {
assert_eq!(lan.host(reserved), None, "{reserved}");
}
}
#[test]
fn a_29_inside_a_24_counts_from_its_own_base() {
let small = block("10.0.0.8/29");
assert_eq!((small.gateway(), small.gateway_octet()), (ip("10.0.0.9"), 9));
assert_eq!(
(small.controller(), small.router(), small.tool(0)),
(ip("10.0.0.13"), ip("10.0.0.14"), ip("10.0.0.12"))
);
assert_eq!((small.host(10), small.host(12)), (Some(ip("10.0.0.10")), Some(ip("10.0.0.12"))));
for outside in [8, 9, 13, 14, 15, 16, 7] {
assert_eq!(small.host(outside), None, "{outside}");
}
}
#[test]
fn a_tool_below_the_block_saturates_at_its_base() {
assert_eq!(block("10.0.0.8/29").tool(usize::MAX), ip("0.0.0.0"));
}
#[test]
fn only_a_24_to_a_29_has_host_addresses() {
for size in ["10.0.0.0/23", "10.0.0.0/16", "10.0.0.0/30", "10.0.0.0/32"] {
assert_eq!(block(size).host(2), None, "{size}");
}
assert_eq!(block("10.0.0.0/25").host(2), Some(ip("10.0.0.2")));
}
fn docker(networks: &str) -> Vec<(String, String)> {
let spec = crate::testing::model(&format!("version: 1\nname: t\nnetworks:\n{networks}")).unwrap();
docker_cidrs(&spec)
.into_iter()
.map(|(n, c)| (n, format!("{}/{}", Ipv4Addr::from(c.base), c.len)))
.collect()
}
fn pairs(list: &[(&str, &str)]) -> Vec<(String, String)> {
list.iter().map(|(a, b)| ((*a).to_owned(), (*b).to_owned())).collect()
}
#[test]
fn a_block_outside_10_moves_into_it_keeping_its_octets() {
assert_eq!(
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"),
pairs(&[("home", "10.192.5.0/24"), ("dmz", "10.20.3.0/24"), ("lab", "10.7.0.0/24")])
);
}
#[test]
fn a_networks_own_docker_block_and_an_outside_network_stay_as_written() {
assert_eq!(
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"),
pairs(&[("home", "10.50.0.0/24"), ("corp", "192.168.50.0/24")])
);
}
#[test]
fn a_taken_block_moves_to_the_next_free_one_from_10_240() {
assert_eq!(
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"),
pairs(&[("lab", "10.192.5.0/24"), ("home", "10.240.0.0/24"), ("also", "10.240.1.0/24")])
);
assert_eq!(
docker(" lab: { cidr: 10.240.0.0/23 }\n dmz: { cidr: 172.240.0.0/24 }\n"),
pairs(&[("lab", "10.240.0.0/23"), ("dmz", "10.240.2.0/24")])
);
}
}