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