I’ve been using Mullvad VPN for a while now but only ever used it with the official client on my workstation. I use DNS extensively in my home network, so as soon as I activate Mullvad, I can’t resolve DNS names locally. Of course, this is by design and expected. I own an OPNsense appliance, so the natural solution is to move the tunnel there.
TL;DR
Use the following shell command to request an IP with no DNS hijacking:
curl -sSL https://api.mullvad.net/app/v1/wireguard-keys \
-H "Content-Type: application/json" \
-H "Authorization: Token YOURMULLVADACCOUNTNUMBER" \
-d '{"pubkey":"YOURWIREGUARDPUBLICKEY"}'
Mullvad Hijacks DNS Queries Over WireGuard Tunnels
Instead of using the OpenVPN protocol, I decided to go with the latest and greatest: WireGuard. OPNsense is a fork of FreeBSD but lacks a kernel implementation of WireGuard, requiring a plugin. It’s good enough for me to try, and I hope WireGuard will be natively supported soon.
During my research on how to best configure this, there seemed to be the caveat of Mullvad hijacking DNS traffic going through WireGuard tunnels, redirecting it to their DNS servers. It decreases the likelihood of DNS leaks, but power users like you and I might not want that. What if I want to query DNS root servers through the VPN tunnel because I use my own DNS resolver? Mullvad hijacking DNS queries would make my DNS resolver trip up.
What a bummer, right? Looking at the Mullvad FAQ, it seemed the only solution was to resort to OpenVPN:
Ports 1400 UDP and 1401 TCP do not have DNS hijacking enabled, which might work better for pfSense users
But Mullvad launched support for custom DNS servers on the Mullvad VPN app back in April 2021. It also works for WireGuard, so what’s the secret?
Reverse-engineering the Mullvad App
Searching through the docs, I found the WireGuard on a router article explaining how to get an IP to use with Mullvad via API:
curl https://api.mullvad.net/wg/ -d account=YOURMULLVADACCOUNTNUMBER --data-urlencode pubkey=YOURPUBLICKEY
Next, let’s look at how the app requests IPs. Fortunately it’s open-source and available on GitHub, so the only reverse-engineering we’re going to be doing is reading some code. It turns out that the app uses a different API to request IPs, found in the push_wg_key
function: https://api.mullvad.net/app/v1/wireguard-keys
.
Testing Both APIs
For testing, we’ll be using the official WireGuard client. Let’s open the client, click Add empty tunnel...
, and give it a name:
The tunnel will initially look like this:
Copy the public key and execute the following to request our Mullvad IPs:
curl https://api.mullvad.net/wg/ -d account=YOURMULLVADACCOUNTNUMBER --data-urlencode pubkey=YOURPUBLICKEY
The response will return an IPv4 and IPv6 address. Add the following to the configuration file:
[Interface]
PrivateKey = <PRIVATE KEY>
Address = <IPv4 ADRESS>
DNS = 9.9.9.9
[Peer]
PublicKey = 9hIGjit4ApkNGuEWYBLpahxokEoP0cT9CMZ+ELEygzo=
AllowedIPs = 0.0.0.0/0
Endpoint = 194.36.25.18:51820
We use the de24-wireguard
Mullvad server as peer and Quad9 as DNS server.
Let’s activate the tunnel and browse to Mullvad’s connection check:
As expected, the Quad9 DNS server is not leaking through because Mullvad hijacks our DNS requests and redirects them to their DNS servers.
Next, we use the API the app uses to request the Mullvad IPs. We expect to get a different IP for which DNS hijacking is disabled. Before we can do this, we need to revoke the WireGuard key on the Mullvad website because we already requested an IP for this public key:
After revoking the key, we run the following command:
curl -sSL https://api.mullvad.net/app/v1/wireguard-keys -H "Content-Type: application/json" -H "Authorization: Token YOURMULLVADACCOUNTNUMBER" -d '{"pubkey":"YOURPUBLICKEY"}'
Next, we replace the IP in Address
field of the WireGuard config with the new IP we received. Then we re-activate the tunnel and visit Mullvad’s connection check:
Hooray, the Quad9 DNS servers leak through, so Mullvad is not hijacking our DNS traffic for this tunnel!