{ config , pkgs , lib , utils , ... }: let cfg = config.services.qbittorrent-nox; gendeepINI = lib.generators.toINI { mkKeyValue = let sep = "="; in k: v: if builtins.isAttrs v then builtins.concatStringsSep "\n" ( lib.collect builtins.isString ( lib.mapAttrsRecursive ( path: value: "${lib.escape [ sep ] (builtins.oncatStringsSep "\\" ([ k ] ++ path))}${sep}${lib.generators.mkValueStringDefault { } value}" ) v ) ) else lib.generators.mkKeyValueDefault { } sep k v; }; configFile = pkgs.writeText "qBittorrent.conf" (gendeepINI cfg.serverConfig); in { options.services.qbittorrent-nox = { enable = lib.mkEnableOption "qbittorrent, BitTorrent client"; package = lib.mkPackageOption pkgs "qbittorrent-nox" { }; user = lib.mkOption { type = lib.types.str; default = "qbittorrent"; description = "User account under which qbittorrent runs."; }; group = lib.mkOption { type = lib.types.str; default = "qbittorrent"; description = "Group under which qbittorrent runs."; }; profileDir = lib.mkOption { type = lib.types.path; default = "/var/lib/qBittorrent/"; description = "the path passed to qbittorrent via --profile."; }; openFirewall = lib.mkEnableOption "opening both the webuiPort and torrentPort over TCP in the firewall"; webuiPort = lib.mkOption { default = 8080; type = lib.types.nullOr lib.types.port; description = "the port passed to qbittorrent via `--webui-port`"; }; torrentingPort = lib.mkOption { default = null; type = lib.types.nullOr lib.types.port; description = "the port passed to qbittorrent via `--torrenting-port`"; }; serverConfig = lib.mkOption { type = lib.types.submodule { freeformType = lib.types.attrsOf (lib.types.attrsOf lib.types.anything); }; description = '' Free-form settings mapped to the `qBittorrent.conf` file in the profile. Refer to [Explanation-of-Options-in-qBittorrent](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent). 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. Alternatively you can run qBittorrent independently first and use its webUI to generate the format. Optionally an alternative webUI can be easily set. VueTorrent for example: ```nix { Preferences = { WebUI = { AlternativeUIEnabled = true; RootFolder = "''${pkgs.vuetorrent}/share/vuetorrent"; }; }; } ]; ``` ''; example = lib.literalExpression '' { LegalNotice.Accepted = true; Preferences = { WebUI = { Username = "user"; Password_PBKDF2 = "generated ByteArray."; }; General.Locale = "en"; }; } ''; }; extraArgs = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = '' 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. ''; example = [ "--confirm-legal-notice" ]; }; }; config = lib.mkIf cfg.enable { systemd = { tmpfiles.settings = { qbittorrent = { "${cfg.profileDir}/qBittorrent/"."d" = { mode = "755"; inherit (cfg) user group; }; "${cfg.profileDir}/qBittorrent/config/"."d" = { mode = "755"; inherit (cfg) user group; }; "${cfg.profileDir}/qBittorrent/config/qBittorrent.conf"."L+" = lib.mkIf (cfg.serverConfig != null) { mode = "1400"; inherit (cfg) user group; argument = "${configFile}"; }; }; }; services.qbittorrent-nox = { description = "qbittorrent BitTorrent client"; wants = [ "network-online.target" ]; after = [ "local-fs.target" "network-online.target" "nss-lookup.target" ]; wantedBy = [ "multi-user.target" ]; restartTriggers = lib.optional (cfg.serverConfig != null) configFile; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; ExecStart = utils.escapeSystemdExecArgs ( [ (lib.getExe cfg.package) "--profile=${cfg.profileDir}" ] ++ lib.optional (cfg.webuiPort != null) "--webui-port=${toString cfg.webuiPort}" ++ lib.optional (cfg.torrentingPort != null) "--torrenting-port=${toString cfg.torrentingPort}" ++ cfg.extraArgs ); TimeoutStopSec = 1800; # https://github.com/qbittorrent/qBittorrent/pull/6806#discussion_r121478661 PrivateTmp = false; PrivateNetwork = false; RemoveIPC = true; NoNewPrivileges = true; PrivateDevices = true; PrivateUsers = true; ProtectHome = "yes"; ProtectProc = "invisible"; ProcSubset = "pid"; ProtectSystem = "full"; ProtectClock = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectControlGroups = true; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" ]; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; LockPersonality = true; MemoryDenyWriteExecute = true; SystemCallArchitectures = "native"; CapabilityBoundingSet = ""; SystemCallFilter = [ "@system-service" ]; }; }; }; users = { users = lib.mkIf (cfg.user == "qbittorrent") { qbittorrent = { inherit (cfg) group; isSystemUser = true; }; }; groups = lib.mkIf (cfg.group == "qbittorrent") { qbittorrent = { }; }; }; networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall ( lib.optional (cfg.torrentingPort != null) cfg.torrentingPort ); }; }