ss_rs/acl/
mod.rs

1//! Access control list.
2
3pub 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
16/// Access control list.
17pub 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    /// Creates a empty ACL.
31    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    /// Creates a new acl from a file.
44    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    /// Creates a new acl from a string.
50    pub fn from_str(data: &str) -> Self {
51        // Trims whitespace and comments.
52        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    /// Returns true if the given ip or host should be bypassed.
111    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    /// Returns true if the given ip or host should be block.
138    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/// Access control list mode.
162#[derive(PartialEq, Eq)]
163pub enum Mode {
164    // Proxies all addresses that didn't match any rules. (default)
165    WhiteList,
166
167    // Bypasses all addresses that didn't match any rules
168    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}