SSD Nodes Learn 8GB RAM — $66/yr
Guides Matt ConnorBy Matt Connor

Fix DNS over WireGuard: three failure modes

Your WireGuard tunnel is up but names do not resolve, or the queries leak to the local router. Find which of the three DNS failures you have, and fix it.

Why DNS breaks the moment the WireGuard tunnel comes up

DNS over WireGuard fails in three ways, and each one has its own fix. Nothing resolves at all, or names resolve but the queries leave your machine outside the tunnel, or the client's own resolver manager overwrites the setting seconds after the interface starts. The tunnel is almost never the problem. The problem is the one line that tells the client which resolver to ask, and the routing that decides how packets to that resolver travel.

WireGuard moves IP packets and knows nothing about DNS (domain name system, the service that turns names such as example.com into IP addresses). The DNS = line in a client [Interface] block is not a WireGuard setting. It is read by wg-quick, the shell wrapper that brings the interface up, and wg-quick then edits the client's resolver configuration while the tunnel is live and restores it on wg-quick down. So every problem below is a routing problem or a wg-quick problem, never a cryptography problem. If the tunnel itself is not built yet, start with a self-hosted WireGuard VPN on your own VPS and come back to this page afterwards.

Confirm the tunnel is healthy before you touch DNS at all.

sudo wg show
ping -c 3 10.8.0.1
ping -c 3 1.1.1.1

wg show should list the peer with a recent latest handshake, and both pings should answer. If ping 1.1.1.1 times out, you have a forwarding or NAT (network address translation) problem rather than a DNS problem, and no amount of resolver config will help. Every example here uses 10.8.0.0/24 as the tunnel subnet and 10.8.0.1 as the server's tunnel address. Substitute your own.

Failure one: nothing resolves, because the resolver never answers

The symptom is precise. ping 1.1.1.1 works, and curl https://example.com returns this:

curl: (6) Could not resolve host: example.com

Ask the tunnel resolver directly from the client. dig comes from the dnsutils package on Ubuntu and Debian.

dig +short @1.1.1.1 example.com
dig +short @10.8.0.1 example.com

The first command returns an address, which proves packets reach the internet through the tunnel. The second returns nothing and prints ;; communication timed out; no servers could be reached. That is the whole diagnosis: your client is pointed at 10.8.0.1, and 10.8.0.1 is not answering on UDP port 53.

Two causes produce it. Either no resolver is running on the server, or the server firewall drops the query before it arrives. Check both on the server.

sudo ss -ulnp | grep ':53'
sudo nft list ruleset

A resolver that is running and bound correctly shows a line with 10.8.0.1:53 or 0.0.0.0:53. On Ubuntu the surprise is usually 127.0.0.53:53: that is the systemd-resolved stub listener, which binds a loopback address and is deliberately unreachable from other machines. Pointing a VPN client at a server whose only resolver is that stub produces exactly this timeout.

The fix is a resolver that listens on the tunnel address, plus one firewall rule that lets peers reach it.

sudo apt update && sudo apt install -y unbound
printf 'server:\n  interface: 10.8.0.1\n  access-control: 10.8.0.0/24 allow\n' \
  | sudo tee /etc/unbound/unbound.conf.d/wireguard.conf
sudo systemctl restart unbound
sudo ss -ulnp | grep '10.8.0.1:53'

Then open the port for tunnel traffic only. With nftables, add these two lines to the input chain in /etc/nftables.conf and reload with sudo systemctl reload nftables.

udp dport 53 iifname "wg0" accept
tcp dport 53 iifname "wg0" accept

With ufw, sudo ufw allow in on wg0 to any port 53 does the same job. Never open port 53 to the public internet. An open recursive resolver is found by scanners within days and used to amplify denial of service attacks, and your provider will notice that traffic before you do.

Re-run dig +short @10.8.0.1 example.com from the client. An address in the output means the resolver path works, so the client now only has to use it. Add the line to the client [Interface] block and restart the interface with sudo wg-quick down wg0 && sudo wg-quick up wg0.

[Interface]
PrivateKey = <client private key>
Address = 10.8.0.2/32
DNS = 10.8.0.1

Failure two: DNS leaks, because a split tunnel does not route the resolver

This one is worse, because everything appears to work. Names resolve, pages load, and the queries travel in cleartext across the local network you did not want to trust.

Two configurations cause it. The first is a client with AllowedIPs = 0.0.0.0/0, ::/0 and no DNS = line. wg-quick installs the default route in its own routing table and adds a rule with suppress_prefixlength 0, which keeps more specific local routes alive on purpose so the machine can still reach its printer. The resolver the client learned by DHCP, typically the router at 192.168.1.1, matches one of those local routes. Your traffic rides the tunnel. The local network still receives the full list of names you look up.

The second is a split tunnel: AllowedIPs = 10.8.0.0/24 with DNS = 9.9.9.9. Because 9.9.9.9 is not inside AllowedIPs, the client has no route to it through the tunnel, so the query leaves over the local link exactly as in the first case.

Prove which resolver is really answering. whoami.akamai.net is a public test name that answers with the IP address of the recursive resolver that asked, so you can compare its answer against your server's public address.

resolvectl status
dig +short whoami.akamai.net
sudo tcpdump -ni any -c 10 port 53

resolvectl status prints one block per link. If the block for your ethernet or wireless link still shows Current DNS Server: 192.168.1.1 while the wg0 block shows none, that is the leak. dig +short whoami.akamai.net returning your home broadband address rather than your server's address confirms it from the far end. The tcpdump line is the proof that settles arguments: healthy output puts every port 53 packet on wg0, and a leak puts them on wlan0 or enp3s0.

The fix has two halves and both are required. Set DNS to an address that lives inside the tunnel, and make sure that address is inside AllowedIPs.

