diff --git a/nixarr/nixarr.nix b/nixarr/nixarr.nix index 3296762..b3a109c 100644 --- a/nixarr/nixarr.nix +++ b/nixarr/nixarr.nix @@ -27,6 +27,7 @@ with lib; let fi chown -R torrenter:media "${cfg.mediaDir}/torrents" + chown -R usenet:media "${cfg.mediaDir}/usenet" chown -R streamer:media "${cfg.mediaDir}/library" find "${cfg.mediaDir}" \( -type d -exec chmod 0775 {} + -true \) -o \( -exec chmod 0664 {} + \) '' + strings.optionalString cfg.jellyfin.enable '' @@ -35,6 +36,9 @@ with lib; let '' + strings.optionalString cfg.transmission.enable '' chown -R torrenter:cross-seed "${cfg.transmission.stateDir}" find "${cfg.transmission.stateDir}" \( -type d -exec chmod 0750 {} + -true \) -o \( -exec chmod 0640 {} + \) + '' + strings.optionalString cfg.sabnzbd.enable '' + chown -R usenet:root "${cfg.sabnzbd.stateDir}" + find "${cfg.sabnzbd.stateDir}" \( -type d -exec chmod 0700 {} + -true \) -o \( -exec chmod 0600 {} + \) '' + strings.optionalString cfg.transmission.privateTrackers.cross-seed.enable '' chown -R cross-seed:root "${cfg.transmission.privateTrackers.cross-seed.stateDir}" find "${cfg.transmission.privateTrackers.cross-seed.stateDir}" \( -type d -exec chmod 0700 {} + -true \) -o \( -exec chmod 0600 {} + \) @@ -70,6 +74,7 @@ in { ./openssh ./prowlarr ./transmission + ./sabnzbd ../util ]; @@ -105,6 +110,7 @@ in { - [Readarr](#nixarr.readarr.enable) - [Sonarr](#nixarr.sonarr.enable) - [Transmission](#nixarr.transmission.enable) + - [SABnzbd](#nixarr.sabnzbd.enable) Remember to read the options. ''; @@ -231,6 +237,7 @@ in { media.members = cfg.mediaUsers; streamer = {}; torrenter = {}; + usenet = {}; }; users.users = { streamer = { @@ -241,6 +248,10 @@ in { isSystemUser = true; group = "torrenter"; }; + usenet = { + isSystemUser = true; + group = "usenet"; + }; }; systemd.tmpfiles.rules = [ @@ -259,6 +270,16 @@ in { "d '${cfg.mediaDir}/torrents/radarr' 0755 torrenter media - -" "d '${cfg.mediaDir}/torrents/sonarr' 0755 torrenter media - -" "d '${cfg.mediaDir}/torrents/readarr' 0755 torrenter media - -" + ] ++ lists.optionals cfg.sabnzbd.enable [ + # only create usenet dirs if sabnzbd is enabled + "d '${cfg.mediaDir}/usenet' 0755 usenet media - -" + "d '${cfg.mediaDir}/usenet/.incomplete' 0755 usenet media - -" + "d '${cfg.mediaDir}/usenet/.watch' 0755 usenet media - -" + "d '${cfg.mediaDir}/usenet/manual' 0775 usenet media - -" + "d '${cfg.mediaDir}/usenet/liadarr' 0775 usenet media - -" + "d '${cfg.mediaDir}/usenet/radarr' 0775 usenet media - -" + "d '${cfg.mediaDir}/usenet/sonarr' 0775 usenet media - -" + "d '${cfg.mediaDir}/usenet/readarr' 0775 usenet media - -" ]; environment.systemPackages = with pkgs; [ diff --git a/nixarr/sabnzbd/config.nix b/nixarr/sabnzbd/config.nix new file mode 100644 index 0000000..a2f6cba --- /dev/null +++ b/nixarr/sabnzbd/config.nix @@ -0,0 +1,85 @@ +{ + config, + pkgs, + lib, + ... +}: let + cfg = config.nixarr.sabnzbd; + nixarr = config.nixarr; + ini-file-target = "${cfg.stateDir}/sabnzbd.ini"; + concatStringsCommaIfExists = with lib.strings; + stringList: ( + optionalString (builtins.length stringList > 0) ( + concatStringsSep "," stringList + ) + ); + + user-configs = { + misc = { + host = + if cfg.openFirewall + then "0.0.0.0" + else "127.0.0.1"; + port = cfg.guiPort; + download_dir = "${nixarr.mediaDir}/usenet/.incomplete"; + complete_dir = "${nixarr.mediaDir}/usenet/manual"; + dirscan_dir = "${nixarr.mediaDir}/usenet/watch"; + host_whitelist = concatStringsCommaIfExists cfg.whitelistHostnames; + local_ranges = concatStringsCommaIfExists cfg.whitelistRanges; + permissions = "775"; + }; + }; + + ini-base-config-file = pkgs.writeTextFile { + name = "base-config.ini"; + text = lib.generators.toINI {} user-configs; + }; + + fix-config-permissions-script = pkgs.writeShellApplication { + name = "sabnzbd-fix-config-permissions"; + runtimeInputs = with pkgs; [util-linux]; + text = '' + if [ ! -f ${ini-file-target} ]; then + echo 'FAILURE: cannot change permissions of ${ini-file-target}, file does not exist' + exit 1 + fi + + chmod 600 ${ini-file-target} + chown usenet:media ${ini-file-target} + ''; + }; + + user-configs-to-python-list = with lib; + attrsets.collect (f: !builtins.isAttrs f) ( + attrsets.mapAttrsRecursive ( + path: value: + "sab_config_map['" + + (lib.strings.concatStringsSep "']['" path) + + "'] = '" + + (builtins.toString value) + + "'" + ) + user-configs + ); + apply-user-configs-script = with lib; (pkgs.writers.writePython3Bin + "sabnzbd-set-user-values" {libraries = [pkgs.python3Packages.configobj];} '' + from pathlib import Path + from configobj import ConfigObj + + sab_config_path = Path("${ini-file-target}") + if not sab_config_path.is_file() or sab_config_path.suffix != ".ini": + raise Exception(f"{sab_config_path} is not a valid config file path.") + + sab_config_map = ConfigObj(str(sab_config_path)) + + ${lib.strings.concatStringsSep "\n" user-configs-to-python-list} + + sab_config_map.write() + ''); +in { + systemd.tmpfiles.rules = ["C ${cfg.stateDir}/sabnzbd.ini - - - - ${ini-base-config-file}"]; + systemd.services.sabnzbd.serviceConfig.ExecStartPre = lib.mkBefore [ + ("+" + fix-config-permissions-script + "/bin/sabnzbd-fix-config-permissions") + (apply-user-configs-script + "/bin/sabnzbd-set-user-values") + ]; +} diff --git a/nixarr/sabnzbd/default.nix b/nixarr/sabnzbd/default.nix new file mode 100644 index 0000000..90ba825 --- /dev/null +++ b/nixarr/sabnzbd/default.nix @@ -0,0 +1,154 @@ +{ + config, + lib, + ... +}: +with lib; let + cfg = config.nixarr.sabnzbd; + nixarr = config.nixarr; +in { + options.nixarr.sabnzbd = { + enable = mkEnableOption "Enable the SABnzbd service."; + + stateDir = mkOption { + type = types.path; + default = "${nixarr.stateDir}/sabnzbd"; + defaultText = literalExpression ''"''${nixarr.stateDir}/sabnzbd"''; + example = "/nixarr/.state/sabnzbd"; + description = '' + The location of the state directory for the SABnzbd service. + + **Warning:** Setting this to any path, where the subpath is not + owned by root, will fail! For example: + + ```nix + stateDir = /home/user/nixarr/.state/sabnzbd + ``` + + Is not supported, because `/home/user` is owned by `user`. + ''; + }; + + guiPort = mkOption { + type = types.port; + default = 8080; + example = 9999; + description = '' + The port that SABnzbd's GUI will listen on for incomming connections. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + defaultText = literalExpression ''!nixarr.SABnzbd.vpn.enable''; + default = !cfg.vpn.enable; + example = true; + description = "Open firewall for SABnzbd"; + }; + + whitelistHostnames = mkOption { + type = types.listOf types.str; + default = [config.networking.hostName]; + defaultText = "[ config.networking.hostName ]"; + example = ''[ "mediaserv" "media.example.com" ]''; + description = '' + A list that specifies what URLs that are allowed to represent your + SABnzbd instance. If you see an error message like this when + trying to connect to SABnzbd from another device... + + ``` + Refused connection with hostname "your.hostname.com" + ``` + + ...then you should add your hostname(s) to this list. + + SABnzbd only allows connections matching these URLs in order to prevent + DNS hijacking. See + for more info. + ''; + }; + + whitelistRanges = mkOption { + type = types.listOf types.str; + default = []; + defaultText = "[ ]"; + example = ''[ "192.168.1.0/24" "10.0.0.0/23" ]''; + description = '' + A list of IP ranges that will be allowed to connect to SABnzbd's + web GUI. This only needs to be set if SABnzbd needs to be accessed + from another machine besides its host. + ''; + }; + + vpn.enable = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + **Required options:** [`nixarr.vpn.enable`](#nixarr.vpn.enable) + + Route SABnzbd traffic through the VPN. + ''; + }; + }; + + imports = [./config.nix]; + + config = mkIf cfg.enable { + systemd.tmpfiles.rules = [ + "d '${cfg.stateDir}' 0700 usenet root - -" + ]; + + services.sabnzbd = { + enable = true; + user = "usenet"; + group = "media"; + configFile = "${cfg.stateDir}/sabnzbd.ini"; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.guiPort]; + + systemd.services.sabnzbd.serviceConfig = { + Restart = "on-failure"; + StartLimitBurst = 5; + }; + + # Enable and specify VPN namespace to confine service in. + systemd.services.sabnzbd.vpnconfinement = mkIf cfg.vpn.enable { + enable = true; + vpnnamespace = "wg"; + }; + + # Port mappings + vpnnamespaces.wg = mkIf cfg.vpn.enable { + portMappings = [ + { + from = cfg.guiPort; + to = cfg.guiPort; + } + ]; + }; + + services.nginx = mkIf cfg.vpn.enable { + enable = true; + + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + + virtualHosts."127.0.0.1:${builtins.toString cfg.guiPort}" = { + listen = [ + { + addr = "0.0.0.0"; + port = cfg.guiPort; + } + ]; + locations."/" = { + recommendedProxySettings = true; + proxyWebsockets = true; + proxyPass = "http://192.168.15.1:${builtins.toString cfg.guiPort}"; + }; + }; + }; + }; +}