OPNsense Forum

English Forums => Virtual private networks => Topic started by: mlenje on August 14, 2026, 07:14:48 AM

Title: [Guide] ProtonVPN WireGuard Failover with Gateway Groups
Post by: mlenje on August 14, 2026, 07:14:48 AM
ProtonVPN WireGuard Failover with Gateway Groups - plus a shared-address NAT gotcha that will silently break your second tunnel

TL;DR

Credit where due: the core of the NAT fix is adapted from this pfSense/ProtonVPN writeup by u/FarsightMeercat (https://old.reddit.com/r/ProtonVPN/comments/127zpbe/protonvpn_wireguard_multiconnection_on_pfsense/), which documents the identical problem independently, just for pfSense rather than OPNsense. I only found it after burning a fair amount of time chasing this myself - hoping this post saves the next OPNsense person that search.



Background / prerequisite

This assumes you already have one working ProtonVPN WireGuard tunnel on OPNsense with accurate gateway monitoring (dpinger genuinely reflecting up/down, not silently stuck on a fake-healthy state). If your single-tunnel monitoring isn't already solid, get that right first - a broken foundation makes failover testing impossible to interpret.

The most common way single-tunnel monitoring silently breaks: the WireGuard instance's own Gateway field (VPN > WireGuard > Instances > edit) must exactly match the System Gateway's IP Address field (System > Gateways > Configuration). A mismatch causes OPNsense to find an unrelated pre-existing host route and skip building the real one - no error in the general log, dpinger just reports a fake-healthy "none" status forever. Worth double-checking this even on an existing single-tunnel setup.



Part 1: Create the second WireGuard instance

Generate a second ProtonVPN WireGuard config (different server than your primary tunnel, ideally different physical facility for real redundancy).

Before importing it into OPNsense, edit its [Interface] Address line. Every ProtonVPN config download shows the identical 10.2.0.2/32, regardless of server - confirmed directly with ProtonVPN support: this is intentional, "to avoid static, identifiable patterns," not a per-device assignment. Their own guidance for running multiple tunnels: manually edit each additional config's Address to something unique.

Example addressing scheme:
Primary (existing) ......... 10.2.0.2/32
Second tunnel (new) ........ 10.4.0.2/32

VPN > WireGuard > Instances > Add:
Public/Private Key .... from the new config
Listen Port ............ any unused port
Addresses ............... 10.4.0.2/32 (edited, NOT the file's original 10.2.0.2/32)
MTU ..................... match your primary (commonly 1420)
DNS Server .............. file also shows a shared 10.2.0.1 - edit this too,
 following the same pattern (10.4.0.1). Note: ProtonVPN
 only confirmed the Address field is shared-by-design;
 I haven't gotten separate confirmation the DNS Server
 field behaves the same way. If DNS fails over the new
 tunnel, fall back to testing the original literal value.
Disable Routes .......... checked - leave routing entirely to the System
 Gateway object, don't let the WireGuard instance
 push its own routes
Gateway ................. LEAVE BLANK FOR NOW

Attach the peer, enable, apply.



Part 2: Set the instance's Gateway field correctly the first time


Don't invent an arbitrary placeholder IP here. This exact mistake is what breaks single-tunnel monitoring in the first place (see Background above) - same failure mode applies to a second tunnel.



Part 3: Assign the WireGuard device as an interface

Easy to forget if your primary tunnel already went through this step long ago and you don't remember doing it. A WireGuard instance existing isn't enough on its own - it won't show up in Gateway/NAT/Firewall dropdowns until explicitly assigned.

Interfaces > Assignments: select the new device, add it, enable, set a clear Description (I used VPNClient_Wireguard_Rollover), leave IPv4 Configuration Type as none.

Naming note: the WireGuard instance's own Name field and the assigned interface's Description field are two separate labels for the same tunnel, shown on different screens - Name shows under VPN > WireGuard, Description shows in Gateways/NAT/Firewall dropdowns. Easy to assume they're the same field; they're not.



Part 4: Create the System Gateway object

System > Gateways > Configuration > Add:
Interface ........... the interface assigned in Part 3
IP Address ........... EXACTLY the Gateway field value from Part 2
Far Gateway ........... checked
Monitor IP ............ anything distinct from your primary tunnel's
 monitor IP, for easy at-a-glance distinction
Disable Host Route .... unchecked

Save, apply.



Part 5: Verify routing - and expect the next failure

route get <monitor IP>Should resolve via the new tunnel interface.

tcpdump -ni <new interface> icmpConfirm real ICMP traffic while pinging the monitor IP from the firewall.

At this point, dpinger_status() will likely still show the new gateway as status => down, 100% loss, 0.0ms delay/stddev - even though the route and manual ping both look completely healthy. This tripped me up for a while because it looks like the exact same bug from the Background section, but it isn't - recheck nothing, go straight to Part 6.



Part 6: The actual fix - ProtonVPN's shared address requires NAT, not routing changes

Here's what a tcpdump -ni <new interface> capture during this "down" state actually showed:
<new tunnel's real address> > <monitor IP>: ICMP echo request
<monitor IP> > 10.2.0.2: ICMP echo reply

The request leaves correctly from the tunnel's real edited address. The reply comes back addressed to 10.2.0.2 - the original shared ProtonVPN address, not the edited local one. ProtonVPN's server-side infrastructure still associates your registered device/key with 10.2.0.2 internally, regardless of what you configure locally. Since nothing on the box (that dpinger's probe socket is bound to, anyway) owns that address anymore, the reply never reaches the monitor - even though the tunnel itself is completely healthy and passing traffic.

This is a documented ProtonVPN limitation for anyone running 2+ simultaneous WireGuard tunnels from one account, not something fixable via local routing/firewall-rule tweaks, and not fixable by regenerating the peer (ProtonVPN's Peer Generator doesn't let you choose this address).

The fix, adapted from the pfSense writeup linked above - NAT on both directions:

1. Inbound: NAT 1:1 (BINAT), scoped to the new tunnel's interface only

Firewall > NAT > 1:1 > Add:
Interface ........ your new tunnel's interface - CRITICAL: must not be
 your primary tunnel's interface
Type .............. BINAT
External network .. 10.2.0.2/32
Source ............ Single host or Network â†' your tunnel's real edited
 address (e.g. 10.4.0.2/32)
Destination ....... any

2. Outbound: Source NAT, translating LAN traffic to the address ProtonVPN actually recognizes

Firewall > NAT > Outbound (Source NAT) > Add:
Interface ............. your new tunnel's interface
Source ................ LAN network
Destination ........... any
Translate Source IP ... Single host or Network â†' 10.2.0.2 (literal) - 
 do NOT use "Interface address", which would
 translate to your synthetic edited address,
 which ProtonVPN doesn't recognize as a
 registered device; traffic would go out and
 get silently dropped

Save and apply both. Rule order relative to other outbound rules doesn't matter - pf matches outbound NAT by scoped interface, so a packet leaving via your new tunnel can only match a rule scoped to that same interface.

Restart dpinger and re-check:
pluginctl -s dpinger restartThe new gateway should now show status => none with real non-zero delay/stddev and 0.0% loss.



Part 7 (optional but recommended): Floating rule for self-originated traffic

Not the fix for Part 6's issue, but worth having for consistency if your primary tunnel already has one. This affects routing for traffic the firewall generates itself (dpinger's own probes, manual curl/ping from the firewall shell) - separate from Part 6's address translation fix.

Firewall > Rules > Floating > Add:
Interface ................. any
Direction .................. In
Action ...................... Pass
Source ....................... new tunnel's interface address alias
Destination ................... new tunnel's interface network alias
Gateway (Source Routing) ...... your new System Gateway



Part 8: Monit alerting + watchdog

Same pattern as any dpinger-based gateway monitoring. A couple of gotchas worth flagging if you haven't already run into them on your primary tunnel:



Part 9: Create the Gateway Group

System > Gateways > Group > Add:
Gateway Priority ... primary â†' Tier 1, new tunnel â†' Tier 2
Trigger Level ....... Member Down to start (tighten to packet-loss/
 latency thresholds later once you trust it)



Part 10: Point LAN policy routing at the Gateway Group

Firewall > Rules > LAN - find the rule enforcing your VPN gateway for LAN egress (source LAN network, destination !RFC1918_Networks). Change its Gateway field from the single primary gateway to the new Gateway Group. Leave other LAN rules untouched.



Part 11: Test failover and failback properly


In my case, this all worked cleanly end to end - failover and failback both confirmed with real client traffic, not just internal monitoring state.



Summary of gotchas that cost the most time


Happy to answer questions if anyone's attempting the same setup - and if anyone's got independent confirmation on the DNS Server field's shared-by-design behavior (see Part 1), I'd love to know either way.
Title: Re: [Guide] ProtonVPN WireGuard Failover with Gateway Groups
Post by: mlenje on August 14, 2026, 04:26:01 PM
Update - alert email duplication if you're using Gateway Groups + Monit

Small follow-up for anyone who builds this and then gets a real (non-test) failover event: if you've got Monit alerting on your individual gateway(s) and you're using Gateway Groups, you'll likely get double emails per event.

Turns out OPNsense native Monit check called "gateway_alert" (script: /usr/local/opnsense/scripts/monit/gateway_alert.php) - separate from any custom per-gateway Monit check you build yourself following a guide like this one - ends up watching the same underlying gateway, so a single real down/up event will fire two separate emails.

A few things worth knowing if you hit this:


What I did instead: upgraded my own custom per-gateway alert script to match the native one's down-message detail (names the routing group) AND added a real recovery message with actual delay/loss numbers - something the native script never provides either way. Once my custom script covered everything the native one did plus more, I just disabled the native "gateway_alert" check in Services > Monit > Settings and kept only my own. One informative email per real transition now, in both directions, with zero risk of losing the customization to a future update since it's entirely in a script I own.

Updated script (drop-in replacement for the alert script from earlier in this thread):

#!/usr/local/bin/php
<?php
require_once 'config.inc';
require_once
'interfaces.inc';
require_once
'util.inc';
require_once
'plugins.inc.d/dpinger.inc';

$gwname = 'YOUR_GATEWAY_NAME';
$groupname = 'YOUR_GATEWAY_GROUP_NAME';
$gateways_status = dpinger_status();

if (empty(
$gateways_status[$gwname])) {
   echo
"Gateway {$gwname} status could not be determined." . PHP_EOL;
   exit(
1);
}

$status = $gateways_status[$gwname]['status'];
$delay = $gateways_status[$gwname]['delay'];
$loss = $gateways_status[$gwname]['loss'];

if (
stristr($status, 'down') !== false) {
   echo
"MONITOR: {$gwname} is down, removing from routing group {$groupname}." . PHP_EOL;
   exit(
1);
}

echo
"MONITOR: {$gwname} is up (delay {$delay}, loss {$loss}), rejoining routing group {$groupname}." . PHP_EOL;
exit(
0);

Don't disable the native check blindly - check this first: "gateway_alert.php" only loops over gateways that are members of a Gateway Group (look at the source, it walks getGroupsConfig() -> tiers). If your plain WAN gateway is also a member of some other group on your box, disabling this native check would blind you to a raw WAN outage too - don't do that. Check System > Gateways > Group and confirm exactly which gateways are covered before disabling anything. In my case only the two ProtonVPN gateways were in any group, so my main WAN was never covered by this check either way - disabling it cost me nothing I actually had. Your setup may differ.

(Side note if this does apply to you: a dedicated alert for a full WAN-down event is arguably moot anyway, since the box usually can't get an email out over a WAN link that's actually down - so this mostly only matters if you also want alerts for partial degradation like high WAN latency/loss, or a "we're back, here's how long we were down" message after the fact.)

Also worth knowing if you're new to this: Monit only alerts on a state transition, not ongoing status - so if only your primary gateway goes down and your rollover/secondary was healthy the whole time, you won't get any email at all for the secondary. That's expected, not a gap in coverage.

Real-world confirmation, for what it's worth: this whole setup got its first genuine (non-test) workout the night after I built it - primary tunnel dropped for about an hour due to a real upstream VPN provider hiccup, failed over automatically, recovered and failed back automatically, zero manual intervention needed. That's the actual point of building this in the first place, so good to see it hold up outside of deliberate testing.
Title: Re: [Guide] ProtonVPN WireGuard Failover with Gateway Groups
Post by: mlenje on August 21, 2026, 06:02:49 AM
Quick addendum if you're using or considering Mullvad instead of a second ProtonVPN peer for your rollover tunnel:

The NAT gotcha above (Part 6) is a ProtonVPN-specific thing, not a "any second simultaneous WireGuard tunnel" problem. Mullvad's account model is different — each key you register gets its own genuinely unique tunnel address via their API, not a shared placeholder. Confirmed live via tcpdump: replies come back addressed to the real assigned address, not 10.2.0.2. So the whole 1:1 BINAT step (Part 6.1) isn't needed at all with Mullvad.

You do still need the Outbound NAT rule though — that's a separate mechanism from the inbound BINAT fix, and skipping it gives you the same "tunnel's healthy, no LAN internet" symptom for a different reason (learned that one the hard way).

What's different if you're building this with Mullvad instead:


Everything else above — Gateway field matching, interface assignment, Gateway Group, Monit alerting — applies identically regardless of provider.
Title: Re: [Guide] ProtonVPN WireGuard Failover with Gateway Groups
Post by: mlenje on August 23, 2026, 09:34:12 PM
Major update — 2026-08-23: the 1:1 NAT / BINAT workaround is NOT universally required — it's per-server

I want to correct something in my original post that I now believe was wrong, or at least incomplete: I described the BINAT workaround as something you need for any additional simultaneous WireGuard tunnel on a ProtonVPN account. That's not accurate. Whether you need it depends on which specific server you're connecting to — some servers need it, some don't, and there's no way to predict which from the outside.

What I found, testing across multiple tunnels and servers on the same account:


I honestly don't know why some servers behave this way and others don't — I've emailed ProtonVPN support asking for clarity and will update this post if I get a useful answer. It could be different infrastructure/software versions across the fleet, something account-side, or something else entirely.

Revised recommendation for anyone building multiple simultaneous tunnels: don't build the BINAT/NAT workaround preemptively for every new tunnel. Build the tunnel plainly first (unique local address, standard outbound NAT to "interface address"), then test:

In short: test empirically per server, don't assume.