Skip to main content

zond_engine/core/models/ip/
range.rs

1// Copyright (c) 2026 Erik Lening (hollowpointer) and Contributors
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4// If a copy of the MPL was not distributed with this file, You can obtain one at
5// https://mozilla.org/MPL/2.0/.
6
7//! # IP Range Management
8//!
9//! This module provides models and utilities for managing contiguous ranges of
10//! IP addresses (both IPv4 and IPv6). It is designed for high-performance network
11//! scanning, where efficient storage and quick membership tests are critical.
12//!
13//! Key components:
14//! - [`Ipv4Range`]: specialized 8-byte container for IPv4 corridors.
15//! - [`Ipv6Range`]: specialized 32-byte container for IPv6 corridors.
16//! - [`IpRange`]: a unified enum for protocol-agnostic API usage.
17//! - [`cidr_range`]: constructor for ranges from CIDR notation.
18
19use std::{
20    net::{IpAddr, Ipv4Addr, Ipv6Addr},
21    str::FromStr,
22};
23use thiserror::Error;
24
25/// Errors associated with IP address range operations.
26#[derive(Debug, Error, PartialEq)]
27pub enum IpError {
28    /// Occurs when the start address is numerically greater than the end address.
29    #[error("Invalid range: start address {0} is greater than end address {1}")]
30    InvalidRange(IpAddr, IpAddr),
31
32    /// Occurs when a CIDR prefix is outside the valid range (0-32 for v4, 0-128 for v6).
33    #[error("Invalid CIDR prefix: {0}")]
34    InvalidPrefix(u8),
35
36    /// Occurs when a network calculation error arises from the underlying network library.
37    #[error("Network error: {0}")]
38    NetworkError(String),
39
40    /// Occurs when an IP address string cannot be parsed.
41    #[error("Failed to parse IP address: {0}")]
42    AddrParse(#[from] std::net::AddrParseError),
43
44    /// Occurs when the provided string format for an IP range is recognized as invalid.
45    #[error("Invalid IP range format: {0}")]
46    InvalidFormat(String),
47
48    /// Occurs when parsing an integer value for a prefix length fails.
49    #[error("Invalid prefix number format: {0}")]
50    PrefixParse(#[from] std::num::ParseIntError),
51}
52
53// ══════════════════════════════════════════════════════════════════════════════
54// IPv4 Range
55// ══════════════════════════════════════════════════════════════════════════════
56
57/// A contiguous range of IPv4 addresses defined by a start and end point.
58///
59/// Both boundaries are inclusive. Stored as two `Ipv4Addr` values (8 bytes total).
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub struct Ipv4Range {
62    /// The inclusive starting address of the range.
63    pub start_addr: Ipv4Addr,
64    /// The inclusive ending address of the range.
65    pub end_addr: Ipv4Addr,
66}
67
68impl Ipv4Range {
69    /// Creates a new `Ipv4Range`.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`IpError::InvalidRange`] if `start` is numerically greater than `end`.
74    pub fn new(start: Ipv4Addr, end: Ipv4Addr) -> Result<Self, IpError> {
75        if u32::from(start) <= u32::from(end) {
76            Ok(Self {
77                start_addr: start,
78                end_addr: end,
79            })
80        } else {
81            Err(IpError::InvalidRange(IpAddr::V4(start), IpAddr::V4(end)))
82        }
83    }
84
85    /// Returns an iterator over every [`IpAddr`] within the range.
86    ///
87    /// # Performance
88    ///
89    /// Iterating over large ranges (e.g., /8) is fast, but collecting the results
90    /// into a `Vec` will consume significant memory.
91    pub fn to_iter(&self) -> impl Iterator<Item = IpAddr> {
92        let start: u32 = self.start_addr.into();
93        let end: u32 = self.end_addr.into();
94        (start..=end).map(|ip| IpAddr::V4(Ipv4Addr::from(ip)))
95    }
96
97    /// Checks if the given [`Ipv4Addr`] falls within this range (inclusive).
98    pub fn contains(&self, ip: &Ipv4Addr) -> bool {
99        let start: u32 = self.start_addr.into();
100        let end: u32 = self.end_addr.into();
101        let ip_u32: u32 = (*ip).into();
102        ip_u32 >= start && ip_u32 <= end
103    }
104
105    /// Returns the number of IP addresses in the range.
106    pub fn len(&self) -> u64 {
107        let s_u32: u64 = u32::from(self.start_addr) as u64;
108        let e_u32: u64 = u32::from(self.end_addr) as u64;
109        (e_u32 - s_u32) + 1
110    }
111
112    /// Returns true if the range contains no addresses.
113    pub fn is_empty(&self) -> bool {
114        self.len() == 0
115    }
116}
117
118// ══════════════════════════════════════════════════════════════════════════════
119// IPv6 Range
120// ══════════════════════════════════════════════════════════════════════════════
121
122/// A contiguous range of IPv6 addresses defined by a start and end point.
123///
124/// Both boundaries are inclusive. Stored as two `Ipv6Addr` values (32 bytes total).
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126pub struct Ipv6Range {
127    /// The inclusive starting address of the range.
128    pub start_addr: Ipv6Addr,
129    /// The inclusive ending address of the range.
130    pub end_addr: Ipv6Addr,
131}
132
133impl Ipv6Range {
134    /// Creates a new `Ipv6Range`.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`IpError::InvalidRange`] if `start` is numerically greater than `end`.
139    pub fn new(start: Ipv6Addr, end: Ipv6Addr) -> Result<Self, IpError> {
140        if u128::from(start) <= u128::from(end) {
141            Ok(Self {
142                start_addr: start,
143                end_addr: end,
144            })
145        } else {
146            Err(IpError::InvalidRange(IpAddr::V6(start), IpAddr::V6(end)))
147        }
148    }
149
150    /// Returns an iterator over every [`IpAddr`] within the range.
151    ///
152    /// # Warning
153    ///
154    /// IPv6 ranges can be astronomically large. Iterating over a typical CIDR (like a /64)
155    /// will take millions of years. This method is provided for small, manually
156    /// defined ranges.
157    pub fn to_iter(&self) -> impl Iterator<Item = IpAddr> {
158        let start: u128 = self.start_addr.into();
159        let end: u128 = self.end_addr.into();
160        (start..=end).map(|ip| IpAddr::V6(Ipv6Addr::from(ip)))
161    }
162
163    /// Checks if the given [`Ipv6Addr`] falls within this range (inclusive).
164    pub fn contains(&self, ip: &Ipv6Addr) -> bool {
165        let start: u128 = self.start_addr.into();
166        let end: u128 = self.end_addr.into();
167        let ip_u128: u128 = (*ip).into();
168        ip_u128 >= start && ip_u128 <= end
169    }
170
171    /// Returns the number of IP addresses in the range.
172    pub fn len(&self) -> u128 {
173        let s_u128: u128 = u128::from(self.start_addr);
174        let e_u128: u128 = u128::from(self.end_addr);
175        (e_u128 - s_u128) + 1
176    }
177
178    /// Returns true if the range contains no addresses.
179    pub fn is_empty(&self) -> bool {
180        self.len() == 0
181    }
182}
183
184// ══════════════════════════════════════════════════════════════════════════════
185// Unified IpRange API
186// ══════════════════════════════════════════════════════════════════════════════
187
188/// A unified representation of either an IPv4 or IPv6 range.
189///
190/// This enum acts as the primary entry point for parsing ranges from user input
191/// via [`FromStr`] or for library consumers who want protocol-agnostic logic.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
193pub enum IpRange {
194    /// An IPv4 address range.
195    V4(Ipv4Range),
196    /// An IPv6 address range.
197    V6(Ipv6Range),
198}
199
200impl IpRange {
201    /// Returns the start address of the range as an [`IpAddr`].
202    pub fn start_addr(&self) -> IpAddr {
203        match self {
204            IpRange::V4(r) => IpAddr::V4(r.start_addr),
205            IpRange::V6(r) => IpAddr::V6(r.start_addr),
206        }
207    }
208
209    /// Returns the end address of the range as an [`IpAddr`].
210    pub fn end_addr(&self) -> IpAddr {
211        match self {
212            IpRange::V4(r) => IpAddr::V4(r.end_addr),
213            IpRange::V6(r) => IpAddr::V6(r.end_addr),
214        }
215    }
216
217    /// Checks if the given [`IpAddr`] falls within this range.
218    ///
219    /// Returns `false` if the protocol versions do not match (e.g., checking
220    /// if a V6 address is in a V4 range).
221    pub fn contains(&self, ip: &IpAddr) -> bool {
222        match (self, ip) {
223            (IpRange::V4(r), IpAddr::V4(ip)) => r.contains(ip),
224            (IpRange::V6(r), IpAddr::V6(ip)) => r.contains(ip),
225            _ => false,
226        }
227    }
228
229    /// Returns the total number of IP addresses in the range.
230    pub fn len(&self) -> u128 {
231        match self {
232            IpRange::V4(r) => r.len() as u128,
233            IpRange::V6(r) => r.len(),
234        }
235    }
236
237    /// Returns true if the range contains no addresses.
238    pub fn is_empty(&self) -> bool {
239        self.len() == 0
240    }
241}
242
243impl FromStr for IpRange {
244    type Err = IpError;
245
246    /// Parses an IP range from a string.
247    ///
248    /// Supports:
249    /// - CIDR notation: `192.168.1.0/24`, `2001:db8::/32`
250    /// - Hyphenated ranges: `10.0.0.1-10.0.0.5`, `::1-::f`
251    /// - Single IPs: `1.1.1.1`, `::1`
252    fn from_str(s: &str) -> Result<Self, Self::Err> {
253        let s = s.trim();
254
255        // Handle CIDR
256        if let Some(pos) = s.find('/') {
257            let ip = s[..pos].parse::<IpAddr>()?;
258            let prefix = s[pos + 1..].parse::<u8>()?;
259            return cidr_range(ip, prefix);
260        }
261
262        // Handle hyphenated range
263        if let Some(pos) = s.find('-') {
264            let start_str = s[..pos].trim();
265            let end_str = s[pos + 1..].trim();
266
267            if let Ok(start) = start_str.parse::<Ipv4Addr>() {
268                let end = end_str.parse::<Ipv4Addr>()?;
269                return Ok(IpRange::V4(Ipv4Range::new(start, end)?));
270            } else if let Ok(start) = start_str.parse::<Ipv6Addr>() {
271                let end = end_str.parse::<Ipv6Addr>()?;
272                return Ok(IpRange::V6(Ipv6Range::new(start, end)?));
273            }
274            return Err(IpError::InvalidFormat(s.to_string()));
275        }
276
277        // Handle single IP
278        let ip = s.parse::<IpAddr>()?;
279        match ip {
280            IpAddr::V4(v4) => Ok(IpRange::V4(Ipv4Range::new(v4, v4).unwrap())),
281            IpAddr::V6(v6) => Ok(IpRange::V6(Ipv6Range::new(v6, v6).unwrap())),
282        }
283    }
284}
285
286/// Constructs an [`IpRange`] from an IP address and a CIDR prefix length.
287///
288/// # Examples
289///
290/// ```
291/// use zond_engine::core::models::ip::range::{cidr_range, IpRange};
292/// use std::net::IpAddr;
293///
294/// let range = cidr_range("192.168.1.5".parse().unwrap(), 24).unwrap();
295/// assert_eq!(range.len(), 256);
296/// ```
297pub fn cidr_range(ip: IpAddr, prefix: u8) -> Result<IpRange, IpError> {
298    match ip {
299        IpAddr::V4(v4) => {
300            if prefix > 32 {
301                return Err(IpError::InvalidPrefix(prefix));
302            }
303
304            let ip_u32 = u32::from(v4);
305            let mask = if prefix == 0 {
306                0
307            } else {
308                !u32::MAX.checked_shr(prefix as u32).unwrap_or(0)
309            };
310
311            let network = ip_u32 & mask;
312            let broadcast = ip_u32 | !mask;
313
314            Ok(IpRange::V4(
315                Ipv4Range::new(Ipv4Addr::from(network), Ipv4Addr::from(broadcast)).unwrap(),
316            ))
317        }
318        IpAddr::V6(v6) => {
319            if prefix > 128 {
320                return Err(IpError::InvalidPrefix(prefix));
321            }
322
323            let ip_u128 = u128::from(v6);
324            let mask = if prefix == 0 {
325                0
326            } else {
327                !u128::MAX.checked_shr(prefix as u32).unwrap_or(0)
328            };
329
330            let network = ip_u128 & mask;
331            let broadcast = ip_u128 | !mask;
332
333            Ok(IpRange::V6(
334                Ipv6Range::new(Ipv6Addr::from(network), Ipv6Addr::from(broadcast)).unwrap(),
335            ))
336        }
337    }
338}
339
340// ╔════════════════════════════════════════════╗
341// ║ ████████╗███████╗███████╗████████╗███████╗ ║
342// ║ ╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝██╔════╝ ║
343// ║    ██║   █████╗  ███████╗   ██║   ███████╗ ║
344// ║    ██║   ██╔══╝  ╚════██║   ██║   ╚════██║ ║
345// ║    ██║   ███████╗███████║   ██║   ███████║ ║
346// ║    ╚═╝   ╚══════╝╚══════╝   ╚═╝   ╚══════╝ ║
347// ╚════════════════════════════════════════════╝
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    // --- IPv4 Specific Tests ---
354
355    #[test]
356    fn new_valid_v4() {
357        let start = Ipv4Addr::new(192, 168, 1, 1);
358        let end = Ipv4Addr::new(192, 168, 1, 10);
359        let range = Ipv4Range::new(start, end).unwrap();
360        assert_eq!(range.start_addr, start);
361        assert_eq!(range.end_addr, end);
362    }
363
364    #[test]
365    fn len_calculations_v4() {
366        let cases = vec![
367            (Ipv4Addr::new(10, 0, 0, 0), Ipv4Addr::new(10, 0, 0, 0), 1),
368            (
369                Ipv4Addr::new(10, 0, 0, 0),
370                Ipv4Addr::new(10, 0, 0, 255),
371                256,
372            ),
373            (Ipv4Addr::new(0, 0, 0, 0), Ipv4Addr::new(0, 0, 0, 10), 11),
374        ];
375
376        for (start, end, expected_len) in cases {
377            let range = Ipv4Range::new(start, end).unwrap();
378            assert_eq!(range.len(), expected_len);
379        }
380    }
381
382    #[test]
383    fn contains_logic_v4() {
384        let range =
385            Ipv4Range::new(Ipv4Addr::new(172, 16, 0, 10), Ipv4Addr::new(172, 16, 0, 20)).unwrap();
386        assert!(range.contains(&Ipv4Addr::new(172, 16, 0, 10)));
387        assert!(range.contains(&Ipv4Addr::new(172, 16, 0, 15)));
388        assert!(range.contains(&Ipv4Addr::new(172, 16, 0, 20)));
389        assert!(!range.contains(&Ipv4Addr::new(172, 16, 0, 9)));
390        assert!(!range.contains(&Ipv4Addr::new(172, 16, 0, 21)));
391    }
392
393    #[test]
394    fn iteration_values_v4() {
395        let range = Ipv4Range::new(Ipv4Addr::new(1, 1, 1, 1), Ipv4Addr::new(1, 1, 1, 3)).unwrap();
396        let ips: Vec<IpAddr> = range.to_iter().collect();
397        assert_eq!(
398            ips,
399            vec![
400                IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
401                IpAddr::V4(Ipv4Addr::new(1, 1, 1, 2)),
402                IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)),
403            ]
404        );
405    }
406
407    #[test]
408    fn max_u32_range_boundaries() {
409        let start = Ipv4Addr::new(255, 255, 255, 254);
410        let end = Ipv4Addr::new(255, 255, 255, 255);
411        let range = Ipv4Range::new(start, end).unwrap();
412        assert_eq!(range.len(), 2);
413    }
414
415    // --- IPv6 Specific Tests ---
416
417    #[test]
418    fn ipv6_range_basics() {
419        let start = Ipv6Addr::from(100);
420        let end = Ipv6Addr::from(200);
421        let range = Ipv6Range::new(start, end).unwrap();
422        assert_eq!(range.len(), 101);
423        assert!(range.contains(&Ipv6Addr::from(150)));
424        assert!(!range.contains(&Ipv6Addr::from(201)));
425    }
426
427    #[test]
428    fn ipv6_large_len() {
429        let range = cidr_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 64).unwrap();
430        assert_eq!(range.len(), 1u128 << 64);
431    }
432
433    #[test]
434    fn iteration_ipv6_small() {
435        let range = Ipv6Range::new(Ipv6Addr::from(1), Ipv6Addr::from(3)).unwrap();
436        let ips: Vec<_> = range.to_iter().collect();
437        assert_eq!(ips.len(), 3);
438        assert_eq!(ips[0], IpAddr::V6(Ipv6Addr::from(1)));
439    }
440
441    // --- Parsing & Global Tests ---
442
443    #[test]
444    fn from_str_comprehensive() {
445        assert_eq!("10.0.0.0/24".parse::<IpRange>().unwrap().len(), 256);
446        assert_eq!("192.168.1.0/24".parse::<IpRange>().unwrap().len(), 256);
447        assert_eq!("::1/120".parse::<IpRange>().unwrap().len(), 256);
448        assert_eq!("1.1.1.1-1.1.1.5".parse::<IpRange>().unwrap().len(), 5);
449        assert_eq!("8.8.8.8".parse::<IpRange>().unwrap().len(), 1);
450    }
451
452    #[test]
453    fn invalid_range_order() {
454        let v4_err = Ipv4Range::new(Ipv4Addr::new(1, 1, 1, 2), Ipv4Addr::new(1, 1, 1, 1));
455        assert!(matches!(v4_err, Err(IpError::InvalidRange(_, _))));
456
457        let v6_err = Ipv6Range::new(Ipv6Addr::from(2), Ipv6Addr::from(1));
458        assert!(matches!(v6_err, Err(IpError::InvalidRange(_, _))));
459    }
460
461    #[test]
462    fn error_formatting() {
463        let prefix_err = IpError::InvalidPrefix(40);
464        assert_eq!(format!("{prefix_err}"), "Invalid CIDR prefix: 40");
465
466        let range_err = IpError::InvalidRange(
467            IpAddr::V4(Ipv4Addr::new(1, 1, 1, 2)),
468            IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
469        );
470        assert!(format!("{range_err}").contains("is greater than"));
471    }
472}
473
474#[cfg(test)]
475mod property_tests {
476    use super::*;
477    use proptest::prelude::*;
478
479    fn any_ipv4() -> impl Strategy<Value = Ipv4Addr> {
480        proptest::prelude::any::<u32>().prop_map(Ipv4Addr::from)
481    }
482
483    fn any_ipv6() -> impl Strategy<Value = Ipv6Addr> {
484        proptest::prelude::any::<u128>().prop_map(Ipv6Addr::from)
485    }
486
487    fn any_ipv4_range() -> impl Strategy<Value = Ipv4Range> {
488        (any_ipv4(), 0..5000u32).prop_map(|(start, len)| {
489            let start_u32 = u32::from(start);
490            let end_u32 = start_u32.saturating_add(len);
491            Ipv4Range::new(start, Ipv4Addr::from(end_u32)).unwrap()
492        })
493    }
494
495    fn any_ipv6_range() -> impl Strategy<Value = Ipv6Range> {
496        (any_ipv6(), 0..5000u128).prop_map(|(start, len)| {
497            let start_u128 = u128::from(start);
498            let end_u128 = start_u128.saturating_add(len);
499            Ipv6Range::new(start, Ipv6Addr::from(end_u128)).unwrap()
500        })
501    }
502
503    proptest::proptest! {
504        #[test]
505        fn ipv4_range_invariant(a in any_ipv4(), b in any_ipv4()) {
506            let start = std::cmp::min(a, b);
507            let end = std::cmp::max(a, b);
508            let range = Ipv4Range::new(start, end).unwrap();
509            prop_assert!(range.contains(&start));
510            prop_assert!(range.contains(&end));
511            prop_assert_eq!(range.len(), (u32::from(end) - u32::from(start)) as u64 + 1);
512        }
513
514        #[test]
515        fn ipv6_range_invariant(a in any_ipv6(), b in any_ipv6()) {
516            let start = std::cmp::min(a, b);
517            let end = std::cmp::max(a, b);
518            let range = Ipv6Range::new(start, end).unwrap();
519            prop_assert!(range.contains(&start));
520            prop_assert!(range.contains(&end));
521            prop_assert_eq!(range.len(), (u128::from(end) - u128::from(start)) + 1);
522        }
523
524        #[test]
525        fn ipv4_iterator_consistency(range in any_ipv4_range()) {
526            prop_assert_eq!(range.to_iter().count() as u64, range.len());
527        }
528
529        #[test]
530        fn ipv6_iterator_consistency(range in any_ipv6_range()) {
531            prop_assert_eq!(range.to_iter().count() as u128, range.len());
532        }
533
534        #[test]
535        fn cidr_v4_roundtrip(v4 in any_ipv4(), prefix in 1..=32u8) {
536            let range = cidr_range(IpAddr::V4(v4), prefix).unwrap();
537            prop_assert_eq!(range.len(), 1u128 << (32 - prefix));
538        }
539
540        #[test]
541        fn cidr_v6_roundtrip(v6 in any_ipv6(), prefix in 1..=128u8) {
542            let range = cidr_range(IpAddr::V6(v6), prefix).unwrap();
543            prop_assert_eq!(range.len(), 1u128 << (128 - prefix));
544        }
545    }
546}