Skip to main content

tuwunel_service/client/
mod.rs

1use std::{
2	ops::Deref,
3	sync::{Arc, LazyLock},
4	time::Duration,
5};
6
7use ipaddress::IPAddress;
8use reqwest::{Certificate, Client, ClientBuilder, dns::Resolve, header::HeaderValue, redirect};
9use tuwunel_core::{Config, Result, either::Either, err, implement, trace};
10
11use crate::{Services, service};
12
13pub struct Clients {
14	pub default: Client,
15	pub url_preview: Client,
16	pub extern_media: Client,
17	pub well_known: Client,
18	pub federation: Client,
19	pub synapse: Client,
20	pub sender: Client,
21	pub appservice: Client,
22	pub pusher: Client,
23	pub oauth: Client,
24}
25
26pub struct Service {
27	pub clients: LazyLock<Clients, Box<dyn FnOnce() -> Clients + Send>>,
28
29	pub cidr_range_denylist: Vec<IPAddress>,
30}
31
32impl Deref for Service {
33	type Target = Clients;
34
35	fn deref(&self) -> &Self::Target { &self.clients }
36}
37
38impl crate::Service for Service {
39	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
40		let config = &args.server.config;
41
42		Ok(Arc::new(Self {
43			clients: LazyLock::new(Box::new({
44				let services = args.services.clone();
45
46				move || make_clients(&services).expect("failed to construct clients")
47			})),
48
49			cidr_range_denylist: config
50				.ip_range_denylist
51				.iter()
52				.map(IPAddress::parse)
53				.inspect(|cidr| trace!("Denied CIDR range: {cidr:?}"))
54				.collect::<Result<_, String>>()
55				.map_err(|e| err!(Config("ip_range_denylist", e)))?,
56		}))
57	}
58
59	fn name(&self) -> &str { service::make_name(std::module_path!()) }
60}
61
62fn make_clients(services: &Services) -> Result<Clients> {
63	macro_rules! with {
64		($builder:ident => $make:expr) => {{
65			let $builder = base(&services.config, None)?;
66			$make.build()?
67		}};
68		($name:literal, $builder:ident => $make:expr) => {{
69			let $builder = base(&services.config, Some($name))?;
70			$make.build()?
71		}};
72	}
73
74	Ok(Clients {
75		default: with!(cb => cb.dns_resolver(Arc::clone(&services.resolver.resolver))),
76
77		url_preview: with!("preview", cb => {
78			let interface = &services
79				.config
80				.url_preview_bound_interface;
81
82			let bind_addr = interface.clone().and_then(Either::left);
83			let bind_iface = interface.clone().and_then(Either::right);
84
85			builder_interface(cb, bind_iface.as_deref())?
86				.local_address(bind_addr)
87				.dns_resolver(Arc::clone(&services.resolver.resolver))
88				.redirect(redirect::Policy::limited(3))
89		}),
90
91		extern_media: with!(cb => cb
92			.dns_resolver(Arc::clone(&services.resolver.resolver))
93			.redirect(redirect::Policy::limited(3))),
94
95		well_known: with!(cb => cb
96			.dns_resolver(Arc::clone(&services.resolver.resolver))
97			.connect_timeout(Duration::from_secs(
98				services.config.well_known_conn_timeout,
99			))
100			.read_timeout(Duration::from_secs(services.config.well_known_timeout))
101			.timeout(Duration::from_secs(services.config.well_known_timeout))
102			.pool_max_idle_per_host(0)
103			.redirect(redirect::Policy::limited(4))),
104
105		federation: with!(cb => cb
106			.dns_resolver(Arc::clone(&services.resolver.resolver.hooked))
107			.read_timeout(Duration::from_secs(services.config.federation_timeout))
108			.pool_max_idle_per_host(services.config.federation_idle_per_host.into())
109			.pool_idle_timeout(Duration::from_secs(
110				services.config.federation_idle_timeout,
111			))
112			.redirect(redirect::Policy::limited(3))),
113
114		synapse: with!(cb => cb
115			.dns_resolver(Arc::clone(&services.resolver.resolver.hooked))
116			.read_timeout(Duration::from_secs(305))
117			.pool_max_idle_per_host(0)
118			.redirect(redirect::Policy::limited(3))),
119
120		sender: with!(cb => cb
121			.dns_resolver(Arc::clone(&services.resolver.resolver.hooked))
122			.read_timeout(Duration::from_secs(services.config.sender_timeout))
123			.timeout(Duration::from_secs(services.config.sender_timeout))
124			.pool_max_idle_per_host(1)
125			.pool_idle_timeout(Duration::from_secs(
126				services.config.sender_idle_timeout,
127			))
128			.redirect(redirect::Policy::limited(2))),
129
130		appservice: with!(cb => cb
131			.dns_resolver(appservice_resolver(services))
132			.connect_timeout(Duration::from_secs(5))
133			.read_timeout(Duration::from_secs(services.config.appservice_timeout))
134			.timeout(Duration::from_secs(services.config.appservice_timeout))
135			.pool_max_idle_per_host(1)
136			.pool_idle_timeout(Duration::from_secs(
137				services.config.appservice_idle_timeout,
138			))
139			.redirect(redirect::Policy::limited(2))),
140
141		pusher: with!(cb => cb
142			.dns_resolver(Arc::clone(&services.resolver.resolver))
143			.pool_max_idle_per_host(1)
144			.pool_idle_timeout(Duration::from_secs(
145				services.config.pusher_idle_timeout,
146			))
147			.redirect(redirect::Policy::limited(2))),
148
149		oauth: with!(cb => cb
150			.dns_resolver(Arc::clone(&services.resolver.resolver))
151			.redirect(redirect::Policy::limited(0))
152			.pool_max_idle_per_host(1)),
153	})
154}
155
156fn base(config: &Config, name: Option<&str>) -> Result<ClientBuilder> {
157	let user_agent = tuwunel_core::version::user_agent();
158	let user_agent: HeaderValue = name
159		.map(|name| format!("{user_agent} {name}").try_into())
160		.unwrap_or_else(|| user_agent.try_into())?;
161
162	let mut builder = Client::builder()
163		.connect_timeout(Duration::from_secs(config.request_conn_timeout))
164		.read_timeout(Duration::from_secs(config.request_timeout))
165		.timeout(Duration::from_secs(config.request_total_timeout))
166		.pool_idle_timeout(Duration::from_secs(config.request_idle_timeout))
167		.pool_max_idle_per_host(config.request_idle_per_host.into())
168		.user_agent(user_agent)
169		.redirect(redirect::Policy::limited(6))
170		.danger_accept_invalid_certs(config.allow_invalid_tls_certificates)
171		.tls_certs_merge(
172			webpki_root_certs::TLS_SERVER_ROOT_CERTS
173				.iter()
174				.map(|der| {
175					Certificate::from_der(der).expect("certificate must be valid der encoding")
176				}),
177		)
178		.connection_verbose(cfg!(debug_assertions));
179
180	#[cfg(feature = "gzip_compression")]
181	{
182		builder = if config.gzip_compression {
183			builder.gzip(true)
184		} else {
185			builder.gzip(false).no_gzip()
186		};
187	};
188
189	#[cfg(feature = "brotli_compression")]
190	{
191		builder = if config.brotli_compression {
192			builder.brotli(true)
193		} else {
194			builder.brotli(false).no_brotli()
195		};
196	};
197
198	#[cfg(feature = "zstd_compression")]
199	{
200		builder = if config.zstd_compression {
201			builder.zstd(true)
202		} else {
203			builder.zstd(false).no_zstd()
204		};
205	};
206
207	#[cfg(not(feature = "gzip_compression"))]
208	{
209		builder = builder.no_gzip();
210	};
211
212	#[cfg(not(feature = "brotli_compression"))]
213	{
214		builder = builder.no_brotli();
215	};
216
217	#[cfg(not(feature = "zstd_compression"))]
218	{
219		builder = builder.no_zstd();
220	};
221
222	match config.proxy.to_proxy()? {
223		| Some(proxy) => Ok(builder.proxy(proxy)),
224		| _ => Ok(builder),
225	}
226}
227
228#[cfg(any(
229	target_os = "android",
230	target_os = "fuchsia",
231	target_os = "linux"
232))]
233fn builder_interface(builder: ClientBuilder, config: Option<&str>) -> Result<ClientBuilder> {
234	if let Some(iface) = config {
235		Ok(builder.interface(iface))
236	} else {
237		Ok(builder)
238	}
239}
240
241#[cfg(not(any(
242	target_os = "android",
243	target_os = "fuchsia",
244	target_os = "linux"
245)))]
246fn builder_interface(builder: ClientBuilder, config: Option<&str>) -> Result<ClientBuilder> {
247	use tuwunel_core::Err;
248
249	if let Some(iface) = config {
250		Err!("Binding to network-interface {iface:?} by name is not supported on this platform.")
251	} else {
252		Ok(builder)
253	}
254}
255
256fn appservice_resolver(services: &Services) -> Arc<dyn Resolve> {
257	if services.server.config.dns_passthru_appservices {
258		services.resolver.resolver.passthru.clone()
259	} else {
260		services.resolver.resolver.clone()
261	}
262}
263
264#[inline]
265#[must_use]
266#[implement(Service)]
267pub fn valid_cidr_range(&self, ip: &IPAddress) -> bool {
268	self.cidr_range_denylist
269		.iter()
270		.all(|cidr| !cidr.includes(ip))
271}