pingap_util/ip.rs
1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use ipnet::IpNet;
16use std::net::{AddrParseError, IpAddr};
17use std::str::FromStr;
18
19/// IpRules stores IP addresses and networks for access control
20/// - ip_net_list: List of IP network ranges (CIDR notation)
21/// - ip_list: List of individual IP addresses as strings
22#[derive(Clone, Debug)]
23pub struct IpRules {
24 ip_net_list: Vec<IpNet>,
25 ip_list: Vec<String>,
26}
27
28impl IpRules {
29 /// Creates a new IpRules instance from a list of IP addresses/networks
30 ///
31 /// Separates the input values into two categories:
32 /// - Valid CIDR networks go into ip_net_list
33 /// - Individual IP addresses go into ip_list
34 pub fn new(values: &Vec<String>) -> Self {
35 let mut ip_net_list = vec![];
36 let mut ip_list = vec![];
37 for item in values {
38 // Try parsing as CIDR network (e.g., "192.168.1.0/24")
39 if let Ok(value) = IpNet::from_str(item) {
40 ip_net_list.push(value);
41 } else {
42 // If not a valid CIDR, treat as individual IP
43 ip_list.push(item.to_string());
44 }
45 }
46 Self {
47 ip_net_list,
48 ip_list,
49 }
50 }
51
52 /// Checks if a given IP address matches any of the stored rules
53 ///
54 /// Returns:
55 /// - Ok(true) if IP matches either an individual IP or falls within a network range
56 /// - Ok(false) if no match is found
57 /// - Err if the IP address string cannot be parsed
58 pub fn is_match(&self, ip: &String) -> Result<bool, AddrParseError> {
59 let found = if self.ip_list.contains(ip) {
60 // First check for exact match in individual IP list
61 true
62 } else {
63 // Then check if IP falls within any of the network ranges
64 let addr = ip.parse::<IpAddr>()?;
65 self.ip_net_list.iter().any(|item| item.contains(&addr))
66 };
67 Ok(found)
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use pretty_assertions::assert_eq;
75
76 #[test]
77 fn test_ip_rules() {
78 let ip_rules = IpRules::new(&vec![
79 "192.168.1.0/24".to_string(),
80 "192.168.2.1".to_string(),
81 ]);
82 assert_eq!(ip_rules.ip_net_list.len(), 1);
83 assert_eq!(ip_rules.ip_list.len(), 1);
84
85 assert_eq!(ip_rules.is_match(&"192.168.1.1".to_string()), Ok(true));
86 assert_eq!(ip_rules.is_match(&"192.168.2.1".to_string()), Ok(true));
87 assert_eq!(ip_rules.is_match(&"192.168.3.1".to_string()), Ok(false));
88 }
89}