]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/torrent/transmission.nix
sourcehut: type-check migrate-on-upgrade
[sourcephile-nix.git] / nixos / modules / services / torrent / transmission.nix
1 { config, lib, pkgs, options, ... }:
2
3 with lib;
4
5 let
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;
16 in
17 {
18 imports = [
19 (mkRenamedOptionModule ["services" "transmission" "port"]
20 ["services" "transmission" "settings" "rpc-port"])
21 (mkAliasOptionModule ["services" "transmission" "openFirewall"]
22 ["services" "transmission" "openPeerPorts"])
23 ];
24 options = {
25 services.transmission = {
26 enable = mkEnableOption ''the headless Transmission BitTorrent daemon.
27
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.
31
32 Torrents are downloaded to <xref linkend="opt-services.transmission.home"/>/${downloadsDir} by default and are
33 accessible to users in the "transmission" group'';
34
35 settings = mkOption {
36 description = ''
37 Settings whose options overwrite fields in
38 <literal>.config/transmission-daemon/settings.json</literal>
39 (each time the service starts).
40
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.
43 '';
44 default = {};
45 type = types.submodule {
46 freeformType = settingsFormat.type;
47 options.download-dir = mkOption {
48 type = types.path;
49 default = "${cfg.home}/${downloadsDir}";
50 description = "Directory where to download torrents.";
51 };
52 options.incomplete-dir = mkOption {
53 type = types.path;
54 default = "${cfg.home}/${incompleteDir}";
55 description = ''
56 When enabled with
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"/>.
62 '';
63 };
64 options.incomplete-dir-enabled = mkOption {
65 type = types.bool;
66 default = true;
67 description = "";
68 };
69 options.message-level = mkOption {
70 type = types.ints.between 0 2;
71 default = 2;
72 description = "Set verbosity of transmission messages.";
73 };
74 options.peer-port = mkOption {
75 type = types.port;
76 default = 51413;
77 description = "The peer port to listen for incoming connections.";
78 };
79 options.peer-port-random-high = mkOption {
80 type = types.port;
81 default = 65535;
82 description = ''
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.
85 '';
86 };
87 options.peer-port-random-low = mkOption {
88 type = types.port;
89 default = 65535;
90 description = ''
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.
93 '';
94 };
95 options.peer-port-random-on-start = mkOption {
96 type = types.bool;
97 default = false;
98 description = "Randomize the peer port.";
99 };
100 options.rpc-bind-address = mkOption {
101 type = types.str;
102 default = "127.0.0.1";
103 example = "0.0.0.0";
104 description = ''
105 Where to listen for RPC connections.
106 Use \"0.0.0.0\" to listen on all interfaces.
107 '';
108 };
109 options.rpc-port = mkOption {
110 type = types.port;
111 default = 9091;
112 description = "The RPC port to listen to.";
113 };
114 options.script-torrent-done-enabled = mkOption {
115 type = types.bool;
116 default = false;
117 description = ''
118 Whether to run
119 <xref linkend="opt-services.transmission.settings.script-torrent-done-filename"/>
120 at torrent completion.
121 '';
122 };
123 options.script-torrent-done-filename = mkOption {
124 type = types.nullOr types.path;
125 default = null;
126 description = "Executable to be run at torrent completion.";
127 };
128 options.umask = mkOption {
129 type = types.int;
130 default = 2;
131 description = ''
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.
138 '';
139 };
140 options.utp-enabled = mkOption {
141 type = types.bool;
142 default = true;
143 description = ''
144 Whether to enable <link xlink:href="http://en.wikipedia.org/wiki/Micro_Transport_Protocol">Micro Transport Protocol (µTP)</link>.
145 '';
146 };
147 options.watch-dir = mkOption {
148 type = types.path;
149 default = "${cfg.home}/${watchDir}";
150 description = "Watch a directory for torrent files and add them to transmission.";
151 };
152 options.watch-dir-enabled = mkOption {
153 type = types.bool;
154 default = false;
155 description = ''Whether to enable the
156 <xref linkend="opt-services.transmission.settings.watch-dir"/>.
157 '';
158 };
159 options.trash-original-torrent-files = mkOption {
160 type = types.bool;
161 default = false;
162 description = ''Whether to delete torrents added from the
163 <xref linkend="opt-services.transmission.settings.watch-dir"/>.
164 '';
165 };
166 };
167 };
168
169 downloadDirPermissions = mkOption {
170 type = types.str;
171 default = "770";
172 example = "775";
173 description = ''
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"/>.
179 '';
180 };
181
182 home = mkOption {
183 type = types.path;
184 default = "/var/lib/transmission";
185 description = ''
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.
191 '';
192 };
193
194 user = mkOption {
195 type = types.str;
196 default = "transmission";
197 description = "User account under which Transmission runs.";
198 };
199
200 group = mkOption {
201 type = types.str;
202 default = "transmission";
203 description = "Group account under which Transmission runs.";
204 };
205
206 credentialsFile = mkOption {
207 type = types.path;
208 description = ''
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"/>.
213 '';
214 default = "/dev/null";
215 example = "/var/lib/secrets/transmission/settings.json";
216 };
217
218 openPeerPorts = mkEnableOption "opening of the peer port(s) in the firewall";
219
220 openRPCPort = mkEnableOption "opening of the RPC port in the firewall";
221
222 performanceNetParameters = mkEnableOption ''tweaking of kernel parameters
223 to open many more connections at the same time.
224
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'';
230 };
231 };
232
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}'
249 '';
250
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;
257
258 serviceConfig = {
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'
265 '')];
266 ExecStart="${pkgs.transmission}/bin/transmission-daemon -f";
267 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
268 User = cfg.user;
269 Group = cfg.group;
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=.
277 UMask = "0066";
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;
289 MountAPIVFS = true;
290 BindPaths =
291 [ "${cfg.home}/${settingsDir}"
292 cfg.settings.download-dir
293 ] ++
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.
301 builtins.storeDir
302 "/etc"
303 "/run"
304 ] ++
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
315 DeviceAllow = "";
316 LockPersonality = true;
317 MemoryDenyWriteExecute = true;
318 NoNewPrivileges = true;
319 PrivateDevices = true;
320 PrivateMounts = true;
321 PrivateNetwork = mkDefault false;
322 PrivateTmp = true;
323 PrivateUsers = true;
324 ProtectClock = true;
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";
337 RemoveIPC = true;
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;
344 SystemCallFilter = [
345 "@system-service"
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).
351 "quotactl"
352 ];
353 SystemCallArchitectures = "native";
354 SystemCallErrorNumber = "EPERM";
355 };
356 };
357
358 # It's useful to have transmission in path, e.g. for remote control
359 environment.systemPackages = [ pkgs.transmission ];
360
361 users.users = optionalAttrs (cfg.user == "transmission") ({
362 transmission = {
363 group = cfg.group;
364 uid = config.ids.uids.transmission;
365 description = "Transmission BitTorrent user";
366 home = cfg.home;
367 };
368 });
369
370 users.groups = optionalAttrs (cfg.group == "transmission") ({
371 transmission = {
372 gid = config.ids.gids.transmission;
373 };
374 });
375
376 networking.firewall = mkMerge [
377 (mkIf cfg.openPeerPorts (
378 if cfg.settings.peer-port-random-on-start
379 then
380 { allowedTCPPortRanges =
381 [ { from = cfg.settings.peer-port-random-low;
382 to = cfg.settings.peer-port-random-high;
383 }
384 ];
385 allowedUDPPortRanges =
386 [ { from = cfg.settings.peer-port-random-low;
387 to = cfg.settings.peer-port-random-high;
388 }
389 ];
390 }
391 else
392 { allowedTCPPorts = [ cfg.settings.peer-port ];
393 allowedUDPPorts = [ cfg.settings.peer-port ];
394 }
395 ))
396 (mkIf cfg.openRPCPort { allowedTCPPorts = [ cfg.settings.rpc-port ]; })
397 ];
398
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
407 })
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;
427 })
428 ];
429
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>
438
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,
446
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}/**,
451 ''}
452 ${optionalString cfg.settings.watch-dir-enabled ''
453 r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
454 ''}
455 profile dirs {
456 rw ${cfg.settings.download-dir}/**,
457 ${optionalString cfg.settings.incomplete-dir-enabled ''
458 rw ${cfg.settings.incomplete-dir}/**,
459 ''}
460 ${optionalString cfg.settings.watch-dir-enabled ''
461 r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
462 ''}
463 }
464
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},
472 ''}
473 }
474 '';
475 security.apparmor.includes."local/bin.transmission-daemon" = "";
476 };
477
478 meta.maintainers = with lib.maintainers; [ julm ];
479 }