diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 65290e41b983..fb6190063625 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -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). +- [rqbit](https://github.com/ikatson/rqbit), a bittorrent client written in Rust. It has HTTP API and Web UI, and can be used as a library. Available as [services.rqbit](#opt-services.rqbit.enable). + - [Tailscale Serve](https://tailscale.com/kb/1552/tailscale-services), configure Tailscale Serve for exposing local services to your tailnet. Available as [services.tailscale.serve](#opt-services.tailscale.serve.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). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 69e3d66046d4..03218f44081b 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1553,6 +1553,7 @@ ./services/torrent/peerflix.nix ./services/torrent/qbittorrent.nix ./services/torrent/qui.nix + ./services/torrent/rqbit.nix ./services/torrent/rtorrent.nix ./services/torrent/torrentstream.nix ./services/torrent/transmission.nix diff --git a/nixos/modules/services/torrent/rqbit.nix b/nixos/modules/services/torrent/rqbit.nix new file mode 100644 index 000000000000..5da9bf0473b4 --- /dev/null +++ b/nixos/modules/services/torrent/rqbit.nix @@ -0,0 +1,151 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + mkEnableOption + mkOption + types + mkPackageOption + mkIf + ; + + cfg = config.services.rqbit; + stateDir = "/var/lib/rqbit"; + defaultDownloadDir = "${stateDir}/downloads"; +in +{ + options.services.rqbit = { + enable = mkEnableOption "rqbit BitTorrent daemon"; + + package = mkPackageOption pkgs "rqbit" { }; + + user = mkOption { + type = types.str; + default = "rqbit"; + description = "User account under which rqbit runs."; + }; + + group = mkOption { + type = types.str; + default = "rqbit"; + description = "Group account under which rqbit runs."; + }; + + downloadDir = mkOption { + type = types.path; + default = defaultDownloadDir; + example = "/mnt/storage/torrents"; + description = "Directory where to download torrents."; + }; + + httpPort = mkOption { + type = types.port; + default = 3030; + description = "The listen port for the HTTP API."; + }; + + httpHost = mkOption { + type = types.str; + default = "127.0.0.1"; + example = "0.0.0.0"; + description = "The listen host for the HTTP API."; + }; + + peerPort = mkOption { + type = types.port; + default = 4240; + description = "The port to listen for incoming BitTorrent peer connections (TCP and uTP)."; + }; + + openFirewall = mkEnableOption "opening of the HTTP and Peer ports in the firewall"; + }; + + config = mkIf cfg.enable { + systemd.services.rqbit = { + description = "rqbit BitTorrent Service"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + HOME = stateDir; + RQBIT_HTTP_API_LISTEN_ADDR = "${ + if (lib.hasInfix ":" cfg.httpHost) then "[${cfg.httpHost}]" else cfg.httpHost + }:${toString cfg.httpPort}"; + RQBIT_LISTEN_PORT = toString cfg.peerPort; + RQBIT_SESSION_PERSISTENCE_LOCATION = stateDir; + }; + + serviceConfig = { + ExecStart = "${lib.getExe cfg.package} server start ${cfg.downloadDir}"; + User = cfg.user; + Group = cfg.group; + + StateDirectory = "rqbit"; + StateDirectoryMode = "0750"; + + # systemd-analyze security rqbit + ReadWritePaths = mkIf (cfg.downloadDir != defaultDownloadDir) [ cfg.downloadDir ]; + ProtectSystem = "strict"; + ProtectHome = "read-only"; + PrivateTmp = true; + PrivateDevices = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + PrivateUsers = true; + RemoveIPC = true; + CapabilityBoundingSet = ""; + NoNewPrivileges = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectClock = true; + ProtectHostname = true; + RestrictNamespaces = true; + SystemCallFilter = [ + "@system-service" + "@network-io" + "@file-system" + "~@privileged" + "~@resources" + ]; + SystemCallArchitectures = "native"; + RestrictRealtime = true; + RestrictSUIDSGID = true; + MemoryDenyWriteExecute = true; + LockPersonality = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + UMask = "0027"; + }; + }; + + users = { + users = mkIf (cfg.user == "rqbit") { + rqbit = { + inherit (cfg) group; + isSystemUser = true; + }; + }; + groups = mkIf (cfg.group == "rqbit") { rqbit = { }; }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ + cfg.httpPort + cfg.peerPort + ]; + allowedUDPPorts = [ cfg.peerPort ]; + }; + }; + + meta.maintainers = with lib.maintainers; [ CodedNil ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 6cd4ac9a4ecb..e56ae264dbe5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1385,6 +1385,7 @@ in rosenpass = runTest ./rosenpass.nix; roundcube = runTest ./roundcube.nix; routinator = handleTest ./routinator.nix { }; + rqbit = handleTest ./rqbit.nix { }; rshim = handleTest ./rshim.nix { }; rspamd = handleTest ./rspamd.nix { }; rspamd-trainer = runTest ./rspamd-trainer.nix; diff --git a/nixos/tests/rqbit.nix b/nixos/tests/rqbit.nix new file mode 100644 index 000000000000..dcef770839b6 --- /dev/null +++ b/nixos/tests/rqbit.nix @@ -0,0 +1,30 @@ +import ./make-test-python.nix ( + { pkgs, ... }: + let + port = 3030; + in + { + name = "rqbit"; + meta = { + maintainers = with pkgs.lib.maintainers; [ CodedNil ]; + }; + + nodes.machine = + { pkgs, ... }: + { + services.rqbit = { + httpPort = port; + enable = true; + openFirewall = true; + }; + }; + + testScript = /* python */ '' + machine.start() + machine.wait_for_unit("rqbit.service") + machine.wait_for_open_port(${toString port}) + + machine.succeed("curl --fail http://localhost:${toString port}") + ''; + } +) diff --git a/pkgs/by-name/rq/rqbit/package.nix b/pkgs/by-name/rq/rqbit/package.nix index 0516e01b776c..edac8459099c 100644 --- a/pkgs/by-name/rq/rqbit/package.nix +++ b/pkgs/by-name/rq/rqbit/package.nix @@ -8,6 +8,7 @@ buildNpmPackage, nodejs, nix-update-script, + nixosTests, }: let pname = "rqbit"; @@ -62,13 +63,15 @@ rustPlatform.buildRustPackage { doCheck = false; - passthru.webui = rqbit-webui; - - passthru.updateScript = nix-update-script { - extraArgs = [ - "--subpackage" - "webui" - ]; + passthru = { + webui = rqbit-webui; + updateScript = nix-update-script { + extraArgs = [ + "--subpackage" + "webui" + ]; + }; + tests.testService = nixosTests.rqbit; }; meta = {