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