Unverified Commit 523d944e authored by Sandro Jäckel's avatar Sandro Jäckel Committed by GitHub
Browse files

nixos/qui: init service (#472934)

parents 237286f3 ba32dedc
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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).

- [nohang](https://github.com/hakavlad/nohang), a daemon for Linux that prevents out of memory (OOM) situations from affecting system responsiveness. Available as [services.nohang](#opt-services.nohang.enable)
+1 −0
Original line number Diff line number Diff line
@@ -1551,6 +1551,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
+191 −0
Original line number Diff line number Diff line
{
  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 −0
Original line number Diff line number Diff line
@@ -1332,6 +1332,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;

nixos/tests/qui.nix

0 → 100644
+47 −0
Original line number Diff line number Diff line
{ 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)
    '';
}
Loading