-
-
Notifications
You must be signed in to change notification settings - Fork 13.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
4 changed files
with
314 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
''; | ||
}) |