tuwunel_service/client/
mod.rs1use 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}