ys1r/
ipaggregator.rs

1use std::net::Ipv4Addr;
2
3/// Represents an IPv4 CIDR block (address + prefix length)
4#[derive(Debug, Clone)]
5pub struct Cidr {
6    pub addr: Ipv4Addr,
7    pub prefix: u8,
8}
9
10#[allow(unused)]
11fn ip_to_u32(ip: &str) -> u32 {
12    let addr: Ipv4Addr = ip.parse().unwrap();
13    u32::from(addr)
14}
15
16#[allow(unused)]
17fn u32_to_ip(n: u32) -> Ipv4Addr {
18    Ipv4Addr::from(n)
19}
20
21#[allow(unused)]
22fn is_continuous(block: &[u32]) -> bool {
23    block.windows(2).all(|w| w[1] == w[0] + 1)
24}
25
26#[allow(unused)]
27fn is_aligned(start: u32, size: usize) -> bool {
28    start.is_multiple_of(size as u32)
29}
30
31#[allow(unused)]
32/// Converts a CIDR block into ACL format:
33/// - /32 → "host x.x.x.x"
34/// - otherwise → "network subnet-mask"
35pub fn cidr_to_acl(cidr: &Cidr) -> String {
36    if cidr.prefix == 32 {
37        return format!("host {}", cidr.addr);
38    }
39
40    // Build subnet mask from prefix
41    let mask = u32::MAX << (32 - cidr.prefix);
42    let octets = mask.to_be_bytes();
43
44    format!(
45        "{} {}.{}.{}.{}",
46        cidr.addr, octets[0], octets[1], octets[2], octets[3]
47    )
48}
49
50#[allow(unused)]
51fn is_valid_segment(i: usize, next_size: usize, ints: &Vec<u32>) -> bool {
52    i + next_size <= ints.len()
53        && is_aligned(ints[i], next_size)
54        && is_continuous(&ints[i..i + next_size])
55}
56
57/// Aggregate a list of IPv4 addresses into the smallest possible set of CIDR blocks.
58///
59/// This function takes a slice of IPv4 addresses and returns a `Vec<Cidr>`
60/// representing the minimal set of CIDR blocks that cover all the input IPs.
61/// It works by:
62/// 1. Converting IPs to their `u32` integer representation.
63/// 2. Sorting the integers to process them in order.
64/// 3. Iteratively finding the largest contiguous, properly aligned block
65///    starting at each IP.
66/// 4. Calculating the CIDR prefix for each block and creating a `Cidr` struct.
67///
68/// # Arguments
69///
70/// * `ip_list` - A slice of `Ipv4Addr` to aggregate.
71///
72/// # Returns
73///
74/// A `Vec<Cidr>` containing the aggregated CIDR blocks.
75///
76/// # Example
77///
78/// ```rust
79/// use std::net::Ipv4Addr;
80/// use ys1r::ipaggregator::aggregate_ips;
81/// use ys1r::ipaggregator::cidr_to_acl;
82///
83/// let mut ip_list: Vec<Ipv4Addr> = Vec::new();
84/// for i in 1..=254 {
85///     ip_list.push(Ipv4Addr::new(10, 0, 0, i));
86/// }
87///
88/// let cidrs = aggregate_ips(&ip_list);
89///
90/// for c in &cidrs {
91///     println!("{}\t<=\t{:?}", cidr_to_acl(c), &c);
92/// }
93/// ```
94///
95/// This will produce the minimal CIDR blocks covering 10.0.0.1–10.0.0.254.
96pub fn aggregate_ips(ip_list: &[Ipv4Addr]) -> Vec<Cidr> {
97    let mut ints: Vec<u32> = ip_list.iter().map(|ip| u32::from(*ip)).collect();
98    ints.sort();
99
100    let mut result = Vec::new();
101    let mut i = 0;
102
103    while i < ints.len() {
104        let mut size = 1;
105
106        loop {
107            let next_size = size * 2;
108            if !is_valid_segment(i, next_size, &ints) {
109                break;
110            }
111            size = next_size;
112        }
113
114        let prefix = (32 - size.trailing_zeros()) as u8;
115        let mask: u32 = if prefix == 0 {
116            0
117        } else {
118            (!0u32) << (32 - prefix)
119        };
120        let network = ints[i] & mask;
121
122        result.push(Cidr {
123            addr: Ipv4Addr::from(network),
124            prefix,
125        });
126
127        i += size;
128    }
129
130    result
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use std::net::Ipv4Addr;
137
138    #[test]
139    fn test_ip_to_u32_and_back() {
140        let ip_str = "192.168.1.1";
141        let ip_num = ip_to_u32(ip_str);
142        let ip_back = u32_to_ip(ip_num);
143        assert_eq!(ip_back, Ipv4Addr::new(192, 168, 1, 1));
144    }
145
146    #[test]
147    fn test_is_continuous() {
148        let block = vec![1, 2, 3, 4, 5];
149        assert!(is_continuous(&block));
150        let block2 = vec![1, 2, 4, 5];
151        assert!(!is_continuous(&block2));
152    }
153
154    #[test]
155    fn test_is_aligned() {
156        assert!(is_aligned(8, 4)); // 8 % 4 == 0
157        assert!(!is_aligned(7, 4)); // 7 % 4 != 0
158    }
159
160    #[test]
161    fn test_aggregate_ips_basic() {
162        // Input: 10.0.0.1 to 10.0.0.4
163        let mut ips: Vec<Ipv4Addr> = Vec::new();
164        for i in 1..=4 {
165            ips.push(Ipv4Addr::new(10, 0, 0, i));
166        }
167
168        let cidrs = aggregate_ips(&ips);
169
170        let expected = vec![
171            Cidr {
172                addr: Ipv4Addr::new(10, 0, 0, 1),
173                prefix: 32,
174            },
175            Cidr {
176                addr: Ipv4Addr::new(10, 0, 0, 2),
177                prefix: 31,
178            },
179            Cidr {
180                addr: Ipv4Addr::new(10, 0, 0, 4),
181                prefix: 32,
182            },
183        ];
184
185        assert_eq!(cidrs.len(), expected.len());
186        assert_eq!(cidrs[0].addr, expected[0].addr);
187        assert_eq!(cidrs[0].prefix, expected[0].prefix);
188    }
189
190    #[test]
191    fn test_aggregate_ips_full_range() {
192        // Generate 10.0.0.1 - 10.0.0.8
193        let mut ips: Vec<Ipv4Addr> = Vec::new();
194        for i in 1..=8 {
195            ips.push(Ipv4Addr::new(10, 0, 0, i));
196        }
197
198        let cidrs = aggregate_ips(&ips);
199
200        assert_eq!(cidrs.len(), 4);
201        assert_eq!(cidrs[0].addr, Ipv4Addr::new(10, 0, 0, 1));
202        assert_eq!(cidrs[0].prefix, 32);
203
204        assert_eq!(cidrs[1].addr, Ipv4Addr::new(10, 0, 0, 2));
205        assert_eq!(cidrs[1].prefix, 31);
206
207        assert_eq!(cidrs[2].addr, Ipv4Addr::new(10, 0, 0, 4));
208        assert_eq!(cidrs[2].prefix, 30);
209
210        assert_eq!(cidrs[3].addr, Ipv4Addr::new(10, 0, 0, 8));
211        assert_eq!(cidrs[3].prefix, 32);
212    }
213}