LinkedIn logo Xing logo DBLP logo University logo GitLab logo

Mullvad WireGuard VPN in OPNsense: Full IPv4 + IPv6 Dual-Stack Setup

Complete dual-stack implementation guide for OPNsense 25.7.3 with Mullvad's WireGuard VPN, covering IPv4 and IPv6 routing, gateway configuration, NAT rules, firewall policies, and automated DHCP provisioning for transparent network-wide VPN connectivity.

First published: 3 October 2025.

The objective is to create a transparent VPN gateway with OPNsense where all client traffic is automatically routed through Mullvad's WireGuard service without requiring individual device configuration. In my last blog post I looked at how to set up Mullvad's WireGuard VPN in pfSense with fully functioning IPv4 and IPv6 connectivity. OPNsense has some subtle, or not so subtle, differences and I have not yet settled on whether to use OPNsense or pfSense in my personal setup. Thus, I decided to write another post on how to set up Mullvad's WireGuard VPN in OPNsense with fully functioning IPv4 and IPv6 connectivity.

This setup uses three interfaces: WAN for upstream connectivity, LAN for management access, and INT for client devices that will have their traffic seamlessly routed through the VPN with full IPv4 and IPv6 support.

Tested and verified on OPNsense 25.7.3_7-amd64.

Internal interface configuration

The interface INT that is connected to the clients has been set up with Static IPv4 as the IPv4 Configuration Type and Static IPv6 as the IPv6 Configuration Type. Under Static IPv4 Configuration and Static IPv6 Configuration private networks have been configured. Both IPv4 gateway rules and IPv6 gateway rules are set to Disabled. Enable interface is checked.

Configuring the Mullvad account and obtaining IP addresses

This part is identical to my previous blog post. Here, we essentially set up the credentials.

First, we will generate our key pair for use with WireGuard and upload the public key to Mullvad. There we will obtain our IPv4 and IPv6 addresses for use within the tunnel. The key pair as well as the IP addresses in this blog post serve as placeholders and you need to replace them with your own. Only the gateways presented in this section are the default Mullvad gateways across different configurations.

  1. Generate a key pair with the following command:

    $ wg genkey | tee >(wg pubkey)
    AOnnPfyXon01+WpNubM9FtSIn3s0PMHd1hPuM0vRokM=
    8oXRo5Gx0351bjVEOY/VBQLVq1zQk79AoFr0cBRi7xY=
    

    The first key is your private key and the second key is your public key.

  2. Log in to your account and go to Account Management > Devices > Advanced.

    Copy your public key into the Public Wireguard Key field and hit Upload. You will receive a name, an IPv4 and an IPv6 address. You can ignore the name, but write down the IPv4 and IPv6 addresses. Let's collect the data we have so far.

    # WireGuard Keys
    private_key: AOnnPfyXon01+WpNubM9FtSIn3s0PMHd1hPuM0vRokM=
    public_key: 8oXRo5Gx0351bjVEOY/VBQLVq1zQk79AoFr0cBRi7xY=
    
    # Mullvad Assigned Addresses
    ipv4_address: 10.69.123.45/32
    ipv6_address: fc00:bbbb:bbbb:bb01::5:4321/128
    
    # Gateways
    ipv4_gateway: 10.64.0.1
    ipv6_gateway: fc00:bbbb:bbbb:bb01::1
    

    The gateways weren't shown in the pop up, but they are actually Mullvad's default gateways and can be found in their WireGuard configuration files. You can log out now.

Set up the WireGuard interface in OPNsense

