Skip to content

feat(pia): native WireGuard support with port forwarding#3393

Draft
neilcorp2kx wants to merge 3 commits into
passteque:masterfrom
neilcorp2kx:feat/pia-wireguard-native
Draft

feat(pia): native WireGuard support with port forwarding#3393
neilcorp2kx wants to merge 3 commits into
passteque:masterfrom
neilcorp2kx:feat/pia-wireguard-native

Conversation

@neilcorp2kx

Copy link
Copy Markdown

Summary

This adds native WireGuard support for Private Internet Access (PIA), including port forwarding, plus a small healthcheck robustness improvement that the native path depends on.

Today PIA is OpenVPN-only in gluetun, and PIA + WireGuard + port forwarding is a long-standing gap (#3070, #2147): the custom-provider workaround panics with server name cannot be empty, and PIA cannot be selected as a WireGuard provider at all. The underlying reason is that PIA does not hand out static WireGuard configs — it registers a fresh WireGuard key per connection through its own API, which does not fit gluetun's normally-static WireGuard model.

This PR implements that dynamic registration inside the PIA provider so the whole flow is just credentials + region:

environment:
  - VPN_SERVICE_PROVIDER=private internet access
  - VPN_TYPE=wireguard
  - OPENVPN_USER=...            # or _SECRETFILE
  - OPENVPN_PASSWORD=...        # or _SECRETFILE
  - SERVER_REGIONS=CA Vancouver
  - VPN_PORT_FORWARDING=on
  - VPN_PORT_FORWARDING_PROVIDER=private internet access

No externally-generated WireGuard config, no static keys, no helper scripts. Server selection and key rotation happen automatically on every (re)connect.

Closes #3070.

What's in it

Three focused commits:

  1. feat(firewall) — scoped temporary bootstrap connection allowances. A registry of narrowly-scoped (destination IP + protocol + port + firewall mark, physical interface) temporary outbound allowances needed before the tunnel is up, with retain-on-failure cleanup, idempotent deletion under a bounded non-cancelable context, and a sweep on shutdown / before reconnect. iptables gains -m mark --mark emit+parse so the rules are mark-scoped and delete symmetrically.
  2. feat(pia) — native PIA WireGuard + port forwarding. At connect time the provider fetches PIA's live server list, selects a WireGuard server for the requested region/name/hostname, obtains a token, generates an ephemeral Curve25519 key pair, registers the public key (addKey) over TLS pinned to the server CN via the bundled PIA CA, and builds the WireGuard connection from the response (re-registering each reconnect). Port forwarding works natively (gateway from the addKey server_vip).
  3. feat(healthcheck) — retry the startup TCP+TLS check within a 60s budget (2s backoff) instead of a single 6s attempt, so a tunnel whose DNS takes a few seconds to become ready doesn't trigger a restart loop. General improvement, not PIA-specific.

Design / safety notes

  • Killswitch integrity was the top priority. The pre-tunnel token / server-list / addKey calls are the only new outbound traffic before the tunnel exists. They go through the temporary, mark-tagged, exact-destination allowances above, resolved by a bootstrap dialer pinned to the physical default route, and are torn down before the tunnel comes up. Discovery/registration failure fails closed (the tunnel does not start). There is no fail-open to arbitrary destinations.
  • Secrets: the ephemeral private key, the auth token and account credentials are never logged (info logging shows only the derived public key, shortened) or persisted; the port-forward token file is kept 0600.
  • Malformed/unsupported provider data (invalid/unspecified/IPv6 server_ip/server_vip/peer_ip) is rejected with errors rather than panicking.

Testing

  • Unit tests added across the firewall (rollback, deletion-failure retry, idempotence, mark scoping, reconnect de-duplication, mark parsing edge cases), PIA WireGuard (server-list parsing, region/name/hostname selection, addKey request/response mapping), healthcheck retry, and settings validation.
  • Built and run end-to-end against a live PIA account: native config connects over WireGuard, obtains a forwarded port, passes healthchecks with 0 restarts, and DNS/egress resolve to the PIA endpoint. Post-startup firewall state was inspected to confirm every temporary bootstrap allowance is torn down (only the active WireGuard endpoint allowance remains).
  • It has been running on a real seedbox to soak before this is taken out of draft.

Known / deferred (pre-existing, not introduced here)

  • PIA WireGuard reuses OpenVPN.User/Password for credentials, which are serialized by the authenticated /v1/vpn/settings control endpoint like all other settings — a pre-existing exposure, called out here because the native path now depends on those fields. A redacted control DTO would be a separate change.
  • The addKey client trusts the bundled PIA CA additively on top of the system pool rather than pinning PIA-only. TLS name verification is enabled and there is no InsecureSkipVerify.

A note on how this was developed

This was built and hardened with heavy, deliberate use of AI tooling — a combination of OpenAI Codex and Anthropic Claude — for implementation, iterative debugging against a live PIA endpoint, and multiple adversarial code-review passes focused on killswitch/leak safety, with all findings driven to resolution and re-verified. Flagging this transparently; every change has been reviewed and validated by a human, and I'm happy to walk through any part of the design or rationale.


Opening as a draft intentionally — soaking on a live setup for a bit before marking it ready. Feedback on the approach (especially the bootstrap-firewall handling and whether a redacted control DTO should be bundled) very welcome.

Add a mechanism to open narrowly-scoped, temporary outbound firewall
allowances (destination IP + protocol + port + firewall mark, on the
physical interface) that are needed before the VPN tunnel is up, then
reliably torn down afterwards.

- temporary.go: central registry of temporary allowances with
  retain-on-failure cleanup, idempotent deletion under a bounded
  non-cancelable context, and a sweep on firewall shutdown and before
  re-adding on reconnect. Attempted interfaces are tracked before the
  iptables append so a rule applied during a cancellation race is never
  left untracked.
- iptables: emit and parse the '-m mark --mark <value>' match so the
  temporary ACCEPT rules are scoped to the bootstrap socket mark and can
  be deleted symmetrically. Reject malformed mark match forms instead of
  silently accepting a zero mark or panicking on a bad value.

Covered by unit tests for rollback, deletion-failure retry, idempotence,
mark scoping, reconnect de-duplication and mark parsing edge cases.
Private Internet Access previously only supported OpenVPN in gluetun
because PIA registers a fresh WireGuard key per connection via its own
API, which does not fit a static WireGuard config. This adds a
provider-driven dynamic WireGuard path for PIA.

At connection time, for VPN_SERVICE_PROVIDER=private internet access +
VPN_TYPE=wireguard, gluetun now:
- fetches PIA's live server list and selects a WireGuard server for the
  chosen region/name/hostname (honouring port-forwarding-only),
- obtains an auth token, generates an ephemeral Curve25519 key pair, and
  registers the public key with the selected server (addKey) over TLS
  pinned to the server CN using the bundled PIA CA,
- builds the WireGuard connection from the response (endpoint, peer key,
  interface address, DNS), re-registering on every reconnect,
- performs the pre-tunnel token/server-list/addKey calls through scoped,
  temporary, mark-tagged firewall allowances so the killswitch is never
  opened to arbitrary destinations, resolving via a bootstrap dialer
  pinned to the physical default route.

Port forwarding now works natively over WireGuard for PIA (gateway taken
from the addKey server_vip). The earlier custom-provider workaround env
VPN_PORT_FORWARDING_SERVER_NAME remains supported.

Invalid, unspecified or non-IPv4 server_ip/server_vip/peer_ip values are
rejected with errors rather than panicking, and the persisted
port-forward token file is written and kept 0600.

Config is just credentials + region, e.g.:
  VPN_SERVICE_PROVIDER=private internet access
  VPN_TYPE=wireguard
  SERVER_REGIONS=CA Vancouver
  VPN_PORT_FORWARDING=on
  VPN_PORT_FORWARDING_PROVIDER=private internet access

Closes passteque#3070.
The VPN startup healthcheck performed a single 6s TCP+TLS check. When a
tunnel's DNS server takes a few seconds to become ready after connect
(e.g. providers that do pre-tunnel work), that single attempt could fail
and trigger an endless VPN restart loop.

Retry the short (6s) parallel TCP+TLS attempts within a 60s total budget
with a 2s backoff, returning on the first success and preserving the
existing aggregated error on budget exhaustion. Periodic checks are
unchanged. This removes the need for any manual startup-grace tuning and
is a general robustness improvement for all providers.
@qdm12

qdm12 commented Jul 16, 2026

Copy link
Copy Markdown
Member

Finally a decent solution, thanks! Although the iptables marking mechanism is risky, given it's not always available on the user's kernel. Instead you should more simply use #3361 (reasoning behind this at #3358)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: PIA Port Forwarding fails with VPN_SERVICE_PROVIDER=custom (WireGuard): Panic or Region Validation Error

2 participants