fix: launch the bypass config for custom providers, stop installing its pushed routes #76

Merged
mysticalsoap merged 2 commits from fix/bypass-custom-config into trunk 2026-08-19 22:26:33 -04:00
Owner

Problem

Issue #75, found live during #68's validation: bypass-over-VPN with a custom provider connects but never carries traffic -- no tun_aqomui_b at first, then (once launched correctly) bypassed apps' traffic entered the tunnel and vanished. It turned out to be three stacked defects, each masking the next, plus a service crash on teardown.

Fix (commit 1)

  1. Wrong config launched: the custom-provider branch overwrote the launch path with <provider>/temp.ovpn (the stale main config) instead of the bypass.ovpn write_config had just written. Now bypass dicts keep the bypass path.
  2. Routing rode a mechanism the netlink backend never calls: the iproute script redirect is inert on this OpenVPN build, so even the right config would have let the bypass tunnel's redirect-gateway hijack the main table. Replaced with route-noexec + route-up feeding table 11 (no metric -- ip route replace keys on prefix AND metric, and the physical-link default it must take over is metric 0). scripts/bypass_route.sh removed.
  3. DCO inherits the app's fwmark into the encrypted packets: the tunnel's own outer traffic matched the fwmark 11 rule and looped back into the tunnel device -- measured as one marked packet transmitting 3x on the device and never leaving the box. Fixed by pinning the server via the physical link in both the main table and table 11 before launch (the host route redirect-gateway would have installed, done by hand because route-noexec suppresses it), so control channel and data path share one route and one NAT. A cgroup masquerade on tun_aqomui_b fixes marked packets' source address, chosen before the mark lands. Both routes are removed on teardown.

Fix (commit 2)

