Merge branch 'main' into dev

This commit is contained in:
rasmus-kirk
2025-10-29 10:01:21 +01:00
14 changed files with 276 additions and 164 deletions
Generated
+10 -10
View File
@@ -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": {
+5 -2
View File
@@ -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];
-1
View File
@@ -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";
+1
View File
@@ -15,6 +15,7 @@ in {
./ddns ./ddns
./jellyfin ./jellyfin
./jellyseerr ./jellyseerr
./lib/api-keys.nix
./komga ./komga
./lidarr ./lidarr
./nixarr-command ./nixarr-command
+109
View File
@@ -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 - -"
];
};
}
+13 -12
View File
@@ -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
User = globals.prowlarr.user; # module (because a user and group with those names exists).
Group = globals.prowlarr.group; User = globals.prowlarr.user;
ExecStart = "${lib.getExe cfg.package} -nobrowser -data=${cfg.stateDir}"; Group = globals.prowlarr.group;
Restart = "on-failure"; ExecStart = mkForce "${lib.getExe cfg.package} -nobrowser -data=${cfg.stateDir}";
}; ReadWritePaths = [cfg.stateDir];
}; };
networking.firewall = mkIf cfg.openFirewall { networking.firewall = mkIf cfg.openFirewall {
+22 -39
View File
@@ -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} - -"
]; ];
}; };
} }
+12 -5
View File
@@ -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}";
}; };
}; };
}; };
+16 -4
View File
@@ -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 =
+5 -6
View File
@@ -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
View File
@@ -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 ===")
''; '';
+30 -30
View File
@@ -1,38 +1,38 @@
/* /*
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
WireGuard tunnel WireGuard tunnel
10.100.0.1 10.100.0.1
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,
+4 -2
View File
@@ -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;
+2 -2
View File
@@ -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"];