diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 9d4f926dc7184..dcd2a11234e3b 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -26,6 +26,8 @@ - [DankMaterialShell](https://danklinux.com), a complete desktop shell for Wayland compositors built with Quickshell. Available as [programs.dms-shell](#opt-programs.dms-shell.enable). +- [nemorosa](https://github.com/KyokoMiki/nemorosa), cross-seeding tool specifically designed for Gazelle-based music trackers. Available as [services.nemorosa](#opt-services.nemorosa.enable). + - [dms-greeter](https://danklinux.com), a modern display manager greeter for DankMaterialShell that works with greetd and supports multiple Wayland compositors. Available as [services.displayManager.dms-greeter](#opt-services.displayManager.dms-greeter.enable). - [dsearch](https://github.com/AvengeMedia/danksearch), a fast filesystem search service with fuzzy matching. Available as [programs.dsearch](#opt-programs.dsearch.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 953444abe5860..7787c38b4d042 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1551,6 +1551,7 @@ ./services/torrent/flexget.nix ./services/torrent/flood.nix ./services/torrent/magnetico.nix + ./services/torrent/nemorosa.nix ./services/torrent/opentracker.nix ./services/torrent/peerflix.nix ./services/torrent/qbittorrent.nix diff --git a/nixos/modules/services/torrent/nemorosa.nix b/nixos/modules/services/torrent/nemorosa.nix new file mode 100644 index 0000000000000..f18a980f94054 --- /dev/null +++ b/nixos/modules/services/torrent/nemorosa.nix @@ -0,0 +1,185 @@ +{ + config, + lib, + pkgs, + utils, + maintainers, + ... +}: +let + cfg = config.services.nemorosa; + + inherit (lib) + getExe + mkEnableOption + mkIf + mkOption + mkPackageOption + ; + inherit (lib.types) + port + str + submodule + ; + + settingsFormat = pkgs.formats.yaml { }; + stateDir = "/var/lib/nemorosa"; + configPath = "${stateDir}/config.yaml"; + # YAML is a JSON superset + secretsReplacement = utils.genJqSecretsReplacement { + loadCredential = true; + } cfg.settings configPath; +in +{ + options.services.nemorosa = { + enable = mkEnableOption "nemorosa"; + + package = mkPackageOption pkgs "nemorosa" { }; + + user = mkOption { + type = str; + default = "nemorosa"; + description = "User to run nemorosa as."; + }; + + group = mkOption { + type = str; + default = "nemorosa"; + description = "Group to run nemorosa as."; + }; + + settings = mkOption { + description = '' + Configuration options for nemorosa. + See [the nemorosa documentation](https://github.com/KyokoMiki/nemorosa/wiki/Configuration) + for all configuration options. + + The configuration is processed using [utils.genJqSecretsReplacement](https://github.com/NixOS/nixpkgs/blob/master/nixos/lib/utils.nix#L232-L331) to handle secret substitution. + Use this for sensitive configuration options. + ''; + default = { }; + example = { + linking = { + enable_linking = true; + link_dirs = [ "/mnt/some/folder" ]; + }; + downloader = { + client = { + _secret = "/run/secrets/torrent-client-url"; + }; + }; + target_site = [ + { + server = "https://someurl.com"; + api_key = { + _secret = "/run/secrets/someurl_api-key"; + }; + } + ]; + }; + type = submodule { + freeformType = settingsFormat.type; + options = { + server = mkOption { + default = { }; + description = "Web server configuration"; + type = submodule { + freeformType = settingsFormat.type; + options = { + host = mkOption { + type = str; + default = "127.0.0.1"; + description = "Host address the nemorosa daemon listens on."; + }; + port = mkOption { + type = port; + default = 8256; + description = "Port the nemorosa daemon listens on."; + }; + }; + }; + }; + }; + }; + }; + }; + + config = mkIf (cfg.enable) { + systemd = { + services.nemorosa = { + description = "nemorosa"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + preStart = secretsReplacement.script; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + # On partial matches the torrent client also needs to write to the + # link_dirs. With default 755/644 permissions this is not possible, + # unless the user is the same as the torrent client user. + UMask = mkIf (cfg.user == "nemorosa") "0002"; + StateDirectory = "nemorosa"; + LoadCredential = secretsReplacement.credentials; + + ExecStart = "${getExe cfg.package} --server --config ${configPath}"; + + SocketBindDeny = "any"; + SocketBindAllow = cfg.settings.server.port; + + # Based the qbittorrent hardening settings + 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"; + ProtectSystem = "full"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" ]; + }; + }; + }; + + users = { + users = mkIf (cfg.user == "nemorosa") { + nemorosa = { + group = cfg.group; + description = "nemorosa user"; + isSystemUser = true; + home = stateDir; + }; + }; + + groups = mkIf (cfg.group == "nemorosa") { + nemorosa = { }; + }; + }; + }; + + meta.maintainers = with maintainers; [ undefined-landmark ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 59e67971f0831..b08c9f4493220 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1037,6 +1037,7 @@ in nebula-lighthouse-service = runTest ./nebula-lighthouse-service.nix; nebula.connectivity = runTest ./nebula/connectivity.nix; nebula.reload = runTest ./nebula/reload.nix; + nemorosa = runTest ./nemorosa.nix; neo4j = runTest ./neo4j.nix; netbird = runTest ./netbird.nix; netbox-upgrade = runTest ./web-apps/netbox-upgrade.nix; diff --git a/nixos/tests/nemorosa.nix b/nixos/tests/nemorosa.nix new file mode 100644 index 0000000000000..a73ed0ca291df --- /dev/null +++ b/nixos/tests/nemorosa.nix @@ -0,0 +1,68 @@ +{ lib, pkgs, ... }: +{ + name = "nemorosa"; + + meta = with pkgs.lib.maintainers; { + maintainers = [ + undefined-landmark + ]; + }; + + nodes.machine = + let + qbitUrl = "qbittorrent+http://user:adminadmin@localhost:8080/"; + # We create this secret in the Nix store (making it readable by everyone). + # DO NOT DO THIS OUTSIDE OF TESTS!! + testSecret = pkgs.writeText "qbitUrl" qbitUrl; + in + { + services.qbittorrent = { + enable = true; + serverConfig.Preferences.WebUI = { + Username = "user"; + # password: "adminadmin" as ByteArray + Password_PBKDF2 = "@ByteArray(6DIf26VOpTCYbgNiO6DAFQ==:e6241eaAWGzRotQZvVA5/up9fj5wwSAThLgXI2lVMsYTu1StUgX9MgmElU3Sa/M8fs+zqwZv9URiUOObjqJGNw==)"; + }; + }; + + services.nemorosa = { + enable = true; + settings.downloader.client = lib.mkDefault qbitUrl; + }; + + specialisation.secretSubstition.configuration = { + services.nemorosa = { + enable = true; + settings = { + downloader.client = { + _secret = toString testSecret; + }; + server.port = 8266; + }; + }; + }; + }; + + testScript = + { nodes, ... }: + let + secretSubst = "${nodes.machine.system.build.toplevel}/specialisation/secretSubstition"; + in + # python + '' + machine.start() + + # Nemorosa initialization expects an available API connection. So the + # service will not start successfully. Do some basic checks on the + # console log as an alternative. + def test_log(port): + machine.wait_for_console_text("Configuration loaded successfully from: /var/lib/nemorosa/config.yaml") + machine.wait_for_console_text(f"Starting Nemorosa web server on 127.0.0.1:{port}") + machine.wait_for_console_text("Successfully connected to torrent client") + + test_log(8256) + + machine.succeed("${secretSubst}/bin/switch-to-configuration test") + test_log(8266) + ''; +} diff --git a/pkgs/by-name/ne/nemorosa/package.nix b/pkgs/by-name/ne/nemorosa/package.nix index b5c281c146a74..ecdcd70daaf3b 100644 --- a/pkgs/by-name/ne/nemorosa/package.nix +++ b/pkgs/by-name/ne/nemorosa/package.nix @@ -1,6 +1,7 @@ { lib, fetchFromGitHub, + nixosTests, python3Packages, }: @@ -54,6 +55,10 @@ python3Packages.buildPythonApplication rec { pythonImportsCheck = [ "nemorosa" ]; + passthru = { + tests.testService = nixosTests.nemorosa; + }; + meta = { description = "Specialized cross-seeding tool designed for music torrents"; homepage = "https://github.com/KyokoMiki/nemorosa";