We have the necessary credentials and IP addresses for use with WireGuard. In this section, we will configure the WireGuard interface, the upstream servers (peers) and the gateways to use with Mullvad. This part is slightly simpler than it was for pfSense. Also, we do not have to install WireGuard as it comes preinstalled with OPNsense.

  1. Go to VPN > WireGuard > Instances and hit the plus sign.

    As Name I like to choose «Mullvad», but you can enter whatever you like. Enter the Public key and Private key that you generated in the first step. I also like to set the Listen port to 51820. Under Tunnel address enter the Mullvad assigned addresses, in this case 10.69.123.45/32 for IPv4 and fc00:bbbb:bbbb:bb01::5:4321/128 for IPv6. Check Disable routes and hit Save. Ensure Enable WireGuard is checked and hit Apply.

  2. Go to Interfaces > Assignments. There should be a wg0 (WireGuard - Mullvad) device. I like to add VPN as the Description and hit Add.

  3. We will now first add Mullvad's IPv4 gateway, then add the IPv6 gateway. Go to System > Gateways > Configuration and click the plus sign. As Name I like to choose «Mullvad4» for the IPv4 gateway, but you can pick whatever you like. The Interface to select is VPN, the Address Family is IPv4 and the IP Address is the IPv4 address of Mullvad's gateway, i.e. 10.64.0.1. Remember to check Far Gateway as the gateway is (obviously) not in the /32 subnet. Then hit Save.

    Repeat for IPv6, where the Address Family is IPv6 and the IP Address is the IPv6 address of Mullvad's gateway, i.e. fc00:bbbb:bbbb:bb01::1. Hit Save then Apply.

  4. Next, we will add the remote server as our WireGuard peer. Select any WireGuard server you like from Mullvad's server overview. As an example in this blog post, I chose the server fi-hel-wg-102 located in Helsinki, Finland:

    domain_name: fi-hel-wg-102.relays.mullvad.net
    ipv4_address: 193.138.7.157
    ipv6_address: 2a02:ed04:3581:2::f001
    public_key: xeHVhXxyyFqUEE+nsu5Tzd/t9en+++4fVFcSFngpcAU=
    
  5. Go to VPN > WireGuard > Peers and click the plus sign. As Name I like to pick the name of the Mullvad server, i.e. fi-hel-wg-102. As Public key enter the server's public key, i.e. in this case xeHVhXxyyFqUEE+nsu5Tzd/t9en+++4fVFcSFngpcAU=. Do not set a Pre-shared key, set Allowed IPs to 0.0.0.0/0 and ::/0. As Endpoint address I like to select the IPv4 address, in this case 193.138.7.157, but you can also go for the IPv6 address or the domain name. The Endpoint port is 51820. Under Instances select «Mullvad» or whatever name you picked in step 3. I also like to set the Keepalive interval to 25. Also ensure that Enabled is checked, it should be by default. Hit Save then Apply.

  6. You can now go to VPN > WireGuard > Status and verify that the tunnel is up. It should show the Handshake Age and some bytes under the Sent and Received columns.

NAT and routing

The tunnel to Mullvad is up. Next we need to route all traffic through the tunnel. This involves first setting up Network Address Translation and then setting up the routes for IPv4 and IPv6 separately.

  1. Go to Firewall > NAT > Outbound and select Hybrid outbound NAT rule generation or Manual outbound NAT rule generation. Choose Hybrid to keep automatic NAT rules while adding custom rules, or Manual for complete control over all NAT rules. Then hit Save.

  2. Hit the plus sign. The Interface to select is VPN, the TCP/IP Version to select is IPv4. Set Protocol to any, the Source address to INT net, the Destination address to any, the Translation / target to VPN address. Leave the remaining settings on default. Hit Save then Apply changes. Repeat for IPv6, where you set the TCP/IP Version to IPv6 instead of IPv4.

  3. Go to Firewall > Rules > INT and hit the plus sign. Leave everything on default but set the Source to INT net and the Gateway to Mullvad4 - 10.64.0.1. Hit Save then Apply. Repeat for IPv6, where you set the TCP/IP Version to IPv6 and the Gateway to Mullvad6 - fc00:bbbb:bbbb:bb01::1.

    For completeness sake here are the remaining important default values: Check Apply the action immediately on match under Quick, select in as the direction and any as the Destination.

If you prefer to manually assign IP addresses and DNS servers to your clients on the INT subnet, you can finish here. The next section configures DHCP for IPv4 and IPv6 with Router Advertisements and automatic assignments of DNS servers.

Setting up DNS and DHCP

I like to automatically assign addresses and DNS servers to the clients on INT via DHCP, so that I do not have to do any configuration on the clients. A few popular choices are AdGuard DNS, Cloudflare, dns0.eu, DNS4EU, Google, Quad9 and UncensoredDNS.

I may have overlooked something, but Dnsmasq and Kea didn't work for me, as I didn't figure out how to configure the DNS servers for the clients. So I decided to use the ISC DHCP server.

  1. Go to Services > Dnsmasq DNS & DHCP > General and uncheck Enable.

  2. Go to Services > ISC DHCPv4 > [INT] and check Enable DHCP server on the INT interface. As Range select some range from your INT interface that you feel is suitable. For example, if 10.0.0.0/24 is your network, you may want to pick 10.0.0.100 to 10.0.0.254.

    Under DNS servers enter the appropriate IPv4 DNS servers. Hit Save.

  3. Go to Services > ISC DHCPv6 > [INT] and check Enable DHCPv6 server on INT interface. As Range select some subnet from your INT interface that you feel is suitable. For example, if fd00::/64 is your network, you may want to pick fd00::100 to fd00::ffff.

    Under DNS servers enter the appropriate IPv6 DNS servers. Hit Save.

  4. Go to Services > Router Advertisements > [INT] and select Stateless as Router Advertisements. Also check Use the DNS configuration of the DHCPv6 server under DNS options. Hit Save.

