1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct Cidr {
21 ip: IpAddr,
22 prefix_len: u8,
23}
24
25impl Cidr {
26 pub fn parse(host: &str) -> Result<Self, String> {
39 if host.contains(':') {
40 parse_v6(host)
41 } else {
42 parse_v4(host)
43 }
44 }
45
46 #[must_use]
61 pub fn contains(&self, ip: IpAddr) -> bool {
62 match (self.ip, ip.to_canonical()) {
63 (IpAddr::V4(_), IpAddr::V4(candidate)) if candidate.is_unspecified() => false,
64 (IpAddr::V4(range), IpAddr::V4(candidate)) => {
65 let mask = v4_mask(self.prefix_len);
66 u32::from(range) & mask == u32::from(candidate) & mask
67 }
68 (IpAddr::V6(range), IpAddr::V6(candidate)) => {
69 let mask = v6_mask(self.prefix_len);
70 u128::from(range) & mask == u128::from(candidate) & mask
71 }
72 (IpAddr::V4(_), IpAddr::V6(_)) | (IpAddr::V6(_), IpAddr::V4(_)) => false,
73 }
74 }
75}
76
77fn parse_v4(cidr: &str) -> Result<Cidr, String> {
81 let parse_fail = || format!("Could not parse [{cidr}]");
82 let (address, prefix) = cidr.split_once('/').ok_or_else(parse_fail)?;
83 let octets: Vec<&str> = address.split('.').collect();
84 if octets.len() != 4
85 || !octets.iter().all(|octet| ascii_digits(octet, 3))
86 || !ascii_digits(prefix, 2)
87 {
88 return Err(parse_fail());
89 }
90 let mut bytes = [0_u8; 4];
91 for (byte, octet) in bytes.iter_mut().zip(&octets) {
92 let value: u32 = octet.parse().map_err(|_| parse_fail())?;
93 *byte = u8::try_from(value).map_err(|_| format!("Value [{value}] not in range [0,255]"))?;
94 }
95 let prefix_len: u8 = prefix.parse().map_err(|_| parse_fail())?;
96 if prefix_len > 32 {
97 return Err(format!("Value [{prefix_len}] not in range [0,32]"));
98 }
99 Ok(Cidr {
100 ip: IpAddr::V4(Ipv4Addr::from(bytes)),
101 prefix_len,
102 })
103}
104
105fn parse_v6(cidr: &str) -> Result<Cidr, String> {
109 let Some((address, prefix)) = cidr.split_once('/') else {
110 return Err(format!("Could not parse [{cidr}] - missing prefix length"));
111 };
112 let prefix_len = java_parse_int(prefix)
113 .ok_or_else(|| format!("Could not parse [{cidr}] - invalid prefix length"))?;
114 let prefix_len = u8::try_from(prefix_len)
115 .ok()
116 .filter(|len| *len <= 128)
117 .ok_or_else(|| {
118 format!("Could not parse [{cidr}] - prefix length must be between 0 and 128")
119 })?;
120 let literal = address
122 .strip_prefix('[')
123 .and_then(|inner| inner.strip_suffix(']'))
124 .unwrap_or(address);
125 let ip: Ipv6Addr = literal
126 .parse()
127 .map_err(|_| format!("Could not parse [{address}]"))?;
128 if ip.to_ipv4_mapped().is_some() {
129 return Err(format!("Could not parse [{address}] - not an IPv6 address"));
130 }
131 Ok(Cidr {
132 ip: IpAddr::V6(ip),
133 prefix_len,
134 })
135}
136
137fn ascii_digits(text: &str, max: usize) -> bool {
139 (1..=max).contains(&text.len()) && text.bytes().all(|b| b.is_ascii_digit())
140}
141
142fn java_parse_int(text: &str) -> Option<i32> {
145 text.parse().ok()
148}
149
150fn v4_mask(prefix_len: u8) -> u32 {
155 if prefix_len == 0 {
156 0
157 } else {
158 u32::MAX << (32 - u32::from(prefix_len))
159 }
160}
161
162fn v6_mask(prefix_len: u8) -> u128 {
164 if prefix_len == 0 {
165 0
166 } else {
167 u128::MAX << (128 - u32::from(prefix_len))
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use {
174 super::*,
175 assert2::check,
176 std::net::{Ipv4Addr, Ipv6Addr},
177 };
178
179 #[yare::parameterized(
180 ipv4_8 = { "10.0.0.0/8", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)), 8 },
181 ipv4_24 = { "192.168.0.0/24", IpAddr::V4(Ipv4Addr::new(192, 168, 0, 0)), 24 },
182 ipv4_single_host = { "10.0.0.5/32", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5)), 32 },
183 ipv6_32 = { "2001:db8::/32", IpAddr::V6("2001:db8::".parse().unwrap()), 32 },
184 decimal_leading_zeroes = { "010.0.0.1/08", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 8 },
185 bracketed_ipv6_signed_prefix = {
186 "[2001:db8::]/+32",
187 IpAddr::V6("2001:db8::".parse().unwrap()),
188 32,
189 },
190 )]
191 fn parse_accepts_valid_cidrs(input: &str, ip: IpAddr, prefix_len: u8) {
192 check!(Cidr::parse(input) == Ok(Cidr { ip, prefix_len }));
193 }
194
195 #[yare::parameterized(
196 not_ip = { "not-an-ip/8", "Could not parse [not-an-ip/8]" },
197 non_numeric_ipv4_prefix = { "10.0.0.0/thirty", "Could not parse [10.0.0.0/thirty]" },
198 ipv4_prefix_too_large = { "10.0.0.0/33", "Value [33] not in range [0,32]" },
199 ipv4_prefix_far_too_large = { "10.0.0.0/100", "Could not parse [10.0.0.0/100]" },
200 ipv4_octet_too_large = { "10.0.256.0/8", "Value [256] not in range [0,255]" },
201 short_ipv4 = { "10.0.0/8", "Could not parse [10.0.0/8]" },
202 repeated_prefix = { "10.0.0.0/8/8", "Could not parse [10.0.0.0/8/8]" },
203 ipv6_prefix_too_large = {
204 "2001:db8::/129",
205 "Could not parse [2001:db8::/129] - prefix length must be between 0 and 128",
206 },
207 ipv6_negative_prefix = {
208 "2001:db8::/-1",
209 "Could not parse [2001:db8::/-1] - prefix length must be between 0 and 128",
210 },
211 ipv6_non_numeric_prefix = {
212 "2001:db8::/x",
213 "Could not parse [2001:db8::/x] - invalid prefix length",
214 },
215 ipv6_repeated_prefix = {
216 "2001:db8::/32/5",
217 "Could not parse [2001:db8::/32/5] - invalid prefix length",
218 },
219 missing_prefix = { "2001:db8::", "Could not parse [2001:db8::] - missing prefix length" },
220 malformed_ipv6 = { "2001:zz8::/32", "Could not parse [2001:zz8::]" },
221 mapped_ipv4_in_ipv6_prefix = {
222 "::ffff:10.0.0.0/104",
223 "Could not parse [::ffff:10.0.0.0] - not an IPv6 address",
224 },
225 )]
226 fn parse_rejects_malformed_cidrs(input: &str, reason: &str) {
227 check!(Cidr::parse(input) == Err(reason.to_owned()));
228 }
229
230 fn check_membership(cidr: &Cidr, members: &[IpAddr], non_members: &[IpAddr]) {
231 for ip in members {
232 check!(cidr.contains(*ip), "ip {ip}");
233 }
234 for ip in non_members {
235 check!(!cidr.contains(*ip), "ip {ip}");
236 }
237 }
238
239 #[test]
240 fn contains_matches_ipv4_ranges() {
241 let cidr = Cidr::parse("10.0.0.0/8").unwrap();
242 check_membership(
243 &cidr,
244 &[
245 IpAddr::V4(Ipv4Addr::new(10, 1, 2, 3)),
246 IpAddr::V4(Ipv4Addr::new(10, 255, 255, 255)),
247 ],
248 &[IpAddr::V4(Ipv4Addr::new(11, 0, 0, 1))],
249 );
250 }
251
252 #[test]
253 fn contains_matches_ipv6_ranges() {
254 let cidr = Cidr::parse("2001:db8::/32").unwrap();
255 check_membership(
256 &cidr,
257 &[
258 IpAddr::V6("2001:db8::5".parse().unwrap()),
259 IpAddr::V6("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff".parse().unwrap()),
260 ],
261 &[IpAddr::V6("2001:db9::5".parse().unwrap())],
262 );
263 }
264
265 #[test]
266 fn contains_matches_ipv4_mapped_ipv6_peer_against_ipv4_cidr() {
267 let v4_cidr = Cidr::parse("10.0.0.0/8").unwrap();
268 let v4_mapped_peer: IpAddr = "::ffff:10.1.2.3".parse().unwrap();
269 check!(v4_cidr.contains(v4_mapped_peer));
270
271 let v4_mapped_miss: IpAddr = "::ffff:11.0.0.1".parse().unwrap();
272 check!(!v4_cidr.contains(v4_mapped_miss));
273 }
274
275 #[test]
276 fn zero_prefix_len_matches_everything_in_family() {
277 let v4_any = Cidr::parse("0.0.0.0/0").unwrap();
278 check!(v4_any.contains(IpAddr::V4(Ipv4Addr::BROADCAST)));
279 check!(!v4_any.contains(IpAddr::V6(Ipv6Addr::LOCALHOST)));
280
281 let v6_any = Cidr::parse("::/0").unwrap();
282 check!(v6_any.contains(IpAddr::V6(Ipv6Addr::LOCALHOST)));
283 }
284
285 #[yare::parameterized(
289 ipv4_any_unspecified = { "0.0.0.0/0", IpAddr::V4(Ipv4Addr::UNSPECIFIED), false },
290 ipv4_eight_unspecified = { "0.0.0.0/8", IpAddr::V4(Ipv4Addr::UNSPECIFIED), false },
291 ipv4_host_unspecified = { "0.0.0.0/32", IpAddr::V4(Ipv4Addr::UNSPECIFIED), false },
292 ipv4_nonzero_in_range = { "0.0.0.0/8", IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), true },
293 ipv4_mapped_unspecified = {
294 "0.0.0.0/0",
295 IpAddr::V6(Ipv4Addr::UNSPECIFIED.to_ipv6_mapped()),
296 false,
297 },
298 ipv6_any_unspecified = { "::/0", IpAddr::V6(Ipv6Addr::UNSPECIFIED), true },
299 ipv6_host_unspecified = { "::/128", IpAddr::V6(Ipv6Addr::UNSPECIFIED), true },
300 )]
301 fn unspecified_address_matches_only_ipv6_ranges(cidr: &str, ip: IpAddr, expected: bool) {
302 check!(Cidr::parse(cidr).unwrap().contains(ip) == expected);
303 }
304}