mirror of
https://github.com/cirruslabs/softnet.git
synced 2026-10-18 04:43:30 +02:00
93 lines
3.5 KiB
Rust
93 lines
3.5 KiB
Rust
use anyhow::{Context, Result};
|
||
use ipnet::{Ipv4AddrRange, Ipv4Net};
|
||
use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig};
|
||
use rand::seq::{IndexedRandom, IteratorRandom};
|
||
use std::net::Ipv4Addr;
|
||
|
||
const MAX_ATTEMPTS: usize = 128;
|
||
|
||
/// Find an available private subnet and its first two usable host addresses
|
||
pub(crate) fn find_available_subnet(prefix_len: u8) -> Result<(Ipv4Addr, Ipv4Addr, Ipv4Net)> {
|
||
// Add private address space (as defined in RFC 1918[1]) with one exception
|
||
//
|
||
// We narrow the 192.168.0.0/16 allocation pool to work around a macOS bug
|
||
// where long subnet address strings lose their CIDR prefix in the generated
|
||
// firewall rules, causing connection issues.
|
||
//
|
||
// [1]: https://datatracker.ietf.org/doc/html/rfc1918#section-3
|
||
let private_subnets: Vec<Ipv4Net> = vec![
|
||
"10.0.0.0/8".parse()?,
|
||
"172.16.0.0/12".parse()?,
|
||
"192.168.0.0/18".parse()?, // third octet 0–63
|
||
"192.168.64.0/19".parse()?, // third octet 64–95
|
||
"192.168.96.0/22".parse()?, // third octet 96–99
|
||
];
|
||
|
||
// Figure out which address space is already utilized on the host
|
||
let mut used_subnets = Vec::new();
|
||
|
||
for interface in NetworkInterface::show()? {
|
||
for addr in interface.addr {
|
||
// We only support IPv4 for now
|
||
if let Addr::V4(addr) = addr {
|
||
let mask = addr.netmask.context("IPv4 interface has no netmask")?;
|
||
used_subnets.push(Ipv4Net::with_netmask(addr.ip, mask)?);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Try up to MAX_ATTEMPTS random subnets until we're able to
|
||
// get an available subnet of the desired length
|
||
for _ in 0..MAX_ATTEMPTS {
|
||
// Give each private range an equal chance, even if a larger one is occupied
|
||
let private_subnet = private_subnets.choose(&mut rand::rng()).unwrap();
|
||
|
||
// Skip private ranges too small to contain the requested subnet
|
||
if prefix_len < private_subnet.prefix_len() {
|
||
continue;
|
||
}
|
||
|
||
// Pick a subnet of the desired length within this private range
|
||
let address = Ipv4AddrRange::new(private_subnet.network(), private_subnet.broadcast())
|
||
.choose(&mut rand::rng())
|
||
.unwrap();
|
||
let candidate = Ipv4Net::new(address, prefix_len)?.trunc();
|
||
|
||
// Only use the subnet if it doesn't overlap address space
|
||
// that is already utilized on the host
|
||
let overlaps = used_subnets
|
||
.iter()
|
||
.any(|used_subnet| candidate.contains(used_subnet) || used_subnet.contains(&candidate));
|
||
if overlaps {
|
||
continue;
|
||
}
|
||
|
||
// Take the first two hosts from the subnet
|
||
let mut hosts = candidate.hosts();
|
||
if let (Some(first_host), Some(second_host)) = (hosts.next(), hosts.next()) {
|
||
return Ok((first_host, second_host, candidate));
|
||
}
|
||
}
|
||
|
||
anyhow::bail!(
|
||
"failed to find an unused private IPv4 /{prefix_len} subnet after {MAX_ATTEMPTS} attempts"
|
||
)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
#[serial_test::serial]
|
||
fn finds_two_usable_addresses() {
|
||
// Find a subnet on the current host
|
||
let (gateway_ip, vm_ip, subnet) = find_available_subnet(29).unwrap();
|
||
|
||
// Check the subnet and its first two usable addresses
|
||
assert_eq!(subnet.prefix_len(), 29);
|
||
assert!(subnet.network().is_private());
|
||
assert_eq!(subnet.hosts().collect::<Vec<_>>()[..2], [gateway_ip, vm_ip]);
|
||
}
|
||
}
|