1 { config, pkgs, lib, ... }:
3 cfg = config.services.heisenbridge;
5 pkg = config.services.heisenbridge.package;
6 bin = "${pkg}/bin/heisenbridge";
8 jsonType = (pkgs.formats.json { }).type;
10 registrationFile = "/var/lib/heisenbridge/registration.yml";
11 # JSON is a proper subset of YAML
12 bridgeConfig = builtins.toFile "heisenbridge-registration.yml" (builtins.toJSON {
14 url = cfg.registrationUrl;
15 # Don't specify as_token and hs_token
17 sender_localpart = "heisenbridge";
18 namespaces = cfg.namespaces;
22 options.services.heisenbridge = {
23 enable = lib.mkEnableOption "the Matrix to IRC bridge";
25 package = lib.mkPackageOption pkgs "heisenbridge" { };
27 homeserver = lib.mkOption {
29 description = "The URL to the home server for client-server API calls";
30 example = "http://localhost:8008";
33 registrationUrl = lib.mkOption {
36 The URL where the application service is listening for HS requests, from the Matrix HS perspective.#
37 The default value assumes the bridge runs on the same host as the home server, in the same network.
39 example = "https://matrix.example.org";
40 default = "http://${cfg.address}:${toString cfg.port}";
41 defaultText = "http://$${cfg.address}:$${toString cfg.port}";
44 address = lib.mkOption {
46 description = "Address to listen on. IPv6 does not seem to be supported.";
47 default = "127.0.0.1";
52 type = lib.types.port;
53 description = "The port to listen on";
57 debug = lib.mkOption {
58 type = lib.types.bool;
59 description = "More verbose logging. Recommended during initial setup.";
63 owner = lib.mkOption {
64 type = lib.types.nullOr lib.types.str;
66 Set owner MXID otherwise first talking local user will claim the bridge
69 example = "@admin:example.org";
72 namespaces = lib.mkOption {
73 description = "Configure the 'namespaces' section of the registration.yml for the bridge and the server";
74 # TODO link to Matrix documentation of the format
75 type = lib.types.submodule {
76 freeformType = jsonType;
91 identd.enable = lib.mkEnableOption "identd service support";
92 identd.port = lib.mkOption {
93 type = lib.types.port;
94 description = "identd listen port";
98 extraArgs = lib.mkOption {
99 type = lib.types.listOf lib.types.str;
100 description = "Heisenbridge is configured over the command line. Append extra arguments here";
105 config = lib.mkIf cfg.enable {
106 systemd.services.heisenbridge = {
107 description = "Matrix<->IRC bridge";
108 before = [ "matrix-synapse.service" ]; # So the registration file can be used by Synapse
109 wantedBy = [ "multi-user.target" ];
113 set -e -u -o pipefail
115 if ! [ -f "${registrationFile}" ]; then
116 # Generate registration file if not present (actually, we only care about the tokens in it)
117 ${bin} --generate --config ${registrationFile}
120 # Overwrite the registration file with our generated one (the config may have changed since then),
121 # but keep the tokens. Two step procedure to be failure safe
122 ${pkgs.yq}/bin/yq --slurp \
123 '.[0] + (.[1] | {as_token, hs_token})' \
125 ${registrationFile} \
126 > ${registrationFile}.new
127 mv -f ${registrationFile}.new ${registrationFile}
129 # Grant Synapse access to the registration
130 if ${pkgs.getent}/bin/getent group matrix-synapse > /dev/null; then
131 chgrp -v matrix-synapse ${registrationFile}
132 chmod -v g+r ${registrationFile}
136 serviceConfig = rec {
138 ExecStart = lib.concatStringsSep " " (
141 (if cfg.debug then "-vvv" else "-v")
145 (lib.escapeShellArg cfg.address)
149 ++ (lib.optionals (cfg.owner != null) [
151 (lib.escapeShellArg cfg.owner)
153 ++ (lib.optionals cfg.identd.enable [
156 (toString cfg.identd.port)
159 (lib.escapeShellArg cfg.homeserver)
161 ++ (map (lib.escapeShellArg) cfg.extraArgs)
166 User = "heisenbridge";
167 Group = "heisenbridge";
168 RuntimeDirectory = "heisenbridge";
169 RuntimeDirectoryMode = "0700";
170 StateDirectory = "heisenbridge";
171 StateDirectoryMode = "0755";
173 ProtectSystem = "strict";
176 PrivateDevices = true;
177 ProtectKernelTunables = true;
178 ProtectControlGroups = true;
179 RestrictSUIDSGID = true;
180 PrivateMounts = true;
181 ProtectKernelModules = true;
182 ProtectKernelLogs = true;
183 ProtectHostname = true;
185 ProtectProc = "invisible";
187 RestrictNamespaces = true;
191 CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ lib.optional (cfg.port < 1024 || (cfg.identd.enable && cfg.identd.port < 1024)) "CAP_NET_BIND_SERVICE";
192 AmbientCapabilities = CapabilityBoundingSet;
193 NoNewPrivileges = true;
194 LockPersonality = true;
195 RestrictRealtime = true;
196 SystemCallFilter = ["@system-service" "~@privileged" "@chown"];
197 SystemCallArchitectures = "native";
198 RestrictAddressFamilies = "AF_INET AF_INET6";
202 users.groups.heisenbridge = {};
203 users.users.heisenbridge = {
204 description = "Service user for the Heisenbridge";
205 group = "heisenbridge";
210 meta.maintainers = [ ];