Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2511.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@

- [Newt](https://github.com/fosrl/newt), a fully user space WireGuard tunnel client and TCP/UDP proxy, designed to securely expose private resources controlled by Pangolin. Available as [services.newt](options.html#opt-services.newt.enable).

- [qBittorrent](https://www.qbittorrent.org/), is a bittorrent client programmed in C++ / Qt that uses libtorrent by Arvid Norberg. Available as [services.qbittorrent](#opt-services.qbittorrent.enable).

- [Szurubooru](https://github.com/rr-/szurubooru), an image board engine inspired by services such as Danbooru, dedicated for small and medium communities. Available as [services.szurubooru](#opt-services.szurubooru.enable).

- The [Neat IP Address Planner](https://spritelink.github.io/NIPAP/) (NIPAP) can now be enabled through [services.nipap.enable](#opt-services.nipap.enable).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,7 @@
./services/torrent/magnetico.nix
./services/torrent/opentracker.nix
./services/torrent/peerflix.nix
./services/torrent/qbittorrent.nix
./services/torrent/rtorrent.nix
./services/torrent/torrentstream.nix
./services/torrent/transmission.nix
Expand Down
238 changes: 238 additions & 0 deletions nixos/modules/services/torrent/qbittorrent.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
{
config,
pkgs,
lib,
utils,
...
}:
let
cfg = config.services.qbittorrent;
inherit (builtins) concatStringsSep isAttrs isString;
inherit (lib)
literalExpression
getExe
mkEnableOption
mkOption
mkPackageOption
mkIf
maintainers
escape
collect
mapAttrsRecursive
optionals
;
inherit (lib.types)
str
port
path
nullOr
listOf
attrsOf
anything
submodule
;
inherit (lib.generators) toINI mkKeyValueDefault mkValueStringDefault;
gendeepINI = toINI {
mkKeyValue =
let
sep = "=";
in
k: v:
if isAttrs v then
concatStringsSep "\n" (
collect isString (
mapAttrsRecursive (
path: value:
"${escape [ sep ] (concatStringsSep "\\" ([ k ] ++ path))}${sep}${mkValueStringDefault { } value}"
) v
)
)
else
mkKeyValueDefault { } sep k v;
};
configFile = pkgs.writeText "qBittorrent.conf" (gendeepINI cfg.serverConfig);
in
{
options.services.qbittorrent = {
enable = mkEnableOption "qbittorrent, BitTorrent client";

package = mkPackageOption pkgs "qbittorrent-nox" { };

user = mkOption {
type = str;
default = "qbittorrent";
description = "User account under which qbittorrent runs.";
};

group = mkOption {
type = str;
default = "qbittorrent";
description = "Group under which qbittorrent runs.";
};

profileDir = mkOption {
type = path;
default = "/var/lib/qBittorrent/";
description = "the path passed to qbittorrent via --profile.";
};

openFirewall = mkEnableOption "opening both the webuiPort and torrentPort over TCP in the firewall";

webuiPort = mkOption {
default = 8080;
type = nullOr port;
description = "the port passed to qbittorrent via `--webui-port`";
};

torrentingPort = mkOption {
default = null;
type = nullOr port;
description = "the port passed to qbittorrent via `--torrenting-port`";
};

serverConfig = mkOption {
default = { };
type = submodule {
freeformType = attrsOf (attrsOf 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 = literalExpression ''
{
LegalNotice.Accepted = true;
Preferences = {
WebUI = {
Username = "user";
Password_PBKDF2 = "generated ByteArray.";
};
General.Locale = "en";
};
}
'';
};

extraArgs = mkOption {
type = listOf 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 = 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+" = mkIf (cfg.serverConfig != { }) {
mode = "1400";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that qBittorrent.conf is created with mode = 1400. Why is the sticky bit set?

Also, it looks like permissions are ignored for symlinks anyway: https://www.man7.org/linux/man-pages/man5/tmpfiles.d.5.html#:~:text=L%2C%20L+%2C%20L?.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the sticky bit set?

if my memory is right, setting it made qbittorrent unable to overwrite the symlink or otherwise within it with a directory/file.

it looks like permissions are ignored for symlinks anyway

if this is the case for the stickybit also then I'm not sure why I observed this behavior.

anyone willing to confirm qbittorrents behavior is consistent with/without this set, please do share.

Copy link
Contributor

@undefined-landmark undefined-landmark Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A PR was made a couple days ago, changing this behavior. We could continue this discussion there: #482534.

inherit (cfg) user group;
argument = "${configFile}";
};
};
};
services.qbittorrent = {
description = "qbittorrent BitTorrent client";
wants = [ "network-online.target" ];
after = [
"local-fs.target"
"network-online.target"
"nss-lookup.target"
];
wantedBy = [ "multi-user.target" ];
restartTriggers = optionals (cfg.serverConfig != { }) [ configFile ];

serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = utils.escapeSystemdExecArgs (
[
(getExe cfg.package)
"--profile=${cfg.profileDir}"
]
++ optionals (cfg.webuiPort != null) [ "--webui-port=${toString cfg.webuiPort}" ]
++ optionals (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 = mkIf (cfg.user == "qbittorrent") {
qbittorrent = {
inherit (cfg) group;
isSystemUser = true;
};
};
groups = mkIf (cfg.group == "qbittorrent") { qbittorrent = { }; };
};

networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall (
optionals (cfg.webuiPort != null) [ cfg.webuiPort ]
++ optionals (cfg.torrentingPort != null) [ cfg.torrentingPort ]
);
};
meta.maintainers = with maintainers; [ fsnkty ];
}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,7 @@ in
public-inbox = runTest ./public-inbox.nix;
pufferpanel = runTest ./pufferpanel.nix;
pulseaudio = discoverTests (import ./pulseaudio.nix);
qbittorrent = runTest ./qbittorrent.nix;
qboot = handleTestOn [ "x86_64-linux" "i686-linux" ] ./qboot.nix { };
qemu-vm-restrictnetwork = handleTest ./qemu-vm-restrictnetwork.nix { };
qemu-vm-volatile-root = runTest ./qemu-vm-volatile-root.nix;
Expand Down
Loading
Loading