Adding a kill switch

A kill switch prevents traffic from the INT subnet from leaking through the WAN interface if the VPN connection drops. This ensures that client devices cannot bypass the VPN accidentally.

Go to Firewall > Rules > Floating and hit the plus sign. Select Block as Action, ensure that Apply the action immediately on match under Quick is checked. As Interface select WAN, under TCP/IP Version select IPv4+IPv6. As Source select INT net and leave Destination on any. Hit Save then Apply changes.

With this rule in place, any traffic from INT attempting to exit through WAN will be blocked, ensuring that clients can only communicate when the VPN is active.

Testing the setup

From the command line on one of the clients in the INT subnet, I like to use curl against IPinfo to test the IPv4 connectivity:

$ curl ipinfo.io
{
  "ip": "193.138.7.169",
  "hostname": "s1931387169.blix.com",
  "city": "Helsinki",
  "region": "Uusimaa",
  "country": "FI",
  "loc": "60.1695,24.9354",
  "org": "AS50304 Blix Solutions AS",
  "postal": "00100",
  "timezone": "Europe/Helsinki",
  "readme": "https://ipinfo.io/missingauth"
}

We can also test for IPv6 connectivity:

$ curl v6.ipinfo.io
{
  "ip": "2a02:ed04:3581:2::e019",
  "city": "Helsinki",
  "region": "Uusimaa",
  "country": "FI",
  "loc": "60.1695,24.9354",
  "org": "AS50304 Blix Solutions AS",
  "postal": "00100",
  "timezone": "Europe/Helsinki",
  "readme": "https://ipinfo.io/missingauth"
}

Both responses should show the Mullvad server's location (in this case, Helsinki) rather than your actual location, confirming that traffic is properly routed through the VPN.

I also like to check web-based tools like Mullvad's Connection Check, AirVPN's IP Leak, BrowserLeaks or IVPN's DNS Leak Test. In my case, all come back with the IP addresses and DNS servers that I configured.

Troubleshooting

This section covers common issues you might encounter during setup and performance optimizations for production deployments.

Disable DAD for SR-IOV

In environments using SR-IOV virtual functions, IPv6 Duplicate Address Detection (DAD) can sometimes cause connectivity issues. This is particularly common when passing SR-IOV virtual functions to virtualized OPNsense instances.

In my home lab setup with a Mellanox ConnectX-4 Lx and SR-IOV enabled, I encountered DAD conflicts when using a virtual function as the WAN interface. The solution was to disable DAD:

Go to System > Settings > Tunables and hit the plus sign. As Tunable enter net.inet6.ip6.dad_count and as Value enter 0. Hit Save then Apply.

This issue typically occurs because the hypervisor and virtual function may both attempt to use the same IPv6 addresses, triggering DAD failures. For home lab environments where address conflicts are unlikely, disabling DAD provides a simple solution. In production environments, consider per-interface IPv6 configuration or VLAN isolation as alternatives.

If you're not using SR-IOV or virtualization, you can skip this configuration.

Maximum Transmission Unit (MTU) and Maximum Segment Size (MSS)

WireGuard adds different overhead depending on the IP version: 60 bytes for IPv4 transport and 80 bytes for IPv6 transport. Since this is a dual-stack setup, we use the larger IPv6 overhead value to accommodate both protocols.

Configure the following settings:

  1. For the VPN interface under Interfaces > [VPN], set MTU to 1420 (standard 1500 MTU minus 80 bytes WireGuard overhead for IPv6).
  2. For the INT interface under Interfaces > [INT], set MSS to 1420. OPNsense will automatically subtract TCP/IP headers (40 bytes for IPv4, 60 bytes for IPv6) to clamp MSS correctly.

These values ensure that packets don't exceed the tunnel capacity and avoid fragmentation, which can significantly impact performance. Different network environments may require fine-tuning these values based on your upstream MTU constraints.

For more details about MTU and its impact on network performance, see Cloudflare's MTU explanation.

Note: You might need to test different MTU values if you experience connectivity issues or poor performance, as some ISPs or network paths have smaller MTU limits.