Skip to content

Commit 50c9b0d

Browse files
Test Userclaude
andcommitted
fix(download-server): add VPN watchdog backoff, port validation, docs
- Add restart backoff to WireGuard watchdog: max 3 restarts per 10 minutes, then suppresses further attempts until the tunnel recovers. Prevents restart-looping all VPN-dependent services when the VPN server is genuinely unreachable. - Add port range validation (1-65535) to the ProtonVPN NAT-PMP port forwarding script to reject malformed responses. - Document why CSRF and Host Header validation are disabled on qBittorrent (nginx reverse proxy rewrites Host/Origin headers). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f091d53 commit 50c9b0d

1 file changed

Lines changed: 32 additions & 4 deletions

File tree

flake-modules/hosts/download-server-1/default.nix

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -339,11 +339,35 @@ in {
339339
INTERFACE="primary-vpn"
340340
# Maximum age in seconds before considering the tunnel stale (5 minutes)
341341
MAX_HANDSHAKE_AGE=300
342+
# Backoff: max 3 restarts within 10 minutes, then stop trying
343+
RESTART_STATE="/run/wireguard-watchdog-restarts"
344+
MAX_RESTARTS=3
345+
BACKOFF_WINDOW=600
346+
347+
do_restart() {
348+
# Clean up restart timestamps older than the backoff window
349+
CURRENT_TIME=$(date +%s)
350+
if [ -f "$RESTART_STATE" ]; then
351+
${pkgs.gawk}/bin/awk -v cutoff="$((CURRENT_TIME - BACKOFF_WINDOW))" '$1 > cutoff' "$RESTART_STATE" > "$RESTART_STATE.tmp"
352+
mv "$RESTART_STATE.tmp" "$RESTART_STATE"
353+
RECENT_RESTARTS=$(wc -l < "$RESTART_STATE")
354+
else
355+
RECENT_RESTARTS=0
356+
fi
357+
358+
if [ "$RECENT_RESTARTS" -ge "$MAX_RESTARTS" ]; then
359+
echo "[$(date)] Suppressing restart — $RECENT_RESTARTS restarts in last $((BACKOFF_WINDOW / 60))min (max: $MAX_RESTARTS)"
360+
return 1
361+
fi
362+
363+
echo "$CURRENT_TIME" >> "$RESTART_STATE"
364+
systemctl restart "wg-quick-$INTERFACE"
365+
}
342366
343367
# Check if the interface exists
344368
if ! ${pkgs.wireguard-tools}/bin/wg show "$INTERFACE" 2>/dev/null | grep -q "interface"; then
345369
echo "[$(date)] Interface $INTERFACE does not exist, restarting..."
346-
systemctl restart "wg-quick-$INTERFACE"
370+
do_restart
347371
exit 0
348372
fi
349373
@@ -352,7 +376,7 @@ in {
352376
353377
if [ -z "$HANDSHAKE_TIME" ] || [ "$HANDSHAKE_TIME" = "0" ]; then
354378
echo "[$(date)] No handshake recorded for $INTERFACE, restarting..."
355-
systemctl restart "wg-quick-$INTERFACE"
379+
do_restart
356380
exit 0
357381
fi
358382
@@ -361,9 +385,11 @@ in {
361385
362386
if [ "$HANDSHAKE_AGE" -gt "$MAX_HANDSHAKE_AGE" ]; then
363387
echo "[$(date)] Handshake for $INTERFACE is $HANDSHAKE_AGE seconds old (max: $MAX_HANDSHAKE_AGE), restarting..."
364-
systemctl restart "wg-quick-$INTERFACE"
388+
do_restart
365389
else
366390
echo "[$(date)] Handshake for $INTERFACE is $HANDSHAKE_AGE seconds old, tunnel is healthy"
391+
# Tunnel is healthy — clear restart history
392+
rm -f "$RESTART_STATE"
367393
fi
368394
'';
369395
};
@@ -533,7 +559,7 @@ in {
533559
if echo "$OUTPUT_TCP" | grep -q "Mapped public port"; then
534560
FORWARDED_PORT=$(echo "$OUTPUT_TCP" | grep "Mapped public port" | grep -oP '\d+' | head -1)
535561
536-
if [ -n "$FORWARDED_PORT" ] && [ "$FORWARDED_PORT" -gt 0 ]; then
562+
if [ -n "$FORWARDED_PORT" ] && [ "$FORWARDED_PORT" -gt 0 ] && [ "$FORWARDED_PORT" -lt 65536 ]; then
537563
echo "[$(date)] Successfully got forwarded port: $FORWARDED_PORT"
538564
539565
# Update Firewall Rules
@@ -1294,6 +1320,8 @@ EOF
12941320
"WebUI\\AuthSubnetWhitelist" = "@Invalid()";
12951321
"WebUI\\Port" = 8080;
12961322
"WebUI\\Username" = "admin";
1323+
# Disabled because qBittorrent sits behind nginx reverse proxy which
1324+
# rewrites the Host header and origin, breaking both validations
12971325
"WebUI\\HostHeaderValidation" = false;
12981326
"WebUI\\CSRFProtection" = false;
12991327
};

0 commit comments

Comments
 (0)