1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.matrix-tuwunel;
9 defaultUser = "tuwunel";
10 defaultGroup = "tuwunel";
11
12 format = pkgs.formats.toml { };
13 configFile = format.generate "tuwunel.toml" cfg.settings;
14in
15{
16 meta.maintainers = with lib.maintainers; [
17 scvalex
18 ];
19 options.services.matrix-tuwunel = {
20 enable = lib.mkEnableOption "tuwunel";
21
22 package = lib.mkPackageOption pkgs "matrix-tuwunel" { };
23
24 user = lib.mkOption {
25 type = lib.types.nonEmptyStr;
26 description = ''
27 The user {command}`tuwunel` is run as. If left as the default, the user will
28 automatically be created by the service.
29 '';
30 example = "conduit";
31 default = defaultUser;
32 };
33
34 group = lib.mkOption {
35 type = lib.types.nonEmptyStr;
36 description = ''
37 The group {command}`tuwunel` is run as. If left as the default, the group will
38 automatically be created by the service.
39 '';
40 example = "conduit";
41 default = defaultGroup;
42 };
43
44 stateDirectory = lib.mkOption {
45 type = lib.types.nonEmptyStr;
46 default = "tuwunel";
47 example = "matrix-conduit";
48 description = ''
49 The name of the directory under /var/lib/ where the database will be stored.
50
51 Note that `stateDirectory` cannot be changed once created because of the service's reliance on
52 systemd `StateDirectory`.
53 '';
54 };
55
56 extraEnvironment = lib.mkOption {
57 type = lib.types.attrsOf lib.types.str;
58 description = "Extra Environment variables to pass to the tuwunel server.";
59 default = { };
60 example = {
61 RUST_BACKTRACE = "yes";
62 };
63 };
64
65 settings = lib.mkOption {
66 type = lib.types.submodule {
67 freeformType = format.type;
68 options = {
69 global.server_name = lib.mkOption {
70 type = lib.types.nonEmptyStr;
71 example = "example.com";
72 description = "The server_name is the name of this server. It is used as a suffix for user and room ids.";
73 };
74 global.address = lib.mkOption {
75 type = lib.types.nullOr (lib.types.listOf lib.types.nonEmptyStr);
76 default = null;
77 example = [
78 "127.0.0.1"
79 "::1"
80 ];
81 description = ''
82 Addresses (IPv4 or IPv6) to listen on for connections by the reverse proxy/tls terminator.
83 If set to `null`, tuwunel will listen on IPv4 and IPv6 localhost.
84 Must be `null` if `unix_socket_path` is set.
85 '';
86 };
87 global.port = lib.mkOption {
88 type = lib.types.listOf lib.types.port;
89 default = [ 6167 ];
90 description = ''
91 The port(s) tuwunel will be running on.
92 You need to set up a reverse proxy in your web server (e.g. apache or nginx),
93 so all requests to /_matrix on port 443 and 8448 will be forwarded to the tuwunel
94 instance running on this port.
95 '';
96 };
97 global.unix_socket_path = lib.mkOption {
98 type = lib.types.nullOr lib.types.path;
99 default = null;
100 description = ''
101 Listen on a UNIX socket at the specified path. If listening on a UNIX socket,
102 listening on an address will be disabled. The `address` option must be set to
103 `null` (the default value). The option {option}`services.tuwunel.group` must
104 be set to a group your reverse proxy is part of.
105 '';
106 };
107 global.unix_socket_perms = lib.mkOption {
108 type = lib.types.ints.positive;
109 default = 660;
110 description = "The default permissions (in octal) to create the UNIX socket with.";
111 };
112 global.max_request_size = lib.mkOption {
113 type = lib.types.ints.positive;
114 default = 20000000;
115 description = "Max request size in bytes. Don't forget to also change it in the proxy.";
116 };
117 global.allow_registration = lib.mkOption {
118 type = lib.types.bool;
119 default = false;
120 description = ''
121 Whether new users can register on this server.
122
123 Registration with token requires `registration_token` or `registration_token_file` to be set.
124
125 If set to true without a token configured, and
126 `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
127 is set to true, users can freely register.
128 '';
129 };
130 global.allow_encryption = lib.mkOption {
131 type = lib.types.bool;
132 default = true;
133 description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work.";
134 };
135 global.allow_federation = lib.mkOption {
136 type = lib.types.bool;
137 default = true;
138 description = ''
139 Whether this server federates with other servers.
140 '';
141 };
142 global.trusted_servers = lib.mkOption {
143 type = lib.types.listOf lib.types.nonEmptyStr;
144 default = [ "matrix.org" ];
145 description = ''
146 Servers listed here will be used to gather public keys of other servers
147 (notary trusted key servers).
148
149 Currently, tuwunel doesn't support inbound batched key requests, so
150 this list should only contain other Synapse servers.
151
152 Example: `[ "matrix.org" "constellatory.net" "tchncs.de" ]`
153 '';
154 };
155 };
156 };
157 default = { };
158 # TOML does not allow null values, so we use null to omit those fields
159 apply = lib.filterAttrsRecursive (_: v: v != null);
160 description = ''
161 Generates the tuwunel.toml configuration file. Refer to
162 <https://matrix-construct.github.io/tuwunel/configuration.html>
163 for details on supported values.
164 '';
165 };
166 };
167
168 config = lib.mkIf cfg.enable {
169 assertions = [
170 {
171 assertion = !(cfg.settings ? global.unix_socket_path) || !(cfg.settings ? global.address);
172 message = ''
173 In `services.matrix-tuwunel.settings.global`, `unix_socket_path` and `address` cannot be set at the
174 same time.
175 Leave one of the two options unset or explicitly set them to `null`.
176 '';
177 }
178 {
179 assertion = cfg.user != defaultUser -> config ? users.users.${cfg.user};
180 message = "If `services.matrix-tuwunel.user` is changed, the configured user must already exist.";
181 }
182 {
183 assertion = cfg.group != defaultGroup -> config ? users.groups.${cfg.group};
184 message = "If `services.matrix-tuwunel.group` is changed, the configured group must already exist.";
185 }
186 {
187 assertion = "/var/lib/${cfg.settings.global.database_path}" != cfg.stateDirectory;
188 message = "The `services.matrix-tuwunel.stateDirectory` and `services.matrix-tuwunel.settings.global.database_path` options must match.";
189 }
190 ];
191
192 users.users = lib.mkIf (cfg.user == defaultUser) {
193 ${defaultUser} = {
194 group = cfg.group;
195 home = cfg.settings.global.database_path;
196 isSystemUser = true;
197 };
198 };
199
200 users.groups = lib.mkIf (cfg.group == defaultGroup) {
201 ${defaultGroup} = { };
202 };
203
204 services.matrix-tuwunel.settings.global.database_path = "/var/lib/${cfg.stateDirectory}/";
205
206 systemd.services.tuwunel = {
207 description = "Tuwunel Matrix Server";
208 documentation = [ "https://matrix-construct.github.io/tuwunel/" ];
209 wantedBy = [ "multi-user.target" ];
210 wants = [ "network-online.target" ];
211 after = [ "network-online.target" ];
212 environment = lib.mkMerge [
213 { TUWUNEL_CONFIG = configFile; }
214 cfg.extraEnvironment
215 ];
216 startLimitBurst = 5;
217 startLimitIntervalSec = 60;
218 serviceConfig = {
219 DynamicUser = true;
220 User = cfg.user;
221 Group = cfg.group;
222
223 DevicePolicy = "closed";
224 LockPersonality = true;
225 MemoryDenyWriteExecute = true;
226 NoNewPrivileges = true;
227 ProtectClock = true;
228 ProtectControlGroups = true;
229 ProtectHome = true;
230 ProtectHostname = true;
231 ProtectKernelLogs = true;
232 ProtectKernelModules = true;
233 ProtectKernelTunables = true;
234 ProtectProc = "invisible";
235 ProtectSystem = "strict";
236 PrivateDevices = true;
237 PrivateMounts = true;
238 PrivateTmp = true;
239 PrivateUsers = true;
240 PrivateIPC = true;
241 RemoveIPC = true;
242 RestrictAddressFamilies = [
243 "AF_INET"
244 "AF_INET6"
245 "AF_UNIX"
246 ];
247 RestrictNamespaces = true;
248 RestrictRealtime = true;
249 RestrictSUIDSGID = true;
250 SystemCallArchitectures = "native";
251 SystemCallFilter = [
252 "@system-service @resources"
253 "~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc"
254 ];
255 SystemCallErrorNumber = "EPERM";
256
257 StateDirectory = cfg.stateDirectory;
258 StateDirectoryMode = "0700";
259 RuntimeDirectory = "tuwunel";
260 RuntimeDirectoryMode = "0750";
261
262 ExecStart = lib.getExe cfg.package;
263 Restart = "on-failure";
264 RestartSec = 10;
265 };
266 };
267 };
268}