feat: add workaround for qBittorrent 5.2.0 stale lock issue with init container
Some checks failed
validate / lint (push) Failing after 1s

This commit is contained in:
Nik Afiq 2026-08-25 22:27:47 +09:00
parent e757850c3b
commit 663337f818
3 changed files with 69 additions and 9 deletions

View File

@ -159,6 +159,18 @@ real.
table, ACCEPT + established/related return inserted into the table, ACCEPT + established/related return inserted into the
`PIA-VLAN50` chain (not `FORWARD` directly), and source-NAT/MASQUERADE `PIA-VLAN50` chain (not `FORWARD` directly), and source-NAT/MASQUERADE
scoped to `10.10.50.0/24` on `pia-wg` only. scoped to `10.10.50.0/24` on `pia-wg` only.
- Clamps TCP MSS on the `pia-wg` forward path (`pia_mss_clamp_enabled:
true`, `iptables -t mangle ... TCPMSS --clamp-mss-to-pmtu`, scoped to
SYN packets sourced from `10.10.50.0/24` outbound on `pia-wg` only, in
`PostUp`/`PreDown` alongside the rules above). Confirmed needed live
(2026-08-25), not enabled speculatively: a real download hung with a
TLS read timeout on the larger handshake response while small
requests worked fine, and a direct `ping -M do -s 1450` test from a
VLAN 50 pod confirmed the real path MTU is `pia-wg`'s `1420` (the ICMP
"Frag needed" reply arrives correctly from minisforum itself — our own
side relays PMTU discovery fine, so the black hole is further out, on
PIA's network or the remote server's own path, where clamping the MSS
up front avoids needing that ICMP round-trip at all).
- Installs an observability-only health check (`pia-gateway-healthcheck - Installs an observability-only health check (`pia-gateway-healthcheck
.timer`, every `pia_healthcheck_interval_sec`) that logs interface, .timer`, every `pia_healthcheck_interval_sec`) that logs interface,
handshake age, rule/route, and firewall-policy state to the journal. It handshake age, rule/route, and firewall-policy state to the journal. It
@ -171,9 +183,6 @@ real.
every run — `tasks/assert-baseline.yaml` fails loudly if that's already every run — `tasks/assert-baseline.yaml` fails loudly if that's already
wrong). wrong).
- Does not enable IPv6 forwarding or any IPv6 handling for VLAN 50. - Does not enable IPv6 forwarding or any IPv6 handling for VLAN 50.
- Does not enable MSS clamping (`pia_mss_clamp_enabled: false` by
default) — flip only after Phase 5 canary MTU testing shows it's
actually needed.
- Does not modify `ansible/roles/wireguard` (the separate `wg0` home-VPN - Does not modify `ansible/roles/wireguard` (the separate `wg0` home-VPN
server role) or its interface. server role) or its interface.

View File

@ -103,12 +103,21 @@ pia_lan_interface: enp1s0
# minisforum would route toward that source itself. # minisforum would route toward that source itself.
pia_rp_filter_mode: 2 pia_rp_filter_mode: 2
# MSS clamping on the pia-wg forward path — OFF by default. plan.md is # MSS clamping on the pia-wg forward path — ON. Confirmed needed live
# explicit: do not guess this. Flip to true only after Phase 5 canary # (2026-08-25), not guessed: a real JDownloader download to
# MTU/path-MTU testing (large-packet ping with DF set, across the # sbs237.sbsf.tech hung indefinitely (TLS read timeout waiting for the
# VLAN 50 -> Flint -> minisforum -> pia-wg path) shows fragmentation or # server's handshake response — small requests like a CDN version check
# black-holing that clamping fixes. # worked fine, only the larger TLS response never arrived). Verified
pia_mss_clamp_enabled: false # with the exact test plan.md called for before flipping this: `ping -M
# do -s 1450 -c 3 1.1.1.1` from a VLAN 50 pod returned `From 10.10.40.53
# Frag needed and DF set (mtu = 1420)` — our own PMTU discovery between
# the pod and minisforum works correctly, which means the black hole is
# further out (PIA's network or the remote server's own path), not on
# our side — exactly the class of problem MSS clamping fixes by
# avoiding the need for that ICMP round-trip at all, clamping the MSS
# at the TCP handshake instead of relying on mid-connection PMTU
# discovery that something further along the path may not relay back.
pia_mss_clamp_enabled: true
# Health check cadence and thresholds. The check only observes and logs — # Health check cadence and thresholds. The check only observes and logs —
# see tasks/healthcheck.yaml — it must never itself open the kill switch. # see tasks/healthcheck.yaml — it must never itself open the kill switch.

View File

@ -167,6 +167,48 @@ spec:
volumeMounts: volumeMounts:
- name: config - name: config
mountPath: /config mountPath: /config
# Works around a real upstream qBittorrent 5.2.0 bug
# (qbittorrent/qBittorrent#24164, confirmed against the actual
# issue thread, not assumed): 5.2.0 switched its single-instance
# lock from the old fcntl-based QtLockedFile to Qt's QLockFile,
# which embeds a PID + hostname. If the previous shutdown wasn't
# clean, 5.2.0 can't parse/verify the stale lock, assumes another
# instance holds it, tries to hand off over the (also stale)
# ipc-socket, that fails too, and it exits immediately — then
# restarts, hits the same stale lock, and repeats forever (a
# crash loop, not a slow startup — confirmed live 2026-08-25 from
# this exact pod: PID incrementing every ~1-2s, 80% CPU, WebUI
# never listening). Every Kubernetes pod restart gets a new
# hostname (the pod name), which is exactly the kind of
# environment where the lock's embedded hostname can't be
# trusted to match — plausibly why this surfaces more here than
# on a stable bare-metal/VM host.
#
# Unconditionally clearing the lock on every start is safe
# specifically in this Deployment: strategy: Recreate plus a
# single replica plus a ReadWriteOnce PVC together guarantee
# Kubernetes itself never runs two instances against this config
# concurrently — by the time this init container runs, any
# previous instance is already fully terminated. So there is no
# scenario here where the lock could be legitimately held by a
# still-running peer.
- name: qbittorrent-clear-stale-lock
image: nicolaka/netshoot:v0.11
securityContext:
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop: ["ALL"]
command:
- /bin/sh
- -c
- |
set -eu
rm -fv /config/qBittorrent/lockfile /config/qBittorrent/ipc-socket
echo "stale lock/IPC state cleared (if any was present)"
volumeMounts:
- name: config
mountPath: /config
containers: containers:
- name: qbittorrent - name: qbittorrent
image: lscr.io/linuxserver/qbittorrent:5.2.0 image: lscr.io/linuxserver/qbittorrent:5.2.0