# SPDX-FileCopyrightText: 2022-2023 Simon Bruder # # SPDX-License-Identifier: AGPL-3.0-or-later { config, inputs, lib, pkgs, ... }: let cfg = config.sbruder.qbittorrent; in { options.sbruder.qbittorrent = { enable = lib.mkEnableOption "the qbittorrent service"; homeDir = lib.mkOption { type = lib.types.path; default = "/var/lib/qbittorrent"; }; configDir = lib.mkOption { type = lib.types.path; default = "${cfg.homeDir}/config"; }; downloadDir = lib.mkOption { type = lib.types.path; default = "${cfg.homeDir}/download"; }; webuiPort = lib.mkOption { type = lib.types.int; default = 8099; }; sopsFile = lib.mkOption { type = lib.types.path; default = ../../machines/${config.networking.hostName}/secrets.yaml; description = '' The sops secret file that includes the wireguard private key (wg-qbittorrent-private-key). ''; }; wireguardConnectionDetails = lib.mkOption { type = lib.types.attrs; default = import ../../machines/${config.networking.hostName}/secrets/wireguard-qbittorrent.nix; example = { ips = [ "10.0.0.1/32" "fd00::1/128" ]; peers = lib.singleton { publicKey = "MbLFO7MIFSdwVmZWQG//IUxVxwSHprRkhr/NWl2gils="; allowedIPs = [ "0.0.0.0/0" "::0/0" ]; endpoint = "192.0.2.1:51820"; }; }; description = "The connection details for the WireGuard tunnel."; }; fqdn = lib.mkOption { type = lib.types.str; description = "The fqdn nginx should listen on. It must not be used for anything else."; }; }; config = lib.mkIf cfg.enable { users.users.qbittorrent = { group = "qbittorrent"; home = cfg.homeDir; isSystemUser = true; }; users.groups.qbittorrent = { }; systemd.tmpfiles.rules = [ "d '${cfg.downloadDir}' 0775 qbittorrent users - -" "d '${cfg.homeDir}' 0771 qbittorrent qbittorrent - -" ]; sops.secrets.wg-qbittorrent-private-key.sopsFile = cfg.sopsFile; networking.wireguard.interfaces.wg-qbittorrent = { interfaceNamespace = "qbittorrent"; preSetup = "ip netns add qbittorrent && ip -n qbittorrent link set lo up"; postShutdown = "ip netns del qbittorrent"; privateKeyFile = config.sops.secrets.wg-qbittorrent-private-key.path; } // (builtins.removeAttrs cfg.wireguardConnectionDetails [ "nameserver" ]); environment.etc."netns/qbittorrent/resolv.conf".text = '' nameserver ${cfg.wireguardConnectionDetails.nameserver} ''; systemd.services.qbittorrent = { description = "qBittorrent Service"; after = [ "wireguard-wg-qbittorrent.service" ]; requires = [ "wireguard-wg-qbittorrent.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { PrivateNetwork = true; NetworkNamespacePath = "/run/netns/qbittorrent"; Restart = "always"; ExecStart = "${pkgs.qbittorrent-nox}/bin/qbittorrent-nox --profile=${cfg.configDir} --webui-port=${toString cfg.webuiPort}"; User = "qbittorrent"; Group = "qbittorrent"; # Increase number of open file descriptors (default: 1024) LimitNOFILE = 65536; # Avoid using nscd (leaks dns) InaccessiblePaths = [ "/run/nscd" "/etc/nsswitch.conf" ]; # Make correct resolv.conf available for unit BindReadOnlyPaths = [ "/etc/netns/qbittorrent/resolv.conf:/etc/resolv.conf" ]; # systemd-analyze --no-pager security qbittorrent.service CapabilityBoundingSet = null; PrivateDevices = true; PrivateTmp = true; PrivateUsers = true; ProtectHome = true; RestrictNamespaces = true; SystemCallFilter = "@system-service"; }; }; systemd.services.qbittorrent-webui-proxy = { wantedBy = [ "multi-user.target" ]; after = [ "wireguard-wg-qbittorrent.service" ]; partOf = [ "wireguard-wg-qbittorrent.service" ]; serviceConfig = { PrivateNetwork = true; NetworkNamespacePath = "/run/netns/qbittorrent"; Restart = "always"; ExecStart = "${pkgs.socat}/bin/socat UNIX-LISTEN:${cfg.homeDir}/webui.sock,fork,reuseaddr,mode=660,unlink-early TCP:127.0.0.1:${toString cfg.webuiPort}"; User = "qbittorrent"; Group = "nginx"; # systemd-analyze --no-pager security qbittorrent-webui-proxy.service CapabilityBoundingSet = null; PrivateDevices = true; PrivateTmp = true; PrivateUsers = true; ProtectHome = true; RestrictNamespaces = true; SystemCallFilter = "@system-service"; }; }; systemd.services.qbittorrent_exporter = { wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; environment = { QBITTORRENT_API_SOCKET = "${cfg.homeDir}/webui.sock"; QBITTORRENT_EXPORTER_LISTEN_ADDRESS = ":9561"; }; serviceConfig = { ExecStart = "${pkgs.callPackage ./exporter { }}/bin/qbittorrent_exporter"; Restart = "always"; User = "qbittorrent"; Group = "qbittorrent"; # systemd-analyze --no-pager security qbittorrent_exporter.service CapabilityBoundingSet = null; PrivateDevices = true; PrivateUsers = true; ProtectHome = true; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; RestrictNamespaces = true; SystemCallArchitectures = "native"; SystemCallFilter = "@system-service"; }; }; services.nginx.virtualHosts."${cfg.fqdn}" = { enableACME = lib.mkDefault true; forceSSL = lib.mkDefault true; # treated as state basicAuthFile = "${cfg.homeDir}/htpasswd"; locations = { "/" = { proxyPass = "http://unix:${cfg.homeDir}/webui.sock"; proxyWebsockets = true; }; "/download/" = { alias = "${cfg.downloadDir}/"; extraConfig = '' autoindex on; ''; }; "=/metrics" = { proxyPass = "http://127.0.0.1:9561/metrics"; }; }; }; }; }