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