1use std::{env::consts::OS, fs::read_to_string, net::SocketAddr};
2
3use either::Either;
4use itertools::Itertools;
5use regex::RegexSet;
6use url::Url;
7
8use super::{DEPRECATED_KEYS, IdentityProvider, IpSource, KNOWN_KEYS};
9use crate::{Config, Err, Result, debug, debug_info, err, error, warn};
10
11pub fn reload(old: &Config, new: &Config) -> Result {
14 check(new)?;
15
16 if new.server_name != old.server_name {
17 return Err!(Config(
18 "server_name",
19 "You can't change the server's name from {:?}.",
20 old.server_name
21 ));
22 }
23
24 if new.ip_source != old.ip_source {
25 return Err!(Config(
26 "ip_source",
27 "ip_source cannot be changed at runtime; restart the server to apply this change."
28 ));
29 }
30
31 Ok(())
32}
33
34pub fn check(config: &Config) -> Result {
35 #[cfg(debug_assertions)]
36 warn!("Note: tuwunel was built without optimisations (i.e. debug build)");
37
38 warn_deprecated(config);
39 warn_unknown_key(config)?;
40
41 #[cfg(all(
42 feature = "hardened_malloc",
43 feature = "jemalloc",
44 not(target_env = "msvc")
45 ))]
46 debug_warn!(
47 "hardened_malloc and jemalloc compile-time features are both enabled, this causes \
48 jemalloc to be used."
49 );
50
51 check_observability(config)?;
52 check_network(config)?;
53 check_storage(config)?;
54 check_registration(config)?;
55 check_registration_terms(config)?;
56 check_turn_and_media_misc(config)?;
57 check_url_previews(config)?;
58 check_room_version(config)?;
59 check_identity_providers(config)?;
60 check_media_providers(config)?;
61 check_well_known_support_contact_validity(config)?;
62 check_email(config)?;
63
64 Ok(())
65}
66
67fn check_observability(config: &Config) -> Result {
68 if config.sentry && config.sentry_endpoint.is_none() {
69 return Err!(Config(
70 "sentry_endpoint",
71 "Sentry cannot be enabled without an endpoint set"
72 ));
73 }
74
75 Ok(())
76}
77
78fn check_network(config: &Config) -> Result {
79 #[cfg(not(unix))]
80 if config.unix_socket_path.is_some() {
81 return Err!(Config(
82 "unix_socket_path",
83 "UNIX socket support is only available on *nix platforms. Please remove \
84 'unix_socket_path' from your config."
85 ));
86 }
87
88 let certs_set = config.tls.certs.is_some();
89 let key_set = config.tls.key.is_some();
90 if certs_set ^ key_set {
91 return Err!(Config("tls", "tls.certs and tls.key must either both be set or unset"));
92 }
93
94 let depth = config.conduit_media_directory_depth;
97 let length = config.conduit_media_directory_length;
98 if depth > 0 && length == 0 {
99 return Err!(Config(
100 "conduit_media_directory_length",
101 "must be non-zero when conduit_media_directory_depth is non-zero"
102 ));
103 }
104 if depth > 0 && usize::from(depth).saturating_mul(usize::from(length)) >= 64 {
105 return Err!(Config(
106 "conduit_media_directory_depth",
107 "conduit_media_directory_depth times conduit_media_directory_length must be less \
108 than 64, the length of a SHA-256 hex digest"
109 ));
110 }
111
112 if let Some(source) = config.ip_source
113 && !matches!(source, IpSource::ConnectInfo)
114 {
115 warn!(
116 "ip_source is set to {source:?}, a header-based source. Ensure a trusted reverse \
117 proxy populates this header for every request; otherwise clients can spoof their \
118 IP address."
119 );
120 }
121
122 if !config.listening {
123 warn!("Configuration item `listening` is set to `false`. Cannot hear anyone.");
124 }
125
126 if config.unix_socket_path.is_none() {
127 config
128 .get_bind_addrs()
129 .iter()
130 .for_each(warn_loopback_in_container);
131 }
132
133 for cidr in &config.ip_range_denylist {
135 if let Err(e) = ipaddress::IPAddress::parse(cidr) {
136 return Err!(Config(
137 "ip_range_denylist",
138 "Parsing specified IP CIDR range from string failed: {e}."
139 ));
140 }
141 }
142
143 Ok(())
144}
145
146fn warn_loopback_in_container(addr: &SocketAddr) {
147 use std::path::Path;
148
149 if !addr.ip().is_loopback() {
150 return;
151 }
152
153 debug_info!(
154 "Found loopback listening address {addr}, running checks if we're in a container."
155 );
156
157 if Path::new("/proc/vz").exists() && !Path::new("/proc/bz").exists()
158 {
160 error!(
161 "You are detected using OpenVZ with a loopback/localhost listening address of \
162 {addr}. If you are using OpenVZ for containers and you use NAT-based networking to \
163 communicate with the host and guest, this will NOT work. Please change this to \
164 \"0.0.0.0\". If this is expected, you can ignore.",
165 );
166 } else if Path::new("/.dockerenv").exists() {
167 error!(
168 "You are detected using Docker with a loopback/localhost listening address of \
169 {addr}. If you are using a reverse proxy on the host and require communication to \
170 tuwunel in the Docker container via NAT-based networking, this will NOT work. \
171 Please change this to \"0.0.0.0\". If this is expected, you can ignore.",
172 );
173 } else if Path::new("/run/.containerenv").exists() {
174 error!(
175 "You are detected using Podman with a loopback/localhost listening address of \
176 {addr}. If you are using a reverse proxy on the host and require communication to \
177 tuwunel in the Podman container via NAT-based networking, this will NOT work. \
178 Please change this to \"0.0.0.0\". If this is expected, you can ignore.",
179 );
180 }
181}
182
183fn check_storage(config: &Config) -> Result {
184 if config.rocksdb_max_log_files == 0 {
186 return Err!(Config(
187 "max_log_files",
188 "rocksdb_max_log_files cannot be 0. Please set a value at least 1."
189 ));
190 }
191
192 #[cfg(not(debug_assertions))]
194 if config.server_name == "your.server.name" {
195 return Err!(Config(
196 "server_name",
197 "You must specify a valid server name for production usage of tuwunel."
198 ));
199 }
200
201 Ok(())
202}
203
204fn check_registration(config: &Config) -> Result {
205 if config
206 .emergency_password
207 .as_ref()
208 .is_some_and(|emergency_password| emergency_password == "F670$2CP@Hw8mG7RY1$%!#Ic7YA")
209 {
210 return Err!(Config(
211 "emergency_password",
212 "The public example emergency password is being used, this is insecure. Please \
213 change this."
214 ));
215 }
216
217 if config
218 .emergency_password
219 .as_ref()
220 .is_some_and(String::is_empty)
221 {
222 return Err!(Config(
223 "emergency_password",
224 "Emergency password was set to an empty string, this is not valid. Unset \
225 emergency_password to disable it or set it to a real password."
226 ));
227 }
228
229 if config
230 .registration_token
231 .as_ref()
232 .is_some_and(String::is_empty)
233 {
234 return Err!(Config(
235 "registration_token",
236 "Registration token was specified but is empty (\"\")"
237 ));
238 }
239
240 if config
242 .registration_token_file
243 .as_ref()
244 .is_some_and(|path| {
245 let Ok(token) = read_to_string(path).inspect_err(|e| {
246 error!("Failed to read the registration token file: {e}");
247 }) else {
248 return true;
249 };
250
251 token == String::new()
252 }) {
253 return Err!(Config(
254 "registration_token_file",
255 "Registration token file was specified but is empty or failed to be read"
256 ));
257 }
258
259 let no_token =
260 config.registration_token.is_none() && config.registration_token_file.is_none();
261
262 if config.allow_registration
263 && no_token
264 && !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
265 {
266 return Err!(Config(
267 "registration_token",
268 "!! You have `allow_registration` enabled without a token configured in your config \
269 which means you are allowing ANYONE to register on your tuwunel instance without \
270 any 2nd-step (e.g. registration token). If this is not the intended behaviour, \
271 please set a registration token. For security and safety reasons, tuwunel will \
272 shut down. If you are extra sure this is the desired behaviour you want, please \
273 set the following config option to true:
274`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`"
275 ));
276 }
277
278 if config.allow_registration
279 && no_token
280 && config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
281 {
282 warn!(
283 "Open registration is enabled via setting \
284 `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse` and \
285 `allow_registration` to true without a registration token configured. You are \
286 expected to be aware of the risks now. If this is not the desired behaviour, \
287 please set a registration token."
288 );
289 }
290
291 Ok(())
292}
293
294fn check_registration_terms(config: &Config) -> Result {
295 for (id, policy) in &config.registration_terms {
296 let opaque = !id.is_empty()
297 && id.len() <= 255
298 && id.bytes().all(
299 |b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'.' | b'_' | b'~' | b'-'),
300 );
301
302 if !opaque {
303 return Err!(Config(
304 "registration_terms",
305 "Policy id {id:?} must be a non-empty opaque identifier of at most 255 \
306 characters from [0-9a-zA-Z._~-]."
307 ));
308 }
309
310 for (lang, translation) in &policy.translations {
311 if !matches!(translation.url.scheme(), "http" | "https") {
312 return Err!(Config(
313 "registration_terms",
314 "Policy {id:?} translation {lang:?} url must use the http or https scheme."
315 ));
316 }
317 }
318 }
319
320 Ok(())
321}
322
323fn check_turn_and_media_misc(config: &Config) -> Result {
324 if !config.turn_uris.is_empty()
325 && config.turn_secret.is_none()
326 && config.turn_secret_file.is_none()
327 && config.turn_username.is_empty()
328 && config.turn_password.is_empty()
329 {
330 warn!(
331 "turn_uris is configured but no credential source is set; the endpoint \
332 /_matrix/client/v3/voip/turnServer will return empty username and password. Set \
333 turn_secret, turn_secret_file, or both turn_username and turn_password."
334 );
335 }
336
337 if config.max_request_size < 10_000_000 {
338 return Err!(Config(
339 "max_request_size",
340 "Max request size is less than 10MB. Please increase it as this is too low for \
341 operable federation."
342 ));
343 }
344
345 if config.allow_outgoing_presence && !config.allow_local_presence {
346 return Err!(Config(
347 "allow_local_presence",
348 "Outgoing presence requires allowing local presence. Please enable \
349 'allow_local_presence' or disable outgoing presence."
350 ));
351 }
352
353 if config.suppress_push_when_active {
354 warn!(
355 "Push suppression when active is enabled (EXPERIMENTAL): behavior may change or be \
356 unstable. Disable by removing or setting suppress_push_when_active to false."
357 );
358 }
359
360 Ok(())
361}
362
363fn check_url_previews(config: &Config) -> Result {
364 let wildcard = "*".to_owned();
365 let url_preview_wildcards = [
366 (
367 "url_preview_domain_contains_allowlist",
368 &config.url_preview_domain_contains_allowlist,
369 ),
370 (
371 "url_preview_domain_explicit_allowlist",
372 &config.url_preview_domain_explicit_allowlist,
373 ),
374 ("url_preview_url_contains_allowlist", &config.url_preview_url_contains_allowlist),
375 ];
376
377 for (name, list) in url_preview_wildcards {
378 if list.contains(&wildcard) {
379 warn!(
380 "All URLs are allowed for URL previews via setting \"{name}\" to \"*\". This \
381 opens up significant attack surface to your server. You are expected to be \
382 aware of the risks by doing this."
383 );
384 }
385 }
386
387 if let Some(Either::Right(_)) = config.url_preview_bound_interface.as_ref()
388 && !matches!(OS, "android" | "fuchsia" | "linux")
389 {
390 return Err!(Config(
391 "url_preview_bound_interface",
392 "Not a valid IP address. Interface names not supported on {OS}."
393 ));
394 }
395
396 Ok(())
397}
398
399fn check_room_version(config: &Config) -> Result {
400 if !config.supported_room_version(&config.default_room_version) {
401 return Err!(Config(
402 "default_room_version",
403 "Room version {:?} is not available",
404 config.default_room_version
405 ));
406 }
407
408 Ok(())
409}
410
411fn check_identity_providers(config: &Config) -> Result {
412 for a in config.identity_provider.values() {
413 let count = config
414 .identity_provider
415 .values()
416 .filter(|b| a.id().eq(b.id()))
417 .count();
418
419 debug_assert_ne!(count, 0, "expected at least one identity_provider");
420 if count > 1 {
421 return Err!(Config(
422 "client_id",
423 "Duplicate identity_provider with client_id {}",
424 a.client_id
425 ));
426 }
427 }
428
429 for (i, provider) in &config.identity_provider {
430 check_identity_provider_secret(i, provider)?;
431 }
432
433 if !config.sso_custom_providers_page
434 && config.identity_provider.len() > 1
435 && config
436 .identity_provider
437 .values()
438 .filter(|idp| idp.default)
439 .count()
440 .eq(&0)
441 {
442 let default = config
443 .identity_provider
444 .values()
445 .next()
446 .map(IdentityProvider::id)
447 .expect("Check at least one provider is configured to reach here");
448
449 warn!(
450 "More than one identity_provider has been configured without any default selected. \
451 To prevent this warning set `default = true` for one provider. Considering \
452 {default} the default for now..."
453 );
454 }
455
456 Ok(())
457}
458
459fn check_identity_provider_secret(i: &str, provider: &IdentityProvider) -> Result {
460 if provider.client_secret.is_some() {
461 return Ok(());
462 }
463
464 let Some(secret_path) = &provider.client_secret_file else {
465 return Err!(Config(
466 "client_secret",
467 "Either client secret or a client secret file must be set on identity provider №{i}."
468 ));
469 };
470
471 let secret = read_to_string(secret_path).map_err(|e| {
472 err!(Config(
473 "client_secret_file",
474 "Failed to read client secret file {secret_path:?} on identity provider №{i}: {e}"
475 ))
476 })?;
477
478 if secret.trim().is_empty() {
479 return Err!(Config(
480 "client_secret_file",
481 "Client secret file {secret_path:?} is empty on identity provider №{i}"
482 ));
483 }
484
485 Ok(())
486}
487
488fn check_media_providers(config: &Config) -> Result {
489 for provider in &config.store_media_on_providers {
490 if !config.media_storage_providers.contains(provider) {
491 return Err!(Config(
492 "store_media_on_providers",
493 "Providers must be listed in 'media_storage_providers'"
494 ));
495 }
496 }
497
498 if config
499 .media_storage_providers
500 .iter()
501 .filter(|&provider| {
502 if config.storage_provider.contains_key(provider) || provider == "media" {
503 return false;
504 }
505
506 error!("`media_storage_providers` references non-existent provider {provider:?}");
507 true
508 })
509 .count()
510 .gt(&0)
511 {
512 return Err!(Config(
513 "media_storage_providers",
514 "Contains missing or unconfigured storage providers."
515 ));
516 }
517
518 if config.media_storage_providers.len() > 1 && config.store_media_on_providers.is_empty() {
519 warn!(
520 "Media will be duplicated to multiple providers {:?} until \
521 `store_media_on_providers` is configured. This warning can be suppressed by \
522 explicitly configuring `store_media_on_providers`",
523 config.media_storage_providers
524 );
525 }
526
527 Ok(())
528}
529
530fn check_well_known_support_contact_validity(config: &Config) -> Result {
531 let well_known = &config.well_known;
532
533 if well_known.support_role.is_some()
534 && well_known.support_email.is_none()
535 && well_known.support_mxid.is_none()
536 {
537 return Err!(
538 "well_known.support_role is set but neither support_email nor support_mxid is \
539 configured to accompany it"
540 );
541 }
542
543 if let Some(pgp_key) = well_known.support_pgp_key.as_deref() {
544 validate_pgp_key(pgp_key).map_err(|e| err!("well_known.support_pgp_key: {e}"))?;
545 }
546
547 for (id, contact) in &well_known.support_contact {
548 if contact.email_address.is_none() && contact.matrix_id.is_none() {
549 return Err!(
550 "well_known.support_contact.{id} has neither email_address nor matrix_id; at \
551 least one is required"
552 );
553 }
554
555 if let Some(pgp_key) = contact.pgp_key.as_deref() {
556 validate_pgp_key(pgp_key)
557 .map_err(|e| err!("well_known.support_contact.{id}.pgp_key: {e}"))?;
558 }
559 }
560
561 Ok(())
562}
563
564fn check_email(config: &Config) -> Result {
565 let smtp = &config.smtp;
566
567 if smtp.connection_uri.is_some() && config.well_known.client.is_none() {
568 return Err!(Config(
569 "well_known.client",
570 "global.smtp is configured but well_known.client is unset. Email verification links \
571 are built from the public client base URL, so set well_known.client to a valid \
572 HTTPS URL alongside global.smtp."
573 ));
574 }
575
576 if smtp.connection_uri.is_none()
577 && (smtp.require_email_for_registration || smtp.require_email_for_token_registration)
578 {
579 return Err!(Config(
580 "smtp.connection_uri",
581 "global.smtp requires a verified email at registration but smtp.connection_uri is \
582 unset. Set smtp.connection_uri so verification mail can be sent, or unset \
583 require_email_for_registration and require_email_for_token_registration."
584 ));
585 }
586
587 Ok(())
588}
589
590fn validate_pgp_key(value: &str) -> Result {
592 if value.contains("BEGIN PGP") {
593 return Err!(
594 "must be a URI, not inlined key material; publish the key and reference it by URI \
595 (for example https://example.com/key.asc or openpgp4fpr:<fingerprint>)"
596 );
597 }
598
599 let uri = Url::parse(value).map_err(|_| {
600 err!("must be a URI; a bare fingerprint must be prefixed with `openpgp4fpr:`")
601 })?;
602
603 if uri.scheme() == "openpgp4fpr" && !valid_openpgp4fpr(uri.path()) {
604 return Err!("`openpgp4fpr:` must be followed by a 40- or 64-character hex fingerprint");
605 }
606
607 Ok(())
608}
609
610fn valid_openpgp4fpr(fpr: &str) -> bool {
611 matches!(fpr.len(), 40 | 64) && fpr.bytes().all(|b| b.is_ascii_hexdigit())
612}
613
614fn warn_deprecated(config: &Config) {
617 debug!("Checking for deprecated config keys");
618 let found_deprecated_keys = config
619 .catchall
620 .keys()
621 .filter(|key| DEPRECATED_KEYS.iter().any(|s| s == key))
622 .inspect(|key| warn!("Config parameter \"{key}\" is deprecated, ignoring."))
623 .next()
624 .is_some();
625
626 if found_deprecated_keys {
627 warn!(
628 "Deprecated config keys were found. Read tuwunel config documentation at https://tuwunel.chat/configuration.html and \
629 check your configuration if any new configuration parameters should be adjusted"
630 );
631 }
632}
633
634fn warn_unknown_key(config: &Config) -> Result {
637 debug!("Checking for unknown config keys");
638 let known_keys =
639 RegexSet::new(KNOWN_KEYS).expect("Invalid regular expression set construction");
640
641 let unknown_keys = config
642 .catchall
643 .keys()
644 .filter(|key| !known_keys.is_match(key))
645 .inspect(|key| {
646 if config.error_on_unknown_config_opts {
647 error!("Config parameter \"{key}\" is unknown to tuwunel");
648 } else {
649 warn!("Config parameter \"{key}\" is unknown to tuwunel, ignoring.");
650 }
651 })
652 .collect_vec();
653
654 if !unknown_keys.is_empty() && config.error_on_unknown_config_opts {
655 Err!("Unknown config options were found: {unknown_keys:?}")
656 } else {
657 Ok(())
658 }
659}