Files
tuwunel/src/core/config/check.rs
2026-02-01 01:22:13 +00:00

437 lines
13 KiB
Rust

use std::env::consts::OS;
use either::Either;
use figment::Figment;
use itertools::Itertools;
use super::{DEPRECATED_KEYS, IdentityProvider};
use crate::{Config, Err, Result, Server, debug, debug_info, debug_warn, error, warn};
/// Performs check() with additional checks specific to reloading old config
/// with new config.
pub fn reload(old: &Config, new: &Config) -> Result {
check(new)?;
if new.server_name != old.server_name {
return Err!(Config(
"server_name",
"You can't change the server's name from {:?}.",
old.server_name
));
}
Ok(())
}
pub fn check(config: &Config) -> Result {
if cfg!(debug_assertions) {
warn!("Note: tuwunel was built without optimisations (i.e. debug build)");
}
if config.allow_invalid_tls_certificates {
warn!(
"\n\nWARNING: \n\nTLS CERTIFICATE VALIDATION IS DISABLED, THIS IS HIGHLY INSECURE \
AND SHOULD NOT BE USED IN PRODUCTION.\n\n"
);
}
warn_deprecated(config);
warn_unknown_key(config)?;
if config.sentry && config.sentry_endpoint.is_none() {
return Err!(Config(
"sentry_endpoint",
"Sentry cannot be enabled without an endpoint set"
));
}
if cfg!(all(feature = "hardened_malloc", feature = "jemalloc", not(target_env = "msvc"))) {
debug_warn!(
"hardened_malloc and jemalloc compile-time features are both enabled, this causes \
jemalloc to be used."
);
}
if cfg!(not(unix)) && config.unix_socket_path.is_some() {
return Err!(Config(
"unix_socket_path",
"UNIX socket support is only available on *nix platforms. Please remove \
'unix_socket_path' from your config."
));
}
if config.unix_socket_path.is_none() && config.get_bind_hosts().is_empty() {
return Err!(Config("address", "No TCP addresses were specified to listen on"));
}
if config.unix_socket_path.is_none() && config.get_bind_ports().is_empty() {
return Err!(Config("port", "No ports were specified to listen on"));
}
if !config.listening {
warn!("Configuration item `listening` is set to `false`. Cannot hear anyone.");
}
if config.unix_socket_path.is_none() {
config.get_bind_addrs().iter().for_each(|addr| {
use std::path::Path;
if addr.ip().is_loopback() {
debug_info!(
"Found loopback listening address {addr}, running checks if we're in a \
container."
);
if Path::new("/proc/vz").exists() /* Guest */ && !Path::new("/proc/bz").exists()
/* Host */
{
error!(
"You are detected using OpenVZ with a loopback/localhost listening \
address of {addr}. If you are using OpenVZ for containers and you use \
NAT-based networking to communicate with the host and guest, this will \
NOT work. Please change this to \"0.0.0.0\". If this is expected, you \
can ignore.",
);
} else if Path::new("/.dockerenv").exists() {
error!(
"You are detected using Docker with a loopback/localhost listening \
address of {addr}. If you are using a reverse proxy on the host and \
require communication to tuwunel in the Docker container via NAT-based \
networking, this will NOT work. Please change this to \"0.0.0.0\". If \
this is expected, you can ignore.",
);
} else if Path::new("/run/.containerenv").exists() {
error!(
"You are detected using Podman with a loopback/localhost listening \
address of {addr}. If you are using a reverse proxy on the host and \
require communication to tuwunel in the Podman container via NAT-based \
networking, this will NOT work. Please change this to \"0.0.0.0\". If \
this is expected, you can ignore.",
);
}
}
});
}
// rocksdb does not allow max_log_files to be 0
if config.rocksdb_max_log_files == 0 {
return Err!(Config(
"max_log_files",
"rocksdb_max_log_files cannot be 0. Please set a value at least 1."
));
}
// yeah, unless the user built a debug build hopefully for local testing only
if cfg!(not(debug_assertions)) && config.server_name == "your.server.name" {
return Err!(Config(
"server_name",
"You must specify a valid server name for production usage of tuwunel."
));
}
if config
.emergency_password
.as_ref()
.is_some_and(|emergency_password| emergency_password == "F670$2CP@Hw8mG7RY1$%!#Ic7YA")
{
return Err!(Config(
"emergency_password",
"The public example emergency password is being used, this is insecure. Please \
change this."
));
}
if config
.emergency_password
.as_ref()
.is_some_and(String::is_empty)
{
return Err!(Config(
"emergency_password",
"Emergency password was set to an empty string, this is not valid. Unset \
emergency_password to disable it or set it to a real password."
));
}
// check if the user specified a registration token as `""`
if config
.registration_token
.as_ref()
.is_some_and(String::is_empty)
{
return Err!(Config(
"registration_token",
"Registration token was specified but is empty (\"\")"
));
}
// check if we can read the token file path, and check if the file is empty
if config
.registration_token_file
.as_ref()
.is_some_and(|path| {
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
error!("Failed to read the registration token file: {e}");
}) else {
return true;
};
token == String::new()
}) {
return Err!(Config(
"registration_token_file",
"Registration token file was specified but is empty or failed to be read"
));
}
if config.max_request_size < 10_000_000 {
return Err!(Config(
"max_request_size",
"Max request size is less than 10MB. Please increase it as this is too low for \
operable federation."
));
}
// check if user specified valid IP CIDR ranges on startup
for cidr in &config.ip_range_denylist {
if let Err(e) = ipaddress::IPAddress::parse(cidr) {
return Err!(Config(
"ip_range_denylist",
"Parsing specified IP CIDR range from string failed: {e}."
));
}
}
if config.allow_registration
&& !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none()
&& config.registration_token_file.is_none()
{
return Err!(Config(
"registration_token",
"!! You have `allow_registration` enabled without a token configured in your config \
which means you are allowing ANYONE to register on your tuwunel instance without \
any 2nd-step (e.g. registration token). If this is not the intended behaviour, \
please set a registration token. For security and safety reasons, tuwunel will \
shut down. If you are extra sure this is the desired behaviour you want, please \
set the following config option to true:
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`"
));
}
if config.allow_registration
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none()
&& config.registration_token_file.is_none()
{
warn!(
"Open registration is enabled via setting \
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse` and \
`allow_registration` to true without a registration token configured. You are \
expected to be aware of the risks now. If this is not the desired behaviour, \
please set a registration token."
);
}
if config.allow_outgoing_presence && !config.allow_local_presence {
return Err!(Config(
"allow_local_presence",
"Outgoing presence requires allowing local presence. Please enable \
'allow_local_presence' or disable outgoing presence."
));
}
if config.suppress_push_when_active {
warn!(
"Push suppression when active is enabled (EXPERIMENTAL): behavior may change or be \
unstable. Disable by removing or setting suppress_push_when_active to false."
);
}
if config
.url_preview_domain_contains_allowlist
.contains(&"*".to_owned())
{
warn!(
"All URLs are allowed for URL previews via setting \
\"url_preview_domain_contains_allowlist\" to \"*\". This opens up significant \
attack surface to your server. You are expected to be aware of the risks by doing \
this."
);
}
if config
.url_preview_domain_explicit_allowlist
.contains(&"*".to_owned())
{
warn!(
"All URLs are allowed for URL previews via setting \
\"url_preview_domain_explicit_allowlist\" to \"*\". This opens up significant \
attack surface to your server. You are expected to be aware of the risks by doing \
this."
);
}
if config
.url_preview_url_contains_allowlist
.contains(&"*".to_owned())
{
warn!(
"All URLs are allowed for URL previews via setting \
\"url_preview_url_contains_allowlist\" to \"*\". This opens up significant attack \
surface to your server. You are expected to be aware of the risks by doing this."
);
}
if let Some(Either::Right(_)) = config.url_preview_bound_interface.as_ref()
&& !matches!(OS, "android" | "fuchsia" | "linux")
{
return Err!(Config(
"url_preview_bound_interface",
"Not a valid IP address. Interface names not supported on {OS}."
));
}
if !Server::available_room_versions()
.any(|(version, _)| version == config.default_room_version)
{
return Err!(Config(
"default_room_version",
"Room version {:?} is not available",
config.default_room_version
));
}
for a in config.identity_provider.values() {
let count = config
.identity_provider
.values()
.filter(|b| a.id().eq(b.id()))
.count();
debug_assert_ne!(count, 0, "expected at least one identity_provider");
if count > 1 {
return Err!(Config(
"client_id",
"Duplicate identity_provider with client_id {}",
a.client_id
));
}
}
for (i, provider) in &config.identity_provider {
if provider.client_secret.is_some() {
continue;
}
let Some(secret_path) = &provider.client_secret_file else {
return Err!(Config(
"client_secret",
"Either client secret or a client secret file must be set on identity provider \
№{i}."
));
};
let Ok(secret) = std::fs::read_to_string(secret_path) else {
return Err!(Config(
"client_secret_file",
"Client secret file was specified but failed to be read at identity provider \
№{i}"
));
};
if secret.is_empty() {
return Err!(Config(
"client_secret_file",
"Client secret file was specified but is empty on identity provider №{i}"
));
}
}
if !config.sso_custom_providers_page
&& config.identity_provider.len() > 1
&& config
.identity_provider
.values()
.filter(|idp| idp.default)
.count()
.eq(&0)
{
let default = config
.identity_provider
.values()
.next()
.map(IdentityProvider::id)
.expect("Check at least one provider is configured to reach here");
warn!(
"More than one identity_provider has been configured without any default selected. \
To prevent this warning set `default = true` for one provider. Considering \
{default} the default for now..."
);
}
Ok(())
}
/// Iterates over all the keys in the config file and warns if there is a
/// deprecated key specified
fn warn_deprecated(config: &Config) {
debug!("Checking for deprecated config keys");
let found_deprecated_keys = config
.catchall
.keys()
.filter(|key| DEPRECATED_KEYS.iter().any(|s| s == key))
.inspect(|key| warn!("Config parameter \"{key}\" is deprecated, ignoring."))
.next()
.is_some();
if found_deprecated_keys {
warn!(
"Deprecated config keys were found. Read tuwunel config documentation at https://tuwunel.chat/configuration.html and \
check your configuration if any new configuration parameters should be adjusted"
);
}
}
/// iterates over all the catchall keys (unknown config options) and warns or
/// errors if there are any.
fn warn_unknown_key(config: &Config) -> Result {
debug!("Checking for unknown config keys");
let unknown_keys = config
.catchall
.keys()
.filter_map(|key| {
if key == "config" {
None
} else {
if config.error_on_unknown_config_opts {
error!("Config parameter \"{key}\" is unknown to tuwunel");
} else {
warn!("Config parameter \"{key}\" is unknown to tuwunel, ignoring.");
}
Some(key.as_str())
}
})
.collect_vec();
if !unknown_keys.is_empty() && config.error_on_unknown_config_opts {
Err!("Unknown config options were found: {unknown_keys:?}")
} else {
Ok(())
}
}
/// Checks the presence of the `address` and `unix_socket_path` keys in the
/// raw_config, exiting the process if both keys were detected.
pub(super) fn is_dual_listening(raw_config: &Figment) -> Result {
let contains_address = raw_config.contains("address");
let contains_unix_socket = raw_config.contains("unix_socket_path");
if contains_address && contains_unix_socket {
return Err!(
"TOML keys \"address\" and \"unix_socket_path\" were both defined. Please specify \
only one option."
);
}
Ok(())
}