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-2605.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

- [reaction](https://reaction.ppom.me/), a daemon that scans program outputs for repeated patterns, and takes action. A common usage is to scan ssh and webserver logs, and to ban hosts that cause multiple authentication errors. A modern alternative to fail2ban. Available as [services.reaction](#opt-services.reaction.enable).

- [qui](https://github.com/autobrr/qui), a modern alternative webUI for qBittorrent, with multi-instance support. Written in Go/React. Available as [services.qui](#opt-services.qui.enable).

- [LibreChat](https://www.librechat.ai/), open-source self-hostable ChatGPT clone with Agents and RAG APIs. Available as [services.librechat](#opt-services.librechat.enable).

- [DankMaterialShell](https://danklinux.com), a complete desktop shell for Wayland compositors built with Quickshell. Available as [programs.dms-shell](#opt-programs.dms-shell.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 @@ -1547,6 +1547,7 @@
./services/torrent/opentracker.nix
./services/torrent/peerflix.nix
./services/torrent/qbittorrent.nix
./services/torrent/qui.nix
./services/torrent/rtorrent.nix
./services/torrent/torrentstream.nix
./services/torrent/transmission.nix
Expand Down
191 changes: 191 additions & 0 deletions nixos/modules/services/torrent/qui.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
{
config,
lib,
pkgs,
...
}:

let
inherit (lib)
getExe
maintainers
mkEnableOption
mkIf
mkOption
mkPackageOption
;
inherit (lib.types)
bool
path
port
str
submodule
;
cfg = config.services.qui;

stateDir = "/var/lib/qui";
configFormat = pkgs.formats.toml { };
configFile = configFormat.generate "qui.toml" cfg.settings;
in
{
options = {
services.qui = {
enable = mkEnableOption "qui";

package = mkPackageOption pkgs "qui" { };

user = mkOption {
type = str;
default = "qui";
description = "User to run qui as.";
};

group = mkOption {
type = str;
default = "qui";
example = "torrents";
description = "Group to run qui as.";
};

openFirewall = mkOption {
type = bool;
default = false;
description = "Whether or not to open ports in the firewall for qui.";
};

secretFile = mkOption {
type = path;
example = "/run/secrets/qui-session.txt";
description = ''
Path to a file that contains the session secret. The session secret
can be generated with `openssl rand -hex 32`.
'';
};

settings = mkOption {
default = { };
example = {
port = 7777;
logLevel = "DEBUG";
metricsEnabled = true;
};
type = submodule {
freeformType = configFormat.type;
options = {
host = mkOption {
type = str;
default = "127.0.0.1";
description = "The host address qui listens on.";
};

port = mkOption {
type = port;
default = 7476;
description = "The port qui listens on.";
};
};
};
description = ''
qui configuration options.

Refer to the [template config](https://github.com/autobrr/qui/blob/main/internal/config/config.go)
in the source code for the available options.
The documentation contains the available [environment variables](https://getqui.com/docs/configuration/environment/),
this can be used to get an overview.
'';
};

};
};

config = mkIf cfg.enable {
assertions = [
{
assertion = !(cfg.settings ? sessionSecret);
message = ''
Session secrets should not be passed via settings, as
these are stored in the world-readable nix store.

Use the secretFile option instead.'';
}
];

systemd.services.qui = {
description = "qui: alternative qBittorrent webUI";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];

serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;

LoadCredential = "sessionSecret:${cfg.secretFile}";
Environment = [ "QUI__SESSION_SECRET_FILE=%d/sessionSecret" ];
StateDirectory = "qui";

ExecStartPre = ''
${pkgs.coreutils}/bin/install -m 600 '${configFile}' '%S/qui/config.toml'
'';
ExecStart = "${getExe cfg.package} serve --config-dir %S/qui";
Restart = "on-failure";

# Based on qbittorrent and nemorosa hardening settings
# Similar to what systemd hardening helper suggests
CapabilityBoundingSet = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateNetwork = false;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = "yes";
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
# This should allow for hardlinking to torrent client files
ProtectSystem = "full";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" ];
};
};

networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.settings.port ];
};

users = {
users = mkIf (cfg.user == "qui") {
qui = {
group = cfg.group;
description = "qui user";
isSystemUser = true;
home = stateDir;
};
};

groups = mkIf (cfg.group == "qui") {
qui = { };
};
};
};

meta.maintainers = with maintainers; [ undefined-landmark ];
}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,7 @@ in
qtile = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./qtile/default.nix;
qtile-extras = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./qtile-extras/default.nix;
quake3 = runTest ./quake3.nix;
qui = runTest ./qui.nix;
quicktun = runTest ./quicktun.nix;
quickwit = runTest ./quickwit.nix;
rabbitmq = runTest ./rabbitmq.nix;
Expand Down
47 changes: 47 additions & 0 deletions nixos/tests/qui.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{ lib, ... }:

{
name = "qui";
meta.maintainers = with lib.maintainers; [ undefined-landmark ];

nodes.machine =
{ pkgs, ... }:
let
# We create this secret in the Nix store (making it readable by everyone).
# DO NOT DO THIS OUTSIDE OF TESTS!!
testSecretFile = pkgs.writeText "session_secret" "not-secret";
in
{
services.qui = {
enable = true;
secretFile = testSecretFile;
};

# Use port other than default to test if settings options work.
specialisation.settingsPort.configuration = {
services.qui = {
enable = true;
secretFile = testSecretFile;
settings.port = 7777;
};
};
};

testScript =
{ nodes, ... }:
let
settingsPort = "${nodes.machine.system.build.toplevel}/specialisation/settingsPort";
in
# python
''
def test_webui(port):
machine.wait_for_unit("qui.service")
machine.wait_for_open_port(port)
machine.wait_until_succeeds(f"curl --fail http://localhost:{port}")

test_webui(7476)

machine.succeed("${settingsPort}/bin/switch-to-configuration test")
test_webui(7777)
'';
}
14 changes: 9 additions & 5 deletions pkgs/by-name/qu/qui/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
buildGoModule,
fetchFromGitHub,
stdenvNoCC,
nixosTests,
nix-update-script,
nodejs,
pnpm_9,
Expand Down Expand Up @@ -72,11 +73,14 @@ buildGoModule (finalAttrs: {
versionCheckProgramArg = "version";
doInstallCheck = true;

passthru.updateScript = nix-update-script {
extraArgs = [
"--subpackage"
"qui-web"
];
passthru = {
updateScript = nix-update-script {
extraArgs = [
"--subpackage"
"qui-web"
];
};
tests.testService = nixosTests.qui;
};

meta = {
Expand Down
Loading