Skip to content

Commit

Permalink
nixos/mautrix-discord: init
Browse files Browse the repository at this point in the history
mautrix-discord has already been packaged, but as of yet no NixOS module has been provided. This is the same module that I've revived from #200462, but now with expanded hardening.
  • Loading branch information
robintown committed Jan 28, 2024
1 parent d3f179b commit 2a21f78
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 0 deletions.
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2405.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ In addition to numerous new and upgraded packages, this release has the followin

- [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).

- [mautrix-discord](https://go.mau.fi/mautrix-discord/), a Matrix to Discord hybrid puppeting/relaybot bridge.

- systemd's gateway, upload, and remote services, which provides ways of sending journals across the network. Enable using [services.journald.gateway](#opt-services.journald.gateway.enable), [services.journald.upload](#opt-services.journald.upload.enable), and [services.journald.remote](#opt-services.journald.remote.enable).

- [GNS3](https://www.gns3.com/), a network software emulator. Available as [services.gns3-server](#opt-services.gns3-server.enable).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@
./services/matrix/dendrite.nix
./services/matrix/hebbot.nix
./services/matrix/maubot.nix
./services/matrix/mautrix-discord.nix
./services/matrix/mautrix-facebook.nix
./services/matrix/mautrix-telegram.nix
./services/matrix/mautrix-whatsapp.nix
Expand Down
157 changes: 157 additions & 0 deletions nixos/modules/services/matrix/mautrix-discord.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
{ config, pkgs, lib, ... }:

with lib;

let
cfg = config.services.mautrix-discord;
dataDir = "/var/lib/mautrix-discord";
registrationFile = "${dataDir}/discord-registration.yaml";
settingsFormat = pkgs.formats.yaml { };
settingsFile = settingsFormat.generate "mautrix-discord-config.yaml" cfg.settings;
runtimeSettingsFile = "${dataDir}/config.yaml";
in {
options = {
services.mautrix-discord = {
enable = mkEnableOption (mdDoc "Matrix to Discord hybrid puppeting/relaybot bridge");

package = mkOption {
type = types.package;
default = pkgs.mautrix-discord;
defaultText = literalExpression "pkgs.mautrix-discord";
description = mdDoc ''
The mautrix-discord package to use.
'';
};

settings = mkOption rec {
apply = recursiveUpdate default;
inherit (settingsFormat) type;
default = {
homeserver = {
software = "standard";
};

appservice = rec {
database = {
type = "sqlite3";
uri = "file:${dataDir}/mautrix-discord.db";
};
port = 8080;
address = "http://localhost:${toString port}";
};

bridge = {
permissions."*" = "relay";
double_puppet_server_map = {};
login_shared_secret_map = {};
};

logging = {
directory = "";
file_name_format = ""; # Disable file logging
file_date_format = "2006-01-02";
file_mode = 384;
timestamp_format = "Jan _2, 2006 15:04:05";
print_level = "warn";
print_json = false;
file_json = false;
};
};
description = mdDoc ''
Bridge configuration as a Nix attribute set.
Configuration options should match those described in
[example-config.yaml](https://github.com/mautrix/discord/blob/main/example-config.yaml).
'';
};

serviceDependencies = mkOption {
type = with types; listOf str;
default = optional config.services.matrix-synapse.enable "matrix-synapse.service";
defaultText = literalExpression ''
optional config.services.matrix-synapse.enable "matrix-synapse.service"
'';
description = mdDoc ''
List of Systemd services to require and wait for when starting the application service.
'';
};
};
};

config = mkIf cfg.enable {
systemd.services.mautrix-discord = mkIf cfg.enable {
description = "Matrix to Discord hybrid puppeting/relaybot bridge";

wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;

preStart = ''
# Generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${cfg.package}/bin/mautrix-discord \
--config '${settingsFile}' \
--registration '${registrationFile}' \
--generate-registration
fi
old_umask=$(umask)
umask 0177
# Extract the AS and HS tokens from the registration and add them to the settings file
${pkgs.yq}/bin/yq -y ".appservice.as_token = $(${pkgs.yq}/bin/yq .as_token ${registrationFile}) | .appservice.hs_token = $(${pkgs.yq}/bin/yq .hs_token ${registrationFile})" ${settingsFile} > ${runtimeSettingsFile}
umask $old_umask
'';

serviceConfig =
let
needsPrivileges = cfg.settings.appservice.port < 1024;
capabilities = [ (if needsPrivileges then "CAP_NET_BIND_SERVICE" else "") ];
in {
Type = "simple";
Restart = "always";

DynamicUser = true;
WorkingDirectory = cfg.package;
StateDirectory = baseNameOf dataDir;
UMask = "0007";

ExecStart = ''
${cfg.package}/bin/mautrix-discord \
--config ${runtimeSettingsFile} \
--no-update
'';

TemporaryFileSystem = [ "/" ];
BindPaths = [ dataDir ];
BindReadOnlyPaths = [ builtins.storeDir ];
AmbientCapabilities = capabilities;
CapabilityBoundingSet = capabilities;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = !needsPrivileges;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
ProtectProc = "invisible";
ProcSubset = "pid";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" ];
};
};
};

meta.maintainers = with maintainers; [ robin ];
}
154 changes: 154 additions & 0 deletions nixos/tests/matrix/mautrix-discord.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import ../make-test-python.nix ({ pkgs, ... }:
let
homeserverUrl = "http://homeserver:8008";
in
{
name = "mautrix-discord";
meta.maintainers = pkgs.mautrix-discord.meta.maintainers;

nodes = {
homeserver = { pkgs, ... }: {
# We'll switch to this once the registration is copied into place
specialisation.running.configuration = {
services.matrix-synapse = {
enable = true;
settings = {
database.name = "sqlite3";
app_service_config_files = [ "/discord-registration.yaml" ];

enable_registration = true;

# don't use this in production, always use some form of verification
enable_registration_without_verification = true;

listeners = [ {
# The default but tls=false
bind_addresses = [
"0.0.0.0"
];
port = 8008;
resources = [ {
"compress" = true;
"names" = [ "client" ];
} {
"compress" = false;
"names" = [ "federation" ];
} ];
tls = false;
type = "http";
} ];
};
};

networking.firewall.allowedTCPPorts = [ 8008 ];
};
};

bridge = { pkgs, ... }: {
services.mautrix-discord = {
enable = true;

settings = {
homeserver = {
address = homeserverUrl;
domain = "homeserver";
};

appservice = {
address = "http://bridge:8009";
port = 8009;
};

bridge.permissions."@alice:homeserver" = "user";
};
};

networking.firewall.allowedTCPPorts = [ 8009 ];
};

client = { pkgs, ... }: {
environment.systemPackages = [
(pkgs.writers.writePython3Bin "do_test"
{
libraries = [ pkgs.python3Packages.matrix-nio ];
flakeIgnore = [
# We don't live in the dark ages anymore.
# Languages like Python that are whitespace heavy will overrun
# 79 characters..
"E501"
];
} ''
import sys
import functools
import asyncio
from nio import AsyncClient, RoomMessageNotice, RoomCreateResponse, RoomInviteResponse
async def message_callback(matrix: AsyncClient, msg: str, _r, e):
print("Received matrix text message: ", e)
assert msg in e.body
exit(0) # Success!
async def run(homeserver: str):
matrix = AsyncClient(homeserver)
response = await matrix.register("alice", "foobar")
print("Matrix register response: ", response)
# Open a DM with the bridge bot
response = await matrix.room_create()
print("Matrix create room response:", response)
assert isinstance(response, RoomCreateResponse)
room_id = response.room_id
response = await matrix.room_invite(room_id, "@discordbot:homeserver")
assert isinstance(response, RoomInviteResponse)
callback = functools.partial(
message_callback, matrix, "Hello, I'm a Discord bridge bot."
)
matrix.add_event_callback(callback, RoomMessageNotice)
print("Waiting for matrix message...")
await matrix.sync_forever(timeout=30000)
if __name__ == "__main__":
asyncio.run(run(sys.argv[1]))
''
)
];
};
};

testScript = ''
import pathlib
import os
start_all()
with subtest("start the bridge"):
bridge.wait_for_unit("mautrix-discord.service")
with subtest("copy the registration file"):
bridge.copy_from_vm("/var/lib/mautrix-discord/discord-registration.yaml")
homeserver.copy_from_host(
str(pathlib.Path(os.environ.get("out", os.getcwd())) / "discord-registration.yaml"), "/"
)
homeserver.succeed("chmod 444 /discord-registration.yaml")
with subtest("start the homeserver"):
homeserver.succeed(
"/run/current-system/specialisation/running/bin/switch-to-configuration test >&2"
)
homeserver.wait_for_unit("matrix-synapse.service")
homeserver.wait_for_open_port(8008)
# Bridge only opens the port after it contacts the homeserver
bridge.wait_for_open_port(8009)
with subtest("ensure messages can be exchanged"):
client.succeed("do_test ${homeserverUrl} >&2")
'';
})

0 comments on commit 2a21f78

Please sign in to comment.