ProtonVPN WireGuard support #152

Closed
opened 2026-08-24 15:13:05 -04:00 by mysticalsoap · 3 comments
Owner

Deferred past the current release by design: OpenVPN import (#33/#146) is the headline feature and the WireGuard tunnel path hasn't been exercised at all yet, even with a manual config.

What it takes, roughly in order:

  1. Validate the WireGuard plumbing itself first — aqomui has the machinery (gen_wg_key, protocol tables, the WireGuard branch in tunnel.py, Mullvad's WG-only import) but none of it has been tested live. A manual WG config from any provider is the cheapest smoke test.
  2. Key registration is Proton's hard part. Unlike Mullvad's register-once key upload, Proton ties WG keys to short-lived certificates issued per session via the authenticated certificate endpoint — the official clients renew them continuously. Needs investigating what Duration the API grants a third-party session; if it's short, renewal machinery is required, which makes #147's persisted-session/refresh-token work a hard prerequisite (a scheduled renewal can't solve a CAPTCHA, same as auto-update).
  3. Server model: /vpn/logicals already carries per-physical-server X25519PublicKey; WG entries would double the imported server count, which raises the stakes on the server-navigation UX work and touches the multi-entry-per-type groundwork from #138.
  4. Config generation is the easy part: standard WG interface (10.2.0.2/32, DNS 10.2.0.1) + peer per server, port 51820.

🤖 Generated with Claude Code

Deferred past the current release by design: OpenVPN import (#33/#146) is the headline feature and the WireGuard tunnel path hasn't been exercised at all yet, even with a manual config. What it takes, roughly in order: 1. **Validate the WireGuard plumbing itself first** — aqomui has the machinery (`gen_wg_key`, protocol tables, the WireGuard branch in tunnel.py, Mullvad's WG-only import) but none of it has been tested live. A manual WG config from any provider is the cheapest smoke test. 2. **Key registration is Proton's hard part.** Unlike Mullvad's register-once key upload, Proton ties WG keys to short-lived certificates issued per session via the authenticated certificate endpoint — the official clients renew them continuously. Needs investigating what `Duration` the API grants a third-party session; if it's short, renewal machinery is required, which makes **#147's persisted-session/refresh-token work a hard prerequisite** (a scheduled renewal can't solve a CAPTCHA, same as auto-update). 3. **Server model**: `/vpn/logicals` already carries per-physical-server `X25519PublicKey`; WG entries would double the imported server count, which raises the stakes on the server-navigation UX work and touches the multi-entry-per-type groundwork from #138. 4. Config generation is the easy part: standard WG interface (10.2.0.2/32, DNS 10.2.0.1) + peer per server, port 51820. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

Step 1 (validate the WG plumbing live) is done, and it found a real bug: a manual ProtonVPN config connects fine (no credentials needed — the private key in the config is the credential, already bound to the account at download time), but bypass leaked. wg-quick's mangle-level mark = ctmark rule runs after aqomui's PREROUTING bypass marking and erases the fwmark from all forwarded UDP — gluetun's whole outer flow double-tunneled through the WG session.

PR #186 replaces wg-quick with manual wg(8)/ip(8) bring-up (def1 routes + endpoint pin, same routing model as the OpenVPN path), which removes the rule that wipes the mark and the old post-up fwmark reshuffle hack with it.

Design decisions from the follow-up discussion, for the record:

  • After #186, the next step is a narrow protocol seam: up(role)/down()/dev/dns/alive() implemented by an OpenVPN and a WireGuard class, held by composition in the tunnel thread. Role-specific routing (main/hop/bypass) stays shared policy outside the classes — role is a parameter, never per-protocol methods.
  • The GUI's "WireGuard not supported for secondary connections" guard is a wg-quick limitation, not a protocol one (kernel WG never inherits the inner fwmark — the DCO loop class doesn't exist); it becomes a capability flag that dies when hop/bypass-over-WG is implemented (#176).
  • alive() is the one legitimate protocol asymmetry: process liveness for OpenVPN, handshake age for WG — relevant to #123.
  • Obfuscation transports (Proton Stealth, udp2tcp) compose around a protocol like the existing stunnel/SSH side channels; AmneziaWG would parameterize the WG class. Neither becomes a new protocol class.
Step 1 (validate the WG plumbing live) is done, and it found a real bug: a manual ProtonVPN config connects fine (no credentials needed — the private key in the config is the credential, already bound to the account at download time), but bypass leaked. wg-quick's mangle-level `mark = ctmark` rule runs after aqomui's PREROUTING bypass marking and erases the fwmark from all forwarded UDP — gluetun's whole outer flow double-tunneled through the WG session. PR #186 replaces wg-quick with manual wg(8)/ip(8) bring-up (def1 routes + endpoint pin, same routing model as the OpenVPN path), which removes the rule that wipes the mark and the old post-up fwmark reshuffle hack with it. Design decisions from the follow-up discussion, for the record: - After #186, the next step is a narrow protocol seam: `up(role)`/`down()`/`dev`/`dns`/`alive()` implemented by an OpenVPN and a WireGuard class, held by composition in the tunnel thread. Role-specific routing (main/hop/bypass) stays shared policy outside the classes — role is a parameter, never per-protocol methods. - The GUI's "WireGuard not supported for secondary connections" guard is a wg-quick limitation, not a protocol one (kernel WG never inherits the inner fwmark — the DCO loop class doesn't exist); it becomes a capability flag that dies when hop/bypass-over-WG is implemented (#176). - `alive()` is the one legitimate protocol asymmetry: process liveness for OpenVPN, handshake age for WG — relevant to #123. - Obfuscation transports (Proton Stealth, udp2tcp) compose around a protocol like the existing stunnel/SSH side channels; AmneziaWG would parameterize the WG class. Neither becomes a new protocol class.
Author
Owner

Status: step 1 is complete and hardened — WG plumbing validated live (#186 manual bring-up replacing wg-quick, #189 protocol class + conf codec, both merged). Step 4's config generation now exists as wireguard.generate_conf, so native support reduces to steps 2–3: certificate/key registration against the authenticated endpoint (hard prerequisite: #147 persisted sessions) and the logicals server model.

Status: step 1 is complete and hardened — WG plumbing validated live (#186 manual bring-up replacing wg-quick, #189 protocol class + conf codec, both merged). Step 4's config generation now exists as wireguard.generate_conf, so native support reduces to steps 2–3: certificate/key registration against the authenticated endpoint (hard prerequisite: #147 persisted sessions) and the logicals server model.
Author
Owner

ProtonVPN WireGuard is live: import (#182), manual bring-up replacing wg-quick so bypass survives (#186), protocol seam + conf codec (#189), and alt-DNS/teardown-status parity (#193). All live-verified including split-horizon DNS and network-change reconnect. Mullvad WG template support exists but stays unverified until an account is around; WG doublehop is #176.

ProtonVPN WireGuard is live: import (#182), manual bring-up replacing wg-quick so bypass survives (#186), protocol seam + conf codec (#189), and alt-DNS/teardown-status parity (#193). All live-verified including split-horizon DNS and network-change reconnect. Mullvad WG template support exists but stays unverified until an account is around; WG doublehop is #176.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
mysticalsoap/aqomui#152
No description provided.