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_database",
"const-str",
"either",
"futures",
"hickory-resolver",
"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",
# 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 =

View file

@ -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(())
}

View file

@ -1250,15 +1250,19 @@ pub struct Config {
#[serde(default = "default_ip_range_denylist")]
pub ip_range_denylist: Vec<String>,
/// 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<String>,
/// example: `"eth0"` or `"1.2.3.4"`
///
/// 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.
/// 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::<String>)
.unwrap_or_default()
.as_str(),
);
line(
"URL preview domain contains allowlist",

View file

@ -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

View file

@ -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::Service>("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<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]
#[must_use]
#[implement(Service)]