Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Add a cache around server ACL checking #16360

Merged
merged 13 commits into from
Sep 26, 2023
100 changes: 100 additions & 0 deletions rust/src/acl/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! An implementation of Matrix server ACL rules.

use crate::push::utils::{glob_to_regex, GlobMatchType};
use anyhow::Error;
use pyo3::prelude::*;
use regex::Regex;
use std::net::Ipv4Addr;
use std::str::FromStr;

/// Called when registering modules with python.
pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
let child_module = PyModule::new(py, "acl")?;
child_module.add_class::<ServerAclEvaluator>()?;

m.add_submodule(child_module)?;

// We need to manually add the module to sys.modules to make `from
// synapse.synapse_rust import acl` work.
py.import("sys")?
.getattr("modules")?
.set_item("synapse.synapse_rust.acl", child_module)?;

Ok(())
}

#[derive(Debug, Clone)]
#[pyclass(frozen)]
pub struct ServerAclEvaluator {
allow_ip_literals: bool,
allow: Vec<Regex>,
deny: Vec<Regex>,
}

#[pymethods]
impl ServerAclEvaluator {
#[new]
pub fn py_new(
allow_ip_literals: bool,
allow: Vec<String>,
deny: Vec<String>,
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<Self, Error> {
let allow = allow
.iter()
.map(|s| glob_to_regex(s, GlobMatchType::Whole).unwrap())
clokep marked this conversation as resolved.
Show resolved Hide resolved
.collect();
let deny = deny
.iter()
.map(|s| glob_to_regex(s, GlobMatchType::Whole).unwrap())
.collect();

Ok(ServerAclEvaluator {
allow_ip_literals,
allow,
deny,
})
}

pub fn server_matches_acl_event(&self, server_name: &str) -> bool {
// first of all, check if literal IPs are blocked, and if so, whether the
// server name is a literal IP
if !self.allow_ip_literals {
// check for ipv6 literals. These start with '['.
if server_name.starts_with("[") {
return false;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This attempts to stay close to the Python code, maybe it would make more sense to do Ipv6Addr. 🤷


// check for ipv4 literals. We can just lift the routine from std::net.
if let Ok(_) = Ipv4Addr::from_str(server_name) {
return false;
}
}

// next, check the deny list
if self.deny.iter().any(|e| e.is_match(server_name)) {
return false;
}

// then the allow list.
if self.allow.iter().any(|e| e.is_match(server_name)) {
return true;
}

// everything else should be rejected.
false
}
}
2 changes: 2 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use lazy_static::lazy_static;
use pyo3::prelude::*;
use pyo3_log::ResetHandle;

pub mod acl;
pub mod push;

lazy_static! {
Expand Down Expand Up @@ -38,6 +39,7 @@ fn synapse_rust(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(get_rust_file_digest, m)?)?;
m.add_function(wrap_pyfunction!(reset_logging_config, m)?)?;

acl::register_module(py, m)?;
push::register_module(py, m)?;

Ok(())
Expand Down
21 changes: 21 additions & 0 deletions stubs/synapse/synapse_rust/acl.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2023 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List

class ServerAclEvaluator:
def __init__(
self, allow_ip_literals: bool, allow: List[str], deny: List[str]
) -> None: ...
def server_matches_acl_event(self, server_name: str) -> bool: ...
54 changes: 3 additions & 51 deletions synapse/federation/federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,12 @@
List,
Mapping,
Optional,
Pattern,
Sequence,
Tuple,
Union,
)

import attr
from matrix_common.regex import glob_to_regex
from prometheus_client import Counter, Gauge, Histogram

from twisted.internet.abstract import isIPAddress
from twisted.python import failure

from synapse.api.constants import (
Expand Down Expand Up @@ -89,6 +84,7 @@
from synapse.storage.databases.main.lock import Lock
from synapse.storage.databases.main.roommember import extract_heroes_from_room_summary
from synapse.storage.roommember import MemberSummary
from synapse.synapse_rust.acl import ServerAclEvaluator
from synapse.types import JsonDict, StateMap, get_domain_from_id
from synapse.util import unwrapFirstError
from synapse.util.async_helpers import Linearizer, concurrently_execute, gather_results
Expand Down Expand Up @@ -130,50 +126,6 @@
_INBOUND_EVENT_HANDLING_LOCK_NAME = "federation_inbound_pdu"


@attr.s(slots=True, frozen=True, auto_attribs=True)
class ServerAclEvaluator:
allow_ip_literals: bool
allow: Sequence[Pattern[str]]
deny: Sequence[Pattern[str]]

def server_matches_acl_event(self, server_name: str) -> bool:
"""Check if the given server is allowed by the ACL event

Args:
server_name: name of server, without any port part

Returns:
True if this server is allowed by the ACLs
"""

# first of all, check if literal IPs are blocked, and if so, whether the
# server name is a literal IP
if not self.allow_ip_literals:
# check for ipv6 literals. These start with '['.
if server_name[0] == "[":
return False

# check for ipv4 literals. We can just lift the routine from twisted.
if isIPAddress(server_name):
return False

# next, check the deny list
for e in self.deny:
if e.match(server_name):
# logger.info("%s matched deny rule %s", server_name, e)
return False

# then the allow list.
for e in self.allow:
if e.match(server_name):
# logger.info("%s matched allow rule %s", server_name, e)
return True

# everything else should be rejected.
# logger.info("%s fell through", server_name)
return False


class FederationServer(FederationBase):
def __init__(self, hs: "HomeServer"):
super().__init__(hs)
Expand Down Expand Up @@ -1411,15 +1363,15 @@ def server_acl_evaluator_from_event(acl_event: EventBase) -> "ServerAclEvaluator
logger.warning("Ignoring non-list deny ACL %s", deny)
deny = []
else:
deny = [glob_to_regex(s) for s in deny if isinstance(s, str)]
deny = [s for s in deny if isinstance(s, str)]
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved

# then the allow list.
allow = acl_event.content.get("allow", [])
if not isinstance(allow, (list, tuple)):
logger.warning("Ignoring non-list allow ACL %s", allow)
allow = []
else:
allow = [glob_to_regex(s) for s in allow if isinstance(s, str)]
allow = [s for s in allow if isinstance(s, str)]

return ServerAclEvaluator(allow_ip_literals, allow, deny)

Expand Down
Loading