refactor: Centralize server forbidden checks into moderation module

This moves all checks related to `forbidden_remote_server_names`,
`forbidden_remote_room_directory_server_names` and
`prevent_media_downloads_from` to a new `moderation` module.
This is useful for implementing more complicated logic globally.
Mostly the changes from #673, but is also relevant for #750
This commit is contained in:
Jade Ellis 2025-04-19 23:02:43 +01:00
parent e71138ab6f
commit 0eb9e4f3d2
No known key found for this signature in database
GPG key ID: 8705A2A3EBF77BD2
18 changed files with 109 additions and 97 deletions

View file

@ -1197,6 +1197,9 @@
# incoming AND outgoing federation with, and block client room joins / # incoming AND outgoing federation with, and block client room joins /
# remote user invites. # remote user invites.
# #
# Additionally, it will hide messages from these servers for all users
# on this server.
#
# This check is applied on the room ID, room alias, sender server name, # This check is applied on the room ID, room alias, sender server name,
# sender user's server name, inbound federation X-Matrix origin, and # sender user's server name, inbound federation X-Matrix origin, and
# outbound federation handler. # outbound federation handler.

View file

@ -52,13 +52,8 @@ pub(crate) async fn get_public_rooms_filtered_route(
) -> Result<get_public_rooms_filtered::v3::Response> { ) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(server) = &body.server { if let Some(server) = &body.server {
if services if services
.config .moderation
.forbidden_remote_room_directory_server_names .is_remote_server_room_directory_forbidden(server)
.is_match(server.host())
|| services
.config
.forbidden_remote_server_names
.is_match(server.host())
{ {
return Err!(Request(Forbidden("Server is banned on this homeserver."))); return Err!(Request(Forbidden("Server is banned on this homeserver.")));
} }
@ -92,15 +87,7 @@ pub(crate) async fn get_public_rooms_route(
body: Ruma<get_public_rooms::v3::Request>, body: Ruma<get_public_rooms::v3::Request>,
) -> Result<get_public_rooms::v3::Response> { ) -> Result<get_public_rooms::v3::Response> {
if let Some(server) = &body.server { if let Some(server) = &body.server {
if services if services.moderation.is_remote_server_forbidden(server) {
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host())
|| services
.config
.forbidden_remote_server_names
.is_match(server.host())
{
return Err!(Request(Forbidden("Server is banned on this homeserver."))); return Err!(Request(Forbidden("Server is banned on this homeserver.")));
} }
} }

View file

@ -83,9 +83,8 @@ async fn banned_room_check(
if let Some(room_id) = room_id { if let Some(room_id) = room_id {
if services.rooms.metadata.is_banned(room_id).await if services.rooms.metadata.is_banned(room_id).await
|| services || services
.config .moderation
.forbidden_remote_server_names .is_remote_server_forbidden(room_id.server_name().expect("legacy room mxid"))
.is_match(room_id.server_name().expect("legacy room mxid").host())
{ {
warn!( warn!(
"User {user_id} who is not an admin attempted to send an invite for or \ "User {user_id} who is not an admin attempted to send an invite for or \

View file

@ -274,9 +274,8 @@ pub(crate) async fn is_ignored_pdu(
let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(&pdu.kind).is_ok(); let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(&pdu.kind).is_ok();
let ignored_server = services let ignored_server = services
.config .moderation
.forbidden_remote_server_names .is_remote_server_forbidden(pdu.sender().server_name());
.is_match(pdu.sender().server_name().host());
if ignored_type if ignored_type
&& (ignored_server || services.users.user_is_ignored(&pdu.sender, user_id).await) && (ignored_server || services.users.user_is_ignored(&pdu.sender, user_id).await)

View file

@ -306,7 +306,7 @@ async fn auth_server(
} }
fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result<()> { fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result<()> {
if !services.server.config.allow_federation { if !services.config.allow_federation {
return Err!(Config("allow_federation", "Federation is disabled.")); return Err!(Config("allow_federation", "Federation is disabled."));
} }
@ -316,11 +316,7 @@ fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result<()> {
} }
let origin = &x_matrix.origin; let origin = &x_matrix.origin;
if services if services.moderation.is_remote_server_forbidden(origin) {
.config
.forbidden_remote_server_names
.is_match(origin.host())
{
return Err!(Request(Forbidden(debug_warn!( return Err!(Request(Forbidden(debug_warn!(
"Federation requests from {origin} denied." "Federation requests from {origin} denied."
)))); ))));

View file

@ -37,19 +37,14 @@ pub(crate) async fn create_invite_route(
} }
if let Some(server) = body.room_id.server_name() { if let Some(server) = body.room_id.server_name() {
if services if services.moderation.is_remote_server_forbidden(server) {
.config
.forbidden_remote_server_names
.is_match(server.host())
{
return Err!(Request(Forbidden("Server is banned on this homeserver."))); return Err!(Request(Forbidden("Server is banned on this homeserver.")));
} }
} }
if services if services
.config .moderation
.forbidden_remote_server_names .is_remote_server_forbidden(body.origin())
.is_match(body.origin().host())
{ {
warn!( warn!(
"Received federated/remote invite from banned server {} for room ID {}. Rejecting.", "Received federated/remote invite from banned server {} for room ID {}. Rejecting.",

View file

@ -42,9 +42,8 @@ pub(crate) async fn create_join_event_template_route(
.await?; .await?;
if services if services
.config .moderation
.forbidden_remote_server_names .is_remote_server_forbidden(body.origin())
.is_match(body.origin().host())
{ {
warn!( warn!(
"Server {} for remote user {} tried joining room ID {} which has a server name that \ "Server {} for remote user {} tried joining room ID {} which has a server name that \
@ -57,11 +56,7 @@ pub(crate) async fn create_join_event_template_route(
} }
if let Some(server) = body.room_id.server_name() { if let Some(server) = body.room_id.server_name() {
if services if services.moderation.is_remote_server_forbidden(server) {
.config
.forbidden_remote_server_names
.is_match(server.host())
{
return Err!(Request(Forbidden(warn!( return Err!(Request(Forbidden(warn!(
"Room ID server name {server} is banned on this homeserver." "Room ID server name {server} is banned on this homeserver."
)))); ))));

View file

@ -33,9 +33,8 @@ pub(crate) async fn create_knock_event_template_route(
.await?; .await?;
if services if services
.config .moderation
.forbidden_remote_server_names .is_remote_server_forbidden(body.origin())
.is_match(body.origin().host())
{ {
warn!( warn!(
"Server {} for remote user {} tried knocking room ID {} which has a server name \ "Server {} for remote user {} tried knocking room ID {} which has a server name \
@ -48,11 +47,7 @@ pub(crate) async fn create_knock_event_template_route(
} }
if let Some(server) = body.room_id.server_name() { if let Some(server) = body.room_id.server_name() {
if services if services.moderation.is_remote_server_forbidden(server) {
.config
.forbidden_remote_server_names
.is_match(server.host())
{
return Err!(Request(Forbidden("Server is banned on this homeserver."))); return Err!(Request(Forbidden("Server is banned on this homeserver.")));
} }
} }

View file

@ -268,9 +268,8 @@ pub(crate) async fn create_join_event_v1_route(
body: Ruma<create_join_event::v1::Request>, body: Ruma<create_join_event::v1::Request>,
) -> Result<create_join_event::v1::Response> { ) -> Result<create_join_event::v1::Response> {
if services if services
.config .moderation
.forbidden_remote_server_names .is_remote_server_forbidden(body.origin())
.is_match(body.origin().host())
{ {
warn!( warn!(
"Server {} tried joining room ID {} through us who has a server name that is \ "Server {} tried joining room ID {} through us who has a server name that is \
@ -282,11 +281,7 @@ pub(crate) async fn create_join_event_v1_route(
} }
if let Some(server) = body.room_id.server_name() { if let Some(server) = body.room_id.server_name() {
if services if services.moderation.is_remote_server_forbidden(server) {
.config
.forbidden_remote_server_names
.is_match(server.host())
{
warn!( warn!(
"Server {} tried joining room ID {} through us which has a server name that is \ "Server {} tried joining room ID {} through us which has a server name that is \
globally forbidden. Rejecting.", globally forbidden. Rejecting.",
@ -314,19 +309,14 @@ pub(crate) async fn create_join_event_v2_route(
body: Ruma<create_join_event::v2::Request>, body: Ruma<create_join_event::v2::Request>,
) -> Result<create_join_event::v2::Response> { ) -> Result<create_join_event::v2::Response> {
if services if services
.config .moderation
.forbidden_remote_server_names .is_remote_server_forbidden(body.origin())
.is_match(body.origin().host())
{ {
return Err!(Request(Forbidden("Server is banned on this homeserver."))); return Err!(Request(Forbidden("Server is banned on this homeserver.")));
} }
if let Some(server) = body.room_id.server_name() { if let Some(server) = body.room_id.server_name() {
if services if services.moderation.is_remote_server_forbidden(server) {
.config
.forbidden_remote_server_names
.is_match(server.host())
{
warn!( warn!(
"Server {} tried joining room ID {} through us which has a server name that is \ "Server {} tried joining room ID {} through us which has a server name that is \
globally forbidden. Rejecting.", globally forbidden. Rejecting.",

View file

@ -26,9 +26,8 @@ pub(crate) async fn create_knock_event_v1_route(
body: Ruma<send_knock::v1::Request>, body: Ruma<send_knock::v1::Request>,
) -> Result<send_knock::v1::Response> { ) -> Result<send_knock::v1::Response> {
if services if services
.config .moderation
.forbidden_remote_server_names .is_remote_server_forbidden(body.origin())
.is_match(body.origin().host())
{ {
warn!( warn!(
"Server {} tried knocking room ID {} who has a server name that is globally \ "Server {} tried knocking room ID {} who has a server name that is globally \
@ -40,11 +39,7 @@ pub(crate) async fn create_knock_event_v1_route(
} }
if let Some(server) = body.room_id.server_name() { if let Some(server) = body.room_id.server_name() {
if services if services.moderation.is_remote_server_forbidden(server) {
.config
.forbidden_remote_server_names
.is_match(server.host())
{
warn!( warn!(
"Server {} tried knocking room ID {} which has a server name that is globally \ "Server {} tried knocking room ID {} which has a server name that is globally \
forbidden. Rejecting.", forbidden. Rejecting.",

View file

@ -1374,6 +1374,9 @@ pub struct Config {
/// incoming AND outgoing federation with, and block client room joins / /// incoming AND outgoing federation with, and block client room joins /
/// remote user invites. /// remote user invites.
/// ///
/// Additionally, it will hide messages from these servers for all users
/// on this server.
///
/// This check is applied on the room ID, room alias, sender server name, /// This check is applied on the room ID, room alias, sender server name,
/// sender user's server name, inbound federation X-Matrix origin, and /// sender user's server name, inbound federation X-Matrix origin, and
/// outbound federation handler. /// outbound federation handler.
@ -1954,7 +1957,7 @@ impl Config {
let mut addrs = Vec::with_capacity( let mut addrs = Vec::with_capacity(
self.get_bind_hosts() self.get_bind_hosts()
.len() .len()
.saturating_add(self.get_bind_ports().len()), .saturating_mul(self.get_bind_ports().len()),
); );
for host in &self.get_bind_hosts() { for host in &self.get_bind_hosts() {
for port in &self.get_bind_ports() { for port in &self.get_bind_ports() {

View file

@ -64,13 +64,7 @@ where
return Err!(Config("allow_federation", "Federation is disabled.")); return Err!(Config("allow_federation", "Federation is disabled."));
} }
if self if self.services.moderation.is_remote_server_forbidden(dest) {
.services
.server
.config
.forbidden_remote_server_names
.is_match(dest.host())
{
return Err!(Request(Forbidden(debug_warn!("Federation with {dest} is not allowed.")))); return Err!(Request(Forbidden(debug_warn!("Federation with {dest} is not allowed."))));
} }

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use conduwuit::{Result, Server}; use conduwuit::{Result, Server};
use crate::{Dep, client, resolver, server_keys}; use crate::{Dep, client, moderation, resolver, server_keys};
pub struct Service { pub struct Service {
services: Services, services: Services,
@ -15,6 +15,7 @@ struct Services {
client: Dep<client::Service>, client: Dep<client::Service>,
resolver: Dep<resolver::Service>, resolver: Dep<resolver::Service>,
server_keys: Dep<server_keys::Service>, server_keys: Dep<server_keys::Service>,
moderation: Dep<moderation::Service>,
} }
impl crate::Service for Service { impl crate::Service for Service {
@ -25,6 +26,7 @@ impl crate::Service for Service {
client: args.depend::<client::Service>("client"), client: args.depend::<client::Service>("client"),
resolver: args.depend::<resolver::Service>("resolver"), resolver: args.depend::<resolver::Service>("resolver"),
server_keys: args.depend::<server_keys::Service>("server_keys"), server_keys: args.depend::<server_keys::Service>("server_keys"),
moderation: args.depend::<moderation::Service>("moderation"),
}, },
})) }))
} }

View file

@ -22,7 +22,7 @@ use tokio::{
use self::data::{Data, Metadata}; use self::data::{Data, Metadata};
pub use self::thumbnail::Dim; pub use self::thumbnail::Dim;
use crate::{Dep, client, globals, sending}; use crate::{Dep, client, globals, moderation, sending};
#[derive(Debug)] #[derive(Debug)]
pub struct FileMeta { pub struct FileMeta {
@ -42,6 +42,7 @@ struct Services {
client: Dep<client::Service>, client: Dep<client::Service>,
globals: Dep<globals::Service>, globals: Dep<globals::Service>,
sending: Dep<sending::Service>, sending: Dep<sending::Service>,
moderation: Dep<moderation::Service>,
} }
/// generated MXC ID (`media-id`) length /// generated MXC ID (`media-id`) length
@ -64,6 +65,7 @@ impl crate::Service for Service {
client: args.depend::<client::Service>("client"), client: args.depend::<client::Service>("client"),
globals: args.depend::<globals::Service>("globals"), globals: args.depend::<globals::Service>("globals"),
sending: args.depend::<sending::Service>("sending"), sending: args.depend::<sending::Service>("sending"),
moderation: args.depend::<moderation::Service>("moderation"),
}, },
})) }))
} }

View file

@ -423,16 +423,8 @@ pub async fn fetch_remote_content_legacy(
fn check_fetch_authorized(&self, mxc: &Mxc<'_>) -> Result<()> { fn check_fetch_authorized(&self, mxc: &Mxc<'_>) -> Result<()> {
if self if self
.services .services
.server .moderation
.config .is_remote_server_media_downloads_forbidden(mxc.server_name)
.prevent_media_downloads_from
.is_match(mxc.server_name.host())
|| self
.services
.server
.config
.forbidden_remote_server_names
.is_match(mxc.server_name.host())
{ {
// we'll lie to the client and say the blocked server's media was not found and // we'll lie to the client and say the blocked server's media was not found and
// log. the client has no way of telling anyways so this is a security bonus. // log. the client has no way of telling anyways so this is a security bonus.

View file

@ -16,6 +16,7 @@ pub mod federation;
pub mod globals; pub mod globals;
pub mod key_backups; pub mod key_backups;
pub mod media; pub mod media;
pub mod moderation;
pub mod presence; pub mod presence;
pub mod pusher; pub mod pusher;
pub mod resolver; pub mod resolver;

62
src/service/moderation.rs Normal file
View file

@ -0,0 +1,62 @@
use std::sync::Arc;
use conduwuit::{Result, Server, implement};
use ruma::ServerName;
pub struct Service {
services: Services,
}
struct Services {
pub server: Arc<Server>,
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services { server: args.server.clone() },
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
#[implement(Service)]
#[must_use]
pub fn is_remote_server_forbidden(&self, server_name: &ServerName) -> bool {
// Forbidden if NOT (allowed is empty OR allowed contains server OR is self)
// OR forbidden contains server
self.services
.server
.config
.forbidden_remote_server_names
.is_match(server_name.host())
}
#[implement(Service)]
#[must_use]
pub fn is_remote_server_room_directory_forbidden(&self, server_name: &ServerName) -> bool {
// Forbidden if NOT (allowed is empty OR allowed contains server OR is self)
// OR forbidden contains server
self.is_remote_server_forbidden(server_name)
|| self
.services
.server
.config
.forbidden_remote_room_directory_server_names
.is_match(server_name.host())
}
#[implement(Service)]
#[must_use]
pub fn is_remote_server_media_downloads_forbidden(&self, server_name: &ServerName) -> bool {
// Forbidden if NOT (allowed is empty OR allowed contains server OR is self)
// OR forbidden contains server
self.is_remote_server_forbidden(server_name)
|| self
.services
.server
.config
.prevent_media_downloads_from
.is_match(server_name.host())
}

View file

@ -12,7 +12,7 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
account_data, admin, appservice, client, config, emergency, federation, globals, key_backups, account_data, admin, appservice, client, config, emergency, federation, globals, key_backups,
manager::Manager, manager::Manager,
media, presence, pusher, resolver, rooms, sending, server_keys, service, media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
service::{Args, Map, Service}, service::{Args, Map, Service},
sync, transaction_ids, uiaa, updates, users, sync, transaction_ids, uiaa, updates, users,
}; };
@ -39,6 +39,7 @@ pub struct Services {
pub uiaa: Arc<uiaa::Service>, pub uiaa: Arc<uiaa::Service>,
pub updates: Arc<updates::Service>, pub updates: Arc<updates::Service>,
pub users: Arc<users::Service>, pub users: Arc<users::Service>,
pub moderation: Arc<moderation::Service>,
manager: Mutex<Option<Arc<Manager>>>, manager: Mutex<Option<Arc<Manager>>>,
pub(crate) service: Arc<Map>, pub(crate) service: Arc<Map>,
@ -106,6 +107,7 @@ impl Services {
uiaa: build!(uiaa::Service), uiaa: build!(uiaa::Service),
updates: build!(updates::Service), updates: build!(updates::Service),
users: build!(users::Service), users: build!(users::Service),
moderation: build!(moderation::Service),
manager: Mutex::new(None), manager: Mutex::new(None),
service, service,