]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/torrent/transmission.nix
transmission: improve the service
[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 apparmor = config.security.apparmor.enable;
8 stateDir = "/var/lib/transmission";
9 # TODO: switch to configGen.json once RFC0042 is implemented
10 settingsFile = pkgs.writeText "settings.json" (builtins.toJSON (cfg.settings // {
11 download-dir = "${stateDir}/Downloads";
12 incomplete-dir = "${stateDir}/.incomplete";
13 }));
14 settingsDir = ".config/transmission-daemon";
15 makeAbsolute = base: path:
16 if builtins.match "^/.*" path == null
17 then base+"/"+path else path;
18 in
19 {
20 options = {
21 services.transmission = {
22 enable = mkEnableOption ''
23 Whether or not to enable the headless Transmission BitTorrent daemon.
24
25 Transmission daemon can be controlled via the RPC interface using
26 transmission-remote, the WebUI (http://${cfg.settings.rpc-bind-address}:${toString cfg.settings.rpc-port}/ by default),
27 or other clients like stig or tremc.
28
29 Torrents are downloaded to ${cfg.settings.download-dir} by default and are
30 accessible to users in the "transmission" group.
31 '';
32
33 settings = mkOption rec {
34 # TODO: switch to types.config.json as prescribed by RFC0042 once it's implemented
35 type = types.attrs;
36 apply = attrs:
37 let super = recursiveUpdate default attrs; in
38 super // {
39 download-dir = makeAbsolute cfg.home super.download-dir;
40 incomplete-dir = makeAbsolute cfg.home super.incomplete-dir;
41 };
42 default =
43 {
44 download-dir = "${cfg.home}/Downloads";
45 incomplete-dir = "${cfg.home}/.incomplete";
46 incomplete-dir-enabled = true;
47 rpc-bind-address = "127.0.0.1";
48 rpc-port = 9091;
49 umask = 63; # 0o077 in decimal as expected by Transmission, obtained with: echo $((8#077))
50 };
51 example =
52 {
53 download-dir = "/srv/torrents/";
54 incomplete-dir = "/srv/torrents/.incomplete/";
55 incomplete-dir-enabled = true;
56 rpc-whitelist = "127.0.0.1,192.168.*.*";
57 };
58 description = ''
59 Attribute set whose fields overwrites fields in settings.json (each
60 time the service starts). String values must be quoted, integer and
61 boolean values must not.
62
63 See https://github.com/transmission/transmission/wiki/Editing-Configuration-Files
64 for documentation.
65 '';
66 };
67
68 downloadDirPermissions = mkOption {
69 type = types.str;
70 default = "770";
71 example = "775";
72 description = ''
73 The permissions set by the <literal>systemd-tmpfiles-setup</literal> service
74 on <literal>settings.download-dir</literal> and <literal>settings.incomplete-dir</literal>.
75 '';
76 };
77
78 port = mkOption {
79 type = types.port;
80 description = "TCP port number to run the RPC/web interface.";
81 };
82
83 home = mkOption {
84 type = types.path;
85 default = stateDir;
86 description = ''
87 The directory where Transmission will create <literal>.config/transmission-daemon/</literal>.
88 as well as <literal>Downloads/</literal> unless <literal>settings.download-dir</literal> is changed,
89 and <literal>.incomplete/</literal> unless <literal>settings.incomplete-dir</literal> is changed.
90 '';
91 };
92
93 user = mkOption {
94 type = types.str;
95 default = "transmission";
96 description = "User account under which Transmission runs.";
97 };
98
99 group = mkOption {
100 type = types.str;
101 default = "transmission";
102 description = "Group account under which Transmission runs.";
103 };
104
105 credentialsFile = mkOption {
106 type = types.path;
107 description = ''
108 Path to a JSON file to be merged with the settings.
109 Useful to merge a file which is better kept out of the Nix store
110 because it contains sensible data like <literal>rpc-password</literal>.
111 '';
112 default = "/dev/null";
113 example = "/var/lib/secrets/transmission/settings.json";
114 };
115
116 enableSandbox = mkOption {
117 default = false;
118 type = types.bool;
119 description = ''
120 Starting Transmission server with additional sandbox/hardening options.
121 '';
122 };
123 };
124 };
125
126 config = mkIf cfg.enable {
127 systemd.tmpfiles.rules =
128 optional (cfg.home != stateDir) "d '${cfg.home}/${settingsDir}' 700 '${cfg.user}' '${cfg.group}' - -"
129 ++ [ "d '${cfg.settings.download-dir}' '${cfg.downloadDirPermissions}' '${cfg.user}' '${cfg.group}' - -" ]
130 ++ optional cfg.settings.incomplete-dir-enabled
131 "d '${cfg.settings.incomplete-dir}' '${cfg.downloadDirPermissions}' '${cfg.user}' '${cfg.group}' - -";
132
133 assertions = [
134 { assertion = builtins.match "^/.*" cfg.home != null;
135 message = "`services.transmission.home' must be an absolute path.";
136 }
137 { assertion = types.port.check cfg.settings.rpc-port;
138 message = "${toString cfg.settings.rpc-port} is not a valid port number for `services.transmission.settings.rpc-port`.";
139 }
140 # In case both port and settings.rpc-port are explicitely defined: they must be the same.
141 { assertion = !options.services.transmission.port.isDefined || cfg.port == cfg.settings.rpc-port;
142 message = "`services.transmission.port' is not equal to `services.transmission.settings.rpc-port'";
143 }
144 ];
145
146 services.transmission.settings =
147 optionalAttrs options.services.transmission.port.isDefined { rpc-port = cfg.port; };
148
149 systemd.services.transmission = {
150 description = "Transmission BitTorrent Service";
151 after = [ "network.target" ] ++ optional apparmor "apparmor.service";
152 requires = optional apparmor "apparmor.service";
153 wantedBy = [ "multi-user.target" ];
154 preStart = ''
155 set -eux
156 ${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' >'${stateDir}/${settingsDir}/settings.json'
157 '';
158
159 serviceConfig = {
160 WorkingDirectory = stateDir;
161 ExecStart = "${pkgs.transmission}/bin/transmission-daemon -f";
162 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
163 User = cfg.user;
164 Group = cfg.group;
165 UMask = "0002";
166 StateDirectory = removePrefix "/var/lib/" stateDir + "/" + settingsDir;
167 StateDirectoryMode = "0700";
168 BindPaths =
169 optional (cfg.home != stateDir) "${cfg.home}/${settingsDir}:${stateDir}/${settingsDir}"
170 ++ [ "${cfg.settings.download-dir}:${stateDir}/Downloads" ]
171 ++ optional cfg.settings.incomplete-dir-enabled "${cfg.settings.incomplete-dir}:${stateDir}/.incomplete";
172 NoNewPrivileges = true;
173 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
174 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
175 } // optionalAttrs cfg.enableSandbox {
176 DevicePolicy = "closed";
177 LockPersonality = true;
178 MemoryDenyWriteExecute = true;
179 PrivateDevices = true;
180 PrivateMounts = true;
181 PrivateTmp = true;
182 ProtectControlGroups = true;
183 ProtectHome = mkDefault true;
184 ProtectHostname = true;
185 ProtectKernelModules = true;
186 ProtectKernelTunables = true;
187 ProtectSystem = mkDefault "strict";
188 ReadWritePaths = [ stateDir ];
189 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
190 RestrictRealtime = true;
191 RestrictSUIDSGID = true;
192 SystemCallArchitectures = "native";
193 };
194 };
195
196 # It's useful to have transmission in path, e.g. for remote control
197 environment.systemPackages = [ pkgs.transmission ];
198
199 users.users = optionalAttrs (cfg.user == "transmission") ({
200 transmission = {
201 group = cfg.group;
202 uid = config.ids.uids.transmission;
203 description = "Transmission BitTorrent user";
204 home = stateDir;
205 createHome = false;
206 };
207 });
208
209 users.groups = optionalAttrs (cfg.group == "transmission") ({
210 transmission = {
211 gid = config.ids.gids.transmission;
212 };
213 });
214
215 # AppArmor profile
216 security.apparmor.profiles = mkIf apparmor [
217 (pkgs.writeText "apparmor-transmission-daemon" ''
218 #include <tunables/global>
219
220 ${pkgs.transmission}/bin/transmission-daemon {
221 #include <abstractions/base>
222 #include <abstractions/nameservice>
223
224 ${getLib pkgs.glibc}/lib/*.so* mr,
225 ${getLib pkgs.libevent}/lib/libevent*.so* mr,
226 ${getLib pkgs.curl}/lib/libcurl*.so* mr,
227 ${getLib pkgs.openssl}/lib/libssl*.so* mr,
228 ${getLib pkgs.openssl}/lib/libcrypto*.so* mr,
229 ${getLib pkgs.zlib}/lib/libz*.so* mr,
230 ${getLib pkgs.libssh2}/lib/libssh2*.so* mr,
231 ${getLib pkgs.systemd}/lib/libsystemd*.so* mr,
232 ${getLib pkgs.xz}/lib/liblzma*.so* mr,
233 ${getLib pkgs.libgcrypt}/lib/libgcrypt*.so* mr,
234 ${getLib pkgs.libgpgerror}/lib/libgpg-error*.so* mr,
235 ${getLib pkgs.nghttp2}/lib/libnghttp2*.so* mr,
236 ${getLib pkgs.c-ares}/lib/libcares*.so* mr,
237 ${getLib pkgs.libcap}/lib/libcap*.so* mr,
238 ${getLib pkgs.attr}/lib/libattr*.so* mr,
239 ${getLib pkgs.lz4}/lib/liblz4*.so* mr,
240 ${getLib pkgs.libkrb5}/lib/lib*.so* mr,
241 ${getLib pkgs.keyutils}/lib/libkeyutils*.so* mr,
242 ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so.* mr,
243 ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so.* mr,
244 ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so.* mr,
245 ${getLib pkgs.gcc.cc.lib}/lib/libstdc++.so.* mr,
246 ${getLib pkgs.gcc.cc.lib}/lib/libgcc_s.so.* mr,
247
248 @{PROC}/sys/kernel/random/uuid r,
249 @{PROC}/sys/vm/overcommit_memory r,
250
251 ${pkgs.openssl.out}/etc/** r,
252 ${pkgs.transmission}/share/transmission/** r,
253
254 owner ${stateDir}/${settingsDir}/** rw,
255
256 ${stateDir}/Downloads/** rw,
257 ${optionalString cfg.settings.incomplete-dir-enabled ''
258 ${stateDir}/.incomplete/** rw,
259 ''}
260 }
261 '')
262 ];
263 };
264
265 }