Skip to main content

quincy_server/server/
address_pool.rs

1use std::collections::HashMap;
2use std::net::IpAddr;
3
4use dashmap::DashSet;
5use ipnet::IpNet;
6
7use quincy::config::AddressRange;
8use quincy::error::{AuthError, Result};
9
10/// A pool of IP addresses from which addresses can be allocated and released.
11///
12/// Stores address ranges lazily and iterates them on each allocation,
13/// avoiding materialization of the full address list into memory.
14pub struct AddressPool {
15    /// The ranges this pool can allocate from, scanned in order.
16    ranges: Vec<AddressRange>,
17    /// Addresses currently allocated or reserved.
18    used_addresses: DashSet<IpAddr>,
19}
20
21impl AddressPool {
22    /// Creates a new pool from the given address ranges.
23    ///
24    /// ### Arguments
25    /// - `ranges` - the address ranges this pool can allocate from
26    pub fn new(ranges: Vec<AddressRange>) -> Self {
27        Self {
28            ranges,
29            used_addresses: DashSet::new(),
30        }
31    }
32
33    /// Returns the next available address, or `None` if the pool is exhausted.
34    ///
35    /// Lazily scans all ranges and atomically claims the first unused address
36    /// via [`DashSet::insert`], which returns `true` only if the address was
37    /// not already present.
38    pub fn next_available_address(&self) -> Option<IpAddr> {
39        self.ranges
40            .iter()
41            .flat_map(|range| range.into_inner())
42            .find(|address| self.used_addresses.insert(*address))
43    }
44
45    /// Releases the specified address so it can be allocated again.
46    ///
47    /// ### Arguments
48    /// - `address` - the address to release
49    pub fn release_address(&self, address: &IpAddr) {
50        self.used_addresses.remove(address);
51    }
52
53    /// Marks a set of addresses as used, preventing them from being allocated.
54    ///
55    /// ### Arguments
56    /// - `addresses` - the addresses to reserve
57    pub fn reserve_addresses(&self, addresses: impl Iterator<Item = IpAddr>) {
58        for address in addresses {
59            self.used_addresses.insert(address);
60        }
61    }
62}
63
64/// Manages IP address allocation across a global pool and optional per-user
65/// reserved pools.
66///
67/// Users with a per-user pool get addresses exclusively from their reserved set.
68/// Users without a per-user pool get addresses from the global (unreserved) pool.
69/// Reserved addresses are pre-inserted into the global pool's used set at
70/// construction time so they are never handed out to unrestricted users.
71pub struct AddressPoolManager {
72    /// The tunnel network (carries server IP + netmask for wrapping allocations).
73    network: IpNet,
74    /// Pool of unreserved addresses available to any user.
75    global_pool: AddressPool,
76    /// Per-user reserved pools, keyed by username.
77    user_pools: HashMap<String, AddressPool>,
78}
79
80impl AddressPoolManager {
81    /// Creates a new address pool manager for the given tunnel network.
82    ///
83    /// The global pool covers the entire tunnel network with the network address,
84    /// server address, and broadcast address pre-reserved. All addresses from
85    /// per-user pools are also pre-reserved in the global pool.
86    ///
87    /// # Performance
88    ///
89    /// Every address in every per-user range is iterated eagerly for validation
90    /// and reservation. Very large ranges (e.g. a `/8` with ~16 M addresses)
91    /// will cause proportional memory and CPU usage at startup. Prefer narrow
92    /// per-user ranges (a `/24` or smaller is typical).
93    ///
94    /// ### Arguments
95    /// - `network` - the tunnel network (server IP + netmask)
96    /// - `user_pools` - per-user address ranges, keyed by username
97    ///
98    /// ### Errors
99    /// Returns `AuthError::InvalidUserStore` if any user pool address falls
100    /// outside the tunnel network or is a reserved tunnel address (network,
101    /// server, or broadcast).
102    pub fn new(network: IpNet, user_pools: HashMap<String, Vec<AddressRange>>) -> Result<Self> {
103        // Build the global pool covering the entire tunnel network
104        let global_pool = AddressPool::new(vec![AddressRange::from(network)]);
105
106        // Pre-reserve network, server, and broadcast addresses
107        let reserved = [network.network(), network.addr(), network.broadcast()];
108        global_pool.reserve_addresses(reserved.iter().copied());
109
110        // Validate and build per-user pools
111        let mut built_user_pools = HashMap::with_capacity(user_pools.len());
112
113        for (username, ranges) in &user_pools {
114            // Validate all addresses in the user's ranges are within the tunnel network
115            // and are not reserved infrastructure addresses
116            for range in ranges {
117                for address in range.into_inner() {
118                    if !network.contains(&address) {
119                        return Err(AuthError::InvalidUserStore {
120                            reason: format!(
121                                "user '{username}': address {address} is outside \
122                                 tunnel network {network}"
123                            ),
124                        }
125                        .into());
126                    }
127                    if reserved.contains(&address) {
128                        return Err(AuthError::InvalidUserStore {
129                            reason: format!(
130                                "user '{username}': address {address} is a reserved \
131                                 tunnel address (network, server, or broadcast)"
132                            ),
133                        }
134                        .into());
135                    }
136                }
137            }
138
139            // Pre-reserve user pool addresses in the global pool
140            global_pool.reserve_addresses(ranges.iter().flat_map(|range| range.into_inner()));
141
142            built_user_pools.insert(username.clone(), AddressPool::new(ranges.clone()));
143        }
144
145        Ok(Self {
146            network,
147            global_pool,
148            user_pools: built_user_pools,
149        })
150    }
151
152    /// Allocates an address for the given user.
153    ///
154    /// If the user has a per-user pool, allocates from that pool. Otherwise
155    /// allocates from the global pool. Returns the address wrapped in an
156    /// [`IpNet`] with the tunnel network's netmask.
157    ///
158    /// ### Arguments
159    /// - `username` - the authenticated username
160    pub fn allocate_address(&self, username: &str) -> Option<IpNet> {
161        let address = match self.user_pools.get(username) {
162            Some(user_pool) => user_pool.next_available_address()?,
163            None => self.global_pool.next_available_address()?,
164        };
165
166        Some(
167            IpNet::with_netmask(address, self.network.netmask())
168                .expect("Netmask is always valid for addresses within the tunnel network"),
169        )
170    }
171
172    /// Releases an address back to the appropriate pool.
173    ///
174    /// If the user has a per-user pool, releases to that pool. Otherwise
175    /// releases to the global pool.
176    ///
177    /// ### Arguments
178    /// - `username` - the authenticated username
179    /// - `address` - the address to release
180    pub fn release_address(&self, username: &str, address: &IpAddr) {
181        match self.user_pools.get(username) {
182            Some(user_pool) => user_pool.release_address(address),
183            None => self.global_pool.release_address(address),
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use ipnet::{IpNet, Ipv4Net};
192    use quincy::config::AddressRange;
193    use std::collections::HashMap;
194    use std::net::Ipv4Addr;
195
196    /// 10.0.0.0/29 = 8 addresses: .0 (network), .1 (server), .2-.6 (usable), .7 (broadcast)
197    fn test_network() -> IpNet {
198        IpNet::V4(
199            Ipv4Net::with_netmask(
200                Ipv4Addr::new(10, 0, 0, 1),
201                Ipv4Addr::new(255, 255, 255, 248),
202            )
203            .unwrap(),
204        )
205    }
206
207    // --- AddressPool tests ---
208
209    #[test]
210    fn pool_allocates_in_order() {
211        let ranges = vec!["10.0.0.2 - 10.0.0.4".parse::<AddressRange>().unwrap()];
212        let pool = AddressPool::new(ranges);
213
214        assert_eq!(
215            pool.next_available_address(),
216            Some(Ipv4Addr::new(10, 0, 0, 2).into())
217        );
218        assert_eq!(
219            pool.next_available_address(),
220            Some(Ipv4Addr::new(10, 0, 0, 3).into())
221        );
222        assert_eq!(
223            pool.next_available_address(),
224            Some(Ipv4Addr::new(10, 0, 0, 4).into())
225        );
226        assert_eq!(pool.next_available_address(), None);
227    }
228
229    #[test]
230    fn pool_release_and_reallocate() {
231        let ranges = vec!["10.0.0.2/32".parse::<AddressRange>().unwrap()];
232        let pool = AddressPool::new(ranges);
233
234        let addr = pool.next_available_address().unwrap();
235        assert_eq!(pool.next_available_address(), None);
236
237        pool.release_address(&addr);
238        assert_eq!(pool.next_available_address(), Some(addr));
239    }
240
241    #[test]
242    fn pool_reserve_addresses() {
243        let ranges = vec!["10.0.0.2 - 10.0.0.4".parse::<AddressRange>().unwrap()];
244        let pool = AddressPool::new(ranges);
245        pool.reserve_addresses([Ipv4Addr::new(10, 0, 0, 3).into()].into_iter());
246
247        assert_eq!(
248            pool.next_available_address(),
249            Some(Ipv4Addr::new(10, 0, 0, 2).into())
250        );
251        // .3 is reserved, skipped
252        assert_eq!(
253            pool.next_available_address(),
254            Some(Ipv4Addr::new(10, 0, 0, 4).into())
255        );
256        assert_eq!(pool.next_available_address(), None);
257    }
258
259    // --- AddressPoolManager tests ---
260
261    #[test]
262    fn manager_global_pool_excludes_reserved() {
263        let user_pools = HashMap::from([(
264            "alice".to_string(),
265            vec!["10.0.0.2/32".parse::<AddressRange>().unwrap()],
266        )]);
267        let manager = AddressPoolManager::new(test_network(), user_pools).unwrap();
268
269        // Global pool should skip .0 (network), .1 (server), .2 (reserved), .7 (broadcast)
270        // First global allocation is .3
271        let addr = manager.allocate_address("bob").unwrap();
272        assert_eq!(addr.addr(), IpAddr::from(Ipv4Addr::new(10, 0, 0, 3)));
273    }
274
275    #[test]
276    fn manager_user_pool_allocates_from_reserved() {
277        let user_pools = HashMap::from([(
278            "alice".to_string(),
279            vec!["10.0.0.5 - 10.0.0.6".parse::<AddressRange>().unwrap()],
280        )]);
281        let manager = AddressPoolManager::new(test_network(), user_pools).unwrap();
282
283        let addr = manager.allocate_address("alice").unwrap();
284        assert_eq!(addr.addr(), IpAddr::from(Ipv4Addr::new(10, 0, 0, 5)));
285    }
286
287    #[test]
288    fn manager_user_pool_exhaustion() {
289        let user_pools = HashMap::from([(
290            "alice".to_string(),
291            vec!["10.0.0.5/32".parse::<AddressRange>().unwrap()],
292        )]);
293        let manager = AddressPoolManager::new(test_network(), user_pools).unwrap();
294
295        assert!(manager.allocate_address("alice").is_some());
296        assert!(manager.allocate_address("alice").is_none());
297        // Global pool still works for other users
298        assert!(manager.allocate_address("bob").is_some());
299    }
300
301    #[test]
302    fn manager_release_user_pool_and_reallocate() {
303        let user_pools = HashMap::from([(
304            "alice".to_string(),
305            vec!["10.0.0.5/32".parse::<AddressRange>().unwrap()],
306        )]);
307        let manager = AddressPoolManager::new(test_network(), user_pools).unwrap();
308
309        let addr = manager.allocate_address("alice").unwrap();
310        assert!(manager.allocate_address("alice").is_none());
311
312        manager.release_address("alice", &addr.addr());
313        assert!(manager.allocate_address("alice").is_some());
314    }
315
316    #[test]
317    fn manager_release_global_and_reallocate() {
318        let manager = AddressPoolManager::new(test_network(), HashMap::new()).unwrap();
319
320        let addr = manager.allocate_address("bob").unwrap();
321        manager.release_address("bob", &addr.addr());
322
323        let addr2 = manager.allocate_address("bob").unwrap();
324        assert_eq!(addr, addr2);
325    }
326
327    #[test]
328    fn manager_rejects_user_pool_outside_network() {
329        let user_pools = HashMap::from([(
330            "alice".to_string(),
331            vec!["192.168.1.1/32".parse::<AddressRange>().unwrap()],
332        )]);
333        let result = AddressPoolManager::new(test_network(), user_pools);
334        assert!(result.is_err());
335    }
336
337    #[test]
338    fn manager_no_user_pools() {
339        let manager = AddressPoolManager::new(test_network(), HashMap::new()).unwrap();
340
341        // Should get .2 through .6 (5 usable addresses)
342        for expected in 2..=6u8 {
343            let addr = manager.allocate_address("anyone").unwrap();
344            assert_eq!(addr.addr(), IpAddr::from(Ipv4Addr::new(10, 0, 0, expected)));
345        }
346        assert!(manager.allocate_address("anyone").is_none());
347    }
348
349    #[test]
350    fn manager_rejects_user_pool_with_network_address() {
351        let user_pools = HashMap::from([(
352            "alice".to_string(),
353            vec!["10.0.0.0/32".parse::<AddressRange>().unwrap()],
354        )]);
355        let result = AddressPoolManager::new(test_network(), user_pools);
356        assert!(result.is_err());
357        let err = result.err().unwrap().to_string();
358        assert!(err.contains("reserved tunnel address"), "error: {err}");
359    }
360
361    #[test]
362    fn manager_rejects_user_pool_with_server_address() {
363        let user_pools = HashMap::from([(
364            "alice".to_string(),
365            vec!["10.0.0.1/32".parse::<AddressRange>().unwrap()],
366        )]);
367        let result = AddressPoolManager::new(test_network(), user_pools);
368        assert!(result.is_err());
369        let err = result.err().unwrap().to_string();
370        assert!(err.contains("reserved tunnel address"), "error: {err}");
371    }
372
373    #[test]
374    fn manager_rejects_user_pool_with_broadcast_address() {
375        let user_pools = HashMap::from([(
376            "alice".to_string(),
377            vec!["10.0.0.7/32".parse::<AddressRange>().unwrap()],
378        )]);
379        let result = AddressPoolManager::new(test_network(), user_pools);
380        assert!(result.is_err());
381        let err = result.err().unwrap().to_string();
382        assert!(err.contains("reserved tunnel address"), "error: {err}");
383    }
384
385    #[test]
386    fn manager_netmask_preserved() {
387        let manager = AddressPoolManager::new(test_network(), HashMap::new()).unwrap();
388        let addr = manager.allocate_address("bob").unwrap();
389        assert_eq!(addr.netmask(), test_network().netmask());
390    }
391}