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}