diff --git a/Cargo.lock b/Cargo.lock index 7a0edb11..3eabe3ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,6 +400,7 @@ dependencies = [ "hyper", "hyperlocal", "image", + "ipaddress", "jsonwebtoken", "lazy_static", "lru-cache", @@ -1080,6 +1081,20 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" +[[package]] +name = "ipaddress" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957bb9f3645d6bb7f36df99d5105b4866aa79749819d7c176a170a27dc477cbf" +dependencies = [ + "lazy_static", + "libc", + "num", + "num-integer", + "num-traits", + "regex", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -1404,6 +1419,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.4" @@ -1415,6 +1444,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1425,6 +1463,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" diff --git a/Cargo.toml b/Cargo.toml index 15befa25..65662269 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,9 @@ tikv-jemallocator = { version = "0.5.0", features = ["unprefixed_malloc_on_suppo lazy_static = "1.4.0" async-trait = "0.1.77" +# used for checking if an IP is in specific subnets / CIDR ranges +ipaddress = "0.1.3" + sd-notify = { version = "0.4.1", optional = true } [target.'cfg(unix)'.dependencies] diff --git a/conduwuit-example.toml b/conduwuit-example.toml index a07b3d66..fcf8b6f2 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -77,7 +77,33 @@ max_request_size = 20_000_000 # in bytes # See https://breachattack.com/ and https://wikipedia.org/wiki/BREACH before deciding to enable this. zstd_compression = false - +# Vector list of IPv4 and IPv6 CIDR ranges / subnets *in quotes* that you do not want conduwuit to send outbound requests to. +# Defaults to RFC1918, unroutable, loopback, multicast, and testnet addresses for security. +# +# To disable, set this to be an empty vector (`[]`). +# +# Currently this does not account for proxies in use like Synapse does. +ip_range_denylist = [ + "127.0.0.0/8", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "100.64.0.0/10", + "192.0.0.0/24", + "169.254.0.0/16", + "192.88.99.0/24", + "198.18.0.0/15", + "192.0.2.0/24", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "::1/128", + "fe80::/10", + "fc00::/7", + "2001:db8::/32", + "ff00::/8", + "fec0::/10", +] ### Moderation / Privacy / Security diff --git a/src/api/server_server.rs b/src/api/server_server.rs index 41c215d8..90c9e8be 100644 --- a/src/api/server_server.rs +++ b/src/api/server_server.rs @@ -11,6 +11,7 @@ use futures_util::future::TryFutureExt; use get_profile_information::v1::ProfileField; use http::header::{HeaderValue, AUTHORIZATION}; +use ipaddress::IPAddress; use ruma::{ api::{ client::error::{Error as RumaError, ErrorKind}, @@ -114,7 +115,6 @@ impl FedDest { } } -#[tracing::instrument(skip(request))] pub(crate) async fn send_request( destination: &ServerName, request: T, @@ -132,6 +132,29 @@ where )); } + if destination.is_ip_literal() { + info!("Destination is an IP literal, checking against IP range denylist."); + let ip = IPAddress::parse(destination.host()).map_err(|e| { + warn!("Failed to parse IP literal from string: {}", e); + Error::BadServerResponse("Invalid IP address") + })?; + + let cidr_ranges_s = services().globals.ip_range_denylist().to_vec(); + let mut cidr_ranges: Vec = Vec::new(); + + for cidr in cidr_ranges_s { + cidr_ranges.push(IPAddress::parse(cidr).expect("we checked this at startup")); + } + + for cidr in cidr_ranges { + if ip.includes(&cidr) { + return Err(Error::BadServerResponse( + "Not allowed to send requests to this IP", + )); + } + } + } + debug!("Preparing to send request to {destination}"); let mut write_destination_to_cache = false; diff --git a/src/config/mod.rs b/src/config/mod.rs index c552013a..ec78cd79 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -6,6 +6,7 @@ use std::{ }; use figment::Figment; + use ruma::{OwnedServerName, RoomVersionId}; use serde::{de::IgnoredAny, Deserialize}; use tracing::{error, warn}; @@ -128,6 +129,9 @@ pub struct Config { #[serde(default = "Vec::new")] pub prevent_media_downloads_from: Vec, + #[serde(default = "default_ip_range_denylist")] + pub ip_range_denylist: Vec, + #[serde(flatten)] pub catchall: BTreeMap, } @@ -307,6 +311,14 @@ impl fmt::Display for Config { } &lst.join(", ") }), + ("Outbound Request IP Range Denylist", { + let mut lst = vec![]; + for item in self.ip_range_denylist.iter().cloned().enumerate() { + let (_, ip): (usize, String) = item; + lst.push(ip); + } + &lst.join(", ") + }), ]; let mut msg: String = "Active config values:\n\n".to_owned(); @@ -408,3 +420,27 @@ fn default_rocksdb_max_log_file_size() -> usize { // 4 megabytes 4 * 1024 * 1024 } + +fn default_ip_range_denylist() -> Vec { + vec![ + "127.0.0.0/8".to_owned(), + "10.0.0.0/8".to_owned(), + "172.16.0.0/12".to_owned(), + "192.168.0.0/16".to_owned(), + "100.64.0.0/10".to_owned(), + "192.0.0.0/24".to_owned(), + "169.254.0.0/16".to_owned(), + "192.88.99.0/24".to_owned(), + "198.18.0.0/15".to_owned(), + "192.0.2.0/24".to_owned(), + "198.51.100.0/24".to_owned(), + "203.0.113.0/24".to_owned(), + "224.0.0.0/4".to_owned(), + "::1/128".to_owned(), + "fe80::/10".to_owned(), + "fc00::/7".to_owned(), + "2001:db8::/32".to_owned(), + "ff00::/8".to_owned(), + "fec0::/10".to_owned(), + ] +} diff --git a/src/main.rs b/src/main.rs index 5ac3ef59..b2282253 100644 --- a/src/main.rs +++ b/src/main.rs @@ -147,6 +147,12 @@ async fn main() { }; let config = &services().globals.config; + // check if user specified valid IP CIDR ranges on startup + for cidr in services().globals.ip_range_denylist() { + let _ = ipaddress::IPAddress::parse(cidr) + .map_err(|e| error!("Error parsing specified IP CIDR range: {e}")); + } + if config.allow_registration && !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse { diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs index 54b410e9..e9da0e15 100644 --- a/src/service/globals/mod.rs +++ b/src/service/globals/mod.rs @@ -427,6 +427,10 @@ impl Service<'_> { &self.config.prevent_media_downloads_from } + pub fn ip_range_denylist(&self) -> &[String] { + &self.config.ip_range_denylist + } + pub fn supported_room_versions(&self) -> Vec { let mut room_versions: Vec = vec![]; room_versions.extend(self.stable_room_versions.clone()); diff --git a/src/service/sending/mod.rs b/src/service/sending/mod.rs index 38034afc..1cab41b2 100644 --- a/src/service/sending/mod.rs +++ b/src/service/sending/mod.rs @@ -1,6 +1,7 @@ mod data; pub use data::Data; +use ipaddress::IPAddress; use std::{ collections::{BTreeMap, HashMap, HashSet}, @@ -43,7 +44,7 @@ use tokio::{ select, sync::{mpsc, Mutex, Semaphore}, }; -use tracing::{debug, error, warn}; +use tracing::{debug, error, info, warn}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum OutgoingKind { @@ -716,6 +717,29 @@ impl Service { where T: Debug, { + if destination.is_ip_literal() { + info!("Destination is an IP literal, checking against IP range denylist."); + let ip = IPAddress::parse(destination.host()).map_err(|e| { + warn!("Failed to parse IP literal from string: {}", e); + Error::BadServerResponse("Invalid IP address") + })?; + + let cidr_ranges_s = services().globals.ip_range_denylist().to_vec(); + let mut cidr_ranges: Vec = Vec::new(); + + for cidr in cidr_ranges_s { + cidr_ranges.push(IPAddress::parse(cidr).expect("we checked this at startup")); + } + + for cidr in cidr_ranges { + if ip.includes(&cidr) { + return Err(Error::BadServerResponse( + "Not allowed to send requests to this IP", + )); + } + } + } + debug!("Waiting for permit"); let permit = self.maximum_requests.acquire().await; debug!("Got permit");