diff --git a/hosts/fw/configuration.nix b/hosts/fw/configuration.nix index d9b8591..8caf67a 100644 --- a/hosts/fw/configuration.nix +++ b/hosts/fw/configuration.nix @@ -43,6 +43,7 @@ # web ./modules/web + ./modules/coturn.nix # git ./modules/forgejo.nix diff --git a/hosts/fw/modules/coturn.nix b/hosts/fw/modules/coturn.nix new file mode 100644 index 0000000..b660d6b --- /dev/null +++ b/hosts/fw/modules/coturn.nix @@ -0,0 +1,32 @@ +{ config, ... }: +let + domain = "turn.cloonar.com"; +in +{ + security.acme.certs."${domain}" = { + group = "turnserver"; + postRun = "systemctl try-restart coturn.service"; + }; + + sops.secrets.coturn-static-secret = { + owner = "turnserver"; + }; + + services.coturn = { + enable = true; + realm = domain; + use-auth-secret = true; + static-auth-secret-file = config.sops.secrets.coturn-static-secret.path; + cert = "${config.security.acme.certs.${domain}.directory}/fullchain.pem"; + pkey = "${config.security.acme.certs.${domain}.directory}/key.pem"; + min-port = 49152; + max-port = 49999; + no-tcp-relay = true; + no-cli = true; + }; + + systemd.services.coturn = { + after = [ "acme-${domain}.service" ]; + wants = [ "acme-${domain}.service" ]; + }; +} diff --git a/hosts/fw/modules/firewall.nix b/hosts/fw/modules/firewall.nix index c876e13..61f9205 100644 --- a/hosts/fw/modules/firewall.nix +++ b/hosts/fw/modules/firewall.nix @@ -74,6 +74,9 @@ # Allow returning traffic from wan and drop everthing else iifname "wan" ct state { established, related } accept comment "Allow established traffic" iifname "wan" icmp type { echo-request, destination-unreachable, time-exceeded } counter accept comment "Allow select ICMP" + iifname "wan" udp dport { 3478, 5349 } counter accept comment "TURN/STUN UDP" + iifname "wan" tcp dport { 3478, 5349 } counter accept comment "TURN/STUN TCP + TURNS/TLS" + iifname "wan" udp dport { 49152-49999 } counter accept comment "TURN relay UDP range" iifname "wan" counter drop comment "Drop all other unsolicited traffic from wan" limit rate 60/minute burst 100 packets log prefix "Input - Drop: " comment "Log any unmatched traffic" diff --git a/hosts/fw/modules/web/default.nix b/hosts/fw/modules/web/default.nix index edd85ee..73c72a3 100644 --- a/hosts/fw/modules/web/default.nix +++ b/hosts/fw/modules/web/default.nix @@ -65,6 +65,7 @@ in ./phpldapadmin.nix ./proxies.nix ./matrix.nix + ../../utils/modules/mautrix-mattermost.nix ./n8n.nix # ./piped.nix # Replaced by Invidious ./invidious.nix @@ -96,6 +97,7 @@ in "/var/lib/mautrix-whatsapp" "/var/lib/mautrix-signal" "/var/lib/mautrix-discord" + "/var/lib/mautrix-mattermost" "/var/log" "/var/lib/systemd/coredump" "/var/backup" diff --git a/hosts/fw/modules/web/matrix.nix b/hosts/fw/modules/web/matrix.nix index 475bab1..d9a8ece 100644 --- a/hosts/fw/modules/web/matrix.nix +++ b/hosts/fw/modules/web/matrix.nix @@ -28,6 +28,8 @@ let endpoint: "http://127.0.0.1:8081" secret_path: ${config.sops.secrets.mas-matrix-secret-synapse.path} ''; + + synapseVoipConfig = "/run/matrix-synapse/voip-config.yaml"; in { # Secrets for MAS sops.secrets.mas-encryption-key = { owner = "mas"; }; @@ -40,9 +42,16 @@ in { key = "mas-matrix-secret"; }; + # TURN shared secret (for Synapse VoIP config) + sops.secrets.coturn-static-secret = { + sopsFile = ./secrets.yaml; + owner = "matrix-synapse"; + }; + sops.secrets.mautrix-whatsapp-env = { }; sops.secrets.mautrix-signal-env = { }; sops.secrets.mautrix-discord-env = { }; + sops.secrets.mautrix-mattermost-env = { }; # MAS system user users.users.mas = { @@ -176,7 +185,7 @@ in { # Synapse homeserver services.matrix-synapse = { enable = true; - extraConfigFiles = [ "${synapseMasConfig}" ]; + extraConfigFiles = [ "${synapseMasConfig}" synapseVoipConfig ]; settings = { server_name = "cloonar.com"; public_baseurl = baseUrl; @@ -207,6 +216,12 @@ in { }; allow_guest_access = false; + + # MSC4190: device management for appservices (required for encrypted bridges with MAS) + experimental_features = { + msc4190_enabled = true; + msc3202_device_masquerading = true; + }; }; }; @@ -217,6 +232,19 @@ in { systemd.services.matrix-synapse.after = [ "matrix-authentication-service.service" ]; systemd.services.matrix-synapse.wants = [ "matrix-authentication-service.service" ]; systemd.services.matrix-synapse.serviceConfig.PrivateUsers = lib.mkForce false; + systemd.services.matrix-synapse.preStart = lib.mkAfter '' + install -m 0600 -o matrix-synapse /dev/null ${synapseVoipConfig} + TURN_SECRET=$(cat ${config.sops.secrets.coturn-static-secret.path}) + cat > ${synapseVoipConfig} < '${settingsFile}.tmp' + mv '${settingsFile}.tmp' '${settingsFile}' + fi + # make sure --generate-registration does not affect config.yaml + cp '${settingsFile}' '${settingsFile}.tmp' + echo "Generating registration file" + mautrix-mattermost \ + --generate-registration \ + --config='${settingsFile}.tmp' \ + --registration='${registrationFile}' + rm '${settingsFile}.tmp' + # no tokens configured, and new were just generated by generate registration for first time + if [[ $config_has_tokens == "false" && $registration_already_exists == "false" ]]; then + echo "Copying newly generated as_token, hs_token from registration into configuration" + yq -sY '.[0].appservice.as_token = .[1].as_token + | .[0].appservice.hs_token = .[1].hs_token + | .[0]' '${settingsFile}' '${registrationFile}' \ + > '${settingsFile}.tmp' + mv '${settingsFile}.tmp' '${settingsFile}' + fi + # Make sure correct tokens are in the registration file + if [[ $config_has_tokens == "true" || $registration_already_exists == "true" ]]; then + echo "Copying as_token, hs_token from configuration to the registration file" + yq -sY '.[1].as_token = .[0].appservice.as_token + | .[1].hs_token = .[0].appservice.hs_token + | .[1]' '${settingsFile}' '${registrationFile}' \ + > '${registrationFile}.tmp' + mv '${registrationFile}.tmp' '${registrationFile}' + fi + umask $old_umask + chown :mautrix-mattermost-registration '${registrationFile}' + chmod 640 '${registrationFile}' + ''; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + UMask = 27; + + User = "mautrix-mattermost"; + Group = "mautrix-mattermost"; + + SystemCallFilter = [ "@system-service" ]; + + ProtectSystem = "strict"; + ProtectHome = true; + + ReadWritePaths = [ dataDir ]; + StateDirectory = "mautrix-mattermost"; + EnvironmentFile = cfg.environmentFile; + }; + + restartTriggers = [ settingsFileUnformatted ]; + }; + + mautrix-mattermost = { + description = "Mautrix-Mattermost, a Matrix-Mattermost puppeting/relaybot bridge"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ] ++ cfg.serviceDependencies; + after = [ "network-online.target" ] ++ cfg.serviceDependencies; + + serviceConfig = { + Type = "simple"; + User = "mautrix-mattermost"; + Group = "mautrix-mattermost"; + PrivateUsers = true; + Restart = "on-failure"; + RestartSec = 30; + WorkingDirectory = dataDir; + ExecStart = '' + ${lib.getExe cfg.package} \ + --config='${settingsFile}' + ''; + EnvironmentFile = cfg.environmentFile; + + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + PrivateDevices = true; + PrivateTmp = true; + RestrictSUIDSGID = true; + RestrictRealtime = true; + LockPersonality = true; + ProtectKernelLogs = true; + ProtectHostname = true; + ProtectClock = true; + + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = "@system-service"; + ReadWritePaths = [ cfg.dataDir ]; + }; + + restartTriggers = [ settingsFileUnformatted ]; + }; + }; + }; +} diff --git a/utils/overlays/packages.nix b/utils/overlays/packages.nix index 25a59b8..719929a 100644 --- a/utils/overlays/packages.nix +++ b/utils/overlays/packages.nix @@ -5,6 +5,7 @@ self: super: { openaudible = (super.callPackage ../pkgs/openaudible.nix { }); openmanus = (super.callPackage ../pkgs/openmanus.nix { }); ai-mailer = self.callPackage ../pkgs/ai-mailer.nix { }; + mautrix-mattermost = self.callPackage ../pkgs/mautrix-mattermost { }; claude-code = self.callPackage ../pkgs/claude-code { claude-code = super.claude-code; }; # Python packages diff --git a/utils/pkgs/mautrix-mattermost/default.nix b/utils/pkgs/mautrix-mattermost/default.nix new file mode 100644 index 0000000..ee7c3ad --- /dev/null +++ b/utils/pkgs/mautrix-mattermost/default.nix @@ -0,0 +1,30 @@ +{ lib, buildGo126Module, fetchFromGitHub, olm }: + +buildGo126Module rec { + pname = "mautrix-mattermost"; + version = "0-unstable-2026-03-01"; + + src = fetchFromGitHub { + owner = "bostrot"; + repo = "mautrix-mattermost"; + rev = "f7996f0e4acd68b24f2a1a88961712682b6017a5"; + hash = "sha256-J8CJd0tsTLHJRyRVP8fVnzsCS5VV9iXr1epA6P2Qec4="; + }; + + vendorHash = "sha256-r4mmSEzx/oSv0OutLuXe7LwODUJaSwuQ/CNFZNqw5+c="; + + buildInputs = [ olm ]; + + # Disable CGO except for olm + env.CGO_ENABLED = 1; + + doCheck = false; + + meta = with lib; { + description = "A Matrix-Mattermost puppeting bridge based on mautrix-go"; + homepage = "https://github.com/bostrot/mautrix-mattermost"; + license = licenses.agpl3Plus; + maintainers = [ ]; + mainProgram = "mautrix-mattermost"; + }; +} diff --git a/utils/pkgs/mautrix-mattermost/update.sh b/utils/pkgs/mautrix-mattermost/update.sh new file mode 100755 index 0000000..2cb220e --- /dev/null +++ b/utils/pkgs/mautrix-mattermost/update.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i bash -p nix-prefetch-github jq cacert + +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +repo_root="$(cd ../../.. && pwd)" + +owner="bostrot" +repo="mautrix-mattermost" + +# Get latest commit from GitHub +echo "Fetching latest commit from $owner/$repo..." +commit_info=$(curl -s "https://api.github.com/repos/$owner/$repo/commits?per_page=1") +rev=$(echo "$commit_info" | jq -r '.[0].sha') +date=$(echo "$commit_info" | jq -r '.[0].commit.committer.date' | cut -dT -f1) +echo "Latest commit: $rev ($date)" + +# Update rev in default.nix +sed -i "s|rev = \".*\";|rev = \"$rev\";|" default.nix +sed -i "s|version = \".*\";|version = \"0-unstable-$date\";|" default.nix + +# Fetch source hash +echo "Fetching source hash..." +prefetch_output=$(nix-prefetch-github "$owner" "$repo" --rev "$rev" --json 2>/dev/null) +src_hash=$(echo "$prefetch_output" | jq -r '.hash') +echo "Source hash: $src_hash" +sed -i "s|hash = \"sha256-.*\";|hash = \"$src_hash\";|" default.nix + +# Set placeholder vendorHash to trigger build failure +sed -i "s|vendorHash = \"sha256-.*\";|vendorHash = \"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\";|" default.nix + +# Build to get the correct vendorHash +echo "Building to determine vendorHash..." +cd "$repo_root" +build_output=$(nix build --impure --no-link --expr 'with import { config.permittedInsecurePackages = ["olm-3.2.16"]; }; callPackage ./utils/pkgs/mautrix-mattermost {}' 2>&1 || true) + +vendor_hash=$(echo "$build_output" | grep -oP "got:\s+sha256-[A-Za-z0-9+/=]+" | tail -1 | awk '{print $2}') + +if [ -z "$vendor_hash" ]; then + echo "Error: Could not determine vendorHash from build output" + echo "Build output:" + echo "$build_output" + exit 1 +fi + +echo "vendorHash: $vendor_hash" +cd "$repo_root/utils/pkgs/mautrix-mattermost" +sed -i "s|vendorHash = \"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\";|vendorHash = \"$vendor_hash\";|" default.nix + +# Verify the build works +echo "" +echo "Verifying build..." +cd "$repo_root" +if nix build --impure --no-link --expr 'with import { config.permittedInsecurePackages = ["olm-3.2.16"]; }; callPackage ./utils/pkgs/mautrix-mattermost {}'; then + echo "" + echo "Successfully updated mautrix-mattermost to $rev ($date)" + echo " Source hash: $src_hash" + echo " vendorHash: $vendor_hash" +else + echo "" + echo "Build failed after updating hashes" + exit 1 +fi