Skip to main content

stix_matcher/
subset.rs

1//! `ISSUBSET` / `ISSUPERSET` for IPv4/IPv6 addresses and CIDR ranges.
2
3use std::net::IpAddr;
4
5/// A parsed network: an address widened to 128 bits, a prefix length, and whether
6/// it is IPv6 (so families are never mixed).
7struct Network {
8    bits: u128,
9    prefix: u32,
10    is_v6: bool,
11}
12
13/// Parse `"addr"` or `"addr/prefix"` into a `Network`. Bare addresses use the full
14/// prefix length (32 for v4, 128 for v6).
15fn parse_network(s: &str) -> Option<Network> {
16    let (addr_part, prefix_part) = match s.split_once('/') {
17        Some((a, p)) => (a, Some(p)),
18        None => (s, None),
19    };
20    let addr: IpAddr = addr_part.parse().ok()?;
21    match addr {
22        IpAddr::V4(v4) => {
23            let prefix = match prefix_part {
24                Some(p) => p.parse::<u32>().ok().filter(|p| *p <= 32)?,
25                None => 32,
26            };
27            Some(Network {
28                bits: u128::from(u32::from(v4)),
29                prefix: prefix + 96, // align v4 into the low 32 bits of a 128-bit space
30                is_v6: false,
31            })
32        }
33        IpAddr::V6(v6) => {
34            let prefix = match prefix_part {
35                Some(p) => p.parse::<u32>().ok().filter(|p| *p <= 128)?,
36                None => 128,
37            };
38            Some(Network {
39                bits: u128::from(v6),
40                prefix,
41                is_v6: true,
42            })
43        }
44    }
45}
46
47/// Mask `bits` to its top `prefix` bits (out of 128).
48fn masked(bits: u128, prefix: u32) -> u128 {
49    if prefix == 0 {
50        0
51    } else if prefix >= 128 {
52        bits
53    } else {
54        let mask = u128::MAX << (128 - prefix);
55        bits & mask
56    }
57}
58
59/// Whether network `a` is entirely contained within network `b`.
60fn network_subset(a: &Network, b: &Network) -> bool {
61    if a.is_v6 != b.is_v6 {
62        return false;
63    }
64    // `a` is inside `b` only if it is at least as specific and shares b's prefix.
65    a.prefix >= b.prefix && masked(a.bits, b.prefix) == masked(b.bits, b.prefix)
66}
67
68/// Returns true if `value` (an IP address or CIDR range) is contained within
69/// `range` (the `ISSUBSET` operator). IPv4/IPv6 only; mixed families and
70/// unparseable inputs never match.
71pub fn is_subset(value: &str, range: &str) -> bool {
72    match (parse_network(value), parse_network(range)) {
73        (Some(a), Some(b)) => network_subset(&a, &b),
74        _ => false,
75    }
76}
77
78/// Returns true if `value` contains `range` (the `ISSUPERSET` operator) — the
79/// inverse of [`is_subset`].
80pub fn is_superset(value: &str, range: &str) -> bool {
81    is_subset(range, value)
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn ipv4_address_within_cidr() {
90        assert!(is_subset("198.51.100.5", "198.51.100.0/24"));
91        assert!(!is_subset("198.51.101.5", "198.51.100.0/24"));
92    }
93
94    #[test]
95    fn ipv4_cidr_within_cidr() {
96        assert!(is_subset("198.51.100.0/25", "198.51.100.0/24"));
97        assert!(!is_subset("198.51.100.0/23", "198.51.100.0/24"));
98    }
99
100    #[test]
101    fn ipv6_within_cidr() {
102        assert!(is_subset("2001:db8::1", "2001:db8::/32"));
103        assert!(!is_subset("2001:dead::1", "2001:db8::/32"));
104    }
105
106    #[test]
107    fn mismatched_family_is_not_subset() {
108        assert!(!is_subset("198.51.100.5", "2001:db8::/32"));
109    }
110
111    #[test]
112    fn garbage_is_not_subset() {
113        assert!(!is_subset("not-an-ip", "198.51.100.0/24"));
114    }
115
116    #[test]
117    fn superset_is_inverse() {
118        assert!(is_superset("198.51.100.0/24", "198.51.100.5"));
119        assert!(!is_superset("198.51.100.5", "198.51.100.0/24"));
120    }
121}