1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 cfg = config.services.matrix-appservice-irc;
9
10 pkg = pkgs.matrix-appservice-irc;
11 bin = "${pkg}/bin/matrix-appservice-irc";
12
13 jsonType = (pkgs.formats.json { }).type;
14
15 configFile =
16 pkgs.runCommand "matrix-appservice-irc.yml"
17 {
18 # Because this program will be run at build time, we need `nativeBuildInputs`
19 nativeBuildInputs = [
20 (pkgs.python3.withPackages (ps: [ ps.jsonschema ]))
21 pkgs.remarshal
22 ];
23 preferLocalBuild = true;
24
25 config = builtins.toJSON cfg.settings;
26 passAsFile = [ "config" ];
27 }
28 ''
29 # The schema is given as yaml, we need to convert it to json
30 remarshal --if yaml --of json -i ${pkg}/config.schema.yml -o config.schema.json
31 python -m jsonschema config.schema.json -i $configPath
32 cp "$configPath" "$out"
33 '';
34 registrationFile = "/var/lib/matrix-appservice-irc/registration.yml";
35in
36{
37 options.services.matrix-appservice-irc = with lib.types; {
38 enable = lib.mkEnableOption "the Matrix/IRC bridge";
39
40 port = lib.mkOption {
41 type = port;
42 description = "The port to listen on";
43 default = 8009;
44 };
45
46 needBindingCap = lib.mkOption {
47 type = bool;
48 description = "Whether the daemon needs to bind to ports below 1024 (e.g. for the ident service)";
49 default = false;
50 };
51
52 passwordEncryptionKeyLength = lib.mkOption {
53 type = ints.unsigned;
54 description = "Length of the key to encrypt IRC passwords with";
55 default = 4096;
56 example = 8192;
57 };
58
59 registrationUrl = lib.mkOption {
60 type = str;
61 description = ''
62 The URL where the application service is listening for homeserver requests,
63 from the Matrix homeserver perspective.
64 '';
65 example = "http://localhost:8009";
66 };
67
68 localpart = lib.mkOption {
69 type = str;
70 description = "The user_id localpart to assign to the appservice";
71 default = "appservice-irc";
72 };
73
74 settings = lib.mkOption {
75 description = ''
76 Configuration for the appservice, see
77 <https://github.com/matrix-org/matrix-appservice-irc/blob/${pkgs.matrix-appservice-irc.version}/config.sample.yaml>
78 for supported values
79 '';
80 default = { };
81 type = submodule {
82 freeformType = jsonType;
83
84 options = {
85 homeserver = lib.mkOption {
86 description = "Homeserver configuration";
87 default = { };
88 type = submodule {
89 freeformType = jsonType;
90
91 options = {
92 url = lib.mkOption {
93 type = str;
94 description = "The URL to the home server for client-server API calls";
95 };
96
97 domain = lib.mkOption {
98 type = str;
99 description = ''
100 The 'domain' part for user IDs on this home server. Usually
101 (but not always) is the "domain name" part of the homeserver URL.
102 '';
103 };
104 };
105 };
106 };
107
108 database = lib.mkOption {
109 default = { };
110 description = "Configuration for the database";
111 type = submodule {
112 freeformType = jsonType;
113
114 options = {
115 engine = lib.mkOption {
116 type = str;
117 description = "Which database engine to use";
118 default = "nedb";
119 example = "postgres";
120 };
121
122 connectionString = lib.mkOption {
123 type = str;
124 description = "The database connection string";
125 default = "nedb://var/lib/matrix-appservice-irc/data";
126 example = "postgres://username:password@host:port/databasename";
127 };
128 };
129 };
130 };
131
132 ircService = lib.mkOption {
133 default = { };
134 description = "IRC bridge configuration";
135 type = submodule {
136 freeformType = jsonType;
137
138 options = {
139 passwordEncryptionKeyPath = lib.mkOption {
140 type = str;
141 description = ''
142 Location of the key with which IRC passwords are encrypted
143 for storage. Will be generated on first run if not present.
144 '';
145 default = "/var/lib/matrix-appservice-irc/passkey.pem";
146 };
147
148 servers = lib.mkOption {
149 type = submodule { freeformType = jsonType; };
150 description = "IRC servers to connect to";
151 };
152
153 mediaProxy = {
154 signingKeyPath = lib.mkOption {
155 type = path;
156 default = "/var/lib/matrix-appservice-irc/media-signingkey.jwk";
157 description = ''
158 Path to the signing key file for authenticated media.
159 '';
160 };
161 ttlSeconds = lib.mkOption {
162 type = ints.unsigned;
163 default = 3600;
164 example = 0;
165 description = ''
166 Lifetime in seconds, that generated URLs stay valid.
167
168 Set the lifetime to 0 to prevent URLs from becoming invalid.
169 '';
170 };
171 bindPort = lib.mkOption {
172 type = port;
173 default = 11111;
174 description = ''
175 Port that the media proxy binds to.
176 '';
177 };
178 publicUrl = lib.mkOption {
179 type = str;
180 example = "https://matrix.example.com/media";
181 description = ''
182 URL under which the media proxy is publicly acccessible.
183 '';
184 };
185 };
186 };
187 };
188 };
189 };
190 };
191 };
192 };
193
194 config = lib.mkIf cfg.enable {
195 systemd.services.matrix-appservice-irc = {
196 description = "Matrix-IRC bridge";
197 before = [ "matrix-synapse.service" ]; # So the registration can be used by Synapse
198 after = lib.optionals (cfg.settings.database.engine == "postgres") [
199 "postgresql.service"
200 ];
201 wantedBy = [ "multi-user.target" ];
202
203 preStart = ''
204 umask 077
205 # Generate key for crypting passwords
206 if ! [ -f "${cfg.settings.ircService.passwordEncryptionKeyPath}" ]; then
207 ${pkgs.openssl}/bin/openssl genpkey \
208 -out "${cfg.settings.ircService.passwordEncryptionKeyPath}" \
209 -outform PEM \
210 -algorithm RSA \
211 -pkeyopt "rsa_keygen_bits:${toString cfg.passwordEncryptionKeyLength}"
212 fi
213 # Generate registration file
214 if ! [ -f "${registrationFile}" ]; then
215 # The easy case: the file has not been generated yet
216 ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
217 else
218 # The tricky case: we already have a generation file. Because the NixOS configuration might have changed, we need to
219 # regenerate it. But this would give the service a new random ID and tokens, so we need to back up and restore them.
220 # 1. Backup
221 id=$(grep "^id:.*$" ${registrationFile})
222 hs_token=$(grep "^hs_token:.*$" ${registrationFile})
223 as_token=$(grep "^as_token:.*$" ${registrationFile})
224 # 2. Regenerate
225 ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
226 # 3. Restore
227 sed -i "s/^id:.*$/$id/g" ${registrationFile}
228 sed -i "s/^hs_token:.*$/$hs_token/g" ${registrationFile}
229 sed -i "s/^as_token:.*$/$as_token/g" ${registrationFile}
230 fi
231 if ! [ -f "${cfg.settings.ircService.mediaProxy.signingKeyPath}" ]; then
232 ${lib.getExe pkgs.nodejs} ${pkg}/lib/generate-signing-key.js > "${cfg.settings.ircService.mediaProxy.signingKeyPath}"
233 fi
234 # Allow synapse access to the registration
235 if ${pkgs.getent}/bin/getent group matrix-synapse > /dev/null; then
236 chgrp matrix-synapse ${registrationFile}
237 chmod g+r ${registrationFile}
238 fi
239 '';
240
241 serviceConfig = rec {
242 Type = "simple";
243 ExecStart = "${bin} --config ${configFile} --file ${registrationFile} --port ${toString cfg.port}";
244
245 ProtectHome = true;
246 PrivateDevices = true;
247 ProtectKernelTunables = true;
248 ProtectKernelModules = true;
249 ProtectControlGroups = true;
250 StateDirectory = "matrix-appservice-irc";
251 StateDirectoryMode = "755";
252
253 User = "matrix-appservice-irc";
254 Group = "matrix-appservice-irc";
255
256 CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ lib.optional (cfg.needBindingCap) "CAP_NET_BIND_SERVICE";
257 AmbientCapabilities = CapabilityBoundingSet;
258 NoNewPrivileges = true;
259
260 LockPersonality = true;
261 RestrictRealtime = true;
262 PrivateMounts = true;
263 SystemCallFilter = [
264 "@system-service @pkey"
265 "~@privileged @resources"
266 "@chown"
267 ];
268 SystemCallArchitectures = "native";
269 # AF_UNIX is required to connect to a postgres socket.
270 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
271 };
272 };
273
274 users.groups.matrix-appservice-irc = { };
275 users.users.matrix-appservice-irc = {
276 description = "Service user for the Matrix-IRC bridge";
277 group = "matrix-appservice-irc";
278 isSystemUser = true;
279 };
280 };
281
282 # uses attributes of the linked package
283 meta.buildDocsInSandbox = false;
284}