change: protocol seam and WireGuard conf codec #189

Merged
mysticalsoap merged 2 commits from protocol-seam into trunk 2026-08-28 10:53:31 -04:00
Owner

Problem

Tunnel-type knowledge was spread as string matches and ad-hoc state: the service tracked WireGuard teardown with wg_connect/wg_provider flags, the GUI hardcoded three tunnel == "WireGuard" guards, and conf-format knowledge sat in four files — including tunnel.py's insert(8/9) Mullvad completion that only worked because inserting past a list's end appends. #152's design discussion settled the target shape; #187 covers the conf side.

Fix

Two commits, independently revertible:

Protocol seam — wireguard.WireGuard carries the contract's first half: up(role) / down() / dev / alive(), with the device firewall accepts inside up/down and secondary roles refused before any side effect. The service holds the object and tears down symmetrically (flags deleted); the thread receives it by composition. Role is a parameter, never per-role methods — main/hop/bypass routing stays shared policy. The GUI's three guards collapse into config.TUNNEL_SECONDARY (same import-cycle rationale as PROVIDER_TYPES, pinned to the class by test), so #176 lifts a flag instead of editing string matches. alive() is handshake age with the idle-tunnel caveat documented for #123. OpenVPN's half is deliberately deferred to #123, its first real consumer — its process lifecycle is entangled with the thread and works; extracting it without a consumer is churn.

