Some checks failed
validate / lint (push) Failing after 1s
Enabling MSS clamping exposed a general hazard: systemctl restart wg-quick@pia-wg tears down the live interface using whatever PreDown rules are on disk right now, not what the interface was actually brought up with. If Ansible had already rewritten pia-wg.conf with a changed rule, the live interface (built under the old rules) won't have whatever the new PreDown line tries to delete - iptables -D fails, wg-quick's own set -e aborts the whole down/up sequence, and the interface is left orphaned (never reaches its own built-in ip link delete step), which then makes the following wg-quick up fail too with "already exists". Confirmed against wg-quick's real source (execute_hooks() runs each hook via `(eval "$hook")`) that appending `2>/dev/null || true` to each PreDown iptables -D line makes wg-quick treat it as succeeded regardless of prior state - verified by rendering the template with Jinja2 directly before trusting it against the live host. This prevents the failure mode entirely for any future PostUp/PreDown content change, not just this one. Add INCIDENT-2026-08-25-vlan50-pia-qbittorrent.md covering all four issues from this troubleshooting window: the zombie WireGuard tunnel, a qBittorrent 5.2.0 stale-lock crash loop, the PMTU black hole this fix addresses, and the restart failure hit while deploying it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
74 lines
4.5 KiB
Django/Jinja
74 lines
4.5 KiB
Django/Jinja
# Managed by Ansible (role: pia-gateway). Do not edit by hand — changes
|
|
# are overwritten on the next run of ansible/playbooks/pia-gateway.yaml.
|
|
#
|
|
# Table = off: wg-quick must NOT touch the main routing table. All routing
|
|
# for this interface lives in the dedicated "{{ pia_route_table_name }}"
|
|
# table (see tasks/routing.yaml), reached only by traffic sourced from
|
|
# {{ pia_vlan50_subnet }}. minisforum's own default route is never
|
|
# affected by this interface coming up or down.
|
|
[Interface]
|
|
PrivateKey = {{ pia_private_key }}
|
|
Address = {{ pia_peer.peer_ip }}/32
|
|
Table = off
|
|
|
|
# Kill-switch ordering: the boot-time pia-killswitch.service (see
|
|
# tasks/routing.yaml) has already seeded an "unreachable" default route in
|
|
# table {{ pia_route_table_name }} and created the dedicated
|
|
# {{ pia_iptables_chain }} chain (jumped into from FORWARD position 1 for
|
|
# source {{ pia_vlan50_subnet }} only — never touches Flannel/k3s traffic)
|
|
# ending in an unconditional LOG+DROP, before this unit ever starts
|
|
# (systemd After=/Requires=). PostUp only ever *opens* the path narrower
|
|
# than that pre-seeded closed state, by inserting ACCEPT rules ahead of
|
|
# the chain's own catch-all; PreDown removes exactly those same rules,
|
|
# re-closing it before removing anything else.
|
|
PostUp = ip route replace default dev %i table {{ pia_route_table_name }}
|
|
PostUp = iptables -I {{ pia_iptables_chain }} 1 -s {{ pia_vlan50_subnet }} -o %i -j ACCEPT
|
|
PostUp = iptables -I {{ pia_iptables_chain }} 1 -d {{ pia_vlan50_subnet }} -i %i -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
PostUp = iptables -t nat -A POSTROUTING -s {{ pia_vlan50_subnet }} -o %i -j MASQUERADE
|
|
{% if pia_mss_clamp_enabled %}
|
|
PostUp = iptables -t mangle -A FORWARD -s {{ pia_vlan50_subnet }} -o %i -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
|
|
{% endif %}
|
|
|
|
# PreDown's iptables -D lines are deliberately tolerant (2>/dev/null ||
|
|
# true) of the rule they target not existing — confirmed against
|
|
# wg-quick's own source (execute_hooks() runs each line via `(eval
|
|
# "$hook")`, and wg-quick's top-level `set -e` treats that subshell's
|
|
# exit code as fatal for the WHOLE up/down sequence): without this, a
|
|
# single -D failing (e.g. the currently-live interface was brought up
|
|
# under an OLDER config that never added the rule this line is now
|
|
# trying to remove — root-caused live, 2026-08-27, when enabling MSS
|
|
# clamping for the first time made this exact thing happen) aborts
|
|
# every PreDown line after it, including wg-quick's own built-in `ip
|
|
# link delete`, leaving an orphaned interface that then makes the
|
|
# following `wg-quick up` fail too ("pia-wg already exists") — turning
|
|
# any future PostUp/PreDown content change into a two-step manual
|
|
# recovery (`ip link delete dev pia-wg` before a fresh start) instead
|
|
# of a clean, unattended `systemctl restart`. `ip route replace` (the
|
|
# first PreDown line) doesn't need this — replace never fails due to
|
|
# prior state the way -D does.
|
|
PreDown = ip route replace unreachable default table {{ pia_route_table_name }}
|
|
PreDown = iptables -t nat -D POSTROUTING -s {{ pia_vlan50_subnet }} -o %i -j MASQUERADE 2>/dev/null || true
|
|
PreDown = iptables -D {{ pia_iptables_chain }} -d {{ pia_vlan50_subnet }} -i %i -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true
|
|
PreDown = iptables -D {{ pia_iptables_chain }} -s {{ pia_vlan50_subnet }} -o %i -j ACCEPT 2>/dev/null || true
|
|
{% if pia_mss_clamp_enabled %}
|
|
PreDown = iptables -t mangle -D FORWARD -s {{ pia_vlan50_subnet }} -o %i -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 2>/dev/null || true
|
|
{% endif %}
|
|
|
|
[Peer]
|
|
# PIA WireGuard server actually used — {{ pia_region_name_used }}
|
|
# ({{ pia_region_used }}), {{ pia_wg_server_used.cn }}. This is whichever
|
|
# region+server in the pia_region_candidates/pia_region fallback order
|
|
# (see defaults/main.yaml) responded first with status: OK — not
|
|
# necessarily the first-configured region. Re-run the playbook with
|
|
# pia_force_reregister (or check journalctl for the last apply's
|
|
# per-attempt diagnostics) to see what else was tried before this one.
|
|
PublicKey = {{ pia_peer.server_key }}
|
|
# The endpoint IP is the server we actually connected to
|
|
# (pia_wg_server_used, set by addkey-attempt.yaml on whichever candidate
|
|
# in servers.wg succeeded), not a field from the addKey JSON response —
|
|
# matching connect_to_wireguard_with_token.sh, which builds the Endpoint
|
|
# from its own $WG_SERVER_IP rather than trusting a response field for it.
|
|
Endpoint = {{ pia_wg_server_used.ip }}:{{ pia_peer.server_port }}
|
|
AllowedIPs = 0.0.0.0/0
|
|
PersistentKeepalive = 25
|