Skip to main content

tuwunel_service/client/
mod.rs

1use std::{
2	net::{IpAddr, SocketAddr},
3	ops::Deref,
4	sync::{Arc, LazyLock},
5	time::Duration,
6};
7
8use bytes::{Bytes, BytesMut};
9use ipaddress::{IPAddress, ipv4::from_u32 as ipv4_from_u32};
10use reqwest::{Client, ClientBuilder, Url, dns::Resolve, header::HeaderValue, redirect::Policy};
11use tuwunel_core::{
12	Config, Err, Result, config::proxy::ProxySnapshot, debug, either::Either, err,
13	error::error_chain, implement, trace,
14};
15use url::Host;
16
17use crate::{Services, resolver::Validating, service};
18
19#[cfg(test)]
20mod tests;
21
22type DisableEncoding = fn(ClientBuilder) -> ClientBuilder;
23
24pub struct Clients {
25	pub default: Client,
26	pub url_preview: Client,
27	pub extern_media: Client,
28	pub well_known: Client,
29	pub federation: Client,
30	pub synapse: Client,
31	pub sender: Client,
32	pub appservice: Client,
33	pub pusher: Client,
34	pub oauth: Client,
35}
36
37pub struct Service {
38	pub clients: LazyLock<Clients, Box<dyn FnOnce() -> Clients + Send>>,
39
40	/// Effective proxy policy for this generation of clients.
41	///
42	/// The snapshot keeps client routing, DNS exemptions, and response checks
43	/// aligned even if configuration or environment values later change.
44	pub proxy: Arc<ProxySnapshot>,
45	pub cidr_range_denylist: Arc<[IPAddress]>,
46}
47
48impl Deref for Service {
49	type Target = Clients;
50
51	fn deref(&self) -> &Self::Target { &self.clients }
52}
53
54impl crate::Service for Service {
55	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
56		let config = &args.server.config;
57		let proxy = Arc::new(ProxySnapshot::new(&config.proxy)?);
58
59		probe_tls(config, &proxy)?;
60
61		Ok(Arc::new(Self {
62			clients: LazyLock::new(Box::new({
63				let services = args.services.clone();
64
65				move || make_clients(&services).expect("failed to construct clients")
66			})),
67
68			proxy,
69			cidr_range_denylist: config
70				.ip_range_denylist
71				.iter()
72				.map(IPAddress::parse)
73				.inspect(|cidr| trace!("Denied CIDR range: {cidr:?}"))
74				.collect::<Result<Vec<_>, String>>()
75				.map(Arc::from)
76				.map_err(|e| err!(Config("ip_range_denylist", e)))?,
77		}))
78	}
79
80	fn name(&self) -> &str { service::make_name(std::module_path!()) }
81}
82
83/// Fails startup when an HTTPS client cannot be constructed.
84///
85/// The clients are built lazily on first use, so a platform trust store that
86/// yields no roots surfaces as a panic inside whichever worker reaches for a
87/// client first. Probing once at startup turns that into a legible boot error.
88fn probe_tls(config: &Config, proxy: &ProxySnapshot) -> Result {
89	base(config, proxy, None)?
90		.build()
91		.map(drop)
92		.map_err(|e| {
93			err!(error!(
94				chain = %error_chain(&e),
95				"Failed to construct an HTTPS client. If the system trust store is empty, \
96				 install a CA bundle (ca-certificates on Debian and Ubuntu) or point \
97				 SSL_CERT_FILE at one.",
98			))
99		})
100}
101
102fn make_clients(services: &Services) -> Result<Clients> {
103	macro_rules! with {
104		($builder:ident => $make:expr) => {{
105			let $builder = base(&services.config, &services.client.proxy, None)?;
106
107			$make.build()?
108		}};
109		($name:literal, $builder:ident => $make:expr) => {{
110			let $builder = base(&services.config, &services.client.proxy, Some($name))?;
111
112			$make.build()?
113		}};
114	}
115
116	Ok(Clients {
117		default: with!(cb => cb.dns_resolver(Arc::clone(&services.resolver.resolver))),
118
119		url_preview: with!("preview", cb => preview_builder(services, cb)?),
120
121		extern_media: with!(cb => cb
122			.dns_resolver(Validating::new(
123				Arc::clone(&services.resolver.resolver),
124				Arc::clone(&services.client.cidr_range_denylist),
125				services.client.proxy.shared_hosts(),
126			))
127			.redirect(guarded_redirect(services, 3))),
128
129		well_known: with!(cb => cb
130			.dns_resolver(Arc::clone(&services.resolver.resolver))
131			.connect_timeout(Duration::from_secs(
132				services.config.well_known_conn_timeout,
133			))
134			.read_timeout(Duration::from_secs(services.config.well_known_timeout))
135			.timeout(Duration::from_secs(services.config.well_known_timeout))
136			.pool_max_idle_per_host(0)
137			.redirect(Policy::limited(4))),
138
139		federation: with!(cb => cb
140			.dns_resolver(Arc::clone(&services.resolver.resolver.hooked))
141			.read_timeout(Duration::from_secs(services.config.federation_timeout))
142			.pool_max_idle_per_host(services.config.federation_idle_per_host.into())
143			.pool_idle_timeout(Duration::from_secs(
144				services.config.federation_idle_timeout,
145			))
146			.redirect(Policy::limited(3))),
147
148		synapse: with!(cb => cb
149			.dns_resolver(Arc::clone(&services.resolver.resolver.hooked))
150			.read_timeout(Duration::from_secs(305))
151			.pool_max_idle_per_host(0)
152			.redirect(Policy::limited(3))),
153
154		sender: with!(cb => cb
155			.dns_resolver(Arc::clone(&services.resolver.resolver.hooked))
156			.read_timeout(Duration::from_secs(services.config.sender_timeout))
157			.timeout(Duration::from_secs(services.config.sender_timeout))
158			.pool_max_idle_per_host(1)
159			.pool_idle_timeout(Duration::from_secs(
160				services.config.sender_idle_timeout,
161			))
162			.redirect(Policy::limited(2))),
163
164		appservice: with!(cb => cb
165			.dns_resolver(appservice_resolver(services))
166			.connect_timeout(Duration::from_secs(5))
167			.read_timeout(Duration::from_secs(services.config.appservice_timeout))
168			.timeout(Duration::from_secs(services.config.appservice_timeout))
169			.pool_max_idle_per_host(1)
170			.pool_idle_timeout(Duration::from_secs(
171				services.config.appservice_idle_timeout,
172			))
173			.redirect(Policy::limited(2))),
174
175		pusher: with!(cb => cb
176			.dns_resolver(Validating::new(
177				Arc::clone(&services.resolver.resolver),
178				Arc::clone(&services.client.cidr_range_denylist),
179				services.client.proxy.shared_hosts(),
180			))
181			.pool_max_idle_per_host(1)
182			.pool_idle_timeout(Duration::from_secs(
183				services.config.pusher_idle_timeout,
184			))
185			.redirect(guarded_redirect(services, 2))),
186
187		oauth: with!(cb => cb
188			.dns_resolver(Arc::clone(&services.resolver.resolver))
189			.redirect(Policy::limited(0))
190			.pool_max_idle_per_host(1)),
191	})
192}
193
194/// Construction for the URL preview client: bound to the configured
195/// interface and resolving through the CIDR-validating resolver.
196///
197/// The configured User-Agent is applied per request rather than here, so a
198/// configuration reload takes effect without rebuilding the client.
199fn preview_builder(services: &Services, builder: ClientBuilder) -> Result<ClientBuilder> {
200	let interface = &services.config.url_preview_bound_interface;
201
202	let bind_addr = interface.clone().and_then(Either::left);
203	let bind_iface = interface.clone().and_then(Either::right);
204
205	let resolver = Validating::new(
206		Arc::clone(&services.resolver.resolver),
207		Arc::clone(&services.client.cidr_range_denylist),
208		services.client.proxy.shared_hosts(),
209	);
210
211	Ok(builder_interface(builder, bind_iface.as_deref())?
212		.local_address(bind_addr)
213		.dns_resolver(resolver)
214		.redirect(guarded_redirect(services, 3)))
215}
216
217fn guarded_redirect(services: &Services, max: usize) -> Policy {
218	let proxy = Arc::clone(&services.client.proxy);
219	let denylist = Arc::clone(&services.client.cidr_range_denylist);
220	let limited = Policy::limited(max);
221
222	Policy::custom(move |attempt| {
223		if proxy.resolver_alias(attempt.url()) || !valid_cidr_range_url(&denylist, attempt.url())
224		{
225			attempt.error("redirect destination is not allowed")
226		} else {
227			limited.redirect(attempt)
228		}
229	})
230}
231
232fn valid_cidr_range_url(denylist: &[IPAddress], url: &Url) -> bool {
233	let ip = match url.host() {
234		| Some(Host::Ipv4(ip)) => IpAddr::V4(ip),
235		| Some(Host::Ipv6(ip)) => IpAddr::V6(ip),
236		| Some(Host::Domain(_)) | None => return true,
237	};
238
239	let ip = ipaddress_from_std(ip);
240
241	denylist.iter().all(|cidr| !cidr.includes(&ip))
242}
243
244fn base(config: &Config, proxy: &ProxySnapshot, name: Option<&str>) -> Result<ClientBuilder> {
245	let user_agent = tuwunel_core::version::user_agent();
246	let user_agent: HeaderValue = name
247		.map(|name| format!("{user_agent} {name}").try_into())
248		.unwrap_or_else(|| user_agent.try_into())?;
249
250	let builder = Client::builder()
251		.connect_timeout(Duration::from_secs(config.request_conn_timeout))
252		.read_timeout(Duration::from_secs(config.request_timeout))
253		.timeout(Duration::from_secs(config.request_total_timeout))
254		.pool_idle_timeout(Duration::from_secs(config.request_idle_timeout))
255		.pool_max_idle_per_host(config.request_idle_per_host.into())
256		.user_agent(user_agent)
257		.redirect(Policy::limited(6))
258		.danger_accept_invalid_certs(config.allow_invalid_tls_certificates)
259		.connection_verbose(cfg!(debug_assertions))
260		// Check if env var is set to avoid locking the keyfile mutex on every connection open
261		.tls_sslkeylogfile(std::env::var_os("SSLKEYLOGFILE").is_some());
262
263	let encodings: [(bool, DisableEncoding); 3] = [
264		(config.request_gzip, ClientBuilder::no_gzip),
265		(config.request_brotli, ClientBuilder::no_brotli),
266		(config.request_zstd, ClientBuilder::no_zstd),
267	];
268
269	let builder = encodings
270		.into_iter()
271		.filter(|(enabled, _)| !enabled)
272		.fold(builder, |builder, (_, disable)| disable(builder));
273
274	Ok(proxy.configure(builder))
275}
276
277/// Prevents a remote peer from forcing unbounded response-body allocation.
278///
279/// An advertised `Content-Length` above `limit` is rejected before the body
280/// buffer is allocated. The streaming loop enforces the same bound when the
281/// length is absent or inaccurate.
282pub async fn read_response_capped(
283	mut response: reqwest::Response,
284	limit: usize,
285) -> Result<Bytes> {
286	let mut body = match response.content_length() {
287		| Some(len) if len > limit.try_into().unwrap_or(u64::MAX) => {
288			debug!(%len, %limit, "rejecting response: advertised body exceeds limit");
289			return Err!(BadServerResponse(
290				"Response body length {len} exceeds the {limit} byte limit"
291			));
292		},
293		| Some(len) => BytesMut::with_capacity(usize::try_from(len).unwrap_or(limit)),
294		| None => BytesMut::new(),
295	};
296	while let Some(chunk) = response.chunk().await? {
297		if body.len().saturating_add(chunk.len()) > limit {
298			debug!(%limit, "rejecting response: streamed body exceeds limit");
299			return Err!(BadServerResponse("Response body exceeds the {limit} byte limit"));
300		}
301
302		body.extend_from_slice(&chunk);
303	}
304
305	Ok(body.freeze())
306}
307
308#[cfg(any(
309	target_os = "android",
310	target_os = "fuchsia",
311	target_os = "linux"
312))]
313fn builder_interface(builder: ClientBuilder, config: Option<&str>) -> Result<ClientBuilder> {
314	if let Some(iface) = config {
315		Ok(builder.interface(iface))
316	} else {
317		Ok(builder)
318	}
319}
320
321#[cfg(not(any(
322	target_os = "android",
323	target_os = "fuchsia",
324	target_os = "linux"
325)))]
326fn builder_interface(builder: ClientBuilder, config: Option<&str>) -> Result<ClientBuilder> {
327	use tuwunel_core::Err;
328
329	config.map_or_else(
330		|| Ok(builder),
331		|iface| {
332			Err!(
333				"Binding to network-interface {iface:?} by name is not supported on this \
334				 platform."
335			)
336		},
337	)
338}
339
340fn appservice_resolver(services: &Services) -> Arc<dyn Resolve> {
341	if services.server.config.dns_passthru_appservices {
342		services.resolver.resolver.passthru.clone()
343	} else {
344		services.resolver.resolver.clone()
345	}
346}
347
348/// Checks a response peer against the CIDR policy used by guarded clients.
349///
350/// A proxied response is screened at the proxy route instead; direct responses
351/// remain subject to the destination denylist.
352#[implement(Service)]
353#[inline]
354#[must_use]
355pub fn valid_cidr_range_remote_addr(&self, url: &Url, remote_addr: SocketAddr) -> bool {
356	self.valid_cidr_range_ip(remote_addr.ip()) || self.proxied(url)
357}
358
359#[inline]
360#[must_use]
361#[implement(Service)]
362pub fn valid_cidr_range(&self, ip: &IPAddress) -> bool {
363	self.cidr_range_denylist
364		.iter()
365		.all(|cidr| !cidr.includes(ip))
366}
367
368#[inline]
369#[must_use]
370#[implement(Service)]
371pub fn valid_cidr_range_ip(&self, ip: IpAddr) -> bool {
372	let addr = ipaddress_from_std(ip);
373	self.cidr_range_denylist
374		.iter()
375		.all(|cidr| !cidr.includes(&addr))
376}
377
378/// Checks an HTTP URL against the CIDR denylist when its host is an IP literal.
379///
380/// Domain names pass here because the validating resolver screens their
381/// addresses later, immediately before connection.
382#[implement(Service)]
383#[inline]
384#[must_use]
385pub fn valid_cidr_range_url(&self, url: &Url) -> bool {
386	valid_cidr_range_url(&self.cidr_range_denylist, url)
387}
388
389/// Reports whether this client generation proxies a request URL.
390///
391/// The result uses the same snapshotted configured or environment route that
392/// was applied when the clients were built.
393#[implement(Service)]
394#[inline]
395#[must_use]
396pub fn proxied(&self, url: &Url) -> bool { self.proxy.intercepts(url) }
397
398#[must_use]
399pub(crate) fn ipaddress_from_std(ip: IpAddr) -> IPAddress {
400	match ip {
401		| IpAddr::V4(v4) =>
402			ipv4_from_u32(u32::from(v4), 32).expect("/32 is always a valid prefix"),
403		// ipv6::from_int would skip the regex parser but pulls in num-bigint.
404		| IpAddr::V6(v6) =>
405			IPAddress::parse(v6.to_string()).expect("Ipv6Addr Display output parses"),
406	}
407}