1 { config, lib, pkgs, options, ... }:
6 cfg = config.services.transmission;
7 inherit (config.environment) etc;
8 apparmor = config.security.apparmor;
9 rootDir = "/run/transmission";
10 settingsDir = ".config/transmission-daemon";
11 downloadsDir = "Downloads";
12 incompleteDir = ".incomplete";
13 watchDir = "watchdir";
14 settingsFormat = pkgs.formats.json {};
15 settingsFile = settingsFormat.generate "settings.json" cfg.settings;
19 (mkRenamedOptionModule ["services" "transmission" "port"]
20 ["services" "transmission" "settings" "rpc-port"])
21 (mkAliasOptionModule ["services" "transmission" "openFirewall"]
22 ["services" "transmission" "openPeerPorts"])
25 services.transmission = {
26 enable = mkEnableOption ''the headless Transmission BitTorrent daemon.
28 Transmission daemon can be controlled via the RPC interface using
29 transmission-remote, the WebUI (http://127.0.0.1:9091/ by default),
30 or other clients like stig or tremc.
32 Torrents are downloaded to <xref linkend="opt-services.transmission.home"/>/${downloadsDir} by default and are
33 accessible to users in the "transmission" group'';
37 Settings whose options overwrite fields in
38 <literal>.config/transmission-daemon/settings.json</literal>
39 (each time the service starts).
41 See <link xlink:href="https://github.com/transmission/transmission/wiki/Editing-Configuration-Files">Transmission's Wiki</link>
42 for documentation of settings not explicitely covered by this module.
45 type = types.submodule {
46 freeformType = settingsFormat.type;
47 options.download-dir = mkOption {
49 default = "${cfg.home}/${downloadsDir}";
50 description = "Directory where to download torrents.";
52 options.incomplete-dir = mkOption {
54 default = "${cfg.home}/${incompleteDir}";
57 services.transmission.home
58 <xref linkend="opt-services.transmission.settings.incomplete-dir-enabled"/>,
59 new torrents will download the files to this directory.
60 When complete, the files will be moved to download-dir
61 <xref linkend="opt-services.transmission.settings.download-dir"/>.
64 options.incomplete-dir-enabled = mkOption {
69 options.message-level = mkOption {
70 type = types.ints.between 0 2;
72 description = "Set verbosity of transmission messages.";
74 options.peer-port = mkOption {
77 description = "The peer port to listen for incoming connections.";
79 options.peer-port-random-high = mkOption {
83 The maximum peer port to listen to for incoming connections
84 when <xref linkend="opt-services.transmission.settings.peer-port-random-on-start"/> is enabled.
87 options.peer-port-random-low = mkOption {
91 The minimal peer port to listen to for incoming connections
92 when <xref linkend="opt-services.transmission.settings.peer-port-random-on-start"/> is enabled.
95 options.peer-port-random-on-start = mkOption {
98 description = "Randomize the peer port.";
100 options.rpc-bind-address = mkOption {
102 default = "127.0.0.1";
105 Where to listen for RPC connections.
106 Use \"0.0.0.0\" to listen on all interfaces.
109 options.rpc-port = mkOption {
112 description = "The RPC port to listen to.";
114 options.script-torrent-done-enabled = mkOption {
119 <xref linkend="opt-services.transmission.settings.script-torrent-done-filename"/>
120 at torrent completion.
123 options.script-torrent-done-filename = mkOption {
124 type = types.nullOr types.path;
126 description = "Executable to be run at torrent completion.";
128 options.umask = mkOption {
132 Sets transmission's file mode creation mask.
133 See the umask(2) manpage for more information.
134 Users who want their saved torrents to be world-writable
135 may want to set this value to 0.
136 Bear in mind that the json markup language only accepts numbers in base 10,
137 so the standard umask(2) octal notation "022" is written in settings.json as 18.
140 options.utp-enabled = mkOption {
144 Whether to enable <link xlink:href="http://en.wikipedia.org/wiki/Micro_Transport_Protocol">Micro Transport Protocol (µTP)</link>.
147 options.watch-dir = mkOption {
149 default = "${cfg.home}/${watchDir}";
150 description = "Watch a directory for torrent files and add them to transmission.";
152 options.watch-dir-enabled = mkOption {
155 description = ''Whether to enable the
156 <xref linkend="opt-services.transmission.settings.watch-dir"/>.
159 options.trash-original-torrent-files = mkOption {
162 description = ''Whether to delete torrents added from the
163 <xref linkend="opt-services.transmission.settings.watch-dir"/>.
169 downloadDirPermissions = mkOption {
174 The permissions set by <literal>systemd.activationScripts.transmission-daemon</literal>
175 on the directories <xref linkend="opt-services.transmission.settings.download-dir"/>
176 and <xref linkend="opt-services.transmission.settings.incomplete-dir"/>.
177 Note that you may also want to change
178 <xref linkend="opt-services.transmission.settings.umask"/>.
184 default = "/var/lib/transmission";
186 The directory where Transmission will create <literal>${settingsDir}</literal>.
187 as well as <literal>${downloadsDir}/</literal> unless
188 <xref linkend="opt-services.transmission.settings.download-dir"/> is changed,
189 and <literal>${incompleteDir}/</literal> unless
190 <xref linkend="opt-services.transmission.settings.incomplete-dir"/> is changed.
196 default = "transmission";
197 description = "User account under which Transmission runs.";
202 default = "transmission";
203 description = "Group account under which Transmission runs.";
206 credentialsFile = mkOption {
209 Path to a JSON file to be merged with the settings.
210 Useful to merge a file which is better kept out of the Nix store
211 because it contains sensible data like
212 <xref linkend="opt-services.transmission.settings.rpc-password"/>.
214 default = "/dev/null";
215 example = "/var/lib/secrets/transmission/settings.json";
218 openPeerPorts = mkEnableOption "opening of the peer port(s) in the firewall";
220 openRPCPort = mkEnableOption "opening of the RPC port in the firewall";
222 performanceNetParameters = mkEnableOption ''tweaking of kernel parameters
223 to open many more connections at the same time.
225 Note that you may also want to increase
226 <xref linkend="opt-services.transmission.settings.peer-limit-global"/>.
227 And be aware that these settings are quite aggressive
228 and might not suite your regular desktop use.
229 For instance, SSH sessions may time out more easily'';
233 config = mkIf cfg.enable {
234 # Note that using systemd.tmpfiles would not work here
235 # because it would fail when creating a directory
236 # with a different owner than its parent directory, by saying:
237 # Detected unsafe path transition /home/foo → /home/foo/Downloads during canonicalization of /home/foo/Downloads
238 # when /home/foo is not owned by cfg.user.
239 # Note also that using an ExecStartPre= wouldn't work either
240 # because BindPaths= needs these directories before.
241 system.activationScripts.transmission-daemon = ''
242 install -d -m 700 '${cfg.home}/${settingsDir}'
243 chown -R '${cfg.user}:${cfg.group}' ${cfg.home}/${settingsDir}
244 install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.download-dir}'
245 '' + optionalString cfg.settings.incomplete-dir-enabled ''
246 install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.incomplete-dir}'
247 '' + optionalString cfg.settings.watch-dir-enabled ''
248 install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.watch-dir}'
251 systemd.services.transmission = {
252 description = "Transmission BitTorrent Service";
253 after = [ "network.target" ] ++ optional apparmor.enable "apparmor.service";
254 requires = optional apparmor.enable "apparmor.service";
255 wantedBy = [ "multi-user.target" ];
256 environment.CURL_CA_BUNDLE = etc."ssl/certs/ca-certificates.crt".source;
259 # Use "+" because credentialsFile may not be accessible to User= or Group=.
260 ExecStartPre = [("+" + pkgs.writeShellScript "transmission-prestart" ''
261 set -eu${lib.optionalString (cfg.settings.message-level >= 3) "x"}
262 ${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' |
263 install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' /dev/stdin \
264 '${cfg.home}/${settingsDir}/settings.json'
266 ExecStart="${pkgs.transmission}/bin/transmission-daemon -f";
267 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
270 # Create rootDir in the host's mount namespace.
271 RuntimeDirectory = [(baseNameOf rootDir)];
272 RuntimeDirectoryMode = "755";
273 # Avoid mounting rootDir in the own rootDir of ExecStart='s mount namespace.
274 InaccessiblePaths = ["-+${rootDir}"];
275 # This is for BindPaths= and BindReadOnlyPaths=
276 # to allow traversal of directories they create in RootDirectory=.
278 # Using RootDirectory= makes it possible
279 # to use the same paths download-dir/incomplete-dir
280 # (which appear in user's interfaces) without requiring cfg.user
281 # to have access to their parent directories,
282 # by using BindPaths=/BindReadOnlyPaths=.
283 # Note that TemporaryFileSystem= could have been used instead
284 # but not without adding some BindPaths=/BindReadOnlyPaths=
285 # that would only be needed for ExecStartPre=,
286 # because RootDirectoryStartOnly=true would not help.
287 RootDirectory = rootDir;
288 RootDirectoryStartOnly = true;
291 [ "${cfg.home}/${settingsDir}"
292 cfg.settings.download-dir
294 optional cfg.settings.incomplete-dir-enabled
295 cfg.settings.incomplete-dir ++
296 optional (cfg.settings.watch-dir-enabled && cfg.settings.trash-original-torrent-files)
297 cfg.settings.watch-dir;
298 BindReadOnlyPaths = [
299 # No confinement done of /nix/store here like in systemd-confinement.nix,
300 # an AppArmor profile is provided to get a confinement based upon paths and rights.
305 optional (cfg.settings.script-torrent-done-enabled &&
306 cfg.settings.script-torrent-done-filename != null)
307 cfg.settings.script-torrent-done-filename ++
308 optional (cfg.settings.watch-dir-enabled && !cfg.settings.trash-original-torrent-files)
309 cfg.settings.watch-dir;
310 # The following options are only for optimizing:
311 # systemd-analyze security transmission
312 AmbientCapabilities = "";
313 CapabilityBoundingSet = "";
314 # ProtectClock= adds DeviceAllow=char-rtc r
316 LockPersonality = true;
317 MemoryDenyWriteExecute = true;
318 NoNewPrivileges = true;
319 PrivateDevices = true;
320 PrivateMounts = true;
321 PrivateNetwork = mkDefault false;
325 ProtectControlGroups = true;
326 # ProtectHome=true would not allow BindPaths= to work accross /home,
327 # and ProtectHome=tmpfs would break statfs(),
328 # preventing transmission-daemon to report the available free space.
329 # However, RootDirectory= is used, so this is not a security concern
330 # since there would be nothing in /home but any BindPaths= wanted by the user.
331 ProtectHome = "read-only";
332 ProtectHostname = true;
333 ProtectKernelLogs = true;
334 ProtectKernelModules = true;
335 ProtectKernelTunables = true;
336 ProtectSystem = "strict";
338 # AF_UNIX may become usable one day:
339 # https://github.com/transmission/transmission/issues/441
340 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
341 RestrictNamespaces = true;
342 RestrictRealtime = true;
343 RestrictSUIDSGID = true;
346 # Groups in @system-service which do not contain a syscall
347 # listed by perf stat -e 'syscalls:sys_enter_*' transmission-daemon -f
348 # in tests, and seem likely not necessary for transmission-daemon.
349 "~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources" "~@setuid" "~@timer"
350 # In the @privileged group, but reached when querying infos through RPC (eg. with stig).
353 SystemCallArchitectures = "native";
354 SystemCallErrorNumber = "EPERM";
358 # It's useful to have transmission in path, e.g. for remote control
359 environment.systemPackages = [ pkgs.transmission ];
361 users.users = optionalAttrs (cfg.user == "transmission") ({
364 uid = config.ids.uids.transmission;
365 description = "Transmission BitTorrent user";
370 users.groups = optionalAttrs (cfg.group == "transmission") ({
372 gid = config.ids.gids.transmission;
376 networking.firewall = mkMerge [
377 (mkIf cfg.openPeerPorts (
378 if cfg.settings.peer-port-random-on-start
380 { allowedTCPPortRanges =
381 [ { from = cfg.settings.peer-port-random-low;
382 to = cfg.settings.peer-port-random-high;
385 allowedUDPPortRanges =
386 [ { from = cfg.settings.peer-port-random-low;
387 to = cfg.settings.peer-port-random-high;
392 { allowedTCPPorts = [ cfg.settings.peer-port ];
393 allowedUDPPorts = [ cfg.settings.peer-port ];
396 (mkIf cfg.openRPCPort { allowedTCPPorts = [ cfg.settings.rpc-port ]; })
399 boot.kernel.sysctl = mkMerge [
400 # Transmission uses a single UDP socket in order to implement multiple uTP sockets,
401 # and thus expects large kernel buffers for the UDP socket,
402 # https://trac.transmissionbt.com/browser/trunk/libtransmission/tr-udp.c?rev=11956.
403 # at least up to the values hardcoded here:
404 (mkIf cfg.settings.utp-enabled {
405 "net.core.rmem_max" = mkDefault "4194304"; # 4MB
406 "net.core.wmem_max" = mkDefault "1048576"; # 1MB
408 (mkIf cfg.performanceNetParameters {
409 # Increase the number of available source (local) TCP and UDP ports to 49151.
410 # Usual default is 32768 60999, ie. 28231 ports.
411 # Find out your current usage with: ss -s
412 "net.ipv4.ip_local_port_range" = mkDefault "16384 65535";
413 # Timeout faster generic TCP states.
414 # Usual default is 600.
415 # Find out your current usage with: watch -n 1 netstat -nptuo
416 "net.netfilter.nf_conntrack_generic_timeout" = mkDefault 60;
417 # Timeout faster established but inactive connections.
418 # Usual default is 432000.
419 "net.netfilter.nf_conntrack_tcp_timeout_established" = mkDefault 600;
420 # Clear immediately TCP states after timeout.
421 # Usual default is 120.
422 "net.netfilter.nf_conntrack_tcp_timeout_time_wait" = mkDefault 1;
423 # Increase the number of trackable connections.
424 # Usual default is 262144.
425 # Find out your current usage with: conntrack -C
426 "net.netfilter.nf_conntrack_max" = mkDefault 1048576;
430 security.apparmor.policies."bin.transmission-daemon".profile = ''
431 include <tunables/global>
432 ${pkgs.transmission}/bin/transmission-daemon {
433 include <abstractions/base>
434 include <abstractions/nameservice>
435 include <abstractions/ssl_certs>
436 include "${pkgs.apparmorRulesFromClosure {} [pkgs.transmission]}"
437 include <local/bin.transmission-daemon>
439 r @{PROC}/sys/kernel/random/uuid,
440 r @{PROC}/sys/vm/overcommit_memory,
441 r @{PROC}/@{pid}/environ,
442 r @{PROC}/@{pid}/mounts,
443 rwk /tmp/tr_session_id_*,
444 r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
445 r /run/systemd/resolve/stub-resolv.conf,
447 owner rw ${cfg.home}/${settingsDir}/**,
448 rw ${cfg.settings.download-dir}/**,
449 ${optionalString cfg.settings.incomplete-dir-enabled ''
450 rw ${cfg.settings.incomplete-dir}/**,
452 ${optionalString cfg.settings.watch-dir-enabled ''
453 r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
456 rw ${cfg.settings.download-dir}/**,
457 ${optionalString cfg.settings.incomplete-dir-enabled ''
458 rw ${cfg.settings.incomplete-dir}/**,
460 ${optionalString cfg.settings.watch-dir-enabled ''
461 r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
465 ${optionalString (cfg.settings.script-torrent-done-enabled &&
466 cfg.settings.script-torrent-done-filename != null) ''
467 # Stack transmission_directories profile on top of
468 # any existing profile for script-torrent-done-filename
469 # FIXME: to be tested as I'm not sure it works well with NoNewPrivileges=
470 # https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorStacking#seccomp-and-no_new_privs
471 px ${cfg.settings.script-torrent-done-filename} -> &@{dirs},
475 security.apparmor.includes."local/bin.transmission-daemon" = "";
478 meta.maintainers = with lib.maintainers; [ julm ];