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