change: WireGuard bring-up moves from wg-quick to manual plumbing #186

Merged
mysticalsoap merged 1 commit from wg-manual-bringup into trunk 2026-08-28 09:59:49 -04:00
Owner

Problem

The first live WireGuard run (#152 step 1, manual ProtonVPN config) leaked bypass traffic into the tunnel. wg-quick installs a mangle-level prerouting rule (meta l4proto udp meta mark set ct mark) that runs after aqomui's PREROUTING bypass marking and erases the fwmark from every forwarded UDP packet — gluetun's entire outer flow (all of qbittorrent's seeding) double-tunneled through the WireGuard session. The existing post-up fwmark-rule reshuffle in tunnel.py ("Necessary, otherwise bypass mode breaks - need to investigate") was a symptom of the same root cause: wg-quick's Table=auto machinery assumes it is the host's only routing manager.

Fix

New aqomui/wireguard.py module does the bring-up with plain wg(8)/ip(8) plumbing; wg-quick is gone:

  • Routing: def1 route pair (0.0.0.0/1 + 128.0.0.0/1, v6 mirror) plus an endpoint host-route pin via the physical link — the same model OpenVPN redirect-gateway tunnels already have, so the bypass machinery needs no WireGuard-specific handling. The pin is main-table-only: kernel WireGuard never inherits the inner packet's fwmark (unlike ovpn-dco), so the bypass table is never consulted for outer packets.
  • No firewall side effects: wg-quick's connmark/anti-spoof rules are simply never installed — the mark-wipe bug class is gone, and the fwmark reshuffle hack is deleted, not ported.
  • Narrower AllowedIPs route verbatim instead of def1, matching wg-quick semantics.
  • v6 gated on firewall.check_ipv6() so ::/0 in AllowedIPs doesn't abort bring-up on a v6-disabled host.
  • Teardown (wireguard.down()) is idempotent, reads the endpoint back from the conf in ROOTDIR, deletes the pin with a proto 111 selector so it can never match a bypass tunnel's pin, and runs in the service startup sweep next to bypass.flush_stale_routes() (#78 pattern).
  • Bring-up failure mid-way tears down what was built and re-raises (becomes a failed attempt via GuardedThread); the device firewall accepts are rolled back in wg().
  • DNS is now applied once by aqomui (wg-quick's resolvconf call previously duplicated it).

Verification

  • pytest: 449 passed (the pre-existing test_mgmt.py socket failures are my sandbox, pass on a normal shell); new tests/test_wireguard.py covers conf splitting, the full bring-up command sequence, v6 gating, narrow AllowedIPs, mid-failure teardown, pin-delete proto selector, and idempotent down.
  • CI lint gate (ruff, compileall) clean locally.
  • Live verification still needed (CONTRIBUTING: real verification is the live system): connect with the ProtonVPN custom WG config via packaging/arch/build-branch-and-install.sh, confirm handshake + traffic, then check bypass with ip route show table 11, nft list ruleset | grep -c wg-quick (expect 0), and watch that gluetun seeding upload rides the physical link, not wg_aqomui.

🤖 Generated with Claude Code

## Problem The first live WireGuard run (#152 step 1, manual ProtonVPN config) leaked bypass traffic into the tunnel. wg-quick installs a mangle-level prerouting rule (`meta l4proto udp meta mark set ct mark`) that runs after aqomui's PREROUTING bypass marking and erases the fwmark from every forwarded UDP packet — gluetun's entire outer flow (all of qbittorrent's seeding) double-tunneled through the WireGuard session. The existing post-up fwmark-rule reshuffle in tunnel.py ("Necessary, otherwise bypass mode breaks - need to investigate") was a symptom of the same root cause: wg-quick's Table=auto machinery assumes it is the host's only routing manager. ## Fix New `aqomui/wireguard.py` module does the bring-up with plain wg(8)/ip(8) plumbing; wg-quick is gone: - **Routing**: def1 route pair (`0.0.0.0/1` + `128.0.0.0/1`, v6 mirror) plus an endpoint host-route pin via the physical link — the same model OpenVPN redirect-gateway tunnels already have, so the bypass machinery needs no WireGuard-specific handling. The pin is main-table-only: kernel WireGuard never inherits the inner packet's fwmark (unlike ovpn-dco), so the bypass table is never consulted for outer packets. - **No firewall side effects**: wg-quick's connmark/anti-spoof rules are simply never installed — the mark-wipe bug class is gone, and the fwmark reshuffle hack is deleted, not ported. - **Narrower AllowedIPs** route verbatim instead of def1, matching wg-quick semantics. - **v6 gated** on `firewall.check_ipv6()` so `::/0` in AllowedIPs doesn't abort bring-up on a v6-disabled host. - **Teardown** (`wireguard.down()`) is idempotent, reads the endpoint back from the conf in ROOTDIR, deletes the pin with a `proto 111` selector so it can never match a bypass tunnel's pin, and runs in the service startup sweep next to `bypass.flush_stale_routes()` (#78 pattern). - Bring-up failure mid-way tears down what was built and re-raises (becomes a failed attempt via GuardedThread); the device firewall accepts are rolled back in `wg()`. - DNS is now applied once by aqomui (wg-quick's resolvconf call previously duplicated it). ## Verification - `pytest`: 449 passed (the pre-existing `test_mgmt.py` socket failures are my sandbox, pass on a normal shell); new `tests/test_wireguard.py` covers conf splitting, the full bring-up command sequence, v6 gating, narrow AllowedIPs, mid-failure teardown, pin-delete proto selector, and idempotent down. - CI lint gate (`ruff`, `compileall`) clean locally. - **Live verification still needed** (CONTRIBUTING: real verification is the live system): connect with the ProtonVPN custom WG config via `packaging/arch/build-branch-and-install.sh`, confirm handshake + traffic, then check bypass with `ip route show table 11`, `nft list ruleset | grep -c wg-quick` (expect 0), and watch that gluetun seeding upload rides the physical link, not `wg_aqomui`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
change: WireGuard bring-up moves from wg-quick to manual plumbing
All checks were successful
ci / test (pull_request) Successful in 26s
ci / test (push) Successful in 25s
0de0b18896
The first live WireGuard run (#152 step 1, a manual ProtonVPN config)
leaked bypass traffic into the tunnel: wg-quick's mangle-level
"mark = ctmark" rule runs after aqomui's PREROUTING marking and
erased the bypass fwmark from every forwarded UDP packet, sending
gluetun's whole outer flow through the tunnel. The old post-up rule
reshuffle ("Necessary, otherwise bypass mode breaks") was fighting
the same split ownership from the other side: two routing managers on
one host.

wg(8)/ip(8) plumbing in the new wireguard module replaces wg-quick:
def1 route pair plus an endpoint pin, the routing model OpenVPN
redirect-gateway tunnels already have, so the bypass machinery needs
no WireGuard-specific handling at all. DNS was already applied by
aqomui (previously duplicated by wg-quick's resolvconf call); the
firewall accepts were already aqomui's too. Teardown and the startup
sweep reuse the conf left in ROOTDIR, mirroring the #78 pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

Live verification on the real system (build installed via build-branch-and-install.sh, connected to the ProtonVPN WG server from the #152 smoke test):

  • wg_aqomui up; def1 pair (0.0.0.0/1, 128.0.0.0/1) on the device; physical default untouched; endpoint pin 154.47.22.90 via 192.168.0.1 dev enp5s0 (proto 111) present
  • ip rule clean: only 32765: fwmark 0xb lookup 11 beside the standard three — none of wg-quick's catch-all/suppress rules exist
  • ip route get 1.1.1.1 → wg_aqomui; ip route get 1.1.1.1 mark 11 → via 192.168.0.1 dev enp5s0 table 11
  • DNS on the tunnel link (10.2.0.1, default route, ~.)
  • Traffic accounting under live seeding: gluetun bridge forwarded 2.67 MiB in a sample window while total tunnel tx was 1.70 MiB (host traffic included) — forwarded traffic exits the physical link, not the tunnel. Longer window: enp5s0 tx ~25 MB vs wg tx ~10 MB, the difference being the bypassed upload. The #152 leak signature is gone.
  • User-observed: connect, disconnect, reconnect all behave through the GUI.
Live verification on the real system (build installed via `build-branch-and-install.sh`, connected to the ProtonVPN WG server from the #152 smoke test): - `wg_aqomui` up; def1 pair (`0.0.0.0/1`, `128.0.0.0/1`) on the device; physical default untouched; endpoint pin `154.47.22.90 via 192.168.0.1 dev enp5s0` (proto 111) present - `ip rule` clean: only `32765: fwmark 0xb lookup 11` beside the standard three — none of wg-quick's catch-all/suppress rules exist - `ip route get 1.1.1.1` → `wg_aqomui`; `ip route get 1.1.1.1 mark 11` → `via 192.168.0.1 dev enp5s0 table 11` - DNS on the tunnel link (10.2.0.1, default route, `~.`) - **Traffic accounting under live seeding**: gluetun bridge forwarded 2.67 MiB in a sample window while total tunnel tx was 1.70 MiB (host traffic included) — forwarded traffic exits the physical link, not the tunnel. Longer window: enp5s0 tx ~25 MB vs wg tx ~10 MB, the difference being the bypassed upload. The #152 leak signature is gone. - User-observed: connect, disconnect, reconnect all behave through the GUI.
mysticalsoap deleted branch wg-manual-bringup 2026-08-28 09:59:50 -04:00
Sign in to join this conversation.
No description provided.