tuwunel_core/config/
check.rs1use std::env::consts::OS;
2
3use either::Either;
4use itertools::Itertools;
5
6use super::{DEPRECATED_KEYS, IdentityProvider, IpSource};
7use crate::{Config, Err, Result, debug, debug_info, error, warn};
8
9pub fn reload(old: &Config, new: &Config) -> Result {
12 check(new)?;
13
14 if new.server_name != old.server_name {
15 return Err!(Config(
16 "server_name",
17 "You can't change the server's name from {:?}.",
18 old.server_name
19 ));
20 }
21
22 if new.ip_source != old.ip_source {
23 return Err!(Config(
24 "ip_source",
25 "ip_source cannot be changed at runtime; restart the server to apply this change."
26 ));
27 }
28
29 Ok(())
30}
31
32pub fn check(config: &Config) -> Result {
33 #[cfg(debug_assertions)]
34 warn!("Note: tuwunel was built without optimisations (i.e. debug build)");
35
36 warn_deprecated(config);
37 warn_unknown_key(config)?;
38
39 if config.sentry && config.sentry_endpoint.is_none() {
40 return Err!(Config(
41 "sentry_endpoint",
42 "Sentry cannot be enabled without an endpoint set"
43 ));
44 }
45
46 #[cfg(all(
47 feature = "hardened_malloc",
48 feature = "jemalloc",
49 not(target_env = "msvc")
50 ))]
51 debug_warn!(
52 "hardened_malloc and jemalloc compile-time features are both enabled, this causes \
53 jemalloc to be used."
54 );
55
56 #[cfg(not(unix))]
57 if config.unix_socket_path.is_some() {
58 return Err!(Config(
59 "unix_socket_path",
60 "UNIX socket support is only available on *nix platforms. Please remove \
61 'unix_socket_path' from your config."
62 ));
63 }
64
65 let certs_set = config.tls.certs.is_some();
66 let key_set = config.tls.key.is_some();
67 if certs_set ^ key_set {
68 return Err!(Config("tls", "tls.certs and tls.key must either both be set or unset"));
69 }
70
71 if let Some(source) = config.ip_source
72 && !matches!(source, IpSource::ConnectInfo)
73 {
74 warn!(
75 "ip_source is set to {source:?}, a header-based source. Ensure a trusted reverse \
76 proxy populates this header for every request; otherwise clients can spoof their \
77 IP address."
78 );
79 }
80
81 if !config.listening {
82 warn!("Configuration item `listening` is set to `false`. Cannot hear anyone.");
83 }
84
85 if config.unix_socket_path.is_none() {
86 config.get_bind_addrs().iter().for_each(|addr| {
87 use std::path::Path;
88
89 if addr.ip().is_loopback() {
90 debug_info!(
91 "Found loopback listening address {addr}, running checks if we're in a \
92 container."
93 );
94
95 if Path::new("/proc/vz").exists() && !Path::new("/proc/bz").exists()
96 {
98 error!(
99 "You are detected using OpenVZ with a loopback/localhost listening \
100 address of {addr}. If you are using OpenVZ for containers and you use \
101 NAT-based networking to communicate with the host and guest, this will \
102 NOT work. Please change this to \"0.0.0.0\". If this is expected, you \
103 can ignore.",
104 );
105 } else if Path::new("/.dockerenv").exists() {
106 error!(
107 "You are detected using Docker with a loopback/localhost listening \
108 address of {addr}. If you are using a reverse proxy on the host and \
109 require communication to tuwunel in the Docker container via NAT-based \
110 networking, this will NOT work. Please change this to \"0.0.0.0\". If \
111 this is expected, you can ignore.",
112 );
113 } else if Path::new("/run/.containerenv").exists() {
114 error!(
115 "You are detected using Podman with a loopback/localhost listening \
116 address of {addr}. If you are using a reverse proxy on the host and \
117 require communication to tuwunel in the Podman container via NAT-based \
118 networking, this will NOT work. Please change this to \"0.0.0.0\". If \
119 this is expected, you can ignore.",
120 );
121 }
122 }
123 });
124 }
125
126 if config.rocksdb_max_log_files == 0 {
128 return Err!(Config(
129 "max_log_files",
130 "rocksdb_max_log_files cannot be 0. Please set a value at least 1."
131 ));
132 }
133
134 #[cfg(not(debug_assertions))]
136 if config.server_name == "your.server.name" {
137 return Err!(Config(
138 "server_name",
139 "You must specify a valid server name for production usage of tuwunel."
140 ));
141 }
142
143 if config
144 .emergency_password
145 .as_ref()
146 .is_some_and(|emergency_password| emergency_password == "F670$2CP@Hw8mG7RY1$%!#Ic7YA")
147 {
148 return Err!(Config(
149 "emergency_password",
150 "The public example emergency password is being used, this is insecure. Please \
151 change this."
152 ));
153 }
154
155 if config
156 .emergency_password
157 .as_ref()
158 .is_some_and(String::is_empty)
159 {
160 return Err!(Config(
161 "emergency_password",
162 "Emergency password was set to an empty string, this is not valid. Unset \
163 emergency_password to disable it or set it to a real password."
164 ));
165 }
166
167 if config
169 .registration_token
170 .as_ref()
171 .is_some_and(String::is_empty)
172 {
173 return Err!(Config(
174 "registration_token",
175 "Registration token was specified but is empty (\"\")"
176 ));
177 }
178
179 if config
181 .registration_token_file
182 .as_ref()
183 .is_some_and(|path| {
184 let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
185 error!("Failed to read the registration token file: {e}");
186 }) else {
187 return true;
188 };
189
190 token == String::new()
191 }) {
192 return Err!(Config(
193 "registration_token_file",
194 "Registration token file was specified but is empty or failed to be read"
195 ));
196 }
197
198 if !config.turn_uris.is_empty()
199 && config.turn_secret.is_none()
200 && config.turn_secret_file.is_none()
201 && config.turn_username.is_empty()
202 && config.turn_password.is_empty()
203 {
204 warn!(
205 "turn_uris is configured but no credential source is set; the endpoint \
206 /_matrix/client/v3/voip/turnServer will return empty username and password. Set \
207 turn_secret, turn_secret_file, or both turn_username and turn_password."
208 );
209 }
210
211 if config.max_request_size < 10_000_000 {
212 return Err!(Config(
213 "max_request_size",
214 "Max request size is less than 10MB. Please increase it as this is too low for \
215 operable federation."
216 ));
217 }
218
219 for cidr in &config.ip_range_denylist {
221 if let Err(e) = ipaddress::IPAddress::parse(cidr) {
222 return Err!(Config(
223 "ip_range_denylist",
224 "Parsing specified IP CIDR range from string failed: {e}."
225 ));
226 }
227 }
228
229 if config.allow_registration
230 && !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
231 && config.registration_token.is_none()
232 && config.registration_token_file.is_none()
233 {
234 return Err!(Config(
235 "registration_token",
236 "!! You have `allow_registration` enabled without a token configured in your config \
237 which means you are allowing ANYONE to register on your tuwunel instance without \
238 any 2nd-step (e.g. registration token). If this is not the intended behaviour, \
239 please set a registration token. For security and safety reasons, tuwunel will \
240 shut down. If you are extra sure this is the desired behaviour you want, please \
241 set the following config option to true:
242`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`"
243 ));
244 }
245
246 if config.allow_registration
247 && config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
248 && config.registration_token.is_none()
249 && config.registration_token_file.is_none()
250 {
251 warn!(
252 "Open registration is enabled via setting \
253 `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse` and \
254 `allow_registration` to true without a registration token configured. You are \
255 expected to be aware of the risks now. If this is not the desired behaviour, \
256 please set a registration token."
257 );
258 }
259
260 if config.allow_outgoing_presence && !config.allow_local_presence {
261 return Err!(Config(
262 "allow_local_presence",
263 "Outgoing presence requires allowing local presence. Please enable \
264 'allow_local_presence' or disable outgoing presence."
265 ));
266 }
267
268 if config.suppress_push_when_active {
269 warn!(
270 "Push suppression when active is enabled (EXPERIMENTAL): behavior may change or be \
271 unstable. Disable by removing or setting suppress_push_when_active to false."
272 );
273 }
274
275 if config
276 .url_preview_domain_contains_allowlist
277 .contains(&"*".to_owned())
278 {
279 warn!(
280 "All URLs are allowed for URL previews via setting \
281 \"url_preview_domain_contains_allowlist\" to \"*\". This opens up significant \
282 attack surface to your server. You are expected to be aware of the risks by doing \
283 this."
284 );
285 }
286 if config
287 .url_preview_domain_explicit_allowlist
288 .contains(&"*".to_owned())
289 {
290 warn!(
291 "All URLs are allowed for URL previews via setting \
292 \"url_preview_domain_explicit_allowlist\" to \"*\". This opens up significant \
293 attack surface to your server. You are expected to be aware of the risks by doing \
294 this."
295 );
296 }
297 if config
298 .url_preview_url_contains_allowlist
299 .contains(&"*".to_owned())
300 {
301 warn!(
302 "All URLs are allowed for URL previews via setting \
303 \"url_preview_url_contains_allowlist\" to \"*\". This opens up significant attack \
304 surface to your server. You are expected to be aware of the risks by doing this."
305 );
306 }
307
308 if let Some(Either::Right(_)) = config.url_preview_bound_interface.as_ref()
309 && !matches!(OS, "android" | "fuchsia" | "linux")
310 {
311 return Err!(Config(
312 "url_preview_bound_interface",
313 "Not a valid IP address. Interface names not supported on {OS}."
314 ));
315 }
316
317 if !config.supported_room_version(&config.default_room_version) {
318 return Err!(Config(
319 "default_room_version",
320 "Room version {:?} is not available",
321 config.default_room_version
322 ));
323 }
324
325 for a in config.identity_provider.values() {
326 let count = config
327 .identity_provider
328 .values()
329 .filter(|b| a.id().eq(b.id()))
330 .count();
331
332 debug_assert_ne!(count, 0, "expected at least one identity_provider");
333 if count > 1 {
334 return Err!(Config(
335 "client_id",
336 "Duplicate identity_provider with client_id {}",
337 a.client_id
338 ));
339 }
340 }
341
342 for (i, provider) in &config.identity_provider {
343 if provider.client_secret.is_some() {
344 continue;
345 }
346
347 let Some(secret_path) = &provider.client_secret_file else {
348 return Err!(Config(
349 "client_secret",
350 "Either client secret or a client secret file must be set on identity provider \
351 №{i}."
352 ));
353 };
354
355 let Ok(secret) = std::fs::read_to_string(secret_path) else {
356 return Err!(Config(
357 "client_secret_file",
358 "Client secret file was specified but failed to be read at identity provider \
359 №{i}"
360 ));
361 };
362
363 if secret.is_empty() {
364 return Err!(Config(
365 "client_secret_file",
366 "Client secret file was specified but is empty on identity provider №{i}"
367 ));
368 }
369 }
370
371 if !config.sso_custom_providers_page
372 && config.identity_provider.len() > 1
373 && config
374 .identity_provider
375 .values()
376 .filter(|idp| idp.default)
377 .count()
378 .eq(&0)
379 {
380 let default = config
381 .identity_provider
382 .values()
383 .next()
384 .map(IdentityProvider::id)
385 .expect("Check at least one provider is configured to reach here");
386
387 warn!(
388 "More than one identity_provider has been configured without any default selected. \
389 To prevent this warning set `default = true` for one provider. Considering \
390 {default} the default for now..."
391 );
392 }
393
394 for provider in &config.store_media_on_providers {
395 if !config.media_storage_providers.contains(provider) {
396 return Err!(Config(
397 "store_media_on_providers",
398 "Providers must be listed in 'media_storage_providers'"
399 ));
400 }
401 }
402
403 if config
404 .media_storage_providers
405 .iter()
406 .filter(|&provider| {
407 if config.storage_provider.contains_key(provider) || provider == "media" {
408 return false;
409 }
410
411 error!("`media_storage_providers` references non-existent provider {provider:?}");
412 true
413 })
414 .count()
415 .gt(&0)
416 {
417 return Err!(Config(
418 "media_storage_providers",
419 "Contains missing or unconfigured storage providers."
420 ));
421 }
422
423 if config.media_storage_providers.len() > 1 && config.store_media_on_providers.is_empty() {
424 warn!(
425 "Media will be duplicated to multiple providers {:?} until \
426 `store_media_on_providers` is configured. This warning can be suppressed by \
427 explicitly configuring `store_media_on_providers`",
428 config.media_storage_providers
429 );
430 }
431
432 Ok(())
433}
434
435fn warn_deprecated(config: &Config) {
438 debug!("Checking for deprecated config keys");
439 let found_deprecated_keys = config
440 .catchall
441 .keys()
442 .filter(|key| DEPRECATED_KEYS.iter().any(|s| s == key))
443 .inspect(|key| warn!("Config parameter \"{key}\" is deprecated, ignoring."))
444 .next()
445 .is_some();
446
447 if found_deprecated_keys {
448 warn!(
449 "Deprecated config keys were found. Read tuwunel config documentation at https://tuwunel.chat/configuration.html and \
450 check your configuration if any new configuration parameters should be adjusted"
451 );
452 }
453}
454
455fn warn_unknown_key(config: &Config) -> Result {
458 debug!("Checking for unknown config keys");
459 let unknown_keys = config
460 .catchall
461 .keys()
462 .filter_map(|key| {
463 if key == "config" {
464 None
465 } else {
466 if config.error_on_unknown_config_opts {
467 error!("Config parameter \"{key}\" is unknown to tuwunel");
468 } else {
469 warn!("Config parameter \"{key}\" is unknown to tuwunel, ignoring.");
470 }
471 Some(key.as_str())
472 }
473 })
474 .collect_vec();
475
476 if !unknown_keys.is_empty() && config.error_on_unknown_config_opts {
477 Err!("Unknown config options were found: {unknown_keys:?}")
478 } else {
479 Ok(())
480 }
481}