]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/torrent/transmission.nix
transmission: fix net.core.{r,w}mem_max
[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.enable;
9 stateDir = "/var/lib/transmission";
10 # TODO: switch to configGen.json once RFC0042 is implemented
11 settingsFile = pkgs.writeText "settings.json" (builtins.toJSON (cfg.settings // {
12 download-dir = "${stateDir}/Downloads";
13 incomplete-dir = "${stateDir}/.incomplete";
14 }));
15 settingsDir = ".config/transmission-daemon";
16 makeAbsolute = base: path:
17 if builtins.match "^/.*" path == null
18 then base+"/"+path else path;
19 in
20 {
21 options = {
22 services.transmission = {
23 enable = mkEnableOption ''
24 Whether or not to enable the headless Transmission BitTorrent daemon.
25
26 Transmission daemon can be controlled via the RPC interface using
27 transmission-remote, the WebUI (http://${cfg.settings.rpc-bind-address}:${toString cfg.settings.rpc-port}/ by default),
28 or other clients like stig or tremc.
29
30 Torrents are downloaded to ${cfg.settings.download-dir} by default and are
31 accessible to users in the "transmission" group.
32 '';
33
34 settings = mkOption rec {
35 # TODO: switch to types.config.json as prescribed by RFC0042 once it's implemented
36 type = types.attrs;
37 apply = attrs:
38 let super = recursiveUpdate default attrs; in
39 super // {
40 download-dir = makeAbsolute cfg.home super.download-dir;
41 incomplete-dir = makeAbsolute cfg.home super.incomplete-dir;
42 };
43 default =
44 {
45 download-dir = "${cfg.home}/Downloads";
46 incomplete-dir = "${cfg.home}/.incomplete";
47 incomplete-dir-enabled = true;
48 peer-port = 51413;
49 peer-port-random-high = 65535;
50 peer-port-random-low = 49152;
51 peer-port-random-on-start = false;
52 rpc-bind-address = "127.0.0.1";
53 rpc-port = 9091;
54 umask = 18; # 0o022 in decimal as expected by Transmission, obtained with: echo $((8#022))
55 utp-enabled = true;
56 };
57 example =
58 {
59 download-dir = "/srv/torrents/";
60 incomplete-dir = "/srv/torrents/.incomplete/";
61 incomplete-dir-enabled = true;
62 rpc-whitelist = "127.0.0.1,192.168.*.*";
63 };
64 description = ''
65 Attribute set whose fields overwrites fields in
66 <literal>.config/transmission-daemon/settings.json</literal>
67 (each time the service starts). String values must be quoted, integer and
68 boolean values must not.
69
70 See https://github.com/transmission/transmission/wiki/Editing-Configuration-Files
71 for documentation.
72 '';
73 };
74
75 downloadDirPermissions = mkOption {
76 type = types.str;
77 default = "770";
78 example = "775";
79 description = ''
80 The permissions set by the <literal>systemd-tmpfiles-setup</literal> service
81 on <link linkend="opt-services.transmission.settings">settings.download-dir</link>
82 and <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link>.
83 '';
84 };
85
86 port = mkOption {
87 type = types.port;
88 description = ''
89 TCP port number to run the RPC/web interface.
90
91 If instead you want to change the peer port,
92 use <link linkend="opt-services.transmission.settings">settings.peer-port</link>
93 or <link linkend="opt-services.transmission.settings">settings.peer-port-random-on-start</link>.
94 '';
95 };
96
97 home = mkOption {
98 type = types.path;
99 default = stateDir;
100 description = ''
101 The directory where Transmission will create <literal>.config/transmission-daemon/</literal>.
102 as well as <literal>Downloads/</literal> unless <link linkend="opt-services.transmission.settings">settings.download-dir</link> is changed,
103 and <literal>.incomplete/</literal> unless <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link> is changed.
104 '';
105 };
106
107 user = mkOption {
108 type = types.str;
109 default = "transmission";
110 description = "User account under which Transmission runs.";
111 };
112
113 group = mkOption {
114 type = types.str;
115 default = "transmission";
116 description = "Group account under which Transmission runs.";
117 };
118
119 credentialsFile = mkOption {
120 type = types.path;
121 description = ''
122 Path to a JSON file to be merged with the settings.
123 Useful to merge a file which is better kept out of the Nix store
124 because it contains sensible data like <link linkend="opt-services.transmission.settings">settings.rpc-password</link>.
125 '';
126 default = "/dev/null";
127 example = "/var/lib/secrets/transmission/settings.json";
128 };
129
130 openFirewall = mkEnableOption "Whether to automatically open the peer port(s) in the firewall.";
131 };
132 };
133
134 config = mkIf cfg.enable {
135 systemd.tmpfiles.rules =
136 optional (cfg.home != stateDir) "d '${cfg.home}/${settingsDir}' 700 '${cfg.user}' '${cfg.group}' - -"
137 ++ [ "d '${cfg.settings.download-dir}' '${cfg.downloadDirPermissions}' '${cfg.user}' '${cfg.group}' - -" ]
138 ++ optional cfg.settings.incomplete-dir-enabled
139 "d '${cfg.settings.incomplete-dir}' '${cfg.downloadDirPermissions}' '${cfg.user}' '${cfg.group}' - -";
140
141 assertions = [
142 { assertion = builtins.match "^/.*" cfg.home != null;
143 message = "`services.transmission.home' must be an absolute path.";
144 }
145 { assertion = types.port.check cfg.settings.rpc-port;
146 message = "${toString cfg.settings.rpc-port} is not a valid port number for `services.transmission.settings.rpc-port`.";
147 }
148 # In case both port and settings.rpc-port are explicitely defined: they must be the same.
149 { assertion = !options.services.transmission.port.isDefined || cfg.port == cfg.settings.rpc-port;
150 message = "`services.transmission.port' is not equal to `services.transmission.settings.rpc-port'";
151 }
152 ];
153
154 services.transmission.settings =
155 optionalAttrs options.services.transmission.port.isDefined { rpc-port = cfg.port; };
156
157 systemd.services.transmission = {
158 description = "Transmission BitTorrent Service";
159 after = [ "network.target" ] ++ optional apparmor "apparmor.service";
160 requires = optional apparmor "apparmor.service";
161 wantedBy = [ "multi-user.target" ];
162 environment.CURL_CA_BUNDLE = etc."ssl/certs/ca-certificates.crt".source;
163 preStart = ''
164 set -eux
165 ${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' >'${stateDir}/${settingsDir}/settings.json'
166 '';
167
168 serviceConfig = {
169 WorkingDirectory = stateDir;
170 ExecStart = "${pkgs.transmission}/bin/transmission-daemon -f";
171 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
172 User = cfg.user;
173 Group = cfg.group;
174 StateDirectory = removePrefix "/var/lib/" stateDir + "/" + settingsDir;
175 StateDirectoryMode = "0700";
176 BindPaths =
177 optional (cfg.home != stateDir) "${cfg.home}/${settingsDir}:${stateDir}/${settingsDir}"
178 ++ [ "${cfg.settings.download-dir}:${stateDir}/Downloads" ]
179 ++ optional cfg.settings.incomplete-dir-enabled "${cfg.settings.incomplete-dir}:${stateDir}/.incomplete";
180 # The following options give:
181 # systemd-analyze security transmission
182 # → Overall exposure level for transmission.service: 1.5 OK
183 AmbientCapabilities = "";
184 CapabilityBoundingSet = "";
185 LockPersonality = true;
186 MemoryDenyWriteExecute = true;
187 NoNewPrivileges = true;
188 PrivateDevices = true;
189 PrivateMounts = true;
190 PrivateNetwork = false;
191 PrivateTmp = true;
192 PrivateUsers = false;
193 ProtectClock = true;
194 ProtectControlGroups = true;
195 ProtectHome = mkDefault true;
196 ProtectHostname = true;
197 ProtectKernelLogs = true;
198 ProtectKernelModules = true;
199 ProtectKernelTunables = true;
200 ProtectSystem = mkDefault "strict";
201 ReadWritePaths = [ stateDir ];
202 RemoveIPC = true;
203 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
204 RestrictNamespaces = true;
205 RestrictRealtime = true;
206 RestrictSUIDSGID = true;
207 # In case transmission crashes with status=31/SYS,
208 # having systemd.coredump.enable = true
209 # and environment.enableDebugInfo = true
210 # enables to use coredumpctl debug to find the denied syscall.
211 SystemCallFilter = [
212 "@default"
213 "@aio"
214 "@basic-io"
215 #"@chown"
216 #"@clock"
217 #"@cpu-emulation"
218 #"@debug"
219 "@file-system"
220 "@io-event"
221 #"@ipc"
222 #"@keyring"
223 #"@memlock"
224 #"@module"
225 #"@mount"
226 "@network-io"
227 #"@obsolete"
228 #"@pkey"
229 #"@privileged"
230 # Reached when querying infos through RPC (eg. with stig)
231 "quotactl"
232 "@process"
233 #"@raw-io"
234 #"@reboot"
235 #"@resources"
236 #"@setuid"
237 "@signal"
238 #"@swap"
239 "@sync"
240 "@system-service"
241 "@timer"
242 ];
243 SystemCallArchitectures = "native";
244 UMask = "0077";
245 };
246 };
247
248 # It's useful to have transmission in path, e.g. for remote control
249 environment.systemPackages = [ pkgs.transmission ];
250
251 users.users = optionalAttrs (cfg.user == "transmission") ({
252 transmission = {
253 group = cfg.group;
254 uid = config.ids.uids.transmission;
255 description = "Transmission BitTorrent user";
256 home = stateDir;
257 createHome = false;
258 };
259 });
260
261 users.groups = optionalAttrs (cfg.group == "transmission") ({
262 transmission = {
263 gid = config.ids.gids.transmission;
264 };
265 });
266
267 networking.firewall = mkIf cfg.openFirewall (
268 if cfg.settings.peer-port-random-on-start
269 then
270 { allowedTCPPortRanges =
271 [ { from = cfg.settings.peer-port-random-low;
272 to = cfg.settings.peer-port-random-high;
273 }
274 ];
275 allowedUDPPortRanges =
276 [ { from = cfg.settings.peer-port-random-low;
277 to = cfg.settings.peer-port-random-high;
278 }
279 ];
280 }
281 else
282 { allowedTCPPorts = [ cfg.settings.peer-port ];
283 allowedUDPPorts = [ cfg.settings.peer-port ];
284 }
285 );
286
287 boot.kernel.sysctl = mkIf cfg.settings.utp-enabled {
288 "net.core.rmem_max" = mkDefault "4194304";
289 "net.core.wmem_max" = mkDefault "1048576";
290 };
291
292 security.apparmor.policies."bin/transmission-daemon".profile = ''
293 #include <tunables/global>
294
295 ${pkgs.transmission}/bin/transmission-daemon {
296 #include <abstractions/base>
297 #include <abstractions/nameservice>
298
299 ${getLib pkgs.stdenv.cc.cc}/lib/*.so* mr,
300 ${getLib pkgs.stdenv.cc.libc}/lib/*.so* mr,
301 ${getLib pkgs.libevent}/lib/libevent*.so* mr,
302 ${getLib pkgs.curl}/lib/libcurl*.so* mr,
303 ${getLib pkgs.openssl}/lib/libssl*.so* mr,
304 ${getLib pkgs.openssl}/lib/libcrypto*.so* mr,
305 ${getLib pkgs.zlib}/lib/libz*.so* mr,
306 ${getLib pkgs.libssh2}/lib/libssh2*.so* mr,
307 ${getLib pkgs.systemd}/lib/libsystemd*.so* mr,
308 ${getLib pkgs.xz}/lib/liblzma*.so* mr,
309 ${getLib pkgs.libgcrypt}/lib/libgcrypt*.so* mr,
310 ${getLib pkgs.libgpgerror}/lib/libgpg-error*.so* mr,
311 ${getLib pkgs.nghttp2}/lib/libnghttp2*.so* mr,
312 ${getLib pkgs.c-ares}/lib/libcares*.so* mr,
313 ${getLib pkgs.libcap}/lib/libcap*.so* mr,
314 ${getLib pkgs.attr}/lib/libattr*.so* mr,
315 ${getLib pkgs.lz4}/lib/liblz4*.so* mr,
316 ${getLib pkgs.libkrb5}/lib/lib*.so* mr,
317 ${getLib pkgs.keyutils}/lib/libkeyutils*.so* mr,
318 ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so* mr,
319 ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so* mr,
320 ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so* mr,
321
322 @{PROC}/sys/kernel/random/uuid r,
323 @{PROC}/sys/vm/overcommit_memory r,
324 #@{PROC}/@{pid}/environ r,
325 @{PROC}/@{pid}/mounts r,
326 /tmp/tr_session_id_* rwk,
327
328 ${pkgs.openssl.out}/etc/** r,
329 ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE} r,
330 ${pkgs.transmission}/share/transmission/** r,
331
332 owner ${stateDir}/${settingsDir}/** rw,
333
334 ${stateDir}/Downloads/** rw,
335 ${optionalString cfg.settings.incomplete-dir-enabled ''
336 ${stateDir}/.incomplete/** rw,
337 ''}
338 }
339 '';
340 };
341
342 meta.maintainers = with lib.maintainers; [ julm ];
343 }