mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 11:17:51 +00:00
Pass the outbound federation config to Rust
Extend the Rust view of the homeserver config with what the federation request layer and agent need: - `config.federation`: timeouts, retry counts and delays, destination backoff, the domain whitelist and the metrics domains. - `config.tls`: certificate verification, the per-host verification whitelist, the minimum TLS version, and the custom CA list. Python now keeps the PEM text of that list too, so Rust never reads the files. - `config.server`: the federation IP range block and allow lists (taken from `netaddr.IPSet`s), the user agent suffix and the proxy config. The verification whitelist holds Python regexes made by `glob_to_regex`. They translate to Rust's syntax with two changes: `\Z` becomes `\z` and `re.IGNORECASE` becomes `(?i)`. The changelog entry needs renaming to the PR number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5b7Ruy4cgt27Mjq6Ptxbb
This commit is contained in:
1 parent
489c09175d
commit
9d4d050a0b
6 files changed
+177
-3
No files matched your search
Generated
+1
@@ -1834,6 +1834,7 @@ dependencies = [
|
||||
"http",
|
||||
"http-body-util",
|
||||
"icu_segmenter",
|
||||
"ipnet",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Add foundations for sending federation requests from Rust.
|
||||
@@ -105,6 +105,8 @@ lru = "0.16.4"
|
||||
# Signing outbound federation requests. 2.x rather than 3.x, which needs a
|
||||
# newer rustc than our MSRV.
|
||||
ed25519-dalek = "2.2.0"
|
||||
# The IP range lists in config.
|
||||
ipnet = "2.11.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.44.2", features = ["macros", "test-util"] }
|
||||
|
||||
+50
-1
@@ -13,7 +13,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
@@ -35,6 +35,8 @@ pub struct SynapseHomeServerConfig {
|
||||
pub server: ServerConfig,
|
||||
pub experimental: ExperimentalConfig,
|
||||
pub retention: RetentionConfig,
|
||||
pub federation: FederationConfig,
|
||||
pub tls: TlsConfig,
|
||||
}
|
||||
|
||||
#[derive(FromPyObject, Clone)]
|
||||
@@ -51,6 +53,53 @@ pub struct ServerConfig {
|
||||
pub msc4140_enabled: bool,
|
||||
pub public_baseurl: String,
|
||||
pub include_profile_updates_in_sync: bool,
|
||||
/// IP ranges outbound federation may not connect to.
|
||||
pub federation_ip_range_blocklist: types::IpRangeSet,
|
||||
/// Exceptions to `federation_ip_range_blocklist`.
|
||||
pub federation_ip_range_allowlist: Option<types::IpRangeSet>,
|
||||
pub user_agent_suffix: Option<String>,
|
||||
pub proxy_config: types::ProxyConfig,
|
||||
}
|
||||
|
||||
/// `config.federation`: outbound federation request settings.
|
||||
#[derive(FromPyObject, Clone)]
|
||||
pub struct FederationConfig {
|
||||
/// If set, the only server names we federate with. Python keeps this as
|
||||
/// a dict of name to `True`.
|
||||
pub federation_domain_whitelist: Option<HashMap<String, bool>>,
|
||||
/// The destinations to label per-destination metrics with.
|
||||
pub federation_metrics_domains: HashSet<String>,
|
||||
pub client_timeout_ms: u64,
|
||||
pub max_long_retry_delay_ms: u64,
|
||||
pub max_short_retry_delay_ms: u64,
|
||||
pub max_long_retries: u32,
|
||||
pub max_short_retries: u32,
|
||||
pub destination_min_retry_interval_ms: u64,
|
||||
pub destination_retry_multiplier: f64,
|
||||
pub destination_max_retry_interval_ms: u64,
|
||||
}
|
||||
|
||||
impl FederationConfig {
|
||||
/// Whether the federation whitelist (if any) lets us talk to `domain`.
|
||||
/// Mirrors `is_domain_allowed_according_to_federation_whitelist`.
|
||||
pub fn is_domain_allowed(&self, domain: &str) -> bool {
|
||||
self.federation_domain_whitelist
|
||||
.as_ref()
|
||||
.is_none_or(|whitelist| whitelist.contains_key(domain))
|
||||
}
|
||||
}
|
||||
|
||||
/// `config.tls`: the TLS policy for outbound federation.
|
||||
#[derive(FromPyObject, Clone)]
|
||||
pub struct TlsConfig {
|
||||
pub federation_verify_certificates: bool,
|
||||
/// One of `1`, `1.1`, `1.2` or `1.3`.
|
||||
pub federation_client_minimum_tls_version: String,
|
||||
/// Hosts whose certificates are not verified, as anchored regexes.
|
||||
pub federation_certificate_verification_whitelist: Vec<types::PythonRegex>,
|
||||
/// The PEM text of `federation_custom_ca_list`, which replaces the
|
||||
/// platform trust roots when set.
|
||||
pub federation_custom_ca_pems: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// `config.retention`: the message retention policy settings.
|
||||
|
||||
+118
-2
@@ -13,9 +13,14 @@
|
||||
*
|
||||
*/
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::{net::IpAddr, str::FromStr};
|
||||
|
||||
use pyo3::{exceptions::PyAssertionError, prelude::*};
|
||||
use ipnet::IpNet;
|
||||
use pyo3::{
|
||||
exceptions::{PyAssertionError, PyValueError},
|
||||
intern,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
/// The presets available when creating a Matrix room according to the Matrix spec:
|
||||
///
|
||||
@@ -58,3 +63,114 @@ impl<'a, 'py> FromPyObject<'a, 'py> for RoomCreationPreset {
|
||||
value.extract::<&str>()?.parse()
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of IP ranges: a `netaddr.IPSet` in Python.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct IpRangeSet(Vec<IpNet>);
|
||||
|
||||
impl IpRangeSet {
|
||||
pub fn new(ranges: Vec<IpNet>) -> Self {
|
||||
IpRangeSet(ranges)
|
||||
}
|
||||
|
||||
pub fn contains(&self, ip: &IpAddr) -> bool {
|
||||
self.0.iter().any(|net| net.contains(ip))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py> FromPyObject<'a, 'py> for IpRangeSet {
|
||||
type Error = PyErr;
|
||||
|
||||
fn extract(value: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
|
||||
let py = value.py();
|
||||
let mut ranges = Vec::new();
|
||||
for cidr in value.call_method0(intern!(py, "iter_cidrs"))?.try_iter()? {
|
||||
let cidr = cidr?.str()?;
|
||||
let cidr = cidr.to_str()?;
|
||||
ranges.push(
|
||||
cidr.parse()
|
||||
.map_err(|e| PyValueError::new_err(format!("bad IP range {cidr}: {e}")))?,
|
||||
);
|
||||
}
|
||||
Ok(IpRangeSet(ranges))
|
||||
}
|
||||
}
|
||||
|
||||
/// A compiled Python regex (`re.Pattern`), translated to a Rust [`regex::Regex`].
|
||||
///
|
||||
/// Only handles what config produces with `glob_to_regex`: `\Z` becomes `\z`
|
||||
/// and `re.IGNORECASE` becomes `(?i)`. Anything else that Rust's regex syntax
|
||||
/// rejects is a config error.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PythonRegex(regex::Regex);
|
||||
|
||||
impl PythonRegex {
|
||||
pub fn from_python_pattern(pattern: &str, ignore_case: bool) -> Result<Self, regex::Error> {
|
||||
let pattern = pattern.replace(r"\Z", r"\z");
|
||||
let pattern = if ignore_case {
|
||||
format!("(?i){pattern}")
|
||||
} else {
|
||||
pattern
|
||||
};
|
||||
Ok(PythonRegex(regex::Regex::new(&pattern)?))
|
||||
}
|
||||
|
||||
/// Whether the pattern matches at the start of `s`, as `re.match` does.
|
||||
pub fn is_match_at_start(&self, s: &str) -> bool {
|
||||
self.0.find(s).is_some_and(|m| m.start() == 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py> FromPyObject<'a, 'py> for PythonRegex {
|
||||
type Error = PyErr;
|
||||
|
||||
fn extract(value: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
|
||||
let py = value.py();
|
||||
let pattern: String = value.getattr(intern!(py, "pattern"))?.extract()?;
|
||||
let flags: u32 = value.getattr(intern!(py, "flags"))?.extract()?;
|
||||
// `re.IGNORECASE`
|
||||
let ignore_case = flags & 2 != 0;
|
||||
|
||||
PythonRegex::from_python_pattern(&pattern, ignore_case)
|
||||
.map_err(|e| PyValueError::new_err(format!("cannot use regex {pattern:?}: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// `config.server.proxy_config`: the forward proxy for outbound HTTP.
|
||||
#[derive(FromPyObject, Clone, Debug, Default)]
|
||||
pub struct ProxyConfig {
|
||||
pub http_proxy: Option<String>,
|
||||
pub https_proxy: Option<String>,
|
||||
pub no_proxy_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn glob_regexes_translate() {
|
||||
// What `glob_to_regex("*.example.com")` compiles to.
|
||||
let regex = PythonRegex::from_python_pattern(r"\A(.{0,}\.example\.com)\Z", true).unwrap();
|
||||
assert!(regex.is_match_at_start("foo.example.com"));
|
||||
assert!(regex.is_match_at_start("FOO.Example.com"));
|
||||
assert!(!regex.is_match_at_start("example.com"));
|
||||
assert!(!regex.is_match_at_start("foo.example.com.evil"));
|
||||
assert!(!regex.is_match_at_start("foo.example.com\n"));
|
||||
|
||||
// `re.escape` escapes `-`, which Rust's regex must accept too.
|
||||
let regex = PythonRegex::from_python_pattern(r"\A(my\-host)\Z", true).unwrap();
|
||||
assert!(regex.is_match_at_start("my-host"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_range_set() {
|
||||
let set = IpRangeSet::new(vec![
|
||||
"10.0.0.0/8".parse().unwrap(),
|
||||
"::1/128".parse().unwrap(),
|
||||
]);
|
||||
assert!(set.contains(&"10.1.2.3".parse().unwrap()));
|
||||
assert!(set.contains(&"::1".parse().unwrap()));
|
||||
assert!(!set.contains(&"11.0.0.1".parse().unwrap()));
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,8 @@ class TlsConfig(Config):
|
||||
|
||||
# Read in and parse custom CA certificates
|
||||
self.federation_ca_trust_root = None
|
||||
# The PEM text of the same certificates, for the Rust federation agent.
|
||||
self.federation_custom_ca_pems: list[str] | None = None
|
||||
if custom_ca_list is not None:
|
||||
if len(custom_ca_list) == 0:
|
||||
# A trustroot cannot be generated without any CA certificates.
|
||||
@@ -112,6 +114,7 @@ class TlsConfig(Config):
|
||||
)
|
||||
|
||||
certs = []
|
||||
pems = []
|
||||
for ca_file in custom_ca_list:
|
||||
logger.debug("Reading custom CA certificate file: %s", ca_file)
|
||||
content = self.read_file(ca_file, "federation_custom_ca_list")
|
||||
@@ -120,12 +123,14 @@ class TlsConfig(Config):
|
||||
try:
|
||||
cert_base = Certificate.loadPEM(content.encode("ascii"))
|
||||
certs.append(cert_base)
|
||||
pems.append(content)
|
||||
except Exception as e:
|
||||
raise ConfigError(
|
||||
"Error parsing custom CA certificate file %s: %s" % (ca_file, e)
|
||||
)
|
||||
|
||||
self.federation_ca_trust_root = trustRootFromCertificates(certs)
|
||||
self.federation_custom_ca_pems = pems
|
||||
|
||||
# This config option applies to non-federation HTTP clients
|
||||
# (e.g. for talking to recaptcha, identity servers, and such)
|
||||
|
||||
Reference in new issue
Block a user