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}