···3637- [Chhoto URL](https://github.com/SinTan1729/chhoto-url), a simple, blazingly fast, selfhosted URL shortener with no unnecessary features, written in Rust. Available as [services.chhoto-url](#opt-services.chhoto-url.enable).
380039- [Broadcast Box](https://github.com/Glimesh/broadcast-box), a WebRTC broadcast server. Available as [services.broadcast-box](options.html#opt-services.broadcast-box.enable).
4041- Docker now defaults to 28.x, because version 27.x stopped receiving security updates and bug fixes after [May 2, 2025](https://github.com/moby/moby/pull/49910).
···3637- [Chhoto URL](https://github.com/SinTan1729/chhoto-url), a simple, blazingly fast, selfhosted URL shortener with no unnecessary features, written in Rust. Available as [services.chhoto-url](#opt-services.chhoto-url.enable).
3839+- [tuwunel](https://matrix-construct.github.io/tuwunel/), a federated chat server implementing the Matrix protocol, forked from Conduwuit. Available as [services.matrix-tuwunel](#opt-services.matrix-tuwunel.enable).
40+41- [Broadcast Box](https://github.com/Glimesh/broadcast-box), a WebRTC broadcast server. Available as [services.broadcast-box](options.html#opt-services.broadcast-box.enable).
4243- Docker now defaults to 28.x, because version 27.x stopped receiving security updates and bug fixes after [May 2, 2025](https://github.com/moby/moby/pull/49910).
···1+{
2+ config,
3+ lib,
4+ pkgs,
5+ ...
6+}:
7+let
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;
14+in
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+}