Conf codec (#187) — parse_conf/generate_conf as inverses over a data form (extras + native (key, value) entries per section, spelling kept, unknown keys like PresharedKey/PersistentKeepalive preserved). wg setconf input is always generated, never passthrough; Mullvad's per-server fields append to the parsed [Peer] section; custom.py splits endpoints through the codec helper — which also fixes bracketed v6 endpoints and no-space Endpoint=host:port confs the old split(" = ") choked on.

Verification

  • pytest: 461 passed (pre-existing test_mgmt socket cases excluded in my sandbox, pass on a normal shell). New coverage: class contract (role refusal before side effects, accept rollback on failed bring-up, symmetric down), handshake-age liveness, codec round-trip incl. unknown-key preservation, section-anchored Mullvad completion, capability-flag pinning.
  • ruff + compileall clean.
  • Live check worth doing before merge since #186's was command-sequence-identical but the dispatch path changed: connect/disconnect the Proton WG server once via the GUI after build-branch-and-install.sh. Mullvad WG and custom-import paths are covered by the new tests; a Mullvad live connect additionally exercises the generated conf end-to-end if you want the belt and braces.

🤖 Generated with Claude Code

## Problem Tunnel-type knowledge was spread as string matches and ad-hoc state: the service tracked WireGuard teardown with `wg_connect`/`wg_provider` flags, the GUI hardcoded three `tunnel == "WireGuard"` guards, and conf-format knowledge sat in four files — including tunnel.py's `insert(8/9)` Mullvad completion that only worked because inserting past a list's end appends. #152's design discussion settled the target shape; #187 covers the conf side. ## Fix Two commits, independently revertible: **Protocol seam** — `wireguard.WireGuard` carries the contract's first half: `up(role)` / `down()` / `dev` / `alive()`, with the device firewall accepts inside up/down and secondary roles refused before any side effect. The service holds the object and tears down symmetrically (flags deleted); the thread receives it by composition. Role is a parameter, never per-role methods — main/hop/bypass routing stays shared policy. The GUI's three guards collapse into `config.TUNNEL_SECONDARY` (same import-cycle rationale as `PROVIDER_TYPES`, pinned to the class by test), so #176 lifts a flag instead of editing string matches. `alive()` is handshake age with the idle-tunnel caveat documented for #123. **OpenVPN's half is deliberately deferred to #123**, its first real consumer — its process lifecycle is entangled with the thread and works; extracting it without a consumer is churn. **Conf codec (#187)** — `parse_conf`/`generate_conf` as inverses over a data form (extras + native `(key, value)` entries per section, spelling kept, unknown keys like `PresharedKey`/`PersistentKeepalive` preserved). `wg setconf` input is always generated, never passthrough; Mullvad's per-server fields append to the parsed `[Peer]` section; custom.py splits endpoints through the codec helper — which also fixes bracketed v6 endpoints and no-space `Endpoint=host:port` confs the old `split(" = ")` choked on. ## Verification - `pytest`: 461 passed (pre-existing test_mgmt socket cases excluded in my sandbox, pass on a normal shell). New coverage: class contract (role refusal before side effects, accept rollback on failed bring-up, symmetric down), handshake-age liveness, codec round-trip incl. unknown-key preservation, section-anchored Mullvad completion, capability-flag pinning. - ruff + compileall clean. - Live check worth doing before merge since #186's was command-sequence-identical but the dispatch path changed: connect/disconnect the Proton WG server once via the GUI after `build-branch-and-install.sh`. Mullvad WG and custom-import paths are covered by the new tests; a Mullvad live connect additionally exercises the generated conf end-to-end if you want the belt and braces. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The service tracked WireGuard teardown with ad-hoc wg_connect/
wg_provider flags because OpenVPN teardown is killing a process while
WireGuard's is a set of commands needing remembered state -- protocol
state living outside any protocol object. The WireGuard class now
carries the contract's first half (up(role)/down()/dev/alive()), the
service holds the object and tears down symmetrically, and the thread
receives it by composition.

Role is a parameter of up(), never a method: main/hop/bypass routing
is shared policy, not protocol logic, so a protocol refusing a role
is a capability gap to lift (#176), not interface surface to grow.
The gui's three hardcoded "tunnel == WireGuard" guards collapse into
the TUNNEL_SECONDARY table (config.py for the same import-cycle
reason as PROVIDER_TYPES, pinned to the class by test).

alive() reads handshake age -- WireGuard has no process to watch; the
idle-tunnel caveat is documented for the #123 supervisor, its first
real consumer. OpenVPN's half of the contract lands with #123, which
needs its lifecycle held by an object anyway; extracting it now would
churn never-broken code without a consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
change: WireGuard confs go through a parse/generate codec (#187)
All checks were successful
ci / test (pull_request) Successful in 24s
ci / test (push) Successful in 25s
4d0ac5b424
Conf-format knowledge sat in four places: custom.py's endpoint regex,
mullvad.py's template, tunnel.py's insert-at-line-8 completion (which
only worked because inserting past the end of a list appends), and
the wireguard module's parser. parse_conf and generate_conf are now
inverses over a data form -- extras plus native (key, value) entries
per section, spelling kept, unknown keys preserved -- and everything
else derives: setconf input is always generated (never passthrough),
Mullvad's per-server peer fields are appended to the parsed [Peer]
section, and custom.py splits endpoints through the codec helper,
which also unbreaks bracketed v6 endpoints and confs written without
spaces around the equals sign.

Comments do not survive the round trip; nothing machine-written here
carries any, and imported confs are only regenerated when a resolved
hostname already forces a rewrite.

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

Live verification: ProtonVPN WG connect/disconnect through the new dispatch path works on the real system (user-tested post-install).

Scope note: the Mullvad WG path (template completion via the codec, Mullvad-specific import) is covered by tests only — no Mullvad account to verify against, so Mullvad-over-WireGuard stays effectively unsupported until one exists to test with (a temp subscription is within the project's established testing scope if it ever becomes worth it; Mullvad dropping OpenVPN in Jan 2026 makes that likelier eventually). The codepath is exercised end-to-end by TestMullvadWgConf up to the point of handing the generated conf to link_up, which the Proton test covers from there.

Live verification: ProtonVPN WG connect/disconnect through the new dispatch path works on the real system (user-tested post-install). Scope note: the Mullvad WG path (template completion via the codec, Mullvad-specific import) is covered by tests only — no Mullvad account to verify against, so Mullvad-over-WireGuard stays effectively unsupported until one exists to test with (a temp subscription is within the project's established testing scope if it ever becomes worth it; Mullvad dropping OpenVPN in Jan 2026 makes that likelier eventually). The codepath is exercised end-to-end by TestMullvadWgConf up to the point of handing the generated conf to link_up, which the Proton test covers from there.
mysticalsoap deleted branch protocol-seam 2026-08-28 10:53:31 -04:00
Sign in to join this conversation.
No description provided.