1pub mod cidr;
4pub mod ip_set;
5pub mod rule_set;
6
7use std::{io, net::IpAddr, path::Path};
8
9use regex::Regex;
10
11use crate::{
12 acl::cidr::Cidr,
13 acl::{ip_set::IpSet, rule_set::RuleSet},
14};
15
16pub struct Acl {
18 bypass_list: IpSet,
19 proxy_list: IpSet,
20 outbound_block_list: IpSet,
21
22 bypass_rules: RuleSet,
23 proxy_rules: RuleSet,
24 outbound_block_rules: RuleSet,
25
26 mode: Mode,
27}
28
29impl Acl {
30 pub fn new() -> Self {
32 Acl {
33 bypass_list: IpSet::new(),
34 proxy_list: IpSet::new(),
35 outbound_block_list: IpSet::new(),
36 bypass_rules: RuleSet::new(),
37 proxy_rules: RuleSet::new(),
38 outbound_block_rules: RuleSet::new(),
39 mode: Mode::WhiteList,
40 }
41 }
42
43 pub fn from_file(path: &Path) -> io::Result<Self> {
45 let data = std::fs::read_to_string(path)?;
46 Ok(Self::from_str(&data))
47 }
48
49 pub fn from_str(data: &str) -> Self {
51 let lines = data
53 .lines()
54 .map(|line| {
55 let line = line.trim();
56 let end = line.find('#').unwrap_or(line.len());
57 &line[..end]
58 })
59 .filter(|line| !line.is_empty());
60
61 let mut acl = Acl::new();
62 let mut cur_ip_set = &mut acl.bypass_list;
63 let mut cur_rule_set = &mut acl.bypass_rules;
64
65 fn insert(record: &str, ip_set: &mut IpSet, rule_set: &mut RuleSet) -> bool {
66 let cidr = record.parse::<Cidr>();
67 if let Ok(cidr) = cidr {
68 ip_set.insert(cidr);
69 log::trace!("Insert {} to the ip set", record);
70 return true;
71 }
72
73 let regex = record.parse::<Regex>();
74 if let Ok(regex) = regex {
75 rule_set.insert(regex);
76 log::trace!("Insert {} to the rule set", record);
77 return true;
78 }
79
80 false
81 }
82
83 for line in lines {
84 match line {
85 "[proxy_all]" | "[accept_all]" => acl.mode = Mode::WhiteList,
86 "[bypass_all]" | "[reject_all]" => acl.mode = Mode::BlackList,
87 "[bypass_list]" | "[black_list]" => {
88 cur_ip_set = &mut acl.bypass_list;
89 cur_rule_set = &mut acl.bypass_rules;
90 }
91 "[proxy_list]" | "[white_list]" => {
92 cur_ip_set = &mut acl.proxy_list;
93 cur_rule_set = &mut acl.proxy_rules;
94 }
95 "[outbound_block_list]" => {
96 cur_ip_set = &mut acl.outbound_block_list;
97 cur_rule_set = &mut acl.outbound_block_rules;
98 }
99 _ => {
100 if !insert(line, cur_ip_set, cur_rule_set) {
101 log::warn!("Insert {} to the ACL failed", line);
102 }
103 }
104 }
105 }
106
107 acl
108 }
109
110 pub fn is_bypass(&self, ip: IpAddr, host: Option<&str>) -> bool {
112 let ip_str = ip.to_string();
113
114 if let Some(host) = host {
115 if host != ip_str {
116 if self.bypass_rules.contains(host) {
117 return true;
118 }
119
120 if self.proxy_rules.contains(host) {
121 return false;
122 }
123 }
124 }
125
126 if self.bypass_list.contains(ip) {
127 return true;
128 }
129
130 if self.proxy_list.contains(ip) {
131 return false;
132 }
133
134 self.mode == Mode::BlackList
135 }
136
137 pub fn is_block_outbound(&self, ip: IpAddr, host: Option<&str>) -> bool {
139 if self.outbound_block_list.contains(ip) {
140 return true;
141 }
142
143 let ip = ip.to_string();
144
145 if self.outbound_block_rules.contains(&ip) {
146 return true;
147 }
148
149 if let Some(host) = host {
150 if host != ip {
151 if self.outbound_block_rules.contains(host) {
152 return true;
153 }
154 }
155 }
156
157 self.mode == Mode::BlackList
158 }
159}
160
161#[derive(PartialEq, Eq)]
163pub enum Mode {
164 WhiteList,
166
167 BlackList,
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_acl() {
177 const DATA: &'static str = r"
178 [proxy_all]
179
180 [bypass_list]
181 127.0.0.0/8
182 192.168.0.0/16
183 ::1/128
184 fc00::/7
185
186 (^|\.)baidu\.com$
187 (^|\.)google\.com$
188 (^|\.)ocfbnj\.cn$
189 ";
190
191 let acl = Acl::from_str(DATA);
192
193 assert_eq!(acl.is_bypass("127.0.0.1".parse().unwrap(), None), true);
194 assert_eq!(acl.is_bypass("192.168.0.1".parse().unwrap(), None), true);
195
196 assert_eq!(acl.is_bypass("::1".parse().unwrap(), None), true);
197 assert_eq!(acl.is_bypass("fc00::".parse().unwrap(), None), true);
198
199 assert_eq!(
200 acl.is_bypass("220.181.38.148".parse().unwrap(), Some("baidu.com")),
201 true
202 );
203
204 assert_eq!(
205 acl.is_bypass("220.181.38.148".parse().unwrap(), Some("www.baidu.com")),
206 true
207 );
208
209 assert_eq!(
210 acl.is_bypass("8.214.121.167".parse().unwrap(), Some("ocfbnj.cn")),
211 true
212 );
213
214 assert_eq!(acl.is_bypass("8.8.8.8".parse().unwrap(), None), false);
215 assert_eq!(acl.is_bypass("126.0.0.1".parse().unwrap(), None), false);
216 assert_eq!(acl.is_bypass("192.167.0.1".parse().unwrap(), None), false);
217 assert_eq!(acl.is_bypass("192.169.0.1".parse().unwrap(), None), false);
218
219 assert_eq!(acl.is_bypass("8888::".parse().unwrap(), None), false);
220 assert_eq!(acl.is_bypass("::2".parse().unwrap(), None), false);
221 assert_eq!(acl.is_bypass("fa00::".parse().unwrap(), None), false);
222
223 assert_eq!(
224 acl.is_bypass("8.8.8.8".parse().unwrap(), Some("qq.com")),
225 false
226 );
227
228 assert_eq!(
229 acl.is_bypass("220.181.38.148".parse().unwrap(), Some("baidu.com ")),
230 false
231 );
232
233 assert_eq!(
234 acl.is_bypass("220.181.38.148".parse().unwrap(), Some("3baidu.com ")),
235 false
236 );
237
238 assert_eq!(
239 acl.is_bypass("220.181.38.148".parse().unwrap(), Some("ocfbnj.com ")),
240 false
241 );
242 }
243
244 #[test]
245 fn test_error() {
246 assert!(Acl::from_file(Path::new("1234567890abcdefghijklmnopqrstuvwxyz")).is_err());
247 }
248}