extend x-platform support for binding URL previews to interfaces via address

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk 2024-12-08 02:39:40 +00:00
parent 52cee65748
commit f0a1aaf7bc
6 changed files with 74 additions and 28 deletions

1
Cargo.lock generated
View file

@ -824,6 +824,7 @@ dependencies = [
"conduit_core", "conduit_core",
"conduit_database", "conduit_database",
"const-str", "const-str",
"either",
"futures", "futures",
"hickory-resolver", "hickory-resolver",
"http", "http",

View file

@ -1117,13 +1117,15 @@
# #
#ip_range_denylist = ["127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", #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. # Optional IP address or network interface-name to bind as the source of
# If not set, it will not bind to a specific interface. # URL preview requests. If not set, it will not bind to a specific
# This uses [`reqwest::ClientBuilder::interface`] under the hood. # 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 = #url_preview_bound_interface =

View file

@ -1,3 +1,6 @@
use std::env::consts::OS;
use either::Either;
use figment::Figment; use figment::Figment;
use super::DEPRECATED_KEYS; 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(()) Ok(())
} }

View file

@ -1250,15 +1250,19 @@ pub struct Config {
#[serde(default = "default_ip_range_denylist")] #[serde(default = "default_ip_range_denylist")]
pub ip_range_denylist: Vec<String>, pub ip_range_denylist: Vec<String>,
/// Optional interface to bind to with SO_BINDTODEVICE for URL previews. /// Optional IP address or network interface-name to bind as the source of
/// If not set, it will not bind to a specific interface. /// URL preview requests. If not set, it will not bind to a specific
/// This uses [`reqwest::ClientBuilder::interface`] under the hood. /// 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"`
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] ///
pub url_preview_bound_interface: Option<String>, /// default:
#[serde(default, with = "either::serde_untagged_optional")]
pub url_preview_bound_interface: Option<Either<IpAddr, String>>,
/// Vector list of domains allowed to send requests to for URL previews. /// Vector list of domains allowed to send requests to for URL previews.
/// Defaults to none. Note: this is a *contains* match, not an explicit /// 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", { line("Forbidden room aliases", {
&self.forbidden_alias_names.patterns().iter().join(", ") &self.forbidden_alias_names.patterns().iter().join(", ")
}); });
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
line( line(
"URL preview bound interface", "URL preview bound interface",
if let Some(interface) = &self.url_preview_bound_interface { self.url_preview_bound_interface
interface .as_ref()
} else { .map(Either::as_ref)
"not set" .map(|either| either.map_left(ToString::to_string))
}, .map(Either::either_into::<String>)
.unwrap_or_default()
.as_str(),
); );
line( line(
"URL preview domain contains allowlist", "URL preview domain contains allowlist",

View file

@ -47,6 +47,7 @@ bytes.workspace = true
conduit-core.workspace = true conduit-core.workspace = true
conduit-database.workspace = true conduit-database.workspace = true
const-str.workspace = true const-str.workspace = true
either.workspace = true
futures.workspace = true futures.workspace = true
hickory-resolver.workspace = true hickory-resolver.workspace = true
http.workspace = true http.workspace = true

View file

@ -1,6 +1,7 @@
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use conduit::{err, implement, trace, Config, Result}; use conduit::{err, implement, trace, Config, Result};
use either::Either;
use ipaddress::IPAddress; use ipaddress::IPAddress;
use reqwest::redirect; use reqwest::redirect;
@ -25,23 +26,27 @@ impl crate::Service for Service {
let config = &args.server.config; let config = &args.server.config;
let resolver = args.require::<resolver::Service>("resolver"); let resolver = args.require::<resolver::Service>("resolver");
let url_preview_builder = base(config)? let url_preview_bind_addr = config
.dns_resolver(resolver.resolver.clone()) .url_preview_bound_interface
.redirect(redirect::Policy::limited(3)); .clone()
.and_then(Either::left);
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] let url_preview_bind_iface = config
let url_preview_builder = if let Some(interface) = &config.url_preview_bound_interface { .url_preview_bound_interface
url_preview_builder.interface(interface) .clone()
} else { .and_then(Either::right);
url_preview_builder
};
Ok(Arc::new(Self { Ok(Arc::new(Self {
default: base(config)? default: base(config)?
.dns_resolver(resolver.resolver.clone()) .dns_resolver(resolver.resolver.clone())
.build()?, .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)? extern_media: base(config)?
.dns_resolver(resolver.resolver.clone()) .dns_resolver(resolver.resolver.clone())
@ -172,6 +177,26 @@ fn base(config: &Config) -> Result<reqwest::ClientBuilder> {
} }
} }
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
fn builder_interface(builder: reqwest::ClientBuilder, config: Option<&str>) -> Result<reqwest::ClientBuilder> {
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<reqwest::ClientBuilder> {
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] #[inline]
#[must_use] #[must_use]
#[implement(Service)] #[implement(Service)]