1use std::net::IpAddr;
4
5struct Network {
8 bits: u128,
9 prefix: u32,
10 is_v6: bool,
11}
12
13fn 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, 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
47fn 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
59fn network_subset(a: &Network, b: &Network) -> bool {
61 if a.is_v6 != b.is_v6 {
62 return false;
63 }
64 a.prefix >= b.prefix && masked(a.bits, b.prefix) == masked(b.bits, b.prefix)
66}
67
68pub 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
78pub 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}