LinkedIn logo Xing logo DBLP logo University logo GitLab logo

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

Complete dual-stack implementation guide for pfSense CE 2.8.1 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: 23 September 2025.

The objective is to create a transparent VPN gateway with pfSense where all client traffic is automatically routed through Mullvad's WireGuard service without requiring individual device configuration. This setup uses three pfSense 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 pfSense CE 2.8.1.

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 Upstream gateway and IPv6 Upstream gateway are set to None. Enable interface is checked.

Configuring the Mullvad account and obtaining IP addresses

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 pfSense

With our data ready, we need to install WireGuard as it does not come preinstalled. Then, we will set up the WireGuard interface. We do not set the peers or gateways in this section. This we will do in the next two sections.

  1. To install WireGuard in pfSense, go to System > Package Manager > Available Packages. Search for WireGuard and hit Install to install the respective package. Click Confirm when asked.

  2. After the installation, go to VPN > WireGuard. In the Tunnels tab click Add Tunnel. In the Description field you can choose any name for the tunnel. I like to simply call it «Mullvad». The Listen Port is arbitrary, but I like to set it to WireGuard's default 51820. Under Interface Keys enter the private key you generated earlier. The Public Key should be automatically generated and populated from the private key. Ignore the Interface Addresses and any other settings for now. Click Save Tunnel.

  3. Go to the Settings Tab in the WireGuard menu. Under General Settings > Enable check Enable WireGuard and click Save. Also click on Apply Changes when asked.

  4. Next, go to Interfaces > Assignments. Under Available network ports there should be the tun_wg0 device. Click on Add then Save. In my case the interface is called OPT2 but it may be different in your case.

  5. Now, go to Interfaces and select the newly created interface. Under Enable check Enable interface. I like to change the Description from OPT2 to VPN to make the interface more easily identifiable. Pick whatever name you like here. For IPv4 Configuration Type select Static IPv4 and for IPv6 Configuration Type select Static IPv6. Under Static IPv4 Configuration > IPv4 Address enter the IPv4 address that was assigned to you by Mullvad. In my case this is 10.69.123.45/32. Under Static IPv6 Configuration > IPv6 address enter the IPv6 address that was assigned to you by Mullvad. In my case this is fc00:bbbb:bbbb:bb01::5:4321/128. Leave the IPv4 Upstream Gateway and IPv6 Upstream Gateway on None for now. Hit Save then Apply Changes.

Set up the Mullvad gateways

In the previous step, we did not select upstream gateways. We need to do some advanced configuration on the gateways. For this, we first need to create the interface, as done in the section before, then create the gateways, then hop back to the interface configuration to set the appropriate gateways.

  1. Go to System > Routing and hit Add. Under Interface select the VPN interface created above and for the Address Family select IPv4. You can choose whatever Name you like. I call it «Mullvad4». As Gateway enter Mullvad's gateway IPv4 address from above, i.e. 10.64.0.1. Hit Display advanced and check Use non-local gateway through interface specific route under Use non-local gateway at the very bottom. Then click on Save.

    Still under System > Routing click on Add again and repeat the procedure for the IPv6 configuration. Select IPv6 as the Address Family and enter fc00:bbbb:bbbb:bb01::1 as the Gateway. I like to give this gateway the Name «Mullvad6». Don't forget to select Use non-local gateway. Save, then click Apply Changes.

    (In the System > Routing overview, I like to set the default gateway for the pfSense firewall itself to my local gateway, i.e. not the Mullvad gateway.)

  2. Go back to Interfaces > VPN and select the newly created «Mullvad4» and «Mullvad6» gateways for IPv4 Upstream gateway and IPv6 Upstream gateway. Then hit Save and Apply Changes.

Add a Mullvad server as WireGuard peer

We have properly configured the WireGuard interface and its gateways so far. But we have not yet configured a peer to talk to, i.e. Mullvad's remote server. This, we will do in this section.

  1. Select any WireGuard server you like from Mullvad's server overview. As an example in this blog post, I chose the server ch-zrh-wg-005 located in Zürich, Switzerland:

    domain_name: ch-zrh-wg-005.relays.mullvad.net
    ipv4_address: 193.32.127.70
    ipv6_address: 2a03:1b20:a:f011::f401
    public_key: dV/aHhwG0fmp0XuvSvrdWjCtdyhPDDFiE/nuv/1xnRM=
    
  2. Go to VPN > WireGuard > Peers and click Add Peer. Select tun_wg0 (Mullvad) as the Tunnel. Again, as Description you can pick whatever you like. I'll give it Mullvad's server name ch-zrh-wg-005. Uncheck Dynamic under Dynamic Endpoint.

    Enter the Endpoint. This can be the hostname, the IPv4 address or the IPv6 address. I use the IPv4 address 193.32.127.70. The port is fixed to WireGuard's default port 51820. I also like to set the Keep Alive to 25.

    Use the Public Key from the server. In this example, this is dV/aHhwG0fmp0XuvSvrdWjCtdyhPDDFiE/nuv/1xnRM=. Leave the Pre-shared Key empty.

    We want to be able to route all traffic over the interface, so we need to allow that separately for IPv4 and IPv6. Under Allowed IPs enter 0.0.0.0/0, then hit Add Allowed IP and enter ::/0. Hit Save Peer and Apply Changes.

  3. Under Status > WireGuard you can open the drop down under tun_wg0 and verify that the connection works, i.e. a handshake took place and there is some RX and TX.

