⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/services/cni/
ipam.rs

1//! IPAM (IP Address Management)
2//!
3//! Provides bitmap-based IP address allocation for container networking
4//! with per-subnet management and overflow detection.
5
6#![allow(dead_code)]
7
8use alloc::{string::String, vec::Vec};
9
10// ---------------------------------------------------------------------------
11// IPAM Config
12// ---------------------------------------------------------------------------
13
14/// IPAM configuration for a subnet.
15#[derive(Debug, Clone)]
16pub struct IpamConfig {
17    /// Subnet base address (host byte order).
18    pub subnet: u32,
19    /// Prefix length (e.g., 24 for /24).
20    pub prefix_len: u8,
21    /// Gateway address.
22    pub gateway: u32,
23    /// Start of allocatable range (host part only).
24    pub range_start: u32,
25    /// End of allocatable range (host part only, inclusive).
26    pub range_end: u32,
27}
28
29impl IpamConfig {
30    /// Create a new IPAM config.
31    ///
32    /// `subnet` is the network address (e.g., 10.244.0.0 = 0x0AF40000).
33    /// `prefix_len` is the CIDR prefix (e.g., 24).
34    /// `gateway` is the gateway host part (e.g., 1 for .1).
35    pub fn new(subnet: u32, prefix_len: u8, gateway_host: u32) -> Self {
36        let mask = if prefix_len >= 32 {
37            0xFFFF_FFFF
38        } else {
39            !((1u32 << (32 - prefix_len)) - 1)
40        };
41        let subnet_base = subnet & mask;
42        let host_bits = 32 - prefix_len as u32;
43        let max_host = if host_bits >= 32 {
44            0xFFFF_FFFF
45        } else {
46            (1u32 << host_bits) - 1
47        };
48
49        IpamConfig {
50            subnet: subnet_base,
51            prefix_len,
52            gateway: subnet_base | gateway_host,
53            range_start: 2, // skip .0 (network) and .1 (gateway)
54            range_end: max_host.saturating_sub(1), // skip broadcast
55        }
56    }
57
58    /// Get the subnet mask.
59    pub fn mask(&self) -> u32 {
60        if self.prefix_len >= 32 {
61            0xFFFF_FFFF
62        } else {
63            !((1u32 << (32 - self.prefix_len)) - 1)
64        }
65    }
66
67    /// Get the total number of allocatable addresses.
68    pub fn total_addresses(&self) -> u32 {
69        if self.range_end >= self.range_start {
70            self.range_end - self.range_start + 1
71        } else {
72            0
73        }
74    }
75
76    /// Format an IP address as a string.
77    pub fn format_ip(ip: u32) -> String {
78        alloc::format!(
79            "{}.{}.{}.{}",
80            (ip >> 24) & 0xFF,
81            (ip >> 16) & 0xFF,
82            (ip >> 8) & 0xFF,
83            ip & 0xFF
84        )
85    }
86}
87
88// ---------------------------------------------------------------------------
89// IPAM Error
90// ---------------------------------------------------------------------------
91
92/// IPAM error.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum IpamError {
95    /// No addresses available.
96    Exhausted,
97    /// Address not allocated.
98    NotAllocated(u32),
99    /// Address already allocated.
100    AlreadyAllocated(u32),
101    /// Address out of range.
102    OutOfRange(u32),
103}
104
105// ---------------------------------------------------------------------------
106// IPAM Allocator
107// ---------------------------------------------------------------------------
108
109/// Bitmap-based IP address allocator for a single subnet.
110#[derive(Debug)]
111pub struct IpamAllocator {
112    /// Bitmap of allocated addresses (1 bit per host address).
113    bitmap: Vec<u64>,
114    /// Subnet base address.
115    subnet_base: u32,
116    /// Prefix length.
117    prefix_len: u8,
118    /// Range start (host part).
119    range_start: u32,
120    /// Range end (host part, inclusive).
121    range_end: u32,
122    /// Number of currently allocated addresses.
123    allocated: u32,
124    /// Gateway address.
125    gateway: u32,
126}
127
128impl IpamAllocator {
129    /// Create a new IPAM allocator from config.
130    pub fn new(config: &IpamConfig) -> Self {
131        // Calculate bitmap size: enough u64s to cover the range
132        let total = config.total_addresses();
133        let bitmap_len = (total as usize).div_ceil(64);
134
135        IpamAllocator {
136            bitmap: alloc::vec![0u64; bitmap_len],
137            subnet_base: config.subnet,
138            prefix_len: config.prefix_len,
139            range_start: config.range_start,
140            range_end: config.range_end,
141            allocated: 0,
142            gateway: config.gateway,
143        }
144    }
145
146    /// Allocate the next available IP address.
147    ///
148    /// Returns the full IP address.
149    pub fn allocate(&mut self) -> Result<u32, IpamError> {
150        let total = self.total_addresses();
151        if self.allocated >= total {
152            return Err(IpamError::Exhausted);
153        }
154
155        // Find first free bit
156        for (word_idx, word) in self.bitmap.iter_mut().enumerate() {
157            if *word == u64::MAX {
158                continue;
159            }
160            // Find first zero bit
161            let bit = (!*word).trailing_zeros();
162            if bit >= 64 {
163                continue;
164            }
165            let offset = (word_idx as u32) * 64 + bit;
166            let host_part = self.range_start + offset;
167            if host_part > self.range_end {
168                break;
169            }
170
171            *word |= 1u64 << bit;
172            self.allocated += 1;
173            return Ok(self.subnet_base | host_part);
174        }
175
176        Err(IpamError::Exhausted)
177    }
178
179    /// Release an allocated IP address.
180    pub fn release(&mut self, ip: u32) -> Result<(), IpamError> {
181        let host_part = ip & !self.mask();
182
183        if host_part < self.range_start || host_part > self.range_end {
184            return Err(IpamError::OutOfRange(ip));
185        }
186
187        let offset = host_part - self.range_start;
188        let word_idx = (offset / 64) as usize;
189        let bit = offset % 64;
190
191        if word_idx >= self.bitmap.len() {
192            return Err(IpamError::OutOfRange(ip));
193        }
194
195        if self.bitmap[word_idx] & (1u64 << bit) == 0 {
196            return Err(IpamError::NotAllocated(ip));
197        }
198
199        self.bitmap[word_idx] &= !(1u64 << bit);
200        self.allocated = self.allocated.saturating_sub(1);
201        Ok(())
202    }
203
204    /// Check if an IP address is currently allocated.
205    pub fn is_allocated(&self, ip: u32) -> bool {
206        let host_part = ip & !self.mask();
207        if host_part < self.range_start || host_part > self.range_end {
208            return false;
209        }
210        let offset = host_part - self.range_start;
211        let word_idx = (offset / 64) as usize;
212        let bit = offset % 64;
213        if word_idx >= self.bitmap.len() {
214            return false;
215        }
216        self.bitmap[word_idx] & (1u64 << bit) != 0
217    }
218
219    /// Get the number of available addresses.
220    pub fn available_count(&self) -> u32 {
221        self.total_addresses().saturating_sub(self.allocated)
222    }
223
224    /// Get the number of allocated addresses.
225    pub fn allocated_count(&self) -> u32 {
226        self.allocated
227    }
228
229    /// Get the total number of allocatable addresses.
230    pub fn total_addresses(&self) -> u32 {
231        if self.range_end >= self.range_start {
232            self.range_end - self.range_start + 1
233        } else {
234            0
235        }
236    }
237
238    /// Get the subnet mask.
239    fn mask(&self) -> u32 {
240        if self.prefix_len >= 32 {
241            0xFFFF_FFFF
242        } else {
243            !((1u32 << (32 - self.prefix_len)) - 1)
244        }
245    }
246
247    /// Get the gateway address.
248    pub fn gateway(&self) -> u32 {
249        self.gateway
250    }
251
252    /// Format the allocator status as a string.
253    pub fn status_string(&self) -> String {
254        alloc::format!(
255            "subnet={}/{} allocated={}/{} available={}",
256            IpamConfig::format_ip(self.subnet_base),
257            self.prefix_len,
258            self.allocated,
259            self.total_addresses(),
260            self.available_count()
261        )
262    }
263}
264
265// ---------------------------------------------------------------------------
266// Tests
267// ---------------------------------------------------------------------------
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    fn ip(a: u8, b: u8, c: u8, d: u8) -> u32 {
274        ((a as u32) << 24) | ((b as u32) << 16) | ((c as u32) << 8) | (d as u32)
275    }
276
277    fn make_allocator() -> IpamAllocator {
278        let config = IpamConfig::new(ip(10, 244, 0, 0), 24, 1);
279        IpamAllocator::new(&config)
280    }
281
282    #[test]
283    fn test_ipam_config_mask() {
284        let config = IpamConfig::new(ip(10, 244, 0, 0), 24, 1);
285        assert_eq!(config.mask(), 0xFFFFFF00);
286        assert_eq!(config.gateway, ip(10, 244, 0, 1));
287    }
288
289    #[test]
290    fn test_ipam_config_total_addresses() {
291        let config = IpamConfig::new(ip(10, 244, 0, 0), 24, 1);
292        // /24: .2 through .254 = 253
293        assert_eq!(config.total_addresses(), 253);
294    }
295
296    #[test]
297    fn test_allocate_first() {
298        let mut alloc = make_allocator();
299        let addr = alloc.allocate().unwrap();
300        assert_eq!(addr, ip(10, 244, 0, 2));
301    }
302
303    #[test]
304    fn test_allocate_sequential() {
305        let mut alloc = make_allocator();
306        let a1 = alloc.allocate().unwrap();
307        let a2 = alloc.allocate().unwrap();
308        let a3 = alloc.allocate().unwrap();
309        assert_eq!(a1, ip(10, 244, 0, 2));
310        assert_eq!(a2, ip(10, 244, 0, 3));
311        assert_eq!(a3, ip(10, 244, 0, 4));
312        assert_eq!(alloc.allocated_count(), 3);
313    }
314
315    #[test]
316    fn test_release_and_realloc() {
317        let mut alloc = make_allocator();
318        let a1 = alloc.allocate().unwrap();
319        let _a2 = alloc.allocate().unwrap();
320        alloc.release(a1).unwrap();
321        assert_eq!(alloc.allocated_count(), 1);
322
323        // Should reuse the released address
324        let a3 = alloc.allocate().unwrap();
325        assert_eq!(a3, a1);
326    }
327
328    #[test]
329    fn test_is_allocated() {
330        let mut alloc = make_allocator();
331        let addr = alloc.allocate().unwrap();
332        assert!(alloc.is_allocated(addr));
333        alloc.release(addr).unwrap();
334        assert!(!alloc.is_allocated(addr));
335    }
336
337    #[test]
338    fn test_release_not_allocated() {
339        let mut alloc = make_allocator();
340        assert_eq!(
341            alloc.release(ip(10, 244, 0, 50)),
342            Err(IpamError::NotAllocated(ip(10, 244, 0, 50)))
343        );
344    }
345
346    #[test]
347    fn test_release_out_of_range() {
348        let mut alloc = make_allocator();
349        assert_eq!(
350            alloc.release(ip(192, 168, 0, 1)),
351            Err(IpamError::OutOfRange(ip(192, 168, 0, 1)))
352        );
353    }
354
355    #[test]
356    fn test_format_ip() {
357        assert_eq!(IpamConfig::format_ip(ip(10, 244, 0, 1)), "10.244.0.1");
358        assert_eq!(IpamConfig::format_ip(ip(192, 168, 1, 100)), "192.168.1.100");
359    }
360}