1{
2 config,
3 pkgs,
4 lib,
5 utils,
6 ...
7}:
8let
9 cfg = config.services.qbittorrent;
10 inherit (builtins) concatStringsSep isAttrs isString;
11 inherit (lib)
12 literalExpression
13 getExe
14 mkEnableOption
15 mkOption
16 mkPackageOption
17 mkIf
18 maintainers
19 escape
20 collect
21 mapAttrsRecursive
22 optionals
23 ;
24 inherit (lib.types)
25 str
26 port
27 path
28 nullOr
29 listOf
30 attrsOf
31 anything
32 submodule
33 ;
34 inherit (lib.generators) toINI mkKeyValueDefault mkValueStringDefault;
35 gendeepINI = toINI {
36 mkKeyValue =
37 let
38 sep = "=";
39 in
40 k: v:
41 if isAttrs v then
42 concatStringsSep "\n" (
43 collect isString (
44 mapAttrsRecursive (
45 path: value:
46 "${escape [ sep ] (concatStringsSep "\\" ([ k ] ++ path))}${sep}${mkValueStringDefault { } value}"
47 ) v
48 )
49 )
50 else
51 mkKeyValueDefault { } sep k v;
52 };
53 configFile = pkgs.writeText "qBittorrent.conf" (gendeepINI cfg.serverConfig);
54in
55{
56 options.services.qbittorrent = {
57 enable = mkEnableOption "qbittorrent, BitTorrent client";
58
59 package = mkPackageOption pkgs "qbittorrent-nox" { };
60
61 user = mkOption {
62 type = str;
63 default = "qbittorrent";
64 description = "User account under which qbittorrent runs.";
65 };
66
67 group = mkOption {
68 type = str;
69 default = "qbittorrent";
70 description = "Group under which qbittorrent runs.";
71 };
72
73 profileDir = mkOption {
74 type = path;
75 default = "/var/lib/qBittorrent/";
76 description = "the path passed to qbittorrent via --profile.";
77 };
78
79 openFirewall = mkEnableOption "opening both the webuiPort and torrentPort over TCP in the firewall";
80
81 webuiPort = mkOption {
82 default = 8080;
83 type = nullOr port;
84 description = "the port passed to qbittorrent via `--webui-port`";
85 };
86
87 torrentingPort = mkOption {
88 default = null;
89 type = nullOr port;
90 description = "the port passed to qbittorrent via `--torrenting-port`";
91 };
92
93 serverConfig = mkOption {
94 default = { };
95 type = submodule {
96 freeformType = attrsOf (attrsOf anything);
97 };
98 description = ''
99 Free-form settings mapped to the `qBittorrent.conf` file in the profile.
100 Refer to [Explanation-of-Options-in-qBittorrent](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent).
101 The Password_PBKDF2 format is oddly unique, you will likely want to use [this tool](https://codeberg.org/feathecutie/qbittorrent_password) to generate the format.
102 Alternatively you can run qBittorrent independently first and use its webUI to generate the format.
103
104 Optionally an alternative webUI can be easily set. VueTorrent for example:
105 ```nix
106 {
107 Preferences = {
108 WebUI = {
109 AlternativeUIEnabled = true;
110 RootFolder = "''${pkgs.vuetorrent}/share/vuetorrent";
111 };
112 };
113 }
114 ];
115 ```
116 '';
117 example = literalExpression ''
118 {
119 LegalNotice.Accepted = true;
120 Preferences = {
121 WebUI = {
122 Username = "user";
123 Password_PBKDF2 = "generated ByteArray.";
124 };
125 General.Locale = "en";
126 };
127 }
128 '';
129 };
130
131 extraArgs = mkOption {
132 type = listOf str;
133 default = [ ];
134 description = ''
135 Extra arguments passed to qbittorrent. See `qbittorrent -h`, or the [source code](https://github.com/qbittorrent/qBittorrent/blob/master/src/app/cmdoptions.cpp), for the available arguments.
136 '';
137 example = [
138 "--confirm-legal-notice"
139 ];
140 };
141 };
142 config = mkIf cfg.enable {
143 systemd = {
144 tmpfiles.settings = {
145 qbittorrent = {
146 "${cfg.profileDir}/qBittorrent/"."d" = {
147 mode = "755";
148 inherit (cfg) user group;
149 };
150 "${cfg.profileDir}/qBittorrent/config/"."d" = {
151 mode = "755";
152 inherit (cfg) user group;
153 };
154 "${cfg.profileDir}/qBittorrent/config/qBittorrent.conf"."L+" = mkIf (cfg.serverConfig != { }) {
155 mode = "1400";
156 inherit (cfg) user group;
157 argument = "${configFile}";
158 };
159 };
160 };
161 services.qbittorrent = {
162 description = "qbittorrent BitTorrent client";
163 wants = [ "network-online.target" ];
164 after = [
165 "local-fs.target"
166 "network-online.target"
167 "nss-lookup.target"
168 ];
169 wantedBy = [ "multi-user.target" ];
170 restartTriggers = optionals (cfg.serverConfig != { }) [ configFile ];
171
172 serviceConfig = {
173 Type = "simple";
174 User = cfg.user;
175 Group = cfg.group;
176 ExecStart = utils.escapeSystemdExecArgs (
177 [
178 (getExe cfg.package)
179 "--profile=${cfg.profileDir}"
180 ]
181 ++ optionals (cfg.webuiPort != null) [ "--webui-port=${toString cfg.webuiPort}" ]
182 ++ optionals (cfg.torrentingPort != null) [ "--torrenting-port=${toString cfg.torrentingPort}" ]
183 ++ cfg.extraArgs
184 );
185 TimeoutStopSec = 1800;
186
187 # https://github.com/qbittorrent/qBittorrent/pull/6806#discussion_r121478661
188 PrivateTmp = false;
189
190 PrivateNetwork = false;
191 RemoveIPC = true;
192 NoNewPrivileges = true;
193 PrivateDevices = true;
194 PrivateUsers = true;
195 ProtectHome = "yes";
196 ProtectProc = "invisible";
197 ProcSubset = "pid";
198 ProtectSystem = "full";
199 ProtectClock = true;
200 ProtectHostname = true;
201 ProtectKernelLogs = true;
202 ProtectKernelModules = true;
203 ProtectKernelTunables = true;
204 ProtectControlGroups = true;
205 RestrictAddressFamilies = [
206 "AF_INET"
207 "AF_INET6"
208 "AF_NETLINK"
209 ];
210 RestrictNamespaces = true;
211 RestrictRealtime = true;
212 RestrictSUIDSGID = true;
213 LockPersonality = true;
214 MemoryDenyWriteExecute = true;
215 SystemCallArchitectures = "native";
216 CapabilityBoundingSet = "";
217 SystemCallFilter = [ "@system-service" ];
218 };
219 };
220 };
221
222 users = {
223 users = mkIf (cfg.user == "qbittorrent") {
224 qbittorrent = {
225 inherit (cfg) group;
226 isSystemUser = true;
227 };
228 };
229 groups = mkIf (cfg.group == "qbittorrent") { qbittorrent = { }; };
230 };
231
232 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall (
233 optionals (cfg.webuiPort != null) [ cfg.webuiPort ]
234 ++ optionals (cfg.torrentingPort != null) [ cfg.torrentingPort ]
235 );
236 };
237 meta.maintainers = with maintainers; [
238 fsnkty
239 undefined-landmark
240 ];
241}