Skip to content

Commit 3b01609

Browse files
committed
fix mullvad bypass, override prowlarr dynamic user (hate it)
1 parent 799fc1d commit 3b01609

2 files changed

Lines changed: 153 additions & 82 deletions

File tree

modules/arr-common/mkArrServiceModule.nix

Lines changed: 63 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,8 @@ with lib; let
1414
mkArrHostConfigService = import ./hostConfigService.nix {inherit lib pkgs;};
1515
mkArrRootFoldersService = import ./rootFoldersService.nix {inherit lib pkgs;};
1616
capitalizedName = toUpper (substring 0 1 serviceName) + substring 1 (-1) serviceName;
17-
usesDynamicUser = elem serviceName ["prowlarr"];
1817
usesMediaDirs = !(elem serviceName ["prowlarr"]);
19-
effectiveUser =
20-
if usesDynamicUser
21-
then serviceName
22-
else cfg.user;
18+
serviceSupportsUserGroup = !(elem serviceName ["prowlarr"]);
2319
in {
2420
options.nixflix.${serviceName} =
2521
{
@@ -37,6 +33,18 @@ in {
3733
};
3834
};
3935

36+
user = mkOption {
37+
type = types.str;
38+
default = serviceName;
39+
description = "User under which the service runs";
40+
};
41+
42+
group = mkOption {
43+
type = types.str;
44+
default = serviceName;
45+
description = "Group under which the service runs";
46+
};
47+
4048
config = mkOption {
4149
type =
4250
arrConfigModule
@@ -58,19 +66,6 @@ in {
5866
description = "${capitalizedName} configuration options that will be set via the API.";
5967
};
6068
}
61-
// optionalAttrs (!usesDynamicUser) {
62-
group = mkOption {
63-
type = types.str;
64-
default = serviceName;
65-
description = "Group under which the service runs";
66-
};
67-
68-
user = mkOption {
69-
type = types.str;
70-
default = serviceName;
71-
description = "User under which the service runs";
72-
};
73-
}
7469
// optionalAttrs usesMediaDirs {
7570
mediaDirs = mkOption {
7671
type = types.listOf (types.submodule {
@@ -92,15 +87,13 @@ in {
9287
};
9388

9489
config = mkIf (nixflix.enable && cfg.enable) {
95-
# Assertion: VPN routing requires Mullvad to be enabled
9690
assertions = [
9791
{
9892
assertion = cfg.vpn.enable -> config.nixflix.mullvad.enable;
9993
message = "Cannot enable VPN routing for ${capitalizedName} (nixflix.${serviceName}.vpn.enable = true) when Mullvad VPN is disabled. Please set nixflix.mullvad.enable = true.";
10094
}
10195
];
10296

103-
# Set pattern-based defaults
10497
nixflix.${serviceName}.config = {
10598
apiKeyPath = mkDefault null;
10699
hostConfig = {
@@ -115,23 +108,13 @@ in {
115108
};
116109
};
117110

118-
# Register directories to be created
119111
nixflix.dirRegistrations =
120112
[
121-
(
122-
if usesDynamicUser
123-
then {
124-
dir = stateDir;
125-
owner = "root";
126-
group = "root";
127-
mode = "0700";
128-
}
129-
else {
130-
inherit (cfg) group;
131-
dir = stateDir;
132-
owner = cfg.user;
133-
}
134-
)
113+
{
114+
inherit (cfg) group;
115+
dir = stateDir;
116+
owner = cfg.user;
117+
}
135118
]
136119
++ optionals usesMediaDirs (map (mediaDir: {
137120
inherit (cfg) group;
@@ -144,6 +127,11 @@ in {
144127
{
145128
inherit (cfg) enable;
146129
dataDir = stateDir;
130+
}
131+
// optionalAttrs serviceSupportsUserGroup {
132+
inherit (cfg) user group;
133+
}
134+
// {
147135
settings =
148136
{
149137
auth = {
@@ -155,23 +143,20 @@ in {
155143
// optionalAttrs config.services.postgresql.enable {
156144
log.dbEnabled = true;
157145
postgres = {
158-
user = effectiveUser;
146+
user = cfg.user;
159147
host = "/run/postgresql";
160148
port = 5432;
161-
mainDb = effectiveUser;
162-
logDb = effectiveUser;
149+
mainDb = cfg.user;
150+
logDb = cfg.user;
163151
};
164152
};
165-
}
166-
// optionalAttrs (!usesDynamicUser) {
167-
inherit (cfg) user group;
168153
};
169154

170155
postgresql = mkIf config.services.postgresql.enable {
171-
ensureDatabases = [effectiveUser];
156+
ensureDatabases = [cfg.user];
172157
ensureUsers = [
173158
{
174-
name = effectiveUser;
159+
name = cfg.user;
175160
ensureDBOwnership = true;
176161
}
177162
];
@@ -195,7 +180,7 @@ in {
195180
};
196181
};
197182

198-
users = mkIf (!usesDynamicUser) {
183+
users = {
199184
groups.${cfg.group} = optionalAttrs (globals.gids ? ${cfg.group}) {
200185
gid = globals.gids.${cfg.group};
201186
};
@@ -218,30 +203,17 @@ in {
218203
before = ["postgresql-ready.target"];
219204
requiredBy = ["postgresql-ready.target"];
220205

221-
serviceConfig =
222-
{
223-
Type = "oneshot";
224-
RemainAfterExit = true;
225-
TimeoutStartSec = "5min";
226-
}
227-
// optionalAttrs (!usesDynamicUser) {
228-
User = cfg.user;
229-
Group = cfg.group;
230-
};
206+
serviceConfig = {
207+
Type = "oneshot";
208+
RemainAfterExit = true;
209+
TimeoutStartSec = "5min";
210+
User = cfg.user;
211+
Group = cfg.group;
212+
};
231213

232-
script = let
233-
dbUser = effectiveUser;
234-
psqlCmd =
235-
if usesDynamicUser
236-
then "${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql}/bin/psql"
237-
else "${pkgs.postgresql}/bin/psql -h /run/postgresql";
238-
checkCmd =
239-
if usesDynamicUser
240-
then "SELECT 1 FROM pg_database WHERE datname='${dbUser}'"
241-
else "SELECT 1";
242-
in ''
214+
script = ''
243215
while true; do
244-
if ${psqlCmd} -d ${dbUser} -c "${checkCmd}" > /dev/null 2>&1; then
216+
if ${pkgs.postgresql}/bin/psql -h /run/postgresql -d ${cfg.user} -c "SELECT 1" > /dev/null 2>&1; then
245217
echo "${capitalizedName} PostgreSQL database is ready"
246218
exit 0
247219
fi
@@ -265,16 +237,29 @@ in {
265237
++ (optional (cfg.config.apiKeyPath != null && cfg.config.hostConfig.passwordPath != null) "${serviceName}-env.service")
266238
++ (optional config.services.postgresql.enable "postgresql-ready.target");
267239
wants = optional config.nixflix.mullvad.enable "mullvad-config.service";
268-
}
269-
// optionalAttrs (cfg.config.apiKeyPath != null && cfg.config.hostConfig.passwordPath != null) {
270-
serviceConfig.EnvironmentFile = "/run/${serviceName}/env";
271-
}
272-
// optionalAttrs (config.nixflix.mullvad.enable && !cfg.vpn.enable) {
273-
# Bypass VPN by wrapping with mullvad-exclude
274-
serviceConfig.ExecStart = mkForce (pkgs.writeShellScript "${serviceName}-vpn-bypass" ''
275-
exec /run/wrappers/bin/mullvad-exclude ${getExe config.services.${serviceName}.package} \
276-
-nobrowser -data='${stateDir}'
277-
'');
240+
241+
# Always use static users and configure VPN bypass
242+
serviceConfig =
243+
{
244+
# DynamicUser causes issues with VPN bypass and permissions
245+
DynamicUser = mkForce false;
246+
User = cfg.user;
247+
Group = cfg.group;
248+
}
249+
// optionalAttrs (cfg.config.apiKeyPath != null && cfg.config.hostConfig.passwordPath != null) {
250+
EnvironmentFile = "/run/${serviceName}/env";
251+
}
252+
// optionalAttrs (config.nixflix.mullvad.enable && !cfg.vpn.enable) {
253+
# Bypass VPN by wrapping with mullvad-exclude
254+
ExecStart = mkForce (pkgs.writeShellScript "${serviceName}-vpn-bypass" ''
255+
exec /run/wrappers/bin/mullvad-exclude ${getExe config.services.${serviceName}.package} \
256+
-nobrowser -data='${stateDir}'
257+
'');
258+
# mullvad-exclude needs CAP_SYS_ADMIN to manipulate cgroups
259+
AmbientCapabilities = "CAP_SYS_ADMIN";
260+
# Delegate allows the service to manage its cgroup subtree
261+
Delegate = mkForce true;
262+
};
278263
};
279264
}
280265
# Only create config and rootfolders services if apiKeyPath is configured
@@ -295,12 +280,8 @@ in {
295280
in ''
296281
mkdir -p /run/${serviceName}
297282
echo "${envVar}=$(cat ${cfg.config.apiKeyPath})" > /run/${serviceName}/env
298-
${optionalString (!usesDynamicUser) "chown ${cfg.user}:${cfg.group} /run/${serviceName}/env"}
299-
chmod 0${
300-
if usesDynamicUser
301-
then "444"
302-
else "400"
303-
} /run/${serviceName}/env
283+
chown ${cfg.user}:${cfg.group} /run/${serviceName}/env
284+
chmod 0400 /run/${serviceName}/env
304285
'';
305286
};
306287

tests/vm-tests/mullvad-integration.nix

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,60 @@ pkgs.testers.runNixOSTest {
2424
};
2525
dns = ["1.1.1.1" "1.0.0.1"];
2626
};
27+
28+
prowlarr = {
29+
enable = true;
30+
config = {
31+
hostConfig = {
32+
port = 9696;
33+
username = "admin";
34+
passwordPath = "${pkgs.writeText "prowlarr-password" "testpass"}";
35+
};
36+
apiKeyPath = "${pkgs.writeText "prowlarr-apikey" "prowlarr11111111111111111111111111"}";
37+
};
38+
};
39+
40+
sonarr = {
41+
enable = true;
42+
user = "mediauser";
43+
mediaDirs = [{dir = "/media/tv";}];
44+
config = {
45+
hostConfig = {
46+
port = 8989;
47+
username = "admin";
48+
passwordPath = "${pkgs.writeText "sonarr-password" "testpass"}";
49+
};
50+
apiKeyPath = "${pkgs.writeText "sonarr-apikey" "sonarr222222222222222222222222222"}";
51+
};
52+
};
53+
54+
radarr = {
55+
enable = true;
56+
user = "mediauser";
57+
mediaDirs = [{dir = "/media/movies";}];
58+
config = {
59+
hostConfig = {
60+
port = 7878;
61+
username = "admin";
62+
passwordPath = "${pkgs.writeText "radarr-password" "testpass"}";
63+
};
64+
apiKeyPath = "${pkgs.writeText "radarr-apikey" "radarr333333333333333333333333333"}";
65+
};
66+
};
67+
68+
lidarr = {
69+
enable = true;
70+
user = "mediauser";
71+
mediaDirs = [{dir = "/media/music";}];
72+
config = {
73+
hostConfig = {
74+
port = 8686;
75+
username = "admin";
76+
passwordPath = "${pkgs.writeText "lidarr-password" "testpass"}";
77+
};
78+
apiKeyPath = "${pkgs.writeText "lidarr-apikey" "lidarr444444444444444444444444444"}";
79+
};
80+
};
2781
};
2882
};
2983

@@ -66,6 +120,42 @@ pkgs.testers.runNixOSTest {
66120
print("Testing disconnect command...")
67121
machine.succeed("mullvad disconnect || true")
68122
123+
# Wait for all services to start
124+
machine.wait_for_unit("prowlarr.service", timeout=30)
125+
machine.wait_for_unit("sonarr.service", timeout=30)
126+
machine.wait_for_unit("radarr.service", timeout=30)
127+
machine.wait_for_unit("lidarr.service", timeout=30)
128+
129+
# Verify services are actually running and stable (not crash-looping)
130+
print("Verifying services are stable and responding...")
131+
import time
132+
time.sleep(5) # Wait for any crashes to happen
133+
134+
# Check that services are still active (not failed)
135+
machine.succeed("systemctl is-active prowlarr.service")
136+
machine.succeed("systemctl is-active sonarr.service")
137+
machine.succeed("systemctl is-active radarr.service")
138+
machine.succeed("systemctl is-active lidarr.service")
139+
140+
# Check that services haven't restarted (restart counter should be 0)
141+
prowlarr_restarts = machine.succeed("systemctl show prowlarr.service -p NRestarts --value")
142+
assert prowlarr_restarts.strip() == "0", f"Prowlarr has restarted {prowlarr_restarts.strip()} times"
143+
144+
sonarr_restarts = machine.succeed("systemctl show sonarr.service -p NRestarts --value")
145+
assert sonarr_restarts.strip() == "0", f"Sonarr has restarted {sonarr_restarts.strip()} times"
146+
147+
radarr_restarts = machine.succeed("systemctl show radarr.service -p NRestarts --value")
148+
assert radarr_restarts.strip() == "0", f"Radarr has restarted {radarr_restarts.strip()} times"
149+
150+
lidarr_restarts = machine.succeed("systemctl show lidarr.service -p NRestarts --value")
151+
assert lidarr_restarts.strip() == "0", f"Lidarr has restarted {lidarr_restarts.strip()} times"
152+
153+
# Verify HTTP endpoints are responding
154+
machine.wait_for_open_port(9696, timeout=60) # Prowlarr
155+
machine.wait_for_open_port(8989, timeout=60) # Sonarr
156+
machine.wait_for_open_port(7878, timeout=60) # Radarr
157+
machine.wait_for_open_port(8686, timeout=60) # Lidarr
158+
69159
print("Mullvad integration test successful! Kill switch configured correctly.")
70160
'';
71161
}

0 commit comments

Comments
 (0)