nixos/synapse: implement more advanced integration test (#400845)

This commit is contained in:
Emily
2026-07-11 20:19:09 +00:00
committed by GitHub
7 changed files with 549 additions and 191 deletions
+1
View File
@@ -821,6 +821,7 @@
./services/matrix/mautrix-whatsapp.nix
./services/matrix/mjolnir.nix
./services/matrix/pantalaimon.nix
./services/matrix/rust-federation-tester.nix
./services/matrix/synapse-auto-compressor.nix
./services/matrix/synapse.nix
./services/matrix/tuwunel.nix
@@ -0,0 +1,153 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.rust-federation-tester;
configFile = "/run/rust-federation-tester/config.yaml";
commonServiceConfig = {
DynamicUser = true;
RuntimeDirectory = "rust-federation-tester";
RuntimeDirectoryPreserve = true;
StateDirectory = "rust-federation-tester";
User = "rust-federation-tester";
WorkingDirectory = "%t/rust-federation-tester";
# Hardening
UMask = "0077";
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
LockPersonality = true;
PrivateDevices = true;
PrivateMounts = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
];
};
secretsInjection = utils.genJqSecretsReplacement {
loadCredential = true;
} cfg.settings configFile;
in
{
options.services.rust-federation-tester = {
enable = lib.mkEnableOption "rust-federation-tester";
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = lib.types.json;
options = {
frontend_url = lib.mkOption {
type = lib.types.str;
example = "federation-tester.example.org";
description = ''
URL of the service's frontend.
'';
};
database_url = lib.mkOption {
type = lib.types.str;
default = "sqlite:///var/lib/rust-federation-tester/db?mode=rwc";
example = "postgres://localhost/db?currentSchema=schema";
description = ''
The database to store accounts and statistics.
'';
};
smtp = {
enabled = lib.mkEnableOption "mail delivery for configured alerts";
};
listen_addr = lib.mkOption {
type = lib.types.str;
default = "[::]:8080";
example = "unix:/run/rust-federation-tester/rust-federation-tester.sock";
description = ''
Address the API server should listen on.
'';
};
};
};
description = ''
Settings representing the values in {file}`config.yaml` of the service.
Refer to [`config.yaml.example`] for supported values.
[`config.yaml.example`]: https://github.com/MTRNord/rust-federation-tester/blob/main/config.yaml.example
'';
};
};
config = lib.mkIf cfg.enable {
systemd.sockets.rust-federation-tester = {
description = "Matrix-Federation-Tester in Rust socket";
wantedBy = [ "sockets.target" ];
listenStreams = [ (lib.removePrefix "unix:" cfg.settings.listen_addr) ];
};
systemd.services.rust-federation-tester-setup = {
description = "Matrix-Federation-Tester in Rust";
path = [ pkgs.rust-federation-tester ];
serviceConfig = lib.mkMerge [
commonServiceConfig
{
Type = "oneshot";
RemainAfterExit = true;
LoadCredential = secretsInjection.credentials;
ExecStart = "${pkgs.writeShellScript "rust-federation-tester-setup" ''
${secretsInjection.script}
migration up
''}";
}
];
};
systemd.services.rust-federation-tester = {
description = "Matrix-Federation-Tester in Rust";
wantedBy = [ "multi-user.target" ];
documentation = [ "https://github.com/MTRNord/rust-federation-tester" ];
after = [ "rust-federation-tester-setup.service" ];
requires = [ "rust-federation-tester-setup.service" ];
serviceConfig = lib.mkMerge [
commonServiceConfig
{
ExecSearchPath = lib.makeBinPath [ pkgs.rust-federation-tester ];
ExecStart = "rust-federation-tester";
Restart = "on-failure";
}
];
};
};
}
-1
View File
@@ -1005,7 +1005,6 @@ in
matrix-conduit = runTest ./matrix/conduit.nix;
matrix-continuwuity = runTest ./matrix/continuwuity.nix;
matrix-synapse = runTest ./matrix/synapse.nix;
matrix-synapse-workers = runTest ./matrix/synapse-workers.nix;
matrix-tuwunel = runTest ./matrix/tuwunel.nix;
matter-server = runTest ./matter-server.nix;
matterjs-server = runTest ./matterjs-server.nix;
-53
View File
@@ -1,53 +0,0 @@
{ pkgs, ... }:
{
name = "matrix-synapse-workers";
meta = {
inherit (pkgs.matrix-synapse.meta) maintainers;
};
nodes = {
homeserver =
{
pkgs,
nodes,
...
}:
{
services.postgresql = {
enable = true;
initialScript = pkgs.writeText "synapse-init.sql" ''
CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
'';
};
services.matrix-synapse = {
enable = true;
settings = {
database = {
name = "psycopg2";
args.password = "synapse";
};
enable_registration = true;
enable_registration_without_verification = true;
federation_sender_instances = [ "federation_sender" ];
};
configureRedisLocally = true;
workers = {
"federation_sender" = { };
};
};
};
};
testScript = ''
start_all()
homeserver.wait_for_unit("matrix-synapse.service");
homeserver.wait_for_unit("matrix-synapse-worker-federation_sender.service");
'';
}
+328 -136
View File
@@ -1,101 +1,194 @@
{ pkgs, ... }:
let
ca_key = mailerCerts.ca.key;
ca_pem = mailerCerts.ca.cert;
bundle =
pkgs.runCommand "bundle"
aliceName = "alice";
alicePassword = "alicealice";
aliceEmail = "alice@example.com";
bobName = "bob";
bobPassword = "hunter2";
mkBundle =
domain:
pkgs.runCommand "bundle-${domain}"
{
nativeBuildInputs = [ pkgs.minica ];
}
''
minica -ca-cert ${ca_pem} -ca-key ${ca_key} \
-domains localhost
install -Dm444 -t $out localhost/{key,cert}.pem
-domains ${domain}
install -Dm444 -t $out ${domain}/{key,cert}.pem
'';
mailerCerts = import ../common/acme/server/snakeoil-certs.nix;
mailerDomain = mailerCerts.domain;
registrationSharedSecret = "unsecure123";
testUser = "alice";
testPassword = "alicealice";
testEmail = "alice@example.com";
listeners = [
anonHash = "hunter2hunter2hunter2";
anonHashF = pkgs.writeText "hash" anonHash;
mkHomeserverBase =
domain: nodes:
let
bundle = mkBundle domain;
in
{ lib, ... }:
{
port = 8448;
bind_addresses = [
"127.0.0.1"
"::1"
security.pki.certificateFiles = [
mailerCerts.ca.cert
];
type = "http";
tls = true;
x_forwarded = false;
resources = [
{
names = [
"client"
];
compress = true;
}
{
names = [
"federation"
];
compress = false;
}
];
}
];
networking = {
hostName = lib.head (lib.strings.splitString "." domain);
domain = lib.last (lib.strings.splitString "." domain);
firewall.allowedTCPPorts = [
80
443
8448
];
};
services.nginx = {
enable = true;
virtualHosts."${domain}" = {
sslCertificate = "${bundle}/cert.pem";
sslCertificateKey = "${bundle}/key.pem";
addSSL = true;
locations."/.well-known/matrix/server".extraConfig = ''
default_type application/json;
return 200 '${
builtins.toJSON {
"m.server" = "${domain}:8448";
}
}';
'';
};
};
};
mkSynapseConfig =
domain: primaryIP:
let
bundle = mkBundle domain;
in
{
enable = true;
settings = {
# We're in a test environment with local machines, so
# we actually _want_ to use RFC1918 addresses.
ip_range_blacklist = [ ];
trusted_key_servers = [
{ server_name = "hs1.test"; }
{ server_name = "hs2.test"; }
];
registration_shared_secret = registrationSharedSecret;
public_baseurl = "https://${domain}";
tls_certificate_path = "${bundle}/cert.pem";
tls_private_key_path = "${bundle}/key.pem";
listeners = [
# Using a local listener, otherwise matrix-synapse-register_new_matrix_user will
# fail because it'll try to connect to the first bind address, but via TLS resulting
# in a signature verification failure.
{
port = 8008;
type = "http";
tls = false;
resources = [
{
names = [ "client" ];
compress = true;
}
];
bind_addresses = [ "::1" ];
}
{
port = 8448;
bind_addresses = [
primaryIP
];
type = "http";
tls = true;
x_forwarded = false;
resources = [
{
names = [
"client"
];
compress = true;
}
{
names = [
"federation"
];
compress = false;
}
];
}
];
};
};
in
{
name = "matrix-synapse";
meta = {
inherit (pkgs.matrix-synapse.meta) maintainers;
};
nodes = {
# Since 0.33.0, matrix-synapse doesn't allow underscores in server names
serverpostgres =
synapse-with-workers =
{
lib,
config,
pkgs,
nodes,
config,
...
}:
let
mailserverIP = nodes.mailserver.networking.primaryIPAddress;
in
{
services.matrix-synapse = {
enable = true;
settings = {
inherit listeners;
database = {
name = "psycopg2";
args.password = "synapse";
imports = [
(mkHomeserverBase "hs1.test" nodes)
];
services.matrix-synapse = lib.mkMerge [
(mkSynapseConfig "hs1.test" config.networking.primaryIPAddress)
{
settings = {
server_name = "hs1.test";
email = {
smtp_host = mailerDomain;
smtp_port = 25;
require_transport_security = true;
notif_from = "matrix <matrix@${mailerDomain}>";
app_name = "Matrix";
};
listeners = [
{
path = "/run/matrix-synapse/main_replication.sock";
type = "http";
resources = [
{
names = [ "replication" ];
compress = false;
}
];
}
];
database = {
name = "psycopg2";
args.password = "synapse";
};
federation_sender_instances = [ "federation_sender" ];
};
redis = {
enabled = true;
host = "localhost";
port = config.services.redis.servers.matrix-synapse.port;
configureRedisLocally = true;
workers = {
"federation_sender" = { };
};
tls_certificate_path = "${bundle}/cert.pem";
tls_private_key_path = "${bundle}/key.pem";
registration_shared_secret = registrationSharedSecret;
public_baseurl = "https://example.com";
email = {
smtp_host = mailerDomain;
smtp_port = 25;
require_transport_security = true;
notif_from = "matrix <matrix@${mailerDomain}>";
app_name = "Matrix";
};
};
};
}
];
services.postgresql = {
enable = true;
@@ -113,20 +206,6 @@ in
'';
};
services.redis.servers.matrix-synapse = {
enable = true;
port = 6380;
};
networking.extraHosts = ''
${mailserverIP} ${mailerDomain}
'';
security.pki.certificateFiles = [
mailerCerts.ca.cert
ca_pem
];
environment.systemPackages =
let
sendTestMailStarttls = pkgs.writeScriptBin "send-testmail-starttls" ''
@@ -140,7 +219,7 @@ in
smtp.ehlo()
smtp.starttls(context=ctx)
smtp.ehlo()
smtp.sendmail('matrix@${mailerDomain}', '${testEmail}', 'Subject: Test STARTTLS\n\nTest data.')
smtp.sendmail('matrix@${mailerDomain}', '${aliceEmail}', 'Subject: Test STARTTLS\n\nTest data.')
smtp.quit()
'';
@@ -149,16 +228,14 @@ in
# adding the email through the API is quite complicated as it involves more than one step and some
# client-side calculation
insertEmailForAlice = pkgs.writeText "alice-email.sql" ''
INSERT INTO user_threepids (user_id, medium, address, validated_at, added_at) VALUES ('${testUser}@serverpostgres', 'email', '${testEmail}', '1629149927271', '1629149927270');
INSERT INTO user_threepids (user_id, medium, address, validated_at, added_at) VALUES ('${aliceName}@serverpostgres', 'email', '${aliceEmail}', '1629149927271', '1629149927270');
'';
in
pkgs.writeScriptBin "obtain-token-and-register-email" ''
#!${pkgs.runtimeShell}
set -o errexit
set -o pipefail
set -o nounset
set -euxo pipefail
su postgres -c "psql -d matrix-synapse -f ${insertEmailForAlice}"
curl --fail -XPOST 'https://localhost:8448/_matrix/client/r0/account/password/email/requestToken' -d '{"email":"${testEmail}","client_secret":"foobar","send_attempt":1}' -v
curl --fail -XPOST 'https://hs1.test:8448/_matrix/client/r0/account/password/email/requestToken' -d '{"email":"${aliceEmail}","client_secret":"foobar","send_attempt":1}' -v
'';
in
[
@@ -168,74 +245,189 @@ in
];
};
# test mail delivery
mailserver = args: {
security.pki.certificateFiles = [
mailerCerts.ca.cert
];
mailserver =
{ lib, ... }:
{
security.pki.certificateFiles = [
mailerCerts.ca.cert
];
networking.firewall.enable = false;
networking = {
hostName = lib.head (lib.strings.splitString "." mailerDomain);
domain = lib.last (lib.strings.splitString "." mailerDomain);
firewall.enable = false;
};
services.postfix = {
enable = true;
enableSubmission = true;
services.postfix = {
enable = true;
enableSubmission = true;
# blackhole transport
transport = "example.com discard:silently";
# blackhole transport
transport = "example.com discard:silently";
settings.main = {
myhostname = "${mailerDomain}";
# open relay for subnet
mynetworks_style = "subnet";
debug_peer_level = "10";
smtpd_relay_restrictions = [
"permit_mynetworks"
"reject_unauth_destination"
];
settings.main = {
myhostname = "${mailerDomain}";
# open relay for subnet
mynetworks_style = "subnet";
debug_peer_level = "10";
smtpd_relay_restrictions = [
"permit_mynetworks"
"reject_unauth_destination"
];
# disable obsolete protocols, something old versions of twisted are still using
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtpd_tls_chain_files = [
"${mailerCerts.${mailerDomain}.key}"
"${mailerCerts.${mailerDomain}.cert}"
];
# disable obsolete protocols, something old versions of twisted are still using
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtpd_tls_chain_files = [
"${mailerCerts.${mailerDomain}.key}"
"${mailerCerts.${mailerDomain}.cert}"
];
};
};
};
};
serversqlite = args: {
services.matrix-synapse = {
enable = true;
settings = {
inherit listeners;
database.name = "sqlite3";
tls_certificate_path = "${bundle}/cert.pem";
tls_private_key_path = "${bundle}/key.pem";
synapse-minimal =
{
nodes,
config,
lib,
...
}:
{
imports = [
(mkHomeserverBase "hs2.test" nodes)
];
services.matrix-synapse = lib.mkMerge [
(mkSynapseConfig "hs2.test" config.networking.primaryIPAddress)
{
settings = {
database.name = "sqlite3";
server_name = "hs2.test";
};
}
];
};
client =
{ pkgs, ... }:
{
environment.systemPackages = [
# `pkgs.olm` is cached on c.n.o, even if this test isn't built due
# to it being allow-listed in `pkgs/top-level/release.nix`.
(import pkgs.path {
config = pkgs.config // {
permittedInsecurePackages = [ "olm-3.2.16" ];
};
}).matrix-commander
pkgs.jq
];
security.pki.certificateFiles = [ mailerCerts.ca.cert ];
users.users = {
alice.isNormalUser = true;
bob.isNormalUser = true;
};
networking.useNetworkd = true;
services.rust-federation-tester = {
enable = true;
settings = {
listen_addr = "unix:/run/rust-federation-tester/rust-federation-tester.sock";
frontend_url = "http://localhost:8080";
database_url = "sqlite:///var/lib/rust-federation-tester/db?mode=rwc";
magic_token_secret = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar";
statistics = {
enabled = true;
prometheus_enabled = true;
anonymization_salt._secret = "${anonHashF}";
raw_retention_days = 30;
};
};
};
};
};
};
testScript = ''
import json
from pprint import pprint
start_all()
mailserver.wait_for_unit("postfix.service")
serverpostgres.succeed("send-testmail-starttls")
serverpostgres.wait_for_unit("matrix-synapse.service")
serverpostgres.wait_until_succeeds(
"curl --fail -L --cacert ${ca_pem} https://localhost:8448/"
)
serverpostgres.wait_until_succeeds(
synapse_with_workers.wait_for_unit("multi-user.target")
synapse_minimal.wait_for_unit("multi-user.target")
mailserver.wait_for_unit("multi-user.target")
synapse_with_workers.wait_until_succeeds(
"journalctl -u matrix-synapse.service | grep -q 'Connected to redis'"
)
serverpostgres.require_unit_state("postgresql.target")
serverpostgres.succeed("REQUESTS_CA_BUNDLE=${ca_pem} register_new_matrix_user -u ${testUser} -p ${testPassword} -a -k ${registrationSharedSecret} https://localhost:8448/")
serverpostgres.succeed("obtain-token-and-register-email")
serversqlite.wait_for_unit("matrix-synapse.service")
serversqlite.wait_until_succeeds(
"curl --fail -L --cacert ${ca_pem} https://localhost:8448/"
)
serversqlite.succeed("[ -e /var/lib/matrix-synapse/homeserver.db ]")
'';
synapse_with_workers.wait_for_unit("matrix-synapse-worker-federation_sender.service");
with subtest("register user with email confirmation"):
synapse_with_workers.succeed("send-testmail-starttls")
synapse_with_workers.succeed("REQUESTS_CA_BUNDLE=${mailerCerts.ca.cert} register_new_matrix_user -u ${aliceName} -p ${alicePassword} -a -k ${registrationSharedSecret} https://hs1.test:8448/")
synapse_with_workers.succeed("obtain-token-and-register-email")
with subtest("matrix-synapse-register_new_matrix_user"):
synapse_minimal.succeed("matrix-synapse-register_new_matrix_user -u ${bobName} -p ${bobPassword} --no-admin")
synapse_minimal.succeed("[ -e /var/lib/matrix-synapse/homeserver.db ]")
with subtest("Federation tester"):
expected_anonymization_hashes = {
"hs1.test": "0558582846d6e2d90612900d449871dfcbf9878ba78de4a8469d8aba2d9c037b",
"hs2.test": "d3d88c2033c03b74ec1e086e15c0251514de060de5e51cb381e896f892a5b0cd",
}
# /metrics are cached for up to 30 seconds
for n, domain in enumerate(["hs2.test", "hs1.test"], start=3):
result = json.loads(
client.succeed(
f"curl --fail --unix-socket /run/rust-federation-tester/rust-federation-tester.sock 'http://localhost:8080/api/federation/report?server_name={domain}&stats_opt_in=true'"
)
)
pprint(result)
t.assertTrue(result["FederationOK"])
t.assertTrue(result["ConnectionReports"][f"192.168.1.{n}:8448"]["Checks"]["AllChecksOK"])
t.assertEqual(f"{domain}:8448", result["WellKnownResult"][f"192.168.1.{n}:443"]["m.server"])
client.sleep(30)
# /metrics are cached for up to 30 seconds
for domain in ["hs2.test", "hs1.test"]:
metrics = client.succeed("curl --unix-socket /run/rust-federation-tester/rust-federation-tester.sock http://localhost:8080/metrics --fail")
t.assertRegex(
metrics,
f'federation_request_total\\{{server=\\"{expected_anonymization_hashes[domain]}\\",result=\\"success\\",[^}}]+\\}} 1'
)
def run_as_alice(cmd):
return client.succeed(f"sudo -u alice matrix-commander -c /home/alice/credentials.json -s /home/alice/matrix {cmd}")
def run_as_bob(cmd):
return client.succeed(f"sudo -u bob matrix-commander -c /home/bob/credentials.json -s /home/bob/matrix {cmd}")
with subtest("Login"):
run_as_alice("--login password --homeserver https://hs1.test:8448 --user-login @${aliceName}:hs1.test --password ${alicePassword} --device commander --room-default '#welcome:hs1.test'")
run_as_bob("--login password --homeserver https://hs2.test:8448 --user-login @${bobName}:hs2.test --password ${bobPassword} --device commander --room-default '#welcome:hs1.test'")
with subtest("Create/Invite/Join"):
run_as_alice("--room-create '#welcome:hs1.test'")
run_as_alice("--room-invite '#welcome:hs1.test' -u @${bobName}:hs2.test")
run_as_bob("--listen once --room-invites 'list+join'")
with subtest("Send/receive messages"):
senders = {
"alice": ("hs1", run_as_alice),
"bob": ("hs2", run_as_bob),
}
for name, (hs, fn) in senders.items():
fn(f"-m 'hello, I am {name}'")
for _, run in senders.values():
msg_data = json.loads(run("--listen once --listen-self --output json"))
pprint(msg_data)
t.assertEqual(msg_data["source"]["content"]["body"], f"hello, I am {name}")
t.assertEqual(msg_data["source"]["sender"], f"@{name}:{hs}.test")
'';
}
@@ -157,7 +157,7 @@ python3Packages.buildPythonApplication rec {
'';
passthru = {
tests = { inherit (nixosTests) matrix-synapse matrix-synapse-workers; };
tests = { inherit (nixosTests) matrix-synapse; };
plugins = python3Packages.callPackage ./plugins { };
inherit (python3Packages) python;
updateScript = nix-update-script { };
@@ -0,0 +1,66 @@
{
rustPlatform,
fetchFromGitHub,
lib,
nixosTests,
cacert,
}:
rustPlatform.buildRustPackage (finalAttrs: {
pname = "rust-federation-tester";
version = "0.5.2";
src = fetchFromGitHub {
owner = "MTRNord";
repo = "rust-federation-tester";
rev = "v${finalAttrs.version}";
hash = "sha256-KHRG+5AeK7h7k7LoTtcIjGmPYlVcV2ZwpJN8iDsBfHg=";
};
__structuredAttrs = true;
strictDeps = true;
cargoHash = "sha256-nTcE8eqQO0CFdeH0jjT6m1fd4qhG7e/CDpdZHkBq9f8=";
cargoTestFlags = finalAttrs.cargoBuildFlags;
cargoBuildFlags = [
"-p"
"rust-federation-tester"
"-p"
"migration"
];
nativeCheckInputs = [ cacert ];
checkFlags = map (test: "--skip=${test}") [
# Tests depending on network access
"test_generate_json_report_ipv4_only_server"
"test_generate_json_report_known_bad_servers"
"test_generate_json_report_known_good_servers"
"test_generate_json_report_valid_domain"
"test_generate_json_report_with_port"
"test_lookup_server_well_known_valid"
"test_matrix_fed_srv_resolution_4msc4040"
"test_matrix_srv_resolution_4s"
"test_step2_explicit_port"
"test_step3b_wellknown_explicit_port"
"test_step3c_wellknown_matrix_fed_srv"
"test_step3c_wellknown_matrix_srv"
"test_step3d_wellknown_default_port"
"test_step6_wellknown_fails_default_port"
"test_generate_report"
"test_concurrent_requests"
];
passthru.tests = {
inherit (nixosTests) matrix-synapse;
};
meta = {
description = "Matrix-Federation-Tester in Rust";
homepage = "https://connectivity-tester.mtrnord.blog/";
license = lib.licenses.agpl3Only;
maintainers = with lib.maintainers; [ ma27 ];
platforms = lib.platforms.linux;
mainProgram = "rust-federation-tester";
};
})