···36363737- [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).
38383939+- [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).
4040+3941- [Broadcast Box](https://github.com/Glimesh/broadcast-box), a WebRTC broadcast server. Available as [services.broadcast-box](options.html#opt-services.broadcast-box.enable).
40424143- 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).
···11+{
22+ config,
33+ lib,
44+ pkgs,
55+ ...
66+}:
77+let
88+ cfg = config.services.matrix-tuwunel;
99+ defaultUser = "tuwunel";
1010+ defaultGroup = "tuwunel";
1111+1212+ format = pkgs.formats.toml { };
1313+ configFile = format.generate "tuwunel.toml" cfg.settings;
1414+in
1515+{
1616+ meta.maintainers = with lib.maintainers; [
1717+ scvalex
1818+ ];
1919+ options.services.matrix-tuwunel = {
2020+ enable = lib.mkEnableOption "tuwunel";
2121+2222+ package = lib.mkPackageOption pkgs "matrix-tuwunel" { };
2323+2424+ user = lib.mkOption {
2525+ type = lib.types.nonEmptyStr;
2626+ description = ''
2727+ The user {command}`tuwunel` is run as. If left as the default, the user will
2828+ automatically be created by the service.
2929+ '';
3030+ example = "conduit";
3131+ default = defaultUser;
3232+ };
3333+3434+ group = lib.mkOption {
3535+ type = lib.types.nonEmptyStr;
3636+ description = ''
3737+ The group {command}`tuwunel` is run as. If left as the default, the group will
3838+ automatically be created by the service.
3939+ '';
4040+ example = "conduit";
4141+ default = defaultGroup;
4242+ };
4343+4444+ stateDirectory = lib.mkOption {
4545+ type = lib.types.nonEmptyStr;
4646+ default = "tuwunel";
4747+ example = "matrix-conduit";
4848+ description = ''
4949+ The name of the directory under /var/lib/ where the database will be stored.
5050+5151+ Note that `stateDirectory` cannot be changed once created because of the service's reliance on
5252+ systemd `StateDirectory`.
5353+ '';
5454+ };
5555+5656+ extraEnvironment = lib.mkOption {
5757+ type = lib.types.attrsOf lib.types.str;
5858+ description = "Extra Environment variables to pass to the tuwunel server.";
5959+ default = { };
6060+ example = {
6161+ RUST_BACKTRACE = "yes";
6262+ };
6363+ };
6464+6565+ settings = lib.mkOption {
6666+ type = lib.types.submodule {
6767+ freeformType = format.type;
6868+ options = {
6969+ global.server_name = lib.mkOption {
7070+ type = lib.types.nonEmptyStr;
7171+ example = "example.com";
7272+ description = "The server_name is the name of this server. It is used as a suffix for user and room ids.";
7373+ };
7474+ global.address = lib.mkOption {
7575+ type = lib.types.nullOr (lib.types.listOf lib.types.nonEmptyStr);
7676+ default = null;
7777+ example = [
7878+ "127.0.0.1"
7979+ "::1"
8080+ ];
8181+ description = ''
8282+ Addresses (IPv4 or IPv6) to listen on for connections by the reverse proxy/tls terminator.
8383+ If set to `null`, tuwunel will listen on IPv4 and IPv6 localhost.
8484+ Must be `null` if `unix_socket_path` is set.
8585+ '';
8686+ };
8787+ global.port = lib.mkOption {
8888+ type = lib.types.listOf lib.types.port;
8989+ default = [ 6167 ];
9090+ description = ''
9191+ The port(s) tuwunel will be running on.
9292+ You need to set up a reverse proxy in your web server (e.g. apache or nginx),
9393+ so all requests to /_matrix on port 443 and 8448 will be forwarded to the tuwunel
9494+ instance running on this port.
9595+ '';
9696+ };
9797+ global.unix_socket_path = lib.mkOption {
9898+ type = lib.types.nullOr lib.types.path;
9999+ default = null;
100100+ description = ''
101101+ Listen on a UNIX socket at the specified path. If listening on a UNIX socket,
102102+ listening on an address will be disabled. The `address` option must be set to
103103+ `null` (the default value). The option {option}`services.tuwunel.group` must
104104+ be set to a group your reverse proxy is part of.
105105+ '';
106106+ };
107107+ global.unix_socket_perms = lib.mkOption {
108108+ type = lib.types.ints.positive;
109109+ default = 660;
110110+ description = "The default permissions (in octal) to create the UNIX socket with.";
111111+ };
112112+ global.max_request_size = lib.mkOption {
113113+ type = lib.types.ints.positive;
114114+ default = 20000000;
115115+ description = "Max request size in bytes. Don't forget to also change it in the proxy.";
116116+ };
117117+ global.allow_registration = lib.mkOption {
118118+ type = lib.types.bool;
119119+ default = false;
120120+ description = ''
121121+ Whether new users can register on this server.
122122+123123+ Registration with token requires `registration_token` or `registration_token_file` to be set.
124124+125125+ If set to true without a token configured, and
126126+ `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
127127+ is set to true, users can freely register.
128128+ '';
129129+ };
130130+ global.allow_encryption = lib.mkOption {
131131+ type = lib.types.bool;
132132+ default = true;
133133+ description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work.";
134134+ };
135135+ global.allow_federation = lib.mkOption {
136136+ type = lib.types.bool;
137137+ default = true;
138138+ description = ''
139139+ Whether this server federates with other servers.
140140+ '';
141141+ };
142142+ global.trusted_servers = lib.mkOption {
143143+ type = lib.types.listOf lib.types.nonEmptyStr;
144144+ default = [ "matrix.org" ];
145145+ description = ''
146146+ Servers listed here will be used to gather public keys of other servers
147147+ (notary trusted key servers).
148148+149149+ Currently, tuwunel doesn't support inbound batched key requests, so
150150+ this list should only contain other Synapse servers.
151151+152152+ Example: `[ "matrix.org" "constellatory.net" "tchncs.de" ]`
153153+ '';
154154+ };
155155+ };
156156+ };
157157+ default = { };
158158+ # TOML does not allow null values, so we use null to omit those fields
159159+ apply = lib.filterAttrsRecursive (_: v: v != null);
160160+ description = ''
161161+ Generates the tuwunel.toml configuration file. Refer to
162162+ <https://matrix-construct.github.io/tuwunel/configuration.html>
163163+ for details on supported values.
164164+ '';
165165+ };
166166+ };
167167+168168+ config = lib.mkIf cfg.enable {
169169+ assertions = [
170170+ {
171171+ assertion = !(cfg.settings ? global.unix_socket_path) || !(cfg.settings ? global.address);
172172+ message = ''
173173+ In `services.matrix-tuwunel.settings.global`, `unix_socket_path` and `address` cannot be set at the
174174+ same time.
175175+ Leave one of the two options unset or explicitly set them to `null`.
176176+ '';
177177+ }
178178+ {
179179+ assertion = cfg.user != defaultUser -> config ? users.users.${cfg.user};
180180+ message = "If `services.matrix-tuwunel.user` is changed, the configured user must already exist.";
181181+ }
182182+ {
183183+ assertion = cfg.group != defaultGroup -> config ? users.groups.${cfg.group};
184184+ message = "If `services.matrix-tuwunel.group` is changed, the configured group must already exist.";
185185+ }
186186+ {
187187+ assertion = "/var/lib/${cfg.settings.global.database_path}" != cfg.stateDirectory;
188188+ message = "The `services.matrix-tuwunel.stateDirectory` and `services.matrix-tuwunel.settings.global.database_path` options must match.";
189189+ }
190190+ ];
191191+192192+ users.users = lib.mkIf (cfg.user == defaultUser) {
193193+ ${defaultUser} = {
194194+ group = cfg.group;
195195+ home = cfg.settings.global.database_path;
196196+ isSystemUser = true;
197197+ };
198198+ };
199199+200200+ users.groups = lib.mkIf (cfg.group == defaultGroup) {
201201+ ${defaultGroup} = { };
202202+ };
203203+204204+ services.matrix-tuwunel.settings.global.database_path = "/var/lib/${cfg.stateDirectory}/";
205205+206206+ systemd.services.tuwunel = {
207207+ description = "Tuwunel Matrix Server";
208208+ documentation = [ "https://matrix-construct.github.io/tuwunel/" ];
209209+ wantedBy = [ "multi-user.target" ];
210210+ wants = [ "network-online.target" ];
211211+ after = [ "network-online.target" ];
212212+ environment = lib.mkMerge [
213213+ { TUWUNEL_CONFIG = configFile; }
214214+ cfg.extraEnvironment
215215+ ];
216216+ startLimitBurst = 5;
217217+ startLimitIntervalSec = 60;
218218+ serviceConfig = {
219219+ DynamicUser = true;
220220+ User = cfg.user;
221221+ Group = cfg.group;
222222+223223+ DevicePolicy = "closed";
224224+ LockPersonality = true;
225225+ MemoryDenyWriteExecute = true;
226226+ NoNewPrivileges = true;
227227+ ProtectClock = true;
228228+ ProtectControlGroups = true;
229229+ ProtectHome = true;
230230+ ProtectHostname = true;
231231+ ProtectKernelLogs = true;
232232+ ProtectKernelModules = true;
233233+ ProtectKernelTunables = true;
234234+ ProtectProc = "invisible";
235235+ ProtectSystem = "strict";
236236+ PrivateDevices = true;
237237+ PrivateMounts = true;
238238+ PrivateTmp = true;
239239+ PrivateUsers = true;
240240+ PrivateIPC = true;
241241+ RemoveIPC = true;
242242+ RestrictAddressFamilies = [
243243+ "AF_INET"
244244+ "AF_INET6"
245245+ "AF_UNIX"
246246+ ];
247247+ RestrictNamespaces = true;
248248+ RestrictRealtime = true;
249249+ RestrictSUIDSGID = true;
250250+ SystemCallArchitectures = "native";
251251+ SystemCallFilter = [
252252+ "@system-service @resources"
253253+ "~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc"
254254+ ];
255255+ SystemCallErrorNumber = "EPERM";
256256+257257+ StateDirectory = cfg.stateDirectory;
258258+ StateDirectoryMode = "0700";
259259+ RuntimeDirectory = "tuwunel";
260260+ RuntimeDirectoryMode = "0750";
261261+262262+ ExecStart = lib.getExe cfg.package;
263263+ Restart = "on-failure";
264264+ RestartSec = 10;
265265+ };
266266+ };
267267+ };
268268+}