diff --git a/ansible/roles/pia-gateway/README.md b/ansible/roles/pia-gateway/README.md index 50afa3a..1be0bcd 100644 --- a/ansible/roles/pia-gateway/README.md +++ b/ansible/roles/pia-gateway/README.md @@ -159,6 +159,18 @@ real. table, ACCEPT + established/related return inserted into the `PIA-VLAN50` chain (not `FORWARD` directly), and source-NAT/MASQUERADE 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 .timer`, every `pia_healthcheck_interval_sec`) that logs interface, 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 wrong). - 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 server role) or its interface. diff --git a/ansible/roles/pia-gateway/defaults/main.yaml b/ansible/roles/pia-gateway/defaults/main.yaml index 3c377a9..516bf89 100644 --- a/ansible/roles/pia-gateway/defaults/main.yaml +++ b/ansible/roles/pia-gateway/defaults/main.yaml @@ -103,12 +103,21 @@ pia_lan_interface: enp1s0 # minisforum would route toward that source itself. pia_rp_filter_mode: 2 -# MSS clamping on the pia-wg forward path — OFF by default. plan.md is -# explicit: do not guess this. Flip to true only after Phase 5 canary -# MTU/path-MTU testing (large-packet ping with DF set, across the -# VLAN 50 -> Flint -> minisforum -> pia-wg path) shows fragmentation or -# black-holing that clamping fixes. -pia_mss_clamp_enabled: false +# MSS clamping on the pia-wg forward path — ON. Confirmed needed live +# (2026-08-25), not guessed: a real JDownloader download to +# sbs237.sbsf.tech hung indefinitely (TLS read timeout waiting for the +# server's handshake response — small requests like a CDN version check +# worked fine, only the larger TLS response never arrived). Verified +# 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 — # see tasks/healthcheck.yaml — it must never itself open the kill switch. diff --git a/manifests/media/qbittorrent.yaml b/manifests/media/qbittorrent.yaml index 722c6fc..500fd04 100644 --- a/manifests/media/qbittorrent.yaml +++ b/manifests/media/qbittorrent.yaml @@ -167,6 +167,48 @@ spec: volumeMounts: - name: 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: - name: qbittorrent image: lscr.io/linuxserver/qbittorrent:5.2.0