[Interface]
Address = 10.8.0.2/32
DNS = 10.8.0.1

[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.8.0.0/24, 10.20.0.0/16

10.8.0.1 sits inside 10.8.0.0/24, so the query is encrypted and sent to the server. If you insist on a public resolver on a split tunnel, add it as a host route: AllowedIPs = 10.8.0.0/24, 9.9.9.9/32. The packets are then tunnelled, though the local network can still see that you chose that provider from earlier sessions. A resolver you run yourself avoids the question.

Resolver assignment is one of the visible differences between hand-built WireGuard and a coordinated mesh, which is part of the tradeoff in WireGuard compared with Tailscale. Running a self-hosted Headscale control server gives you that coordination without handing your key material to a third party.

Failure three: resolvconf and systemd-resolved fight on Linux clients

macOS, Windows, iOS and Android clients apply DNS = through the official app and cause little trouble. Linux is where the setting is applied by a shell script that has to guess which of several resolver managers you run.

The first failure is loud. sudo wg-quick up wg0 stops with:

resolvconf: command not found

wg-quick calls out to resolvconf, and that binary is not installed. Install the implementation that talks to systemd-resolved, then bring the interface up again.

sudo apt install -y openresolv
sudo systemctl restart systemd-resolved
sudo wg-quick down wg0 && sudo wg-quick up wg0
resolvectl status wg0

The second failure is quiet, and it is the one that costs an evening. The interface comes up, resolvectl status wg0 correctly shows DNS Servers: 10.8.0.1, and lookups still go to the old resolver. systemd-resolved keeps a separate resolver list per link and picks a link per query. Unless one link is marked as the default route for names, it keeps using the wireless link's resolver, because that link carries a search domain and yours does not.

Set the resolver and claim the default route in the same step. %i expands to the interface name, so this block works unchanged on any interface.

[Interface]
Address = 10.8.0.2/32
PostUp = resolvectl dns %i 10.8.0.1; resolvectl domain %i ~.
PostDown = resolvectl revert %i

Delete the DNS = line when you use PostUp this way, because otherwise two mechanisms write resolver state and only one of them cleans up afterwards. The ~. argument is the important half: it marks wg0 as the routing domain for every name, so systemd-resolved sends all queries there instead of choosing a link per query. Verify it.

resolvectl status wg0

Healthy output contains DNS Servers: 10.8.0.1 and Default Route: yes. If Default Route reads no, the resolvectl domain half did not run, and you are back to link selection.

One more case is worth naming. If /etc/resolv.conf is a real file rather than a symlink to /run/systemd/resolve/stub-resolv.conf, something else owns it, usually NetworkManager or a container runtime. Run ls -l /etc/resolv.conf before you debug anything else, because a tool that rewrites that file on every network change will undo your work at the least helpful moment.

The upgrade: your own filtering resolver over the tunnel

Once queries reliably travel down the tunnel, the resolver at the far end becomes a control point. Running AdGuard Home there gives every connected device blocklist filtering and a query log, with no client software and no per device configuration. The official install script, checked July 2026, is one line.

curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v

The setup wizard listens on port 3000 on first run. Reach it over the tunnel at http://10.8.0.1:3000 instead of opening that port publicly, and in the wizard set both the DNS listen address and the admin listen address to 10.8.0.1. If unbound from failure one still holds the same address, stop it first with sudo systemctl disable --now unbound, because two processes cannot bind UDP port 53 on one address and the second one exits with listen udp 10.8.0.1:53: bind: address already in use.

Client configs need no change if they already say DNS = 10.8.0.1. The query log will now show every lookup from every peer, which is a real privacy decision rather than a free win: you move trust from your internet provider to yourself, and you are the one who has to keep that box patched. A server exposed to the internet needs the basics in place first, and the first ten minutes on a new VPS covers them.

FAQ

Why does my WireGuard tunnel connect but names do not resolve?

The tunnel carries packets and does not handle names at all, so a working tunnel with broken lookups means the resolver you point at is not answering. Test with dig +short @10.8.0.1 example.com from the client. A communication timed out reply means either no resolver is listening on that tunnel address, often because the systemd-resolved stub only binds 127.0.0.53, or the server firewall is dropping UDP port 53 arriving on wg0. Fix the listener first, then open the port for wg0 only.

How do I check whether my DNS is leaking over WireGuard?

Run sudo tcpdump -ni any -c 10 port 53 on the client and watch the interface column while you browse. Every packet should be on wg0. If they appear on your wireless or ethernet interface, the queries are leaving in cleartext. dig +short whoami.akamai.net gives a second opinion, because it answers with the public address of whichever recursive resolver asked, so an answer that is not your server's address confirms the leak.

Do I need the DNS = line if I use a split tunnel?

Yes, and the resolver address must also sit inside AllowedIPs or the client has no route to it. With AllowedIPs = 10.8.0.0/24, a resolver at 10.8.0.1 is covered and the query is encrypted. A public resolver such as 9.9.9.9 is not covered, so the query leaves over the local link even though the DNS line looks correct.

Why does resolvectl show the right server but lookups still go elsewhere?

systemd-resolved keeps one resolver list per link and chooses a link per query, so a correct entry on wg0 is ignored while another link holds the default route for names. Add PostUp = resolvectl dns %i 10.8.0.1; resolvectl domain %i ~. to the client [Interface] block and remove the DNS = line. resolvectl status wg0 should then report Default Route: yes.

Which client should I fix first when several are broken?

Fix one Linux client, because it is the only platform that shows you the mechanism. resolvectl status and tcpdump tell you which resolver answered and which interface carried the packet. Phone and desktop apps apply the same DNS and AllowedIPs values with no visible plumbing, so once the Linux client is correct you are copying a configuration you have already proved.