use std::collections::HashMap;
use std::net::IpAddr;
use dashmap::DashSet;
use ipnet::IpNet;
use quincy::config::AddressRange;
use quincy::error::{AuthError, Result};
pub struct AddressPool {
ranges: Vec<AddressRange>,
used_addresses: DashSet<IpAddr>,
}
impl AddressPool {
pub fn new(ranges: Vec<AddressRange>) -> Self {
Self {
ranges,
used_addresses: DashSet::new(),
}
}
pub fn next_available_address(&self) -> Option<IpAddr> {
self.ranges
.iter()
.flat_map(|range| range.into_inner())
.find(|address| self.used_addresses.insert(*address))
}
pub fn release_address(&self, address: &IpAddr) {
self.used_addresses.remove(address);
}
pub fn reserve_addresses(&self, addresses: impl Iterator<Item = IpAddr>) {
for address in addresses {
self.used_addresses.insert(address);
}
}
}
pub struct AddressPoolManager {
network: IpNet,
global_pool: AddressPool,
user_pools: HashMap<String, AddressPool>,
}
impl AddressPoolManager {
pub fn new(network: IpNet, user_pools: HashMap<String, Vec<AddressRange>>) -> Result<Self> {
let global_pool = AddressPool::new(vec![AddressRange::from(network)]);
let reserved = [network.network(), network.addr(), network.broadcast()];
global_pool.reserve_addresses(reserved.iter().copied());
let mut built_user_pools = HashMap::with_capacity(user_pools.len());
for (username, ranges) in &user_pools {
for range in ranges {
for address in range.into_inner() {
if !network.contains(&address) {
return Err(AuthError::InvalidUserStore {
reason: format!(
"user '{username}': address {address} is outside \
tunnel network {network}"
),
}
.into());
}
if reserved.contains(&address) {
return Err(AuthError::InvalidUserStore {
reason: format!(
"user '{username}': address {address} is a reserved \
tunnel address (network, server, or broadcast)"
),
}
.into());
}
}
}
global_pool.reserve_addresses(ranges.iter().flat_map(|range| range.into_inner()));
built_user_pools.insert(username.clone(), AddressPool::new(ranges.clone()));
}
Ok(Self {
network,
global_pool,
user_pools: built_user_pools,
})
}
pub fn allocate_address(&self, username: &str) -> Option<IpNet> {
let address = match self.user_pools.get(username) {
Some(user_pool) => user_pool.next_available_address()?,
None => self.global_pool.next_available_address()?,
};
Some(
IpNet::with_netmask(address, self.network.netmask())
.expect("Netmask is always valid for addresses within the tunnel network"),
)
}
pub fn release_address(&self, username: &str, address: &IpAddr) {
match self.user_pools.get(username) {
Some(user_pool) => user_pool.release_address(address),
None => self.global_pool.release_address(address),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ipnet::{IpNet, Ipv4Net};
use quincy::config::AddressRange;
use std::collections::HashMap;
use std::net::Ipv4Addr;
fn test_network() -> IpNet {
IpNet::V4(
Ipv4Net::with_netmask(
Ipv4Addr::new(10, 0, 0, 1),
Ipv4Addr::new(255, 255, 255, 248),
)
.unwrap(),
)
}
#[test]
fn pool_allocates_in_order() {
let ranges = vec!["10.0.0.2 - 10.0.0.4".parse::<AddressRange>().unwrap()];
let pool = AddressPool::new(ranges);
assert_eq!(
pool.next_available_address(),
Some(Ipv4Addr::new(10, 0, 0, 2).into())
);
assert_eq!(
pool.next_available_address(),
Some(Ipv4Addr::new(10, 0, 0, 3).into())
);
assert_eq!(
pool.next_available_address(),
Some(Ipv4Addr::new(10, 0, 0, 4).into())
);
assert_eq!(pool.next_available_address(), None);
}
#[test]
fn pool_release_and_reallocate() {
let ranges = vec!["10.0.0.2/32".parse::<AddressRange>().unwrap()];
let pool = AddressPool::new(ranges);
let addr = pool.next_available_address().unwrap();
assert_eq!(pool.next_available_address(), None);
pool.release_address(&addr);
assert_eq!(pool.next_available_address(), Some(addr));
}
#[test]
fn pool_reserve_addresses() {
let ranges = vec!["10.0.0.2 - 10.0.0.4".parse::<AddressRange>().unwrap()];
let pool = AddressPool::new(ranges);
pool.reserve_addresses([Ipv4Addr::new(10, 0, 0, 3).into()].into_iter());
assert_eq!(
pool.next_available_address(),
Some(Ipv4Addr::new(10, 0, 0, 2).into())
);
assert_eq!(
pool.next_available_address(),
Some(Ipv4Addr::new(10, 0, 0, 4).into())
);
assert_eq!(pool.next_available_address(), None);
}
#[test]
fn manager_global_pool_excludes_reserved() {
let user_pools = HashMap::from([(
"alice".to_string(),
vec!["10.0.0.2/32".parse::<AddressRange>().unwrap()],
)]);
let manager = AddressPoolManager::new(test_network(), user_pools).unwrap();
let addr = manager.allocate_address("bob").unwrap();
assert_eq!(addr.addr(), IpAddr::from(Ipv4Addr::new(10, 0, 0, 3)));
}
#[test]
fn manager_user_pool_allocates_from_reserved() {
let user_pools = HashMap::from([(
"alice".to_string(),
vec!["10.0.0.5 - 10.0.0.6".parse::<AddressRange>().unwrap()],
)]);
let manager = AddressPoolManager::new(test_network(), user_pools).unwrap();
let addr = manager.allocate_address("alice").unwrap();
assert_eq!(addr.addr(), IpAddr::from(Ipv4Addr::new(10, 0, 0, 5)));
}
#[test]
fn manager_user_pool_exhaustion() {
let user_pools = HashMap::from([(
"alice".to_string(),
vec!["10.0.0.5/32".parse::<AddressRange>().unwrap()],
)]);
let manager = AddressPoolManager::new(test_network(), user_pools).unwrap();
assert!(manager.allocate_address("alice").is_some());
assert!(manager.allocate_address("alice").is_none());
assert!(manager.allocate_address("bob").is_some());
}
#[test]
fn manager_release_user_pool_and_reallocate() {
let user_pools = HashMap::from([(
"alice".to_string(),
vec!["10.0.0.5/32".parse::<AddressRange>().unwrap()],
)]);
let manager = AddressPoolManager::new(test_network(), user_pools).unwrap();
let addr = manager.allocate_address("alice").unwrap();
assert!(manager.allocate_address("alice").is_none());
manager.release_address("alice", &addr.addr());
assert!(manager.allocate_address("alice").is_some());
}
#[test]
fn manager_release_global_and_reallocate() {
let manager = AddressPoolManager::new(test_network(), HashMap::new()).unwrap();
let addr = manager.allocate_address("bob").unwrap();
manager.release_address("bob", &addr.addr());
let addr2 = manager.allocate_address("bob").unwrap();
assert_eq!(addr, addr2);
}
#[test]
fn manager_rejects_user_pool_outside_network() {
let user_pools = HashMap::from([(
"alice".to_string(),
vec!["192.168.1.1/32".parse::<AddressRange>().unwrap()],
)]);
let result = AddressPoolManager::new(test_network(), user_pools);
assert!(result.is_err());
}
#[test]
fn manager_no_user_pools() {
let manager = AddressPoolManager::new(test_network(), HashMap::new()).unwrap();
for expected in 2..=6u8 {
let addr = manager.allocate_address("anyone").unwrap();
assert_eq!(addr.addr(), IpAddr::from(Ipv4Addr::new(10, 0, 0, expected)));
}
assert!(manager.allocate_address("anyone").is_none());
}
#[test]
fn manager_rejects_user_pool_with_network_address() {
let user_pools = HashMap::from([(
"alice".to_string(),
vec!["10.0.0.0/32".parse::<AddressRange>().unwrap()],
)]);
let result = AddressPoolManager::new(test_network(), user_pools);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(err.contains("reserved tunnel address"), "error: {err}");
}
#[test]
fn manager_rejects_user_pool_with_server_address() {
let user_pools = HashMap::from([(
"alice".to_string(),
vec!["10.0.0.1/32".parse::<AddressRange>().unwrap()],
)]);
let result = AddressPoolManager::new(test_network(), user_pools);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(err.contains("reserved tunnel address"), "error: {err}");
}
#[test]
fn manager_rejects_user_pool_with_broadcast_address() {
let user_pools = HashMap::from([(
"alice".to_string(),
vec!["10.0.0.7/32".parse::<AddressRange>().unwrap()],
)]);
let result = AddressPoolManager::new(test_network(), user_pools);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(err.contains("reserved tunnel address"), "error: {err}");
}
#[test]
fn manager_netmask_preserved() {
let manager = AddressPoolManager::new(test_network(), HashMap::new()).unwrap();
let addr = manager.allocate_address("bob").unwrap();
assert_eq!(addr.netmask(), test_network().netmask());
}
}