Skip to main content

tuwunel_core/config/
check.rs

1use 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
9/// Performs check() with additional checks specific to reloading old config
10/// with new config.
11pub 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() /* Guest */ && !Path::new("/proc/bz").exists()
96				/* Host */
97				{
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	// rocksdb does not allow max_log_files to be 0
127	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	// yeah, unless the user built a debug build hopefully for local testing only
135	#[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	// check if the user specified a registration token as `""`
168	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	// check if we can read the token file path, and check if the file is empty
180	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	// check if user specified valid IP CIDR ranges on startup
220	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
435/// Iterates over all the keys in the config file and warns if there is a
436/// deprecated key specified
437fn 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
455/// iterates over all the catchall keys (unknown config options) and warns or
456/// errors if there are any.
457fn 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}