cgroup_vpn read self.net unguarded in a Qt slot; the attribute only exists after the GUI calls bypass(). After a service restart under a running GUI (the #73 scenario) the first bypass teardown core-dumped the root service, killing every tunnel. Now guarded like the disconnect path already was.

Verification

  • 89 tests pass + lint gate. New: bypass config carries route-noexec/route-up, no iproute; custom-provider bypass launches bypass.ovpn with tun_aqomui_b; create_cgroup installs the tunnel masquerade.
  • Live-verified end to end 2026-08-19 (driven over D-Bus, main NJ + bypass VA, distinct servers): bypassed cgroup egress = VA exit, main egress = NJ exit, home IP nowhere; marked single-packet TX delta = 1 (loop gone; was 3); DNS through the bypass dnsmasq works. Teardown: service stays up, main tunnel survives, table 11 default restored to the physical link, both server pins removed, bypassed apps back to physical-link egress.

Stacked on #68 -- merge that first; this rebases to two commits.

🤖 Generated with Claude Code

**Problem** Issue #75, found live during #68's validation: bypass-over-VPN with a custom provider connects but never carries traffic -- no `tun_aqomui_b` at first, then (once launched correctly) bypassed apps' traffic entered the tunnel and vanished. It turned out to be three stacked defects, each masking the next, plus a service crash on teardown. **Fix** (commit 1) 1. **Wrong config launched**: the custom-provider branch overwrote the launch path with `<provider>/temp.ovpn` (the stale main config) instead of the `bypass.ovpn` write_config had just written. Now bypass dicts keep the bypass path. 2. **Routing rode a mechanism the netlink backend never calls**: the `iproute` script redirect is inert on this OpenVPN build, so even the right config would have let the bypass tunnel's `redirect-gateway` hijack the main table. Replaced with `route-noexec` + `route-up` feeding table 11 (no metric -- `ip route replace` keys on prefix AND metric, and the physical-link default it must take over is metric 0). `scripts/bypass_route.sh` removed. 3. **DCO inherits the app's fwmark into the encrypted packets**: the tunnel's own outer traffic matched the `fwmark 11` rule and looped back into the tunnel device -- measured as one marked packet transmitting 3x on the device and never leaving the box. Fixed by pinning the server via the physical link in **both** the main table and table 11 before launch (the host route `redirect-gateway` would have installed, done by hand because `route-noexec` suppresses it), so control channel and data path share one route and one NAT. A cgroup masquerade on `tun_aqomui_b` fixes marked packets' source address, chosen before the mark lands. Both routes are removed on teardown. **Fix** (commit 2) `cgroup_vpn` read `self.net` unguarded in a Qt slot; the attribute only exists after the GUI calls `bypass()`. After a service restart under a running GUI (the #73 scenario) the first bypass teardown core-dumped the root service, killing every tunnel. Now guarded like the disconnect path already was. **Verification** - 89 tests pass + lint gate. New: bypass config carries `route-noexec`/`route-up`, no `iproute`; custom-provider bypass launches `bypass.ovpn` with `tun_aqomui_b`; create_cgroup installs the tunnel masquerade. - **Live-verified end to end 2026-08-19** (driven over D-Bus, main NJ + bypass VA, distinct servers): bypassed cgroup egress = VA exit, main egress = NJ exit, home IP nowhere; marked single-packet TX delta = 1 (loop gone; was 3); DNS through the bypass dnsmasq works. Teardown: service stays up, main tunnel survives, table 11 default restored to the physical link, both server pins removed, bypassed apps back to physical-link egress. Stacked on #68 -- merge that first; this rebases to two commits. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
change: read connection state from OpenVPN's management interface
All checks were successful
ci / test (pull_request) Successful in 45s
5ab32b8a8b
The stdout state machine matched strings three of which OpenVPN 2.6+
prints zero times under DCO -- the root cause of the device-detection
and DNS failures. State, auth failures and fatal errors now arrive as
structured events over a management unix socket, one per tunnel role,
connectable by root only.

The pushed-DNS scrape is eliminated rather than migrated: OpenVPN's own
dns-updown hook applies server-pushed DNS itself, with "~." routing,
on resolved systems. It is passed "disable" wherever aqomui owns DNS
instead: alternative servers, DNS off, hop and bypass tunnels, and
systems without resolved. Credentials stay in auth-user-pass files --
verification failures reach the socket without them.

stdout is still read, for the log, the raw-config device fallback and
the double-hop iptables retry.

Closes #22

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix: launch the bypass config for custom providers, stop installing its pushed routes
All checks were successful
ci / test (pull_request) Successful in 36s
c94d5c1d73
The custom-provider branch pointed the launch at <provider>/temp.ovpn --
the stale file from the last main connect -- while write_config had put
the bypass tunnel's real config in bypass.ovpn. And that config steered
routes through an iproute script the netlink backend never calls, so
the bypass tunnel's redirect-gateway would land in the main table.
route-noexec installs no pushed routes; route-up alone feeds table 11.

Closes #75

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mysticalsoap force-pushed fix/bypass-custom-config from c94d5c1d73
All checks were successful
ci / test (pull_request) Successful in 36s
to 92b340f477
All checks were successful
ci / test (pull_request) Successful in 33s
2026-08-19 20:44:23 -04:00
Compare
mysticalsoap force-pushed fix/bypass-custom-config from 92b340f477
All checks were successful
ci / test (pull_request) Successful in 33s
to 2fe35c7e16
All checks were successful
ci / test (pull_request) Successful in 29s
2026-08-19 21:10:48 -04:00
Compare
mysticalsoap force-pushed fix/bypass-custom-config from 2fe35c7e16
All checks were successful
ci / test (pull_request) Successful in 29s
to 0abb93a787
All checks were successful
ci / test (pull_request) Successful in 32s
2026-08-19 21:34:48 -04:00
Compare
mysticalsoap force-pushed fix/bypass-custom-config from 0abb93a787
All checks were successful
ci / test (pull_request) Successful in 32s
to 832906517c
All checks were successful
ci / test (pull_request) Successful in 35s
2026-08-19 21:41:43 -04:00
Compare
mysticalsoap force-pushed fix/bypass-custom-config from 832906517c
All checks were successful
ci / test (pull_request) Successful in 35s
to 6d21a0662a
All checks were successful
ci / test (pull_request) Successful in 38s
2026-08-19 21:52:31 -04:00
Compare
fix: a bypass rebuild without registered network info must not crash the service
All checks were successful
ci / test (pull_request) Successful in 36s
f959ffb2c8
cgroup_vpn runs as a Qt slot in the root service and read self.net
unguarded; the attribute only exists once the gui has called bypass().
After a service restart underneath a running gui (or any client driving
the D-Bus API directly) the first bypass teardown raised AttributeError
at the top of the slot and PyQt aborted the process -- a core dump that
took the main tunnel down with it. The disconnect path already guards
the same attribute.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mysticalsoap force-pushed fix/bypass-custom-config from f959ffb2c8
All checks were successful
ci / test (pull_request) Successful in 36s
to c7386615b3
All checks were successful
ci / test (pull_request) Successful in 40s
2026-08-19 22:12:59 -04:00
Compare
mysticalsoap force-pushed fix/bypass-custom-config from c7386615b3
All checks were successful
ci / test (pull_request) Successful in 40s
to 263f38c244
All checks were successful
ci / test (pull_request) Successful in 52s
ci / test (push) Successful in 42s
2026-08-19 22:22:22 -04:00
Compare
mysticalsoap deleted branch fix/bypass-custom-config 2026-08-19 22:26:33 -04:00
Sign in to join this conversation.
No description provided.