NAT and routing

We're finished with the WireGuard configuration so far, the connection to Mullvad is up. Next, we want to route and masquerade all traffic from the INT interface over the VPN interface. First, we'll configure Network Address Translation, then we will configure the routes over Mullvad's gateways.

  1. Go to Firewall > NAT > Outbound. I prefer to select Manual Outbound NAT and delete all unnecessary rules. But you can also go for Hybrid Outbound NAT, which is easier in most cases, but would probably require a kill switch in case the VPN goes down and you don't want your traffic to pass through your WAN interface. After the selection, hit Save and Apply Changes. Hit Add with the arrow up.

    Select VPN as the Interface, leave IPv4+IPv6 as the Address Family, select Any for the Protocol. Select INT subnets as the Source and leave Any as the Destination. Under Translation select VPN Address as Address. Hit Save and Apply Changes.

  2. Go to Firewall > Rules and go to the INT tab and hit Add with the arrow up. Select IPv4 as the Address Family, change the Protocol to Any, set Source to INT subnets. Under Advanced Options select Display Advanced and select Mullvad4 as the Gateway. Hit Save and Apply Changes. Repeat for IPv6, where you select IPv6 as the Address Family and Mullvad6 as the Gateway.

If you prefer to manually assign IP addresses and DNS servers to your clients in 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 DHCP and DNS

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.

  1. Go to System > Advanced > Networking and select Kea DHCP as the Server Backend. Hit Save.

  2. Go to Services > DHCP Server and select the INT tab. Check the Enable DHCP server on INT interface checkbox. Under Primary Address Pool and Address Pool Range select some subnet 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 select the appropriate IPv4 DNS servers. Hit Save then Apply Changes.

  3. Go to Services > DHCPv6 Server and select the INT tab. Check the Enable DHCPv6 server on INT interface checkbox. Under Primary Address Pool and Address Pool 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 select the appropriate IPv6 DNS servers. Ensure that Provide DNS servers to DHCPv6 clients is checked. Hit Save then Apply Changes.

  4. Go to Services > Router Advertisements and select the INT tab. Select Stateless DHCP as Router Mode. Under DNS Configuration check Mirror DHCPv6. Hit Save.

Any client connected to the INT subnet will now automatically be assigned an IPv4 and an IPv6 address. Gateways and DNS servers will be automatically configured and traffic will be routed over the VPN tunnel.

Adding a kill switch

If you want to block any traffic from INT passing through the WAN interface, you can add a kill switch under Firewall > Rules in the Floating tab.

Click on Add and select Block or Reject as Action, WAN as the Interface, IPv4+IPv6 as the Address Family, Any as the Protocol and INT subnets as the Source. Also select the Apply the action immediately on match under Quick. Hit Save then Apply Changes.

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.32.127.244",
  "city": "Opfikon",
  "region": "Zurich",
  "country": "CH",
  "loc": "47.4317,8.5759",
  "org": "AS39351 31173 Services AB",
  "postal": "8152",
  "timezone": "Europe/Zurich",
  "readme": "https://ipinfo.io/missingauth"
}

We can also test for IPv6 connectivity:

$ curl v6.ipinfo.io
{
  "ip": "2a03:1b20:a:f011::e419",
  "city": "Opfikon",
  "region": "Zurich",
  "country": "CH",
  "loc": "47.4317,8.5759",
  "org": "AS39351 31173 Services AB",
  "postal": "8152",
  "timezone": "Europe/Zurich",
  "readme": "https://ipinfo.io/missingauth"
}

Both responses should show the Mullvad server's location (in this case, Zürich) 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.

Duplicate Address Detection with 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 pfSense instances.

In my home lab setup with a Mellanox ConnectX-4 Lx network card and SR-IOV enabled, I encountered DAD conflicts when using a virtual function as the WAN interface. The solution was to disable DAD by setting net.inet6.ip6.dad_count to 0 under System > Advanced > System Tunables.

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. pfSense 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.