at 24.11-pre 6.5 kB view raw
1{ config, pkgs, lib, ... }: 2 3with lib; 4 5let 6 cfg = config.services.heisenbridge; 7 8 pkg = config.services.heisenbridge.package; 9 bin = "${pkg}/bin/heisenbridge"; 10 11 jsonType = (pkgs.formats.json { }).type; 12 13 registrationFile = "/var/lib/heisenbridge/registration.yml"; 14 # JSON is a proper subset of YAML 15 bridgeConfig = builtins.toFile "heisenbridge-registration.yml" (builtins.toJSON { 16 id = "heisenbridge"; 17 url = cfg.registrationUrl; 18 # Don't specify as_token and hs_token 19 rate_limited = false; 20 sender_localpart = "heisenbridge"; 21 namespaces = cfg.namespaces; 22 }); 23in 24{ 25 options.services.heisenbridge = { 26 enable = mkEnableOption "the Matrix to IRC bridge"; 27 28 package = mkPackageOption pkgs "heisenbridge" { }; 29 30 homeserver = mkOption { 31 type = types.str; 32 description = "The URL to the home server for client-server API calls"; 33 example = "http://localhost:8008"; 34 }; 35 36 registrationUrl = mkOption { 37 type = types.str; 38 description = '' 39 The URL where the application service is listening for HS requests, from the Matrix HS perspective.# 40 The default value assumes the bridge runs on the same host as the home server, in the same network. 41 ''; 42 example = "https://matrix.example.org"; 43 default = "http://${cfg.address}:${toString cfg.port}"; 44 defaultText = "http://$${cfg.address}:$${toString cfg.port}"; 45 }; 46 47 address = mkOption { 48 type = types.str; 49 description = "Address to listen on. IPv6 does not seem to be supported."; 50 default = "127.0.0.1"; 51 example = "0.0.0.0"; 52 }; 53 54 port = mkOption { 55 type = types.port; 56 description = "The port to listen on"; 57 default = 9898; 58 }; 59 60 debug = mkOption { 61 type = types.bool; 62 description = "More verbose logging. Recommended during initial setup."; 63 default = false; 64 }; 65 66 owner = mkOption { 67 type = types.nullOr types.str; 68 description = '' 69 Set owner MXID otherwise first talking local user will claim the bridge 70 ''; 71 default = null; 72 example = "@admin:example.org"; 73 }; 74 75 namespaces = mkOption { 76 description = "Configure the 'namespaces' section of the registration.yml for the bridge and the server"; 77 # TODO link to Matrix documentation of the format 78 type = types.submodule { 79 freeformType = jsonType; 80 }; 81 82 default = { 83 users = [ 84 { 85 regex = "@irc_.*"; 86 exclusive = true; 87 } 88 ]; 89 aliases = [ ]; 90 rooms = [ ]; 91 }; 92 }; 93 94 identd.enable = mkEnableOption "identd service support"; 95 identd.port = mkOption { 96 type = types.port; 97 description = "identd listen port"; 98 default = 113; 99 }; 100 101 extraArgs = mkOption { 102 type = types.listOf types.str; 103 description = "Heisenbridge is configured over the command line. Append extra arguments here"; 104 default = [ ]; 105 }; 106 }; 107 108 config = mkIf cfg.enable { 109 systemd.services.heisenbridge = { 110 description = "Matrix<->IRC bridge"; 111 before = [ "matrix-synapse.service" ]; # So the registration file can be used by Synapse 112 wantedBy = [ "multi-user.target" ]; 113 114 preStart = '' 115 umask 077 116 set -e -u -o pipefail 117 118 if ! [ -f "${registrationFile}" ]; then 119 # Generate registration file if not present (actually, we only care about the tokens in it) 120 ${bin} --generate --config ${registrationFile} 121 fi 122 123 # Overwrite the registration file with our generated one (the config may have changed since then), 124 # but keep the tokens. Two step procedure to be failure safe 125 ${pkgs.yq}/bin/yq --slurp \ 126 '.[0] + (.[1] | {as_token, hs_token})' \ 127 ${bridgeConfig} \ 128 ${registrationFile} \ 129 > ${registrationFile}.new 130 mv -f ${registrationFile}.new ${registrationFile} 131 132 # Grant Synapse access to the registration 133 if ${pkgs.getent}/bin/getent group matrix-synapse > /dev/null; then 134 chgrp -v matrix-synapse ${registrationFile} 135 chmod -v g+r ${registrationFile} 136 fi 137 ''; 138 139 serviceConfig = rec { 140 Type = "simple"; 141 ExecStart = lib.concatStringsSep " " ( 142 [ 143 bin 144 (if cfg.debug then "-vvv" else "-v") 145 "--config" 146 registrationFile 147 "--listen-address" 148 (lib.escapeShellArg cfg.address) 149 "--listen-port" 150 (toString cfg.port) 151 ] 152 ++ (lib.optionals (cfg.owner != null) [ 153 "--owner" 154 (lib.escapeShellArg cfg.owner) 155 ]) 156 ++ (lib.optionals cfg.identd.enable [ 157 "--identd" 158 "--identd-port" 159 (toString cfg.identd.port) 160 ]) 161 ++ [ 162 (lib.escapeShellArg cfg.homeserver) 163 ] 164 ++ (map (lib.escapeShellArg) cfg.extraArgs) 165 ); 166 167 # Hardening options 168 169 User = "heisenbridge"; 170 Group = "heisenbridge"; 171 RuntimeDirectory = "heisenbridge"; 172 RuntimeDirectoryMode = "0700"; 173 StateDirectory = "heisenbridge"; 174 StateDirectoryMode = "0755"; 175 176 ProtectSystem = "strict"; 177 ProtectHome = true; 178 PrivateTmp = true; 179 PrivateDevices = true; 180 ProtectKernelTunables = true; 181 ProtectControlGroups = true; 182 RestrictSUIDSGID = true; 183 PrivateMounts = true; 184 ProtectKernelModules = true; 185 ProtectKernelLogs = true; 186 ProtectHostname = true; 187 ProtectClock = true; 188 ProtectProc = "invisible"; 189 ProcSubset = "pid"; 190 RestrictNamespaces = true; 191 RemoveIPC = true; 192 UMask = "0077"; 193 194 CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.port < 1024 || (cfg.identd.enable && cfg.identd.port < 1024)) "CAP_NET_BIND_SERVICE"; 195 AmbientCapabilities = CapabilityBoundingSet; 196 NoNewPrivileges = true; 197 LockPersonality = true; 198 RestrictRealtime = true; 199 SystemCallFilter = ["@system-service" "~@privileged" "@chown"]; 200 SystemCallArchitectures = "native"; 201 RestrictAddressFamilies = "AF_INET AF_INET6"; 202 }; 203 }; 204 205 users.groups.heisenbridge = {}; 206 users.users.heisenbridge = { 207 description = "Service user for the Heisenbridge"; 208 group = "heisenbridge"; 209 isSystemUser = true; 210 }; 211 }; 212 213 meta.maintainers = [ ]; 214}