sandbox_runtime/proxy/
filter.rs

1//! Domain filtering logic for proxy servers.
2
3use crate::config::{matches_domain_pattern, NetworkConfig};
4
5/// Filter decision for a domain.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum FilterDecision {
8    /// Allow the connection.
9    Allow,
10    /// Deny the connection.
11    Deny,
12    /// Route through MITM proxy.
13    Mitm,
14}
15
16/// Domain filter for proxy connections.
17#[derive(Debug, Clone)]
18pub struct DomainFilter {
19    allowed_domains: Vec<String>,
20    denied_domains: Vec<String>,
21    mitm_domains: Vec<String>,
22}
23
24impl DomainFilter {
25    /// Create a new domain filter from network config.
26    pub fn from_config(config: &NetworkConfig) -> Self {
27        let mitm_domains = config
28            .mitm_proxy
29            .as_ref()
30            .map(|m| m.domains.clone())
31            .unwrap_or_default();
32
33        Self {
34            allowed_domains: config.allowed_domains.clone(),
35            denied_domains: config.denied_domains.clone(),
36            mitm_domains,
37        }
38    }
39
40    /// Create an allow-all filter.
41    pub fn allow_all() -> Self {
42        Self {
43            allowed_domains: vec![],
44            denied_domains: vec![],
45            mitm_domains: vec![],
46        }
47    }
48
49    /// Check if a domain should be allowed, denied, or routed through MITM.
50    pub fn check(&self, hostname: &str, _port: u16) -> FilterDecision {
51        // Check denied list first (highest priority)
52        for pattern in &self.denied_domains {
53            if matches_domain_pattern(hostname, pattern) {
54                return FilterDecision::Deny;
55            }
56        }
57
58        // Check MITM list
59        for pattern in &self.mitm_domains {
60            if matches_domain_pattern(hostname, pattern) {
61                return FilterDecision::Mitm;
62            }
63        }
64
65        // If we have an allow list, check against it
66        if !self.allowed_domains.is_empty() {
67            for pattern in &self.allowed_domains {
68                if matches_domain_pattern(hostname, pattern) {
69                    return FilterDecision::Allow;
70                }
71            }
72            // Not in allow list = denied
73            return FilterDecision::Deny;
74        }
75
76        // No allow list = allow all (except denied)
77        FilterDecision::Allow
78    }
79
80    /// Check if a domain is allowed.
81    pub fn is_allowed(&self, hostname: &str, port: u16) -> bool {
82        matches!(self.check(hostname, port), FilterDecision::Allow | FilterDecision::Mitm)
83    }
84
85    /// Check if a domain should be routed through MITM.
86    pub fn should_mitm(&self, hostname: &str) -> bool {
87        for pattern in &self.mitm_domains {
88            if matches_domain_pattern(hostname, pattern) {
89                return true;
90            }
91        }
92        false
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_domain_filter_allow_all() {
102        let filter = DomainFilter::allow_all();
103        assert_eq!(filter.check("example.com", 443), FilterDecision::Allow);
104        assert_eq!(filter.check("evil.com", 443), FilterDecision::Allow);
105    }
106
107    #[test]
108    fn test_domain_filter_with_allowed() {
109        let filter = DomainFilter {
110            allowed_domains: vec!["github.com".to_string(), "*.npmjs.org".to_string()],
111            denied_domains: vec![],
112            mitm_domains: vec![],
113        };
114
115        assert_eq!(filter.check("github.com", 443), FilterDecision::Allow);
116        assert_eq!(filter.check("registry.npmjs.org", 443), FilterDecision::Allow);
117        assert_eq!(filter.check("evil.com", 443), FilterDecision::Deny);
118    }
119
120    #[test]
121    fn test_domain_filter_with_denied() {
122        let filter = DomainFilter {
123            allowed_domains: vec!["*.example.com".to_string()],
124            denied_domains: vec!["evil.example.com".to_string()],
125            mitm_domains: vec![],
126        };
127
128        assert_eq!(filter.check("api.example.com", 443), FilterDecision::Allow);
129        assert_eq!(filter.check("evil.example.com", 443), FilterDecision::Deny);
130    }
131
132    #[test]
133    fn test_domain_filter_with_mitm() {
134        let filter = DomainFilter {
135            allowed_domains: vec!["*.example.com".to_string()],
136            denied_domains: vec![],
137            mitm_domains: vec!["api.example.com".to_string()],
138        };
139
140        assert_eq!(filter.check("api.example.com", 443), FilterDecision::Mitm);
141        assert_eq!(filter.check("other.example.com", 443), FilterDecision::Allow);
142    }
143}