add: WireGuard bypass tunnel #217

Merged
mysticalsoap merged 3 commits from add/wireguard-bypass-216 into trunk 2026-09-17 01:05:41 -04:00
Owner

Closes the last WireGuard capability gap (#216), on the #176 seams.

Change:

  • WireGuard.roles grows "bypass" (device wg_aqomui_b, already reserved); TUNNEL_BYPASS["WireGuard"] flips, which alone lifts the gui gate.
  • Bypass-leg routing: endpoint pin in the main table only - the outer packets are unmarked (kernel WireGuard never inherits the inner fwmark, unlike ovpn-dco's marked data, which is why the OpenVPN bypass pins both tables) - and no default of its own. The tunnel thread installs default dev wg_aqomui_b table 11: the WireGuard analogue of bypass_up.sh, minus the $route_vpn_gateway a tun-like device doesn't need.
  • wg() is role-aware: _bypass suffix on statuses, role state on "bypass", no resolved entry (#59's rule - the service's bypass resolver reads the role's dns fields; conf DNS lands there, reachable through the tunnel it belongs to).
  • The handshake nudge rides a throwaway TEST-NET-1 host route into the device (one code path for every role - a bypass leg's default lives in a table no plain ping consults). The nudge route is installed and removed around the wait.
  • Service: bypass_protocol constructed at connect, torn down on disconnect("bypass") with tunnel_terminated_bypass announced and a cgroup rebuild - the wg default in table 11 dies with the device, and the physical-link one must return for still-bypassed apps.

Verification: suite 541 passed - bypass routing policy (pin-no-default), table-11 default, resolved-untouched with dns riding the role state, nudge route install/cleanup, service teardown + cgroup rebuild.

Live checks before merge (needs #215's rule too - merge order free, they're disjoint):

  1. Main tunnel + WG bypass server up → aqomui-bypass curl icanhazip.com shows the bypass server's exit; plain curl shows the main tunnel's.
  2. ip route show table 11 → default via wg_aqomui_b; resolvectl domain → ~. on the main link only.
  3. Disconnect bypass → bypassed apps fall back to the physical link (table 11 default restored), no route residue.
  4. A bypassed app's DNS still resolves while the bypass tunnel is up (dnsmasq upstreams).

Closes #216.

🤖 Generated with Claude Code

Closes the last WireGuard capability gap (#216), on the #176 seams. **Change**: - `WireGuard.roles` grows "bypass" (device `wg_aqomui_b`, already reserved); `TUNNEL_BYPASS["WireGuard"]` flips, which alone lifts the gui gate. - Bypass-leg routing: endpoint pin in the **main table only** - the outer packets are unmarked (kernel WireGuard never inherits the inner fwmark, unlike ovpn-dco's marked data, which is why the OpenVPN bypass pins both tables) - and no default of its own. The tunnel thread installs `default dev wg_aqomui_b table 11`: the WireGuard analogue of bypass_up.sh, minus the $route_vpn_gateway a tun-like device doesn't need. - `wg()` is role-aware: `_bypass` suffix on statuses, role state on "bypass", **no resolved entry** (#59's rule - the service's bypass resolver reads the role's dns fields; conf DNS lands there, reachable through the tunnel it belongs to). - The handshake nudge rides a throwaway TEST-NET-1 host route into the device (one code path for every role - a bypass leg's default lives in a table no plain ping consults). The nudge route is installed and removed around the wait. - Service: `bypass_protocol` constructed at connect, torn down on `disconnect("bypass")` with `tunnel_terminated_bypass` announced and a cgroup rebuild - the wg default in table 11 dies with the device, and the physical-link one must return for still-bypassed apps. **Verification**: suite 541 passed - bypass routing policy (pin-no-default), table-11 default, resolved-untouched with dns riding the role state, nudge route install/cleanup, service teardown + cgroup rebuild. **Live checks before merge** (needs #215's rule too - merge order free, they're disjoint): 1. Main tunnel + WG bypass server up → `aqomui-bypass curl icanhazip.com` shows the **bypass** server's exit; plain curl shows the main tunnel's. 2. `ip route show table 11` → default via `wg_aqomui_b`; `resolvectl domain` → `~.` on the main link only. 3. Disconnect bypass → bypassed apps fall back to the physical link (table 11 default restored), no route residue. 4. A bypassed app's DNS still resolves while the bypass tunnel is up (dnsmasq upstreams). Closes #216. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
add: WireGuard bypass tunnel
All checks were successful
ci / test (pull_request) Successful in 25s
0520f90915
The last WireGuard capability gap. The bypass role joins the class's
role table (device wg_aqomui_b was already reserved) with the routing
a bypass leg needs: endpoint pin in the main table only - the outer
packets are unmarked, kernel WireGuard never inherits the inner
fwmark, unlike ovpn-dco's marked data - and no default of its own.
The tunnel thread installs the table-11 default, the WireGuard
analogue of bypass_up.sh minus the gateway a tun-like device does not
need.

wg() becomes role-aware: bypass suffix on its statuses, role state on
"bypass", and no resolved entry (#59's rule) - the service's bypass
resolver reads the role's dns fields instead. The handshake nudge now
rides a throwaway TEST-NET-1 host route into the device, since a
bypass leg's default lives in a table no plain ping consults.

The service constructs and tears down the bypass protocol object like
the hop one; teardown rebuilds the cgroup so the physical-link
default returns to table 11 for the still-bypassed apps.

Closes #216

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mysticalsoap force-pushed add/wireguard-bypass-216 from 0520f90915
All checks were successful
ci / test (pull_request) Successful in 25s
to 31341b0bc4
All checks were successful
ci / test (pull_request) Successful in 24s
2026-09-16 20:50:51 -04:00
Compare
mysticalsoap force-pushed add/wireguard-bypass-216 from 31341b0bc4
All checks were successful
ci / test (pull_request) Successful in 24s
to 5075e187b5
All checks were successful
ci / test (pull_request) Successful in 24s
2026-09-16 21:26:52 -04:00
Compare
fix: masquerade the WireGuard bypass device too
All checks were successful
ci / test (pull_request) Successful in 25s
97162df0eb
The bypass tunnel's source-rewrite rule was keyed on the OpenVPN
device alone. A WireGuard bypass handshook, took the table-11
default, and then hung every connection: marked packets entered
wg_aqomui_b carrying the physical link's source, and the server's
cryptokey check dropped them on arrival.

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

Live round 1 findings, fixed in 97162df:

  • Hung bypass traffic: the tunnel handshook and took the table-11 default, then every connection through it hung. Root cause is the mechanism bypass.py already documents for the OpenVPN device - marked packets get their source address before the mark reroutes them - but the MASQUERADE rule that rewrites it was keyed on tun_aqomui_b only. Packets entered wg_aqomui_b with the physical link's source and Proton's cryptokey check dropped them on arrival. Now one rule per protocol's bypass device; test pins both.
  • Not a bug: one Proton WG node ignored every handshake init (tcpdump: init leaves, nothing returns, no conntrack entry). That is WireGuard's silent-drop for an unknown key - stale imported peer data after a Proton-side rotation. A different node connected fine; the handshake gate turned it into a clean failure instead of a fake connection.

Round 2 needs a reinstall from the branch, then the same checks: bypass egress via aqomui-bypass curl (and a fresh bypassed app), ip route show table 11, teardown fallback.

Live round 1 findings, fixed in 97162df: - **Hung bypass traffic**: the tunnel handshook and took the table-11 default, then every connection through it hung. Root cause is the mechanism bypass.py already documents for the OpenVPN device - marked packets get their source address before the mark reroutes them - but the MASQUERADE rule that rewrites it was keyed on `tun_aqomui_b` only. Packets entered `wg_aqomui_b` with the physical link's source and Proton's cryptokey check dropped them on arrival. Now one rule per protocol's bypass device; test pins both. - **Not a bug**: one Proton WG node ignored every handshake init (tcpdump: init leaves, nothing returns, no conntrack entry). That is WireGuard's silent-drop for an unknown key - stale imported peer data after a Proton-side rotation. A different node connected fine; the handshake gate turned it into a clean failure instead of a fake connection. Round 2 needs a reinstall from the branch, then the same checks: bypass egress via `aqomui-bypass curl` (and a fresh bypassed app), `ip route show table 11`, teardown fallback.
mysticalsoap force-pushed add/wireguard-bypass-216 from 97162df0eb
All checks were successful
ci / test (pull_request) Successful in 25s
to a5b9a6c675
All checks were successful
ci / test (pull_request) Successful in 28s
2026-09-17 00:26:28 -04:00
Compare
Author
Owner

Round 2 root cause, fixed in the commit above: a WireGuard routing loop. The inner tcpdump on wg_aqomui_b showed the leg's own outer packets (192.168.0.205:41957 → endpoint:51820) re-entering the device and growing 64 bytes per pass until they fragmented - the outer data packets carry the bypassed app's fwmark, consult table 11, and its default steers them straight back in. Handshakes are WireGuard's own unmarked packets, so the tunnel came up fine and reported established; only data looped. Gigabytes counted as sent, nothing on the wire, no bypassed connection ever reached the server.

Same reason the OpenVPN bypass pins its endpoint in both tables - the WireGuard docstring's claim that outer packets never inherit the mark was wrong and is gone. The bypass leg now pins in table 11 as well; link_down clears both. Suite green. Round 3: reinstall, WG bypass, the same three checks.

Round 2 root cause, fixed in the commit above: a WireGuard routing loop. The inner tcpdump on wg_aqomui_b showed the leg's own outer packets (192.168.0.205:41957 → endpoint:51820) re-entering the device and growing 64 bytes per pass until they fragmented - the outer *data* packets carry the bypassed app's fwmark, consult table 11, and its default steers them straight back in. Handshakes are WireGuard's own unmarked packets, so the tunnel came up fine and reported established; only data looped. Gigabytes counted as sent, nothing on the wire, no bypassed connection ever reached the server. Same reason the OpenVPN bypass pins its endpoint in both tables - the WireGuard docstring's claim that outer packets never inherit the mark was wrong and is gone. The bypass leg now pins in table 11 as well; link_down clears both. Suite green. Round 3: reinstall, WG bypass, the same three checks.
fix: pin the WireGuard bypass endpoint in the bypass table too
All checks were successful
ci / test (pull_request) Successful in 23s
e6bb2a412e
The outer data packets of a bypass leg carry the inner packet's fwmark
and consult the bypass table, whose default steers into the device
itself. With the endpoint pinned in the main table alone they looped:
re-encapsulated on every pass, 64 bytes larger each time, until they
fragmented - gigabytes counted as sent with nothing on the wire, and
no bypassed connection ever reached the server. Handshakes still
succeeded, being WireGuard's own unmarked packets, which is why the
tunnel reported up.

Same reason the OpenVPN bypass pins both tables. The docstring that
claimed WireGuard never inherits the mark is gone with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mysticalsoap force-pushed add/wireguard-bypass-216 from e6bb2a412e
All checks were successful
ci / test (pull_request) Successful in 23s
to d60eadc220
Some checks failed
ci / test (pull_request) Successful in 25s
ci / test (push) Has been cancelled
2026-09-17 00:51:39 -04:00
Compare
Author
Owner

Round 3 passed live (2026-09-17): WireGuard bypass carries bypassed traffic to the bypass server's exit, with the endpoint pinned in both tables and the gluetun network entry on table 12 (#222). Three faults fixed on the way, each with its own commit and test: the missing wg_aqomui_b masquerade, the outer-packet routing loop through table 11, and (separately merged) network entries riding the bypass tunnel. Ready to merge.

Round 3 passed live (2026-09-17): WireGuard bypass carries bypassed traffic to the bypass server's exit, with the endpoint pinned in both tables and the gluetun network entry on table 12 (#222). Three faults fixed on the way, each with its own commit and test: the missing wg_aqomui_b masquerade, the outer-packet routing loop through table 11, and (separately merged) network entries riding the bypass tunnel. Ready to merge.
mysticalsoap deleted branch add/wireguard-bypass-216 2026-09-17 01:05:41 -04:00
Sign in to join this conversation.
No description provided.