diff --git a/Cargo.lock b/Cargo.lock index 52110f914..e6659e031 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -824,6 +824,7 @@ dependencies = [ "conduit_core", "conduit_database", "const-str", + "either", "futures", "hickory-resolver", "http", diff --git a/conduwuit-example.toml b/conduwuit-example.toml index b4bce140e..bdccdb990 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1117,13 +1117,15 @@ # #ip_range_denylist = ["127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", -# Optional interface to bind to with SO_BINDTODEVICE for URL previews. -# If not set, it will not bind to a specific interface. -# This uses [`reqwest::ClientBuilder::interface`] under the hood. +# Optional IP address or network interface-name to bind as the source of +# URL preview requests. If not set, it will not bind to a specific +# address or interface. # -# To list the interfaces on your system, use the command `ip link show` +# Interface names only supported on Linux, Android, and Fuchsia platforms; +# all other platforms can specify the IP address. To list the interfaces +# on your system, use the command `ip link show`. # -# Example: `"eth0"` +# example: `"eth0"` or `"1.2.3.4"` # #url_preview_bound_interface = diff --git a/src/core/config/check.rs b/src/core/config/check.rs index c0d055337..c75fb31ea 100644 --- a/src/core/config/check.rs +++ b/src/core/config/check.rs @@ -1,3 +1,6 @@ +use std::env::consts::OS; + +use either::Either; use figment::Figment; use super::DEPRECATED_KEYS; @@ -191,6 +194,15 @@ For security and safety reasons, conduwuit will shut down. If you are extra sure ); } + if let Some(Either::Right(_)) = config.url_preview_bound_interface.as_ref() { + if !matches!(OS, "android" | "fuchsia" | "linux") { + return Err!(Config( + "url_preview_bound_interface", + "Not a valid IP address. Interface names not supported on {OS}." + )); + } + } + Ok(()) } diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index c613c8197..117b4da51 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -1250,15 +1250,19 @@ pub struct Config { #[serde(default = "default_ip_range_denylist")] pub ip_range_denylist: Vec, - /// Optional interface to bind to with SO_BINDTODEVICE for URL previews. - /// If not set, it will not bind to a specific interface. - /// This uses [`reqwest::ClientBuilder::interface`] under the hood. + /// Optional IP address or network interface-name to bind as the source of + /// URL preview requests. If not set, it will not bind to a specific + /// address or interface. /// - /// To list the interfaces on your system, use the command `ip link show` + /// Interface names only supported on Linux, Android, and Fuchsia platforms; + /// all other platforms can specify the IP address. To list the interfaces + /// on your system, use the command `ip link show`. /// - /// Example: `"eth0"` - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - pub url_preview_bound_interface: Option, + /// example: `"eth0"` or `"1.2.3.4"` + /// + /// default: + #[serde(default, with = "either::serde_untagged_optional")] + pub url_preview_bound_interface: Option>, /// Vector list of domains allowed to send requests to for URL previews. /// Defaults to none. Note: this is a *contains* match, not an explicit @@ -1970,14 +1974,15 @@ impl fmt::Display for Config { line("Forbidden room aliases", { &self.forbidden_alias_names.patterns().iter().join(", ") }); - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] line( "URL preview bound interface", - if let Some(interface) = &self.url_preview_bound_interface { - interface - } else { - "not set" - }, + self.url_preview_bound_interface + .as_ref() + .map(Either::as_ref) + .map(|either| either.map_left(ToString::to_string)) + .map(Either::either_into::) + .unwrap_or_default() + .as_str(), ); line( "URL preview domain contains allowlist", diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index 7578ef64f..197478472 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -47,6 +47,7 @@ bytes.workspace = true conduit-core.workspace = true conduit-database.workspace = true const-str.workspace = true +either.workspace = true futures.workspace = true hickory-resolver.workspace = true http.workspace = true diff --git a/src/service/client/mod.rs b/src/service/client/mod.rs index bcd881584..71545541c 100644 --- a/src/service/client/mod.rs +++ b/src/service/client/mod.rs @@ -1,6 +1,7 @@ use std::{sync::Arc, time::Duration}; use conduit::{err, implement, trace, Config, Result}; +use either::Either; use ipaddress::IPAddress; use reqwest::redirect; @@ -25,23 +26,27 @@ impl crate::Service for Service { let config = &args.server.config; let resolver = args.require::("resolver"); - let url_preview_builder = base(config)? - .dns_resolver(resolver.resolver.clone()) - .redirect(redirect::Policy::limited(3)); + let url_preview_bind_addr = config + .url_preview_bound_interface + .clone() + .and_then(Either::left); - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - let url_preview_builder = if let Some(interface) = &config.url_preview_bound_interface { - url_preview_builder.interface(interface) - } else { - url_preview_builder - }; + let url_preview_bind_iface = config + .url_preview_bound_interface + .clone() + .and_then(Either::right); Ok(Arc::new(Self { default: base(config)? .dns_resolver(resolver.resolver.clone()) .build()?, - url_preview: url_preview_builder.build()?, + url_preview: base(config) + .and_then(|builder| builder_interface(builder, url_preview_bind_iface.as_deref()))? + .local_address(url_preview_bind_addr) + .dns_resolver(resolver.resolver.clone()) + .redirect(redirect::Policy::limited(3)) + .build()?, extern_media: base(config)? .dns_resolver(resolver.resolver.clone()) @@ -172,6 +177,26 @@ fn base(config: &Config) -> Result { } } +#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] +fn builder_interface(builder: reqwest::ClientBuilder, config: Option<&str>) -> Result { + if let Some(iface) = config { + Ok(builder.interface(iface)) + } else { + Ok(builder) + } +} + +#[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] +fn builder_interface(builder: reqwest::ClientBuilder, config: Option<&str>) -> Result { + use conduit::Err; + + if let Some(iface) = config { + Err!("Binding to network-interface {iface:?} by name is not supported on this platform.") + } else { + Ok(builder) + } +} + #[inline] #[must_use] #[implement(Service)]