From a44e4910541f8fe8098964f40e828a6d9033dae1 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Sat, 25 May 2019 17:29:30 -0700 Subject: [PATCH] ircd::m::room: Add server_acl interface and protocol module. --- include/ircd/m/room/room.h | 2 + include/ircd/m/room/server_acl.h | 76 ++++++++ modules/Makefile.am | 2 + modules/console.cc | 29 +++ modules/m_room_server_acl.cc | 321 +++++++++++++++++++++++++++++++ 5 files changed, 430 insertions(+) create mode 100644 include/ircd/m/room/server_acl.h create mode 100644 modules/m_room_server_acl.cc diff --git a/include/ircd/m/room/room.h b/include/ircd/m/room/room.h index 754539f4c..b47534db4 100644 --- a/include/ircd/m/room/room.h +++ b/include/ircd/m/room/room.h @@ -109,6 +109,7 @@ struct ircd::m::room struct power; struct aliases; struct stats; + struct server_acl; using id = m::id::room; using alias = m::id::room_alias; @@ -195,6 +196,7 @@ struct ircd::m::room #include "power.h" #include "aliases.h" #include "stats.h" +#include "server_acl.h" inline ircd::m::room::operator const ircd::m::room::id &() diff --git a/include/ircd/m/room/server_acl.h b/include/ircd/m/room/server_acl.h new file mode 100644 index 000000000..f01ed12e6 --- /dev/null +++ b/include/ircd/m/room/server_acl.h @@ -0,0 +1,76 @@ +// Matrix Construct +// +// Copyright (C) Matrix Construct Developers, Authors & Contributors +// Copyright (C) 2016-2019 Jason Volk +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice is present in all copies. The +// full license for this software is available in the LICENSE file. + +#pragma once +#define HAVE_IRCD_M_ROOM_SERVER_ACL_H + +/// Interface to the server access control lists +/// +/// This interface focuses specifically on the state event type +/// `m.room.server_acl` which allows for access control at server scope. This +/// is necessary because access controls via `m.room.member` operate at the +/// scope of individual `state_key` cells in the room state, thus lacking the +/// ability to assert control over cells which do not yet exist. In other +/// words, for example, this prevent a server from generating new users to +/// evade bans set on other users. +/// +/// Our implementation is keyed on the `origin` field of an event as well as +/// the `origin` field of an m::request depending on the callsite and options. +/// If the `origin` field is not available (it is possibly slated to be phased +/// out) expect this interface to fall back to the `sender` hostpart. +/// +struct ircd::m::room::server_acl +{ + using closure_bool = std::function; + using view_closure = std::function; + + static conf::item enable_write; // request origin / event origin + static conf::item enable_read; // request origin + static conf::item enable_fetch; // request destination / event origin + static conf::item enable_send; // request destination + + m::room room; + json::object content; + event::idx event_idx {0}; + + bool view(const view_closure &) const; + + public: + bool exists() const; + + // Iterate a list property; spec properties are "allow" and "deny". + bool for_each(const string_view &prop, const closure_bool &) const; + size_t count(const string_view &prop) const; + bool has(const string_view &prop) const; + + // Get top-level boolean value returning 1 or 0; -1 for not found. + int getbool(const string_view &prop) const; + + // Test if *exact string* is listed in property list; not expr match. + bool has(const string_view &prop, const string_view &expr) const; + + // Test if string is expression-matched in property list. + bool match(const string_view &prop, const string_view &server) const; + + // Test if server passes or fails the ACL; this factors matching in + // "allow", "deny" and "allow_ip_literals" per the input with any default. + bool operator()(const string_view &server) const; + + server_acl(const m::room &, const event::idx &acl_event_idx = 0); + server_acl(const m::room &, const json::object &content); + server_acl() = default; +}; + +inline +ircd::m::room::server_acl::server_acl(const m::room &room, + const json::object &content) +:room{room} +,content{content} +{} diff --git a/modules/Makefile.am b/modules/Makefile.am index f5380c718..e0355195b 100644 --- a/modules/Makefile.am +++ b/modules/Makefile.am @@ -124,6 +124,7 @@ m_room_canonical_alias_la_SOURCES = m_room_canonical_alias.cc m_room_aliases_la_SOURCES = m_room_aliases.cc m_room_message_la_SOURCES = m_room_message.cc m_room_power_levels_la_SOURCES = m_room_power_levels.cc +m_room_server_acl_la_SOURCES = m_room_server_acl.cc m_module_LTLIBRARIES = \ m_noop.la \ @@ -146,6 +147,7 @@ m_module_LTLIBRARIES = \ m_room_aliases.la \ m_room_message.la \ m_room_power_levels.la \ + m_room_server_acl.la \ ### ############################################################################### diff --git a/modules/console.cc b/modules/console.cc index 1f0e37a75..d40f4a6d4 100644 --- a/modules/console.cc +++ b/modules/console.cc @@ -7931,6 +7931,35 @@ console_cmd__room__alias__cache__del(opt &out, const string_view &line) return true; } +bool +console_cmd__room__server_acl(opt &out, const string_view &line) +{ + const params param{line, " ", + { + "room_id", "server" + }}; + + const auto &room_id + { + m::room_id(param.at("room_id")) + }; + + const m::room::server_acl server_acl + { + room_id + }; + + const bool allowed + { + server_acl(param.at("server")) + }; + + out << (allowed? "ALLOWED"_sv : "DENIED"_sv) + << std::endl; + + return true; +} + bool console_cmd__room__members(opt &out, const string_view &line) { diff --git a/modules/m_room_server_acl.cc b/modules/m_room_server_acl.cc new file mode 100644 index 000000000..f776a57ee --- /dev/null +++ b/modules/m_room_server_acl.cc @@ -0,0 +1,321 @@ +// Matrix Construct +// +// Copyright (C) Matrix Construct Developers, Authors & Contributors +// Copyright (C) 2016-2019 Jason Volk +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice is present in all copies. The +// full license for this software is available in the LICENSE file. + +ircd::mapi::header +IRCD_MODULE +{ + "Matrix Room Server Access Control List" +}; + +// +// Notify hook +// + +namespace ircd::m +{ + static void on_changed_room_server_acl(const m::event &, m::vm::eval &); + extern m::hookfn changed_room_server_acl; +} + +decltype(ircd::m::changed_room_server_acl) +ircd::m::changed_room_server_acl +{ + on_changed_room_server_acl, + { + { "_site", "vm.notify" }, + { "type", "m.room.server_acl" }, + } +}; + +void +ircd::m::on_changed_room_server_acl(const event &event, + vm::eval &) +{ + log::info + { + m::log, "%s changed server access control list in %s [%s]", + json::get<"sender"_>(event), + json::get<"room_id"_>(event), + json::get<"event_id"_>(event) + }; +} + +/////////////////////////////////////////////////////////////////////////////// +// +// ircd/m/room/server_acl.h +// + +/// Coarse control over whether ACL's are considered during the vm::eval of an +/// event, ACL's will be checked against the event's origin during processing +/// of the event, regardless of how the event was received, fetched, etc. The +/// m::vm options may dictate further detailed behavior (hard-fail, soft- +/// fail, auth integration, etc). This is the principal configuration option +/// for effecting the server access control list functionality. Though this +/// conf item is independent of other conf items in this module, setting it +/// to false denudes the core functionality. +/// +/// Setting this to true is *stricter* than the official specification and +/// fixes several vulnerabilities for bypassing ACL's. This also applies to +/// both PDU's and EDU's, and is agnostic to the method or endpoint by which +/// this server obtained the event. This departs from the specification. +/// +/// This option has no effect on the room::server_acl interface itself, it is +/// available for the callsite to check independently before using the iface. +decltype(ircd::m::room::server_acl::enable_write) +IRCD_MODULE_EXPORT_DATA +ircd::m::room::server_acl::enable_write +{ + { "name", "ircd.m.room.server_acl.enable.write" }, + { "default", true }, +}; + +/// Coarse control over whether ACL's apply to endpoints considered +/// non-modifying/passive to the room. If false, ACL's are not checked on +/// endpoints which have no visible effects to the federation; this can +/// increase performance. +/// +/// Setting this option to false relaxes the list of endpoints covered by ACL's +/// and departs from the official specification. +/// +/// This option has no effect on the room::server_acl interface itself, it is +/// available for the callsite to check independently before using the iface. +decltype(ircd::m::room::server_acl::enable_read) +IRCD_MODULE_EXPORT_DATA +ircd::m::room::server_acl::enable_read +{ + { "name", "ircd.m.room.server_acl.enable.read" }, + { "default", false }, +}; + +/// Coarse control over whether ACL's are considered for event fetching. If +/// true, events originating from an ACL'ed server will not be fetched, nor +/// will an ACL'ed server be queried by the fetch unit for any event. Note that +/// this cannot fully apply for newer event_id's without hostparts, but the +/// fetch unit may discard such events for an ACL'ed server after receiving. +/// +/// Setting this to true is *stricter* than the official specification, which +/// is vulnerable to "bouncing" around ACL's. +/// (see: https://github.com/maubot/bouncybot) +/// +/// This option has no effect on the room::server_acl interface itself, it is +/// available for the callsite to check independently before using the iface. +decltype(ircd::m::room::server_acl::enable_fetch) +IRCD_MODULE_EXPORT_DATA +ircd::m::room::server_acl::enable_fetch +{ + { "name", "ircd.m.room.server_acl.enable.fetch" }, + { "default", true }, +}; + +/// Coarse control over whether ACL's are considered when this server +/// transmits transactions to the participants in a room. If true, transactions +/// with all contained PDU's and EDU's will not be sent to ACL'ed servers. +/// +/// Setting this to true is *stricter* than the official specification, which +/// leaks all transmissions to ACL'ed servers. +/// +/// This option has no effect on the room::server_acl interface itself, it is +/// available for the callsite to check independently before using the iface. +decltype(ircd::m::room::server_acl::enable_send) +IRCD_MODULE_EXPORT_DATA +ircd::m::room::server_acl::enable_send +{ + { "name", "ircd.m.room.server_acl.enable.send" }, + { "default", true }, +}; + +// +// server_acl::server_acl +// + +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::server_acl(const m::room &room, + const event::idx &event_idx) +:room +{ + room +} +,event_idx +{ + !event_idx? + room.get(std::nothrow, "m.room.server_acl", ""): + event_idx +} +{ +} + +bool +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::operator()(const string_view &server) +const +{ + // c2s 13.29.1 rules: + + // 1. If there is no m.room.server_acl event in the room state, allow. + if(!exists()) + return true; + + // 2. If the server name is an IP address (v4 or v6) literal, and + // allow_ip_literals is present and false, deny. + if(getbool("allow_ip_literals") == false) + if(rfc3986::valid(std::nothrow, rfc3986::parser::ip_remote, server)) + return false; + + // 3. If the server name matches an entry in the deny list, deny. + if(match("deny", server)) + return false; + + // 4. If the server name matches an entry in the allow list, allow. + if(match("allow", server)) + return true; + + // 5. Otherwise, deny. + return false; +} + +bool +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::match(const string_view &prop, + const string_view &server) +const +{ + return !for_each(prop, [&server] + (const string_view &expression) + { + const gmatch match + { + expression + }; + + // return false to break on match. + return match(server)? false : true; + }); +} + +bool +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::has(const string_view &prop, + const string_view &expr) +const +{ + return !for_each(prop, [&expr] + (const string_view &_expr) + { + // false to break on match + return _expr == expr? false : true; + }); +} + +int +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::getbool(const string_view &prop) +const +{ + int ret(-1); + view([&ret, &prop] + (const json::object &object) + { + const string_view &value + { + object[prop] + }; + + if(value == json::literal_true) + ret = 1; + else if(value == json::literal_false) + ret = 0; + }); + + return ret; +} + +bool +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::has(const string_view &prop) +const +{ + bool ret{false}; + view([&ret, &prop] + (const json::object &object) + { + ret = object.has(prop); + }); + + return ret; +} + +size_t +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::count(const string_view &prop) +const +{ + size_t ret(0); + for_each(prop, [&ret] + (const string_view &) + { + ++ret; + return true; + }); + + return ret; +} + +bool +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::for_each(const string_view &prop, + const closure_bool &closure) +const +{ + bool ret{true}; + view([&ret, &closure, &prop] + (const json::object &content) + { + const json::array &list + { + content[prop] + }; + + if(!list || json::type(list, std::nothrow) != json::ARRAY) + return; + + for(auto it(begin(list)); it != end(list) && ret; ++it) + { + if(json::type(*it, json::strict, std::nothrow) != json::STRING) + continue; + + if(!closure(json::string(*it))) + ret = false; + } + }); + + return ret; +} + +bool +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::exists() +const +{ + return content || event_idx; +} + +bool +IRCD_MODULE_EXPORT +ircd::m::room::server_acl::view(const view_closure &closure) +const +{ + if(content) + { + closure(content); + return true; + } + + return event_idx && m::get(std::nothrow, event_idx, "content", closure); +}