1 { config, options, pkgs, lib, ... }:
7 cfg = config.services.rtorrent;
8 opt = options.services.rtorrent;
11 meta.maintainers = with lib.maintainers; [ thiagokokada ];
13 options.services.rtorrent = {
14 enable = mkEnableOption "rtorrent";
18 default = "/var/lib/rtorrent";
20 The directory where rtorrent stores its data files.
24 dataPermissions = mkOption {
29 Unix Permissions in octal on the rtorrent directory.
33 downloadDir = mkOption {
35 default = "${cfg.dataDir}/download";
36 defaultText = literalExpression ''"''${config.${opt.dataDir}}/download"'';
38 Where to put downloaded files.
46 User account under which rtorrent runs.
54 Group under which rtorrent runs.
58 package = mkPackageOption pkgs "rtorrent" { };
68 openFirewall = mkOption {
72 Whether to open the firewall for the port in {option}`services.rtorrent.port`.
76 rpcSocket = mkOption {
79 default = "/run/rtorrent/rpc.sock";
85 configText = mkOption {
89 The content of {file}`rtorrent.rc`. The [modernized configuration template](https://rtorrent-docs.readthedocs.io/en/latest/cookbook.html#modernized-configuration-template) with the values specified in this module will be prepended using mkBefore. You can use mkForce to overwrite the config completely.
94 config = mkIf cfg.enable {
96 users.groups = mkIf (cfg.group == "rtorrent") {
100 users.users = mkIf (cfg.user == "rtorrent") {
103 shell = pkgs.bashInteractive;
105 description = "rtorrent Daemon user";
110 networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ];
112 services.rtorrent.configText = mkBefore ''
113 # Instance layout (base paths)
114 method.insert = cfg.basedir, private|const|string, (cat,"${cfg.dataDir}/")
115 method.insert = cfg.watch, private|const|string, (cat,(cfg.basedir),"watch/")
116 method.insert = cfg.logs, private|const|string, (cat,(cfg.basedir),"log/")
117 method.insert = cfg.logfile, private|const|string, (cat,(cfg.logs),(system.time),".log")
118 method.insert = cfg.rpcsock, private|const|string, (cat,"${cfg.rpcSocket}")
120 # Create instance directories
121 execute.throw = sh, -c, (cat, "mkdir -p ", (cfg.basedir), "/session ", (cfg.watch), " ", (cfg.logs))
123 # Listening port for incoming peer traffic (fixed; you can also randomize it)
124 network.port_range.set = ${toString cfg.port}-${toString cfg.port}
125 network.port_random.set = no
127 # Tracker-less torrent and UDP tracker support
128 # (conservative settings for 'private' trackers, change for 'public')
129 dht.mode.set = disable
130 protocol.pex.set = no
131 trackers.use_udp.set = no
134 throttle.max_uploads.set = 100
135 throttle.max_uploads.global.set = 250
137 throttle.min_peers.normal.set = 20
138 throttle.max_peers.normal.set = 60
139 throttle.min_peers.seed.set = 30
140 throttle.max_peers.seed.set = 80
141 trackers.numwant.set = 80
143 protocol.encryption.set = allow_incoming,try_outgoing,enable_retry
145 # Limits for file handle resources, this is optimized for
146 # an `ulimit` of 1024 (a common default). You MUST leave
147 # a ceiling of handles reserved for rTorrent's internal needs!
148 network.http.max_open.set = 50
149 network.max_open_files.set = 600
150 network.max_open_sockets.set = 3000
152 # Memory resource usage (increase if you have a large number of items loaded,
153 # and/or the available resources to spend)
154 pieces.memory.max.set = 1800M
155 network.xmlrpc.size_limit.set = 4M
157 # Basic operational settings (no need to change these)
158 session.path.set = (cat, (cfg.basedir), "session/")
159 directory.default.set = "${cfg.downloadDir}"
160 log.execute = (cat, (cfg.logs), "execute.log")
161 ##log.xmlrpc = (cat, (cfg.logs), "xmlrpc.log")
162 execute.nothrow = sh, -c, (cat, "echo >", (session.path), "rtorrent.pid", " ", (system.pid))
164 # Other operational settings (check & adapt)
166 system.umask.set = 0027
167 system.cwd.set = (cfg.basedir)
168 network.http.dns_cache_timeout.set = 25
169 schedule2 = monitor_diskspace, 15, 60, ((close_low_diskspace, 1000M))
171 # Watch directories (add more as you like, but use unique schedule names)
172 #schedule2 = watch_start, 10, 10, ((load.start, (cat, (cfg.watch), "start/*.torrent")))
173 #schedule2 = watch_load, 11, 10, ((load.normal, (cat, (cfg.watch), "load/*.torrent")))
176 # Levels = critical error warn notice info debug
177 # Groups = connection_* dht_* peer_* rpc_* storage_* thread_* tracker_* torrent_*
178 print = (cat, "Logging to ", (cfg.logfile))
179 log.open_file = "log", (cfg.logfile)
180 log.add_output = "info", "log"
181 ##log.add_output = "tracker_debug", "log"
184 scgi_local = (cfg.rpcsock)
185 schedule = scgi_group,0,0,"execute.nothrow=chown,\":${cfg.group}\",(cfg.rpcsock)"
186 schedule = scgi_permission,0,0,"execute.nothrow=chmod,\"g+w,o=\",(cfg.rpcsock)"
192 rtorrentConfigFile = pkgs.writeText "rtorrent.rc" cfg.configText;
194 description = "rTorrent system service";
195 after = [ "network.target" ];
196 path = [ cfg.package pkgs.bash ];
197 wantedBy = [ "multi-user.target" ];
202 Restart = "on-failure";
203 WorkingDirectory = cfg.dataDir;
204 ExecStartPre=''${pkgs.bash}/bin/bash -c "if test -e ${cfg.dataDir}/session/rtorrent.lock && test -z $(${pkgs.procps}/bin/pidof rtorrent); then rm -f ${cfg.dataDir}/session/rtorrent.lock; fi"'';
205 ExecStart="${cfg.package}/bin/rtorrent -n -o system.daemon.set=true -o import=${rtorrentConfigFile}";
206 RuntimeDirectory = "rtorrent";
207 RuntimeDirectoryMode = 750;
209 CapabilityBoundingSet = [ "" ];
210 LockPersonality = true;
211 NoNewPrivileges = true;
212 PrivateDevices = true;
215 ProtectControlGroups = true;
216 # If the default user is changed, there is a good chance that they
217 # want to store data in e.g.: $HOME directory
218 # Relax hardening in this case
219 ProtectHome = lib.mkIf (cfg.user == "rtorrent") true;
220 ProtectHostname = true;
221 ProtectKernelLogs = true;
222 ProtectKernelModules = true;
223 ProtectKernelTunables = true;
224 ProtectProc = "invisible";
225 ProtectSystem = "full";
226 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
227 RestrictNamespaces = true;
228 RestrictRealtime = true;
229 RestrictSUIDSGID = true;
230 SystemCallArchitectures = "native";
231 SystemCallFilter = [ "@system-service" "~@privileged" ];
236 tmpfiles.rules = [ "d '${cfg.dataDir}' ${cfg.dataPermissions} ${cfg.user} ${cfg.group} -" ];