Merge branch 'main' into dev
This commit is contained in:
Generated
+10
-10
@@ -2,16 +2,16 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1750183894,
|
"lastModified": 1761016216,
|
||||||
"narHash": "sha256-ZtOgEt70keBVB4YJc+z7m0h7J1BOlv/GjHE1YC6KxeA=",
|
"narHash": "sha256-G/iC4t/9j/52i/nm+0/4ybBmAF4hzR8CNHC75qEhjHo=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "f45e75fc63fc8a7ffc3da382b2f6b681c5b71875",
|
"rev": "481cf557888e05d3128a76f14c76397b7d7cc869",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "nixpkgs-unstable",
|
"ref": "nixos-25.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
},
|
},
|
||||||
"vpnconfinement": {
|
"vpnconfinement": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1749672087,
|
"lastModified": 1759956062,
|
||||||
"narHash": "sha256-j8LG0s0QcvNkZZLcItl78lvTZemvsScir0dG3Ii4B1c=",
|
"narHash": "sha256-NUZu0Rb0fwUjfdp51zMm0xM3lcK8Kw4c97LLog7+JjA=",
|
||||||
"owner": "Maroka-chan",
|
"owner": "Maroka-chan",
|
||||||
"repo": "VPN-Confinement",
|
"repo": "VPN-Confinement",
|
||||||
"rev": "880b3bd2c864dce4f6afc79f6580ca699294c011",
|
"rev": "fabe7247b720b5eb4c3c053e24a2b3b70e64c52b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -45,11 +45,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1750317638,
|
"lastModified": 1753958235,
|
||||||
"narHash": "sha256-B4RWcXXOLO6gMeYyV+K4olu+kGGsYamKH+JAm0cIXqI=",
|
"narHash": "sha256-Rd27XQJKv8Z4BCr3gdbaHFd0TmumiGxdjGRzsEf/mOg=",
|
||||||
"owner": "rasmus-kirk",
|
"owner": "rasmus-kirk",
|
||||||
"repo": "website-builder",
|
"repo": "website-builder",
|
||||||
"rev": "b54192000a00e865947f45bacf3184d56363ee38",
|
"rev": "00a14b7ae7baef2197978ba7c3fe72dfca7bc475",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
description = "The Nixarr Media Server Nixos Module";
|
description = "The Nixarr Media Server Nixos Module";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
|
||||||
|
|
||||||
vpnconfinement.url = "github:Maroka-chan/VPN-Confinement";
|
vpnconfinement.url = "github:Maroka-chan/VPN-Confinement";
|
||||||
|
|
||||||
@@ -29,7 +29,10 @@
|
|||||||
forAllSystems = f:
|
forAllSystems = f:
|
||||||
nixpkgs.lib.genAttrs supportedSystems (system:
|
nixpkgs.lib.genAttrs supportedSystems (system:
|
||||||
f {
|
f {
|
||||||
pkgs = import nixpkgs { inherit system; config.allowUnfree = true; };
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config.allowUnfree = true;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
in {
|
in {
|
||||||
nixosModules.default.imports = [./nixarr vpnconfinement.nixosModules.default];
|
nixosModules.default.imports = [./nixarr vpnconfinement.nixosModules.default];
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ in {
|
|||||||
--port ${toString cfg.port} \
|
--port ${toString cfg.port} \
|
||||||
--no-update True
|
--no-update True
|
||||||
'';
|
'';
|
||||||
KillSignal = "SIGINT";
|
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
KillSignal = "SIGINT";
|
KillSignal = "SIGINT";
|
||||||
SuccessExitStatus = "0 156";
|
SuccessExitStatus = "0 156";
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ in {
|
|||||||
./ddns
|
./ddns
|
||||||
./jellyfin
|
./jellyfin
|
||||||
./jellyseerr
|
./jellyseerr
|
||||||
|
./lib/api-keys.nix
|
||||||
./komga
|
./komga
|
||||||
./lidarr
|
./lidarr
|
||||||
./nixarr-command
|
./nixarr-command
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
with lib; let
|
||||||
|
cfg = config.nixarr;
|
||||||
|
|
||||||
|
serviceCfgFile = {
|
||||||
|
bazarr = "${cfg.bazarr.stateDir}/config/config.yaml";
|
||||||
|
jellyseerr = "${cfg.jellyseerr.stateDir}/settings.json";
|
||||||
|
lidarr = "${cfg.lidarr.stateDir}/config.xml";
|
||||||
|
prowlarr = "${cfg.prowlarr.stateDir}/config.xml";
|
||||||
|
radarr = "${cfg.radarr.stateDir}/config.xml";
|
||||||
|
readarr-audiobook = "${cfg.readarr-audiobook.stateDir}/config.xml";
|
||||||
|
readarr = "${cfg.readarr.stateDir}/config.xml";
|
||||||
|
sabnzbd = "${cfg.sabnzbd.stateDir}/sabnzbd.ini";
|
||||||
|
sonarr = "${cfg.sonarr.stateDir}/config.xml";
|
||||||
|
transmission = "${cfg.transmission.stateDir}/.config/transmission-daemon/settings.json";
|
||||||
|
};
|
||||||
|
|
||||||
|
printServiceApiKey = let
|
||||||
|
yq = getExe' pkgs.yq "yq";
|
||||||
|
xq = getExe' pkgs.yq "xq";
|
||||||
|
grep = getExe pkgs.gnugrep;
|
||||||
|
sed = getExe pkgs.gnused;
|
||||||
|
in {
|
||||||
|
bazarr = pkgs.writeShellScript "print-bazarr-api-key" ''
|
||||||
|
${yq} -r .auth.apiKey '${serviceCfgFile.bazarr}'
|
||||||
|
'';
|
||||||
|
jellyseerr = pkgs.writeShellScript "print-jellyseerr-api-key" ''
|
||||||
|
${yq} -r .main.apiKey '${serviceCfgFile.jellyseerr}'
|
||||||
|
'';
|
||||||
|
lidarr = pkgs.writeShellScript "print-lidarr-api-key" ''
|
||||||
|
${xq} -r .Config.ApiKey '${serviceCfgFile.lidarr}'
|
||||||
|
'';
|
||||||
|
prowlarr = pkgs.writeShellScript "print-prowlarr-api-key" ''
|
||||||
|
${xq} -r .Config.ApiKey '${serviceCfgFile.prowlarr}'
|
||||||
|
'';
|
||||||
|
radarr = pkgs.writeShellScript "print-radarr-api-key" ''
|
||||||
|
${xq} -r .Config.ApiKey '${serviceCfgFile.radarr}'
|
||||||
|
'';
|
||||||
|
readarr-audiobook = pkgs.writeShellScript "print-readarr-audiobook-api-key" ''
|
||||||
|
${xq} -r .Config.ApiKey '${serviceCfgFile.readarr-audiobook}'
|
||||||
|
'';
|
||||||
|
readarr = pkgs.writeShellScript "print-readarr-api-key" ''
|
||||||
|
${xq} -r .Config.ApiKey '${serviceCfgFile.readarr}'
|
||||||
|
'';
|
||||||
|
sabnzbd = pkgs.writeShellScript "print-sabnzbd-api-key" ''
|
||||||
|
${grep} api_key '${serviceCfgFile.sabnzbd}' | ${sed} 's/^api_key.*= *//g'
|
||||||
|
'';
|
||||||
|
sonarr = pkgs.writeShellScript "print-sonarr-api-key" ''
|
||||||
|
${xq} -r .Config.ApiKey '${serviceCfgFile.sonarr}'
|
||||||
|
'';
|
||||||
|
transmission = pkgs.writeShellScript "print-transmission-api-key" ''
|
||||||
|
${yq} -r .["rpc-password"] '${serviceCfgFile.transmission}'
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
servicesWithApiKeys = builtins.attrNames printServiceApiKey;
|
||||||
|
|
||||||
|
# Helper to create API key extraction for a service
|
||||||
|
mkApiKeyExtractor = serviceName: {
|
||||||
|
description = "Extract ${serviceName} API key";
|
||||||
|
after = ["${serviceName}.service"];
|
||||||
|
requires = ["${serviceName}.service"];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
Group = "${serviceName}-api";
|
||||||
|
UMask = "0027"; # Results in 0640 permissions
|
||||||
|
|
||||||
|
ExecStartPre = [
|
||||||
|
(pkgs.writeShellScript "wait-for-${serviceName}-config" ''
|
||||||
|
while [ ! -f '${serviceCfgFile.${serviceName}}' ]; do sleep 1; done
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
|
||||||
|
ExecStart = pkgs.writeShellScript "extract-${serviceName}-api-key" ''
|
||||||
|
${printServiceApiKey.${serviceName}} > '${cfg.stateDir}/api-keys/${serviceName}.key'
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
# Create per-service API key groups
|
||||||
|
users.groups = mkMerge (
|
||||||
|
builtins.map
|
||||||
|
(serviceName: mkIf cfg.${serviceName}.enable {"${serviceName}-api" = {};})
|
||||||
|
servicesWithApiKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
systemd.services = mkMerge (
|
||||||
|
# Create API key extractors for enabled services
|
||||||
|
builtins.map
|
||||||
|
(serviceName: mkIf cfg.${serviceName}.enable {"${serviceName}-api-key" = mkApiKeyExtractor serviceName;})
|
||||||
|
servicesWithApiKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
# Create the api-keys directory
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
# Needs to be world-executable for members of the `*-api` groups to access
|
||||||
|
# the files inside.
|
||||||
|
"d ${cfg.stateDir}/api-keys 0701 root root - -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
+11
-10
@@ -84,19 +84,20 @@ in {
|
|||||||
"d '${cfg.stateDir}' 0700 ${globals.prowlarr.user} root - -"
|
"d '${cfg.stateDir}' 0700 ${globals.prowlarr.user} root - -"
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services.prowlarr = {
|
services.prowlarr = {
|
||||||
description = "prowlarr";
|
enable = cfg.enable;
|
||||||
after = ["network.target"];
|
package = cfg.package;
|
||||||
wantedBy = ["multi-user.target"];
|
settings.server.port = cfg.port;
|
||||||
environment.PROWLARR__SERVER__PORT = builtins.toString cfg.port;
|
openFirewall = cfg.openFirewall;
|
||||||
|
};
|
||||||
|
|
||||||
serviceConfig = {
|
systemd.services.prowlarr.serviceConfig = {
|
||||||
Type = "simple";
|
# `User` and `Group` override `DynamicUser = true` from the NixOS Prowlarr
|
||||||
|
# module (because a user and group with those names exists).
|
||||||
User = globals.prowlarr.user;
|
User = globals.prowlarr.user;
|
||||||
Group = globals.prowlarr.group;
|
Group = globals.prowlarr.group;
|
||||||
ExecStart = "${lib.getExe cfg.package} -nobrowser -data=${cfg.stateDir}";
|
ExecStart = mkForce "${lib.getExe cfg.package} -nobrowser -data=${cfg.stateDir}";
|
||||||
Restart = "on-failure";
|
ReadWritePaths = [cfg.stateDir];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = mkIf cfg.openFirewall {
|
networking.firewall = mkIf cfg.openFirewall {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
inputs,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
with lib; let
|
with lib; let
|
||||||
@@ -11,39 +10,6 @@ with lib; let
|
|||||||
nixarr = config.nixarr;
|
nixarr = config.nixarr;
|
||||||
format = pkgs.formats.yaml {};
|
format = pkgs.formats.yaml {};
|
||||||
|
|
||||||
# Helper function to extract API keys
|
|
||||||
extractApiKeys = pkgs.writeShellApplication {
|
|
||||||
name = "extract-recyclarr-api-keys";
|
|
||||||
runtimeInputs = with pkgs; [yq];
|
|
||||||
text = ''
|
|
||||||
# Ensure state directory exists with proper permissions
|
|
||||||
mkdir -p "${cfg.stateDir}"
|
|
||||||
chown ${config.services.recyclarr.user}:${config.services.recyclarr.group} "${cfg.stateDir}"
|
|
||||||
chmod 755 "${cfg.stateDir}"
|
|
||||||
|
|
||||||
${optionalString nixarr.radarr.enable ''
|
|
||||||
# Extract Radarr API key
|
|
||||||
API_KEY_FILE="${cfg.stateDir}/radarr-api-key"
|
|
||||||
xq -r '.Config.ApiKey' "${nixarr.radarr.stateDir}/config.xml" > "$API_KEY_FILE"
|
|
||||||
chmod 400 "$API_KEY_FILE"
|
|
||||||
chown ${config.services.recyclarr.user}:${config.services.recyclarr.group} "$API_KEY_FILE"
|
|
||||||
echo "RADARR_API_KEY=$(tr -d '\n' < "$API_KEY_FILE")" >> "${cfg.stateDir}/env"
|
|
||||||
''}
|
|
||||||
|
|
||||||
${optionalString nixarr.sonarr.enable ''
|
|
||||||
# Extract Sonarr API key
|
|
||||||
API_KEY_FILE="${cfg.stateDir}/sonarr-api-key"
|
|
||||||
xq -r '.Config.ApiKey' "${nixarr.sonarr.stateDir}/config.xml" > "$API_KEY_FILE"
|
|
||||||
chmod 400 "$API_KEY_FILE"
|
|
||||||
chown ${config.services.recyclarr.user}:${config.services.recyclarr.group} "$API_KEY_FILE"
|
|
||||||
echo "SONARR_API_KEY=$(tr -d '\n' < "$API_KEY_FILE")" >> "${cfg.stateDir}/env"
|
|
||||||
''}
|
|
||||||
|
|
||||||
chmod 400 "${cfg.stateDir}/env"
|
|
||||||
chown ${config.services.recyclarr.user}:${config.services.recyclarr.group} "${cfg.stateDir}/env"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Generate configuration file from Nix attribute set if provided
|
# Generate configuration file from Nix attribute set if provided
|
||||||
generatedConfigFile = format.generate "recyclarr-config.yml" cfg.configuration;
|
generatedConfigFile = format.generate "recyclarr-config.yml" cfg.configuration;
|
||||||
|
|
||||||
@@ -193,6 +159,9 @@ in {
|
|||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = globals.recyclarr.group;
|
group = globals.recyclarr.group;
|
||||||
uid = globals.uids.${globals.recyclarr.user};
|
uid = globals.uids.${globals.recyclarr.user};
|
||||||
|
extraGroups =
|
||||||
|
(optional nixarr.radarr.enable "radarr-api")
|
||||||
|
++ (optional nixarr.sonarr.enable "sonarr-api");
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -207,16 +176,29 @@ in {
|
|||||||
requiredBy = ["recyclarr.service"];
|
requiredBy = ["recyclarr.service"];
|
||||||
before = ["recyclarr.service"];
|
before = ["recyclarr.service"];
|
||||||
requires =
|
requires =
|
||||||
(optionals nixarr.radarr.enable ["radarr.service"])
|
(optionals nixarr.radarr.enable ["radarr.service" "radarr-api-key.service"])
|
||||||
++ (optionals nixarr.sonarr.enable ["sonarr.service"]);
|
++ (optionals nixarr.sonarr.enable ["sonarr.service" "sonarr-api-key.service"]);
|
||||||
after =
|
after =
|
||||||
(optionals nixarr.radarr.enable ["radarr.service"])
|
(optionals nixarr.radarr.enable ["radarr.service" "radarr-api-key.service"])
|
||||||
++ (optionals nixarr.sonarr.enable ["sonarr.service"]);
|
++ (optionals nixarr.sonarr.enable ["sonarr.service" "sonarr-api-key.service"]);
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
ExecStart = "${extractApiKeys}/bin/extract-recyclarr-api-keys";
|
UMask = "0077"; # Results in 0600 permissions
|
||||||
|
User = config.services.recyclarr.user;
|
||||||
|
ExecStart = pkgs.writeShellScript "recyclar-setup" ''
|
||||||
|
set -euo pipefail
|
||||||
|
echo -n > '${cfg.stateDir}/env'
|
||||||
|
${optionalString nixarr.radarr.enable ''
|
||||||
|
printf RADARR_API_KEY= >> '${cfg.stateDir}/env'
|
||||||
|
cat '${nixarr.stateDir}/api-keys/radarr.key' >> '${cfg.stateDir}/env'
|
||||||
|
''}
|
||||||
|
${optionalString nixarr.sonarr.enable ''
|
||||||
|
printf SONARR_API_KEY= >> '${cfg.stateDir}/env'
|
||||||
|
cat '${nixarr.stateDir}/api-keys/sonarr.key' >> '${cfg.stateDir}/env'
|
||||||
|
''}
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,6 +214,7 @@ in {
|
|||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d '${cfg.stateDir}' 0750 ${config.services.recyclarr.user} root - -"
|
"d '${cfg.stateDir}' 0750 ${config.services.recyclarr.user} root - -"
|
||||||
|
"f '${cfg.stateDir}/env' 0600 ${config.services.recyclarr.user} ${config.services.recyclarr.group} - -"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ in {
|
|||||||
|
|
||||||
package = mkPackageOption pkgs "sonarr" {};
|
package = mkPackageOption pkgs "sonarr" {};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = defaultPort;
|
||||||
|
description = "Port for Sonarr to use.";
|
||||||
|
};
|
||||||
|
|
||||||
stateDir = mkOption {
|
stateDir = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = "${nixarr.stateDir}/sonarr";
|
default = "${nixarr.stateDir}/sonarr";
|
||||||
@@ -91,6 +97,7 @@ in {
|
|||||||
package = cfg.package;
|
package = cfg.package;
|
||||||
user = globals.sonarr.user;
|
user = globals.sonarr.user;
|
||||||
group = globals.sonarr.group;
|
group = globals.sonarr.group;
|
||||||
|
settings.server.port = cfg.port;
|
||||||
openFirewall = cfg.openFirewall;
|
openFirewall = cfg.openFirewall;
|
||||||
dataDir = cfg.stateDir;
|
dataDir = cfg.stateDir;
|
||||||
};
|
};
|
||||||
@@ -105,8 +112,8 @@ in {
|
|||||||
vpnNamespaces.wg = mkIf cfg.vpn.enable {
|
vpnNamespaces.wg = mkIf cfg.vpn.enable {
|
||||||
portMappings = [
|
portMappings = [
|
||||||
{
|
{
|
||||||
from = defaultPort;
|
from = cfg.port;
|
||||||
to = defaultPort;
|
to = cfg.port;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -118,17 +125,17 @@ in {
|
|||||||
recommendedOptimisation = true;
|
recommendedOptimisation = true;
|
||||||
recommendedGzipSettings = true;
|
recommendedGzipSettings = true;
|
||||||
|
|
||||||
virtualHosts."127.0.0.1:${builtins.toString defaultPort}" = {
|
virtualHosts."127.0.0.1:${builtins.toString cfg.port}" = {
|
||||||
listen = [
|
listen = [
|
||||||
{
|
{
|
||||||
addr = "0.0.0.0";
|
addr = "0.0.0.0";
|
||||||
port = defaultPort;
|
port = cfg.port;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
recommendedProxySettings = true;
|
recommendedProxySettings = true;
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
proxyPass = "http://192.168.15.1:${builtins.toString defaultPort}";
|
proxyPass = "http://192.168.15.1:${builtins.toString cfg.port}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,14 +54,14 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
type = types.str;
|
type = types.nullOr types.str;
|
||||||
default = "cross-seed";
|
default = null;
|
||||||
description = "User account under which cross-seed runs.";
|
description = "User account under which cross-seed runs.";
|
||||||
};
|
};
|
||||||
|
|
||||||
group = mkOption {
|
group = mkOption {
|
||||||
type = types.str;
|
type = types.nullOr types.str;
|
||||||
default = "cross-seed";
|
default = null;
|
||||||
description = "Group under which cross-seed runs.";
|
description = "Group under which cross-seed runs.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -81,6 +81,18 @@ in {
|
|||||||
The settings.torrentDir option must be set if cross-seed is enabled.
|
The settings.torrentDir option must be set if cross-seed is enabled.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.enable -> cfg.user != null;
|
||||||
|
message = ''
|
||||||
|
The user option must be set if cross-seed is enabled.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.enable -> cfg.group != null;
|
||||||
|
message = ''
|
||||||
|
The group option must be set if cross-seed is enabled.
|
||||||
|
'';
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.tmpfiles.rules =
|
systemd.tmpfiles.rules =
|
||||||
|
|||||||
@@ -287,7 +287,6 @@ in {
|
|||||||
|
|
||||||
users = {
|
users = {
|
||||||
groups.${globals.transmission.group}.gid = globals.gids.${globals.transmission.group};
|
groups.${globals.transmission.group}.gid = globals.gids.${globals.transmission.group};
|
||||||
groups.${globals.cross-seed.group}.gid = globals.gids.${globals.cross-seed.group};
|
|
||||||
users.${globals.transmission.user} = {
|
users.${globals.transmission.user} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = globals.transmission.group;
|
group = globals.transmission.group;
|
||||||
@@ -296,10 +295,10 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d '${cfg.stateDir}' 0750 ${globals.transmission.user} ${globals.cross-seed.group} - -"
|
"d '${cfg.stateDir}' 0750 ${globals.transmission.user} ${globals.transmission.group} - -"
|
||||||
# This is fixes a bug in nixpks (https://github.com/NixOS/nixpkgs/issues/291883)
|
# This is fixes a bug in nixpks (https://github.com/NixOS/nixpkgs/issues/291883)
|
||||||
"d '${cfg.stateDir}/.config' 0750 ${globals.transmission.user} ${globals.cross-seed.group} - -"
|
"d '${cfg.stateDir}/.config' 0750 ${globals.transmission.user} ${globals.transmission.group} - -"
|
||||||
"d '${cfg.stateDir}/.config/transmission-daemon' 0750 ${globals.transmission.user} ${globals.cross-seed.group} - -"
|
"d '${cfg.stateDir}/.config/transmission-daemon' 0750 ${globals.transmission.user} ${globals.transmission.group} - -"
|
||||||
|
|
||||||
# Media Dirs
|
# Media Dirs
|
||||||
"d '${nixarr.mediaDir}/torrents' 0755 ${globals.transmission.user} ${globals.transmission.group} - -"
|
"d '${nixarr.mediaDir}/torrents' 0755 ${globals.transmission.user} ${globals.transmission.group} - -"
|
||||||
@@ -315,8 +314,8 @@ in {
|
|||||||
util-nixarr.services.cross-seed = mkIf cfg-cross-seed.enable {
|
util-nixarr.services.cross-seed = mkIf cfg-cross-seed.enable {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = cfg-cross-seed.stateDir;
|
dataDir = cfg-cross-seed.stateDir;
|
||||||
user = globals.cross-seed.user;
|
user = globals.transmission.user;
|
||||||
group = globals.cross-seed.group;
|
group = globals.transmission.group;
|
||||||
settings =
|
settings =
|
||||||
{
|
{
|
||||||
torrentDir = "${cfg.stateDir}/.config/transmission-daemon/torrents";
|
torrentDir = "${cfg.stateDir}/.config/transmission-daemon/torrents";
|
||||||
|
|||||||
+47
-51
@@ -19,9 +19,9 @@ pkgs.nixosTest {
|
|||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
jellyfin.enable = true;
|
jellyfin.enable = true;
|
||||||
|
plex.enable = true;
|
||||||
jellyseerr.enable = true;
|
jellyseerr.enable = true;
|
||||||
audiobookshelf.enable = true;
|
audiobookshelf.enable = true;
|
||||||
plex.enable = true;
|
|
||||||
|
|
||||||
transmission = {
|
transmission = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -37,51 +37,51 @@ pkgs.nixosTest {
|
|||||||
sabnzbd.enable = true;
|
sabnzbd.enable = true;
|
||||||
lidarr.enable = true;
|
lidarr.enable = true;
|
||||||
prowlarr.enable = true;
|
prowlarr.enable = true;
|
||||||
recyclarr = {
|
# recyclarr = {
|
||||||
enable = true;
|
# enable = true;
|
||||||
configuration = {
|
# configuration = {
|
||||||
sonarr.series = {
|
# sonarr.series = {
|
||||||
base_url = "http://localhost:8989";
|
# base_url = "http://localhost:8989";
|
||||||
api_key = "!env_var SONARR_API_KEY";
|
# api_key = "!env_var SONARR_API_KEY";
|
||||||
quality_definition.type = "series";
|
# quality_definition.type = "series";
|
||||||
delete_old_custom_formats = true;
|
# delete_old_custom_formats = true;
|
||||||
custom_formats = [
|
# custom_formats = [
|
||||||
{
|
# {
|
||||||
trash_ids = [
|
# trash_ids = [
|
||||||
"85c61753df5da1fb2aab6f2a47426b09" # BR-DISK
|
# "85c61753df5da1fb2aab6f2a47426b09" # BR-DISK
|
||||||
"9c11cd3f07101cdba90a2d81cf0e56b4" # LQ
|
# "9c11cd3f07101cdba90a2d81cf0e56b4" # LQ
|
||||||
];
|
# ];
|
||||||
assign_scores_to = [
|
# assign_scores_to = [
|
||||||
{
|
# {
|
||||||
name = "WEB-DL (1080p)";
|
# name = "WEB-DL (1080p)";
|
||||||
score = -10000;
|
# score = -10000;
|
||||||
}
|
# }
|
||||||
];
|
# ];
|
||||||
}
|
# }
|
||||||
];
|
# ];
|
||||||
};
|
# };
|
||||||
radarr.movies = {
|
# radarr.movies = {
|
||||||
base_url = "http://localhost:7878";
|
# base_url = "http://localhost:7878";
|
||||||
api_key = "!env_var RADARR_API_KEY";
|
# api_key = "!env_var RADARR_API_KEY";
|
||||||
quality_definition.type = "movie";
|
# quality_definition.type = "movie";
|
||||||
delete_old_custom_formats = true;
|
# delete_old_custom_formats = true;
|
||||||
custom_formats = [
|
# custom_formats = [
|
||||||
{
|
# {
|
||||||
trash_ids = [
|
# trash_ids = [
|
||||||
"570bc9ebecd92723d2d21500f4be314c" # Remaster
|
# "570bc9ebecd92723d2d21500f4be314c" # Remaster
|
||||||
"eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster
|
# "eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster
|
||||||
];
|
# ];
|
||||||
assign_scores_to = [
|
# assign_scores_to = [
|
||||||
{
|
# {
|
||||||
name = "HD Bluray + WEB";
|
# name = "HD Bluray + WEB";
|
||||||
score = 25;
|
# score = 25;
|
||||||
}
|
# }
|
||||||
];
|
# ];
|
||||||
}
|
# }
|
||||||
];
|
# ];
|
||||||
};
|
# };
|
||||||
};
|
# };
|
||||||
};
|
# };
|
||||||
};
|
};
|
||||||
|
|
||||||
# Create a test user to verify mediaUsers functionality
|
# Create a test user to verify mediaUsers functionality
|
||||||
@@ -109,11 +109,7 @@ pkgs.nixosTest {
|
|||||||
machine.succeed("systemctl is-active sabnzbd")
|
machine.succeed("systemctl is-active sabnzbd")
|
||||||
machine.succeed("systemctl is-active lidarr")
|
machine.succeed("systemctl is-active lidarr")
|
||||||
machine.succeed("systemctl is-active prowlarr")
|
machine.succeed("systemctl is-active prowlarr")
|
||||||
machine.succeed("systemctl is-active recyclarr")
|
# machine.succeed("systemctl is-active recyclarr")
|
||||||
|
|
||||||
machine.succeed("nixarr list-api-keys")
|
|
||||||
machine.succeed("nixarr fix-permissions")
|
|
||||||
machine.succeed("nixarr wipe-uids-gids")
|
|
||||||
|
|
||||||
print("\n=== Nixarr Simple Test Completed ===")
|
print("\n=== Nixarr Simple Test Completed ===")
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
/*
|
/*
|
||||||
VPN Confinement Integration Test
|
VPN Confinement Integration Test
|
||||||
|
|
||||||
This test validates that Nixarr services are properly confined to a VPN namespace
|
This test validates that Nixarr services are properly confined to a VPN namespace
|
||||||
and cannot leak traffic when the VPN connection fails. It uses a 3-VM topology
|
and cannot leak traffic when the VPN connection fails. It uses a 3-VM topology
|
||||||
to simulate real-world network conditions.
|
to simulate real-world network conditions.
|
||||||
|
|
||||||
Network Topology:
|
Network Topology:
|
||||||
┌──────────────┐ VLAN 2 ┌─────────────┐ VLAN 1 ┌─────────────┐
|
┌──────────────┐ VLAN 2 ┌─────────────┐ VLAN 1 ┌─────────────┐
|
||||||
│internetClient│ ◄──────────── │ gateway │ ◄──────────── │ nixarrHost │
|
│internetClient│ ◄──────────── │ gateway │ ◄──────────── │ nixarrHost │
|
||||||
│ 10.0.1.2 │ │ 10.0.1.1 │ │192.168.1.2 │
|
│ 10.0.1.2 │ │ 10.0.1.1 │ │192.168.1.2 │
|
||||||
│ fd00:2::2 │ │192.168.1.1 │ │ fd00:1::2 │
|
│ fd00:2::2 │ │192.168.1.1 │ │ fd00:1::2 │
|
||||||
└──────────────┘ │ fd00:2::1 │ └─────────────┘
|
└──────────────┘ │ fd00:2::1 │ └─────────────┘
|
||||||
│ fd00:1::1 │ │
|
│ fd00:1::1 │ │
|
||||||
└─────────────┘ │
|
└─────────────┘ │
|
||||||
│ │
|
│ │
|
||||||
@@ -19,20 +19,20 @@
|
|||||||
fd00:100::1 VPN namespace
|
fd00:100::1 VPN namespace
|
||||||
(10.100.0.2, fd00:100::2)
|
(10.100.0.2, fd00:100::2)
|
||||||
|
|
||||||
Test Coverage:
|
Test Coverage:
|
||||||
- VPN namespace isolation (transmission confined to wg namespace)
|
- VPN namespace isolation (transmission confined to wg namespace)
|
||||||
- IPv4 and IPv6 traffic routing through VPN tunnel
|
- IPv4 and IPv6 traffic routing through VPN tunnel
|
||||||
- Traffic leak prevention when VPN is down
|
- Traffic leak prevention when VPN is down
|
||||||
- Port forwarding from external clients through gateway to VPN services
|
- Port forwarding from external clients through gateway to VPN services
|
||||||
- DNS configuration in VPN namespace
|
- DNS configuration in VPN namespace
|
||||||
- Service recovery after VPN reconnection
|
- Service recovery after VPN reconnection
|
||||||
|
|
||||||
The test ensures that:
|
The test ensures that:
|
||||||
1. All transmission traffic goes through the VPN tunnel
|
1. All transmission traffic goes through the VPN tunnel
|
||||||
2. Source IP is preserved (shows VPN client IP: 10.100.0.2/fd00:100::2)
|
2. Source IP is preserved (shows VPN client IP: 10.100.0.2/fd00:100::2)
|
||||||
3. No traffic leaks to host network when VPN fails
|
3. No traffic leaks to host network when VPN fails
|
||||||
4. External port forwarding works correctly
|
4. External port forwarding works correctly
|
||||||
5. Both IPv4 and IPv6 work identically through the tunnel
|
5. Both IPv4 and IPv6 work identically through the tunnel
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
|
|||||||
@@ -36,13 +36,15 @@ in {
|
|||||||
recyclarr = 269;
|
recyclarr = 269;
|
||||||
sabnzbd = 38;
|
sabnzbd = 38;
|
||||||
transmission = 70;
|
transmission = 70;
|
||||||
cross-seed = 183;
|
# Removed 2025-10-29
|
||||||
|
# cross-seed = 183;
|
||||||
whisparr = 272;
|
whisparr = 272;
|
||||||
stash = 69;
|
stash = 69;
|
||||||
};
|
};
|
||||||
gids = {
|
gids = {
|
||||||
autobrr = 188;
|
autobrr = 188;
|
||||||
cross-seed = 183;
|
# Removed 2025-10-29
|
||||||
|
# cross-seed = 183;
|
||||||
jellyseerr = 250;
|
jellyseerr = 250;
|
||||||
media = 169;
|
media = 169;
|
||||||
prowlarr = 287;
|
prowlarr = 287;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ in {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
mkIf cfg.upnp.enable {
|
mkIf cfg.enable {
|
||||||
enable = true;
|
enable = true;
|
||||||
description = "Sets port on router";
|
description = "Sets port on router";
|
||||||
script = "${upnp-ports}/bin/upnp-ports";
|
script = "${upnp-ports}/bin/upnp-ports";
|
||||||
@@ -89,7 +89,7 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
timers = {
|
timers = {
|
||||||
upnpc = mkIf cfg.upnp.enable {
|
upnpc = mkIf cfg.enable {
|
||||||
description = "Sets port on router";
|
description = "Sets port on router";
|
||||||
wantedBy = ["timers.target"];
|
wantedBy = ["timers.target"];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user