Skip to main content

microsandbox_network/tls/
bypass.rs

1//! Bypass list matching for TLS interception.
2//!
3//! Connections to bypassed domains are spliced directly to the real server
4//! without TLS termination. The guest's TLS session reaches the real server
5//! unmodified.
6
7//--------------------------------------------------------------------------------------------------
8// Types
9//--------------------------------------------------------------------------------------------------
10
11/// Matches domain names against a bypass list to decide whether to intercept
12/// or pass through a TLS connection.
13pub struct BypassMatcher {
14    entries: Vec<BypassEntry>,
15}
16
17/// A single bypass entry: either an exact domain or a suffix wildcard.
18enum BypassEntry {
19    /// Exact domain match (case-insensitive).
20    Exact(String),
21
22    /// Suffix match: `*.example.com` matches `foo.example.com` and
23    /// `bar.baz.example.com` but not `example.com` itself.
24    Suffix(String),
25}
26
27//--------------------------------------------------------------------------------------------------
28// Methods
29//--------------------------------------------------------------------------------------------------
30
31impl BypassMatcher {
32    /// Creates a new bypass matcher from a list of patterns.
33    ///
34    /// Patterns starting with `*.` are treated as suffix matches. All other
35    /// patterns are exact matches. Matching is case-insensitive.
36    pub fn new(patterns: &[String]) -> Self {
37        let entries = patterns
38            .iter()
39            .map(|p| {
40                if let Some(suffix) = p.strip_prefix("*.") {
41                    BypassEntry::Suffix(suffix.to_ascii_lowercase())
42                } else {
43                    BypassEntry::Exact(p.to_ascii_lowercase())
44                }
45            })
46            .collect();
47
48        Self { entries }
49    }
50
51    /// Returns `true` if the given SNI domain should bypass TLS interception.
52    pub fn is_bypassed(&self, sni: &str) -> bool {
53        let lower = sni.to_ascii_lowercase();
54        self.entries.iter().any(|entry| match entry {
55            BypassEntry::Exact(domain) => lower == *domain,
56            BypassEntry::Suffix(suffix) => {
57                lower.ends_with(suffix) && lower.len() > suffix.len() && {
58                    // Ensure the character before the suffix is a dot.
59                    let prefix_len = lower.len() - suffix.len();
60                    lower.as_bytes()[prefix_len - 1] == b'.'
61                }
62            }
63        })
64    }
65}
66
67//--------------------------------------------------------------------------------------------------
68// Tests
69//--------------------------------------------------------------------------------------------------
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_exact_match() {
77        let matcher = BypassMatcher::new(&["example.com".to_string()]);
78        assert!(matcher.is_bypassed("example.com"));
79        assert!(matcher.is_bypassed("EXAMPLE.COM"));
80        assert!(!matcher.is_bypassed("foo.example.com"));
81        assert!(!matcher.is_bypassed("notexample.com"));
82    }
83
84    #[test]
85    fn test_suffix_match() {
86        let matcher = BypassMatcher::new(&["*.pinned.com".to_string()]);
87        assert!(matcher.is_bypassed("foo.pinned.com"));
88        assert!(matcher.is_bypassed("bar.baz.pinned.com"));
89        assert!(matcher.is_bypassed("FOO.PINNED.COM"));
90        assert!(!matcher.is_bypassed("pinned.com"));
91        assert!(!matcher.is_bypassed("notpinned.com"));
92    }
93
94    #[test]
95    fn test_no_match() {
96        let matcher = BypassMatcher::new(&["example.com".to_string(), "*.pinned.com".to_string()]);
97        assert!(!matcher.is_bypassed("other.com"));
98        assert!(!matcher.is_bypassed("evil.net"));
99    }
100
101    #[test]
102    fn test_empty_list() {
103        let matcher = BypassMatcher::new(&[]);
104        assert!(!matcher.is_bypassed("anything.com"));
105    }
106
107    #[test]
108    fn test_multiple_patterns() {
109        let matcher = BypassMatcher::new(&[
110            "exact.com".to_string(),
111            "*.wildcard.org".to_string(),
112            "another.net".to_string(),
113        ]);
114        assert!(matcher.is_bypassed("exact.com"));
115        assert!(matcher.is_bypassed("sub.wildcard.org"));
116        assert!(matcher.is_bypassed("another.net"));
117        assert!(!matcher.is_bypassed("wildcard.org"));
118    }
119}