Skip to main content

tuwunel_core/config/
proxy.rs

1use reqwest::{Proxy, Url};
2use serde::Deserialize;
3
4use crate::Result;
5
6/// ## Examples:
7/// - No proxy (default):
8/// ```toml
9/// proxy ="none"
10/// ```
11/// - Global proxy
12/// ```toml
13/// [global.proxy]
14/// global = { url = "socks5h://localhost:9050" }
15/// ```
16/// - Proxy some domains
17/// ```toml
18/// [global.proxy]
19/// [[global.proxy.by_domain]]
20/// url = "socks5h://localhost:9050"
21/// include = ["*.onion", "matrix.myspecial.onion"]
22/// exclude = ["*.myspecial.onion"]
23/// ```
24/// ## Include vs. Exclude
25/// If include is an empty list, it is assumed to be `["*"]`.
26///
27/// If a domain matches both the exclude and include list, the proxy will only
28/// be used if it was included because of a more specific rule than it was
29/// excluded. In the above example, the proxy would be used for
30/// `ordinary.onion`, `matrix.myspecial.onion`, but not `hello.myspecial.onion`.
31#[derive(Clone, Default, Debug, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum ProxyConfig {
34	#[default]
35	None,
36	Global {
37		#[serde(deserialize_with = "crate::utils::deserialize_from_str")]
38		url: Url,
39	},
40	ByDomain(Vec<PartialProxyConfig>),
41}
42impl ProxyConfig {
43	pub fn to_proxy(&self) -> Result<Option<Proxy>> {
44		Ok(match self.clone() {
45			| Self::None => None,
46			| Self::Global { url } => Some(Proxy::all(url)?),
47			| Self::ByDomain(proxies) => Some(Proxy::custom(move |url| {
48				// first matching proxy
49				proxies
50					.iter()
51					.find_map(|proxy| proxy.for_url(url))
52					.cloned()
53			})),
54		})
55	}
56}
57
58#[derive(Clone, Debug, Deserialize)]
59pub struct PartialProxyConfig {
60	#[serde(deserialize_with = "crate::utils::deserialize_from_str")]
61	url: Url,
62	#[serde(default)]
63	include: Vec<WildCardedDomain>,
64	#[serde(default)]
65	exclude: Vec<WildCardedDomain>,
66}
67impl PartialProxyConfig {
68	#[must_use]
69	pub fn for_url(&self, url: &Url) -> Option<&Url> {
70		let domain = url.domain()?;
71		let mut included_because = None; // most specific reason it was included
72		let mut excluded_because = None; // most specific reason it was excluded
73		if self.include.is_empty() {
74			// treat empty include list as `*`
75			included_because = Some(&WildCardedDomain::WildCard);
76		}
77		for wc_domain in &self.include {
78			if wc_domain.matches(domain) {
79				match included_because {
80					| Some(prev) if !wc_domain.more_specific_than(prev) => (),
81					| _ => included_because = Some(wc_domain),
82				}
83			}
84		}
85		for wc_domain in &self.exclude {
86			if wc_domain.matches(domain) {
87				match excluded_because {
88					| Some(prev) if !wc_domain.more_specific_than(prev) => (),
89					| _ => excluded_because = Some(wc_domain),
90				}
91			}
92		}
93		match (included_because, excluded_because) {
94			| (Some(a), Some(b)) if a.more_specific_than(b) => Some(&self.url),
95			| (Some(_), None) => Some(&self.url),
96			| _ => None,
97		}
98	}
99}
100
101/// A domain name, that optionally allows a * as its first subdomain.
102#[derive(Clone, Debug)]
103enum WildCardedDomain {
104	WildCard,
105	WildCarded(String),
106	Exact(String),
107}
108impl WildCardedDomain {
109	fn matches(&self, domain: &str) -> bool {
110		match self {
111			| Self::WildCard => true,
112			| Self::WildCarded(d) => domain.ends_with(d),
113			| Self::Exact(d) => domain == d,
114		}
115	}
116
117	fn more_specific_than(&self, other: &Self) -> bool {
118		match (self, other) {
119			| (Self::WildCard, Self::WildCard) => false,
120			| (_, Self::WildCard) => true,
121			| (Self::Exact(a), Self::WildCarded(_)) => other.matches(a),
122			| (Self::WildCarded(a), Self::WildCarded(b)) => a != b && a.ends_with(b),
123			| _ => false,
124		}
125	}
126}
127impl std::str::FromStr for WildCardedDomain {
128	type Err = std::convert::Infallible;
129
130	#[expect(clippy::string_slice)]
131	fn from_str(s: &str) -> Result<Self, Self::Err> {
132		// maybe do some domain validation?
133		Ok(if s.starts_with("*.") {
134			Self::WildCarded(s[1..].to_owned())
135		} else if s == "*" {
136			Self::WildCarded(String::new())
137		} else {
138			Self::Exact(s.to_owned())
139		})
140	}
141}
142impl<'de> Deserialize<'de> for WildCardedDomain {
143	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144	where
145		D: serde::de::Deserializer<'de>,
146	{
147		crate::utils::deserialize_from_str(deserializer)
148	}
149}