Recent posts

#1
Virtual private networks / [Guide] ProtonVPN WireGuard Fa...
Last post by mlenje - Today at 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
    • Building automatic failover between two simultaneous ProtonVPN WireGuard tunnels on OPNsense using Gateway Groups.
    • Every ProtonVPN WireGuard config shares the identical internal address 10.2.0.2, by design, account-wide. Editing the local [Interface] Address on a second tunnel is necessary but not sufficient - ProtonVPN's server keeps replying to 10.2.0.2 regardless, which silently breaks dpinger monitoring on the second tunnel (route looks fine, manual ping/tcpdump looks fine, but gateway status sits at 100% loss forever).
    • The fix is NAT on both directions (1:1 BINAT inbound + Outbound NAT translating to the literal shared address), not a routing/firewall-rule change.
    • Full walkthrough below, plus the failover Gateway Group setup and a real-world tested failover/failback procedure.

    Credit where due: the core of the NAT fix is adapted from this pfSense/ProtonVPN writeup by u/FarsightMeercat, 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

    • With the instance up, confirm a real handshake: wg show <device>
    • Set the instance's Gateway field. In my case, the working pattern was "one address up from the tunnel address" (10.2.0.2 â†' gateway 10.2.0.3), so I tried the same for the second tunnel: Address 10.4.0.2 â†' Gateway 10.4.0.3. Worked first try.
    • Save, apply.

    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:

    • Cron's "Minutes" field: "2" means "once per hour at :02," not "every 2 minutes." Use */2.
    • Cron only runs predefined configd actions - you need a .conf file under /usr/local/opnsense/service/conf/actions.d/ plus a configd restart before the action appears in the Cron job dropdown. Use "service configd restart" - "configctl configd restart" will error with "Action not allowed or missing," since configd can't restart itself through its own action-dispatch mechanism.
    • Don't trust crontab -l or /etc/crontab to confirm a GUI-managed cron job is actually scheduled - both came back empty for a legitimately-configured, working job in my case. System > Settings > Cron in the GUI is the reliable source of truth. If you want airtight proof of execution (not just that it's configured), temporarily add an unconditional trace line to the top of the script, wait a few cron cycles, and check for timestamped entries.
    • dpinger doesn't reliably self-clear a stuck "down" state after a real outage resolves - a small watchdog script comparing WireGuard's own handshake freshness against dpinger's reported status, restarting dpinger only when they disagree, fixes this. "pluginctl -s dpinger restart" restarts all dpinger instances on the box (global side effect, not a bug) - worth folding multiple tunnels' checks into one combined watchdog rather than running several independent ones that'll all trigger the same restart anyway.


    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

    • Confirm baseline: both gateways healthy, traffic only on the primary tunnel.
    • Disable the primary WireGuard instance (cleaner than physically blocking - exercises the real failure mode dpinger detects).
    • Confirm dpinger flips the primary to "down," confirm any Monit alert fires.
    • Test from an actual LAN client, not the firewall's own SSH shell. Self-originated firewall traffic follows different routing than forwarded LAN traffic (same category of gotcha as dpinger's own probes needing the Part 7 floating rule) - a curl ifconfig.me run directly on the firewall can silently fall through to raw WAN and give you a false negative. From a real device on your LAN: curl ifconfig.me should show the second tunnel's exit IP.
    • Re-enable the primary, confirm recovery, re-run the LAN-client test to confirm traffic shifted back.

    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

    • WireGuard instance Gateway field and System Gateway IP Address field must match exactly, for every tunnel - no placeholders.
    • ProtonVPN WireGuard configs all share the same internal address (10.2.0.2) by design, account-wide. A local Address edit alone is not sufficient for a second simultaneous tunnel - you need NAT 1:1 (inbound) + Outbound NAT translating to the literal 10.2.0.2 on the new tunnel's interface.
    • Self-originated firewall traffic (dpinger probes, a manual curl/ping from the SSH shell) follows different routing than real LAN client traffic - always validate failover from an actual LAN device.
    • A WireGuard instance's Name field and its assigned interface's Description field are two different labels for the same tunnel on different screens.
    • pluginctl -s dpinger restart is global, not per-gateway.

    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.
    #2
    26.7 Series / Re: upgrade from 26.7 to 26.7....
    Last post by newsense - Today at 06:59:46 AM
    Which mirror are you using ?

    Try changing the mirror. If it fails again post the full health heck output here.
    #3
    General Discussion / Re: [SOLVED] Roku DNS storm is...
    Last post by keeka - Today at 06:56:47 AM
    @OPNenthu Dealing with the problem before the queries hit a DNS server is the favourite solution IMO. Thanks for posting (post #25).
    #4
    26.7 Series / Re: Boot stuck for 1-2 min aft...
    Last post by newsense - Today at 06:55:28 AM
    Likely a slow system where unbound takes a while to start and WireGuard depends on it because it is configured with fqdn instead of IP
    #5
    Nobody public CA will issue certificates for .internal.

    You either need a self signed certifiate, or set the domain to http instead of https.

    Using http will make caddy receive on port 80 and proxy to port 2283.

    In curl you will only see port 80 though as thats what the proxy will use.

    Your DNS override points immich.homelab.internal to 192.168.10.247, so you're bypassing Caddy entirely. It needs to resolve to the OPNsense/Caddy IP address. The upstream port 2283 is only used by Caddy after it receives the request.

    Use the FAQ here for more infos:
    https://docs.opnsense.org/manual/how-tos/caddy.html#caddy-troubleshooting
    #6
    General Discussion / Re: Let's AI Opnsense!
    Last post by patlegu - Today at 06:09:02 AM
    Hello,

    I've done some experiment around AI and opnsense. I start last year by training a SLM with some opnsense functions, less than 12, to see how it goes. Two month later, my Lora was working well with the 102 functions and I decide to make a lab to check what it can do alone on a real opnsense connected directly to internet.

    My lab was an Agentic SOC (Security Operation Center) with 3 kind of SLMs (1 opnsense, 1 SOC, 1 CERT). The purpose was to check what was possible to do with some small servers without GPU. You can find some informations on this here . It has worked pretty good and the opnsense agent was able to had or remove some rules to the firewall. It's not what you have in mind but can give some ideas. It worked well, in the sense that more specialized agents had to be created as the attacks occurred.

    I've tried after that to go from the side-car version to the full opnsense integrated SLM. version. Still in French. It was working pretty good excepted that you have to have more CPU and RAM due to inference directly in the box.

    I hope this information regarding the use of SLMs with OPNsense has been helpful.

    I am currently looking into whether this operational approach using LoRAs (OPNsense, WireGuard, CrowdSec) is still relevant, given how much LLMs have evolved over the past eight months.
    I have started a new project(English) addressing various issues related to managing sensitive information in cybersecurity, particularly concerning firewalls—specifically, how to use an LLM without compromising sovereignty or the confidentiality of configurations. With a small testing interface. Should have bug as it's under heavy dev for the moment.

    I hope I haven't been too long-winded or boring.
    Pat.
    #7
    26.7 Series / Boot stuck for 1-2 min after u...
    Last post by Gilgamesh - Today at 05:23:35 AM
    Successfully upgraded to 26.7.x from 26.1.11_10, but then the boot process took longer than usual. Tried to restart the OPNsense multiple times and every time it stops on the line Configuring WireGuard VPN... for around 1.5 min.

    The VPN service then is up and running, no issues.

    Any ideas?
    #8
    26.7 Series / Re: How reliable is Firewall:D...
    Last post by hharry - Today at 01:57:41 AM
    here is a 'sample' additional debug info ( when running with patch applied, and after reboot ) for the 'Block IPC8' rule session/state

    This rule isn't even applied to the vmx3 interface, it is applied to the vmx2 interface, yet the session appears under this rule, as originating from vmx3 interface on rule 'Block IPC8', which is not applied to vmx3 interface, really messed up!

    vmx2 and vmx3 are separate interfaces, not connected to each other, they are separate and completely mutually exclusive L2 domains entirely...handled by ESXi vswitch vlans.

    mappings as below, from OPNsense point of view

    vmx2 <> untagged native VLAN 1 <> 10.0.0.0/24
    vmx3 <> untagged VLAN 4 <> 10.0.1.0/24

    In addition, the the state/session does not match the 'Block IPC8' match criteria either.





    root@OPNsense_LAB:~ # pfctl -vvPsr | grep -A4 @41
    No ALTQ support in kernel
    ALTQ related functions disabled
    @41 block drop in quick on vmx2 inet from <IPC8:1> to any label "dadac13d-fc61-4e50-9ff3-4769740d87b1"
      [ Evaluations: 0         Packets: 0         Bytes: 0           States: 0     ]
      [ Source Nodes: 0      Limit: 0      NAT/RDR: 0      Route: 0      ]
      [ Inserted: uid 0 pid 0 State Creations: 0     ]
      [ Last Active Time: N/A ]
    root@OPNsense_LAB:~ # pfctl -vvs states | grep -C2 rule.41
    No ALTQ support in kernel
    ALTQ related functions disabled
    all udp 255.255.255.255:6667 <- 10.0.1.2:59239       NO_TRAFFIC:SINGLE
       age 00:21:12, expires in 00:00:25, 254:0 pkts, 58928:0 bytes, rule 41, rlabel c0b243c6-f1d4-471d-8dcf-62046c6215b0
       id: a8547e6a00000000 creatorid: 30335776
       origif: vmx3
    root@OPNsense_LAB:~ #

    A tcpdump shows the traffic is, as expected arriving on vmx3 interface only


    root@OPNsense_LAB:~ # tcpdump -nevpi vmx2 host 10.0.1.2
    tcpdump: listening on vmx2, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    ^[[A^C
    0 packets captured
    827 packets received by filter
    0 packets dropped by kernel
    root@OPNsense_LAB:~ # tcpdump -nevpi vmx3 src host 10.0.1.2 and dst host 255.255.255.255
    tcpdump: listening on vmx3, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    12:34:18.270377 3c:0b:59:df:a5:cc > ff:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 246: (tos 0x0, ttl 255, id 54285, offset 0, flags [none], proto UDP (17), length 232)
        10.0.1.2.59239 > 255.255.255.255.6667: UDP, length 204
    12:34:23.273712 3c:0b:59:df:a5:cc > ff:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 246: (tos 0x0, ttl 255, id 54286, offset 0, flags [none], proto UDP (17), length 232)
        10.0.1.2.59239 > 255.255.255.255.6667: UDP, length 204
    ^C
    2 packets captured
    2 packets received by filter
    0 packets dropped by kernel
    root@OPNsense_LAB:~ # tcpdump -nevpi vmx2 src host 10.0.1.2 and dst host 255.255.255.255
    tcpdump: listening on vmx2, link-type EN10MB (Ethernet), snapshot length 262144 bytes





    #9
    I tried to make a post under the Caddy FAQ, but it was a pretty dead thread.

    So far I've determined that Caddy isn't redirecting traffic to the right port for my services. I run most of my services in Docker on a Debian VM in Proxmox, the IP of the Debian VM (and my services) is 192.168.10.247. All of this is meant to run internally only.

    I made an Override in Unbound DNS for this server as *.homelab.internal. I am able to resolve the domain name, so when I do nslookup immich.homelab.internal, jellyfin.homelab.internal, etc., it works both in the OPNsense terminal and on my laptop connected via WiFi.

    When I do
    curl -vk http://immich.homelab.internal:2283it resolves and pulls the site html.

    But when I do curl -vk http://immich.homelab.internal, it fails:

    * Host immich.homelab.internal:80 was resolved.
    * IPv6: (none)
    * IPv4: 192.168.10.247
    *   Trying 192.168.10.247:80...
    * connect to 192.168.10.247 port 80 from 192.168.10.89 port 53232 failed: Connection refused
    * Failed to connect to immich.homelab.internal port 80 after 5 ms: Could not connect to server
    * closing connection #0
    curl: (7) Failed to connect to immich.homelab.internal port 80 after 5 ms: Could not connect to server

    It's trying to contact port 80 despite me setting the upstream port in the Caddy settings.

    This is the same behavior for my proxmox.internal domain.

    You cannot view this attachment.

    I even tried to do Host {upstream_hostport}
    Here's my caddyfile:

    # DO NOT EDIT THIS FILE -- OPNsense auto-generated file


    # caddy_user=root

    # Global Options
    {
    log {
    output net unixgram//var/run/caddy/log.sock {
    }
    format json {
    time_format rfc3339
    }
    }

    http_port 80
    https_port 443

    servers {
    protocols h1 h2 h3
    }

    auto_https disable_redirects
    grace_period 10s
    skip_install_trust
    import /usr/local/etc/caddy/caddy.d/*.global
    }

    # Reverse Proxy Configuration


    immich.homelab.internal {
    handle {
    reverse_proxy 192.168.10.247:2283 {
    header_up Host {upstream_hostport}

    transport http {
    }
    }
    }
    }

    proxmox.internal {
    handle {
    reverse_proxy 192.168.10.4:8006 {
    header_up Host {upstream_hostport}

    transport http {
    }
    }
    }
    }

    import /usr/local/etc/caddy/caddy.d/*.conf

    I have    auto_https disable_redirects set.

    What am I missing?
    #10
    General Discussion / Re: Roku DNS storm is impactin...
    Last post by OPNenthu - Today at 01:23:26 AM
    I'm going to call this a win.  Been following the issue for some days now and the pf rate limiter is reliably capping the queries from the Roku group to just around 300/min, which corresponds perfectly to the 50 / 10s rate that I set in the rules.

    You cannot view this attachment.

    The millions of queries that were bogging down the Unbound reports have also now dropped off naturally and are capped:

    You cannot view this attachment.

    One of the Rokus has recently stopped its spamming but another one remains 'active'.  It's no longer making the firewall unresponsive though and now the reporting also loads normally.

    I think pf rate limiting is a nice one to have in the toolbox.  Good feature update :)