SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor

How to Fix DNS Wey No Dey Work for WireGuard

WireGuard tunnel dey up but DNS no resolve, leaks to router, or gets overwritten. Find the three failure modes and the exact fix for each one.

Why DNS dey break as soon as WireGuard tunnel come up

DNS over WireGuard dey fail for three ways, and each one get im own fix. Nothing dey resolve at all, or names dey resolve but queries dey leave your machine outside the tunnel, or the client resolver manager dey overwrite the setting seconds after interface start. Tunnel almost never be the problem. Na the one line wey tell client which resolver to ask, plus the routing wey decide how packets go reach that resolver, dey cause the problem.

WireGuard dey move IP packets and e no know anything about DNS (domain name system, the service wey dey turn names like example.com into IP addresses). The DNS = line for client [Interface] block no be WireGuard setting. wg-quick dey read am, na the shell wrapper wey dey bring interface up, and wg-quick come edit the client resolver configuration while tunnel dey live, then restore am for wg-quick down. So every problem below na routing problem or wg-quick problem, never cryptography problem. If tunnel never build yet, start with self-hosted WireGuard VPN for your own VPS and come back to this page afterwards.

Confirm say tunnel dey 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 suppose list the peer with recent latest handshake, and both pings suppose answer. If ping 1.1.1.1 timeout, you most likely get forwarding or NAT (network address translation) problem instead of DNS problem, and no amount of resolver config go help. Firewall wey dey drop ICMP fit produce the same timeout, so confirm with curl -sI https://1.1.1.1 before you start changing forwarding rules. If the pings answer but throughput collapse once real traffic start, na separate fault line be that, and slow WireGuard almost always trace back to MTU instead of anything for this page. Every example here dey use 10.8.0.0/24 as tunnel subnet and 10.8.0.1 as server tunnel address. Substitute your own.

Failure one: nothing dey resolve, because resolver no dey answer

The symptom dey clear. ping 1.1.1.1 dey work, and curl https://example.com dey return this:

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

Ask the tunnel resolver directly from the client. dig dey come from the dnsutils package for Ubuntu and Debian.

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

The first command dey return an address, wey prove say packets dey reach internet through the tunnel. The second one no dey return anything and e dey print ;; communication timed out; no servers could be reached. Na the complete diagnosis be this: your client dey point to 10.8.0.1, and 10.8.0.1 no dey answer for UDP port 53.

Two causes fit produce am. Either no resolver dey run for the server, or server firewall dey drop the query before e arrive. Check both for the server.

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

Resolver wey dey run and bind correctly go show line with 10.8.0.1:53 or 0.0.0.0:53. For Ubuntu, the surprise normally be 127.0.0.53:53: na the systemd-resolved stub listener be that. E dey bind loopback address, and other machines no fit reach am by design. If you point VPN client to server wey only get that stub as resolver, timeout like this one go happen.

The fix na resolver wey dey listen on the tunnel address, plus one firewall rule wey allow peers reach am.

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 for /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 dey do the same work. Never open port 53 to public internet. Scanners go find open recursive resolver within days and use am to amplify denial of service attacks, and your provider go notice that traffic before you notice am.

Run dig +short @10.8.0.1 example.com again from the client. Address for the output mean say resolver path dey work, so na only client remain to use am. Add the line to 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 split tunnel no dey route the resolver

This one worse, because everything go look like e dey work. Names go resolve, pages go load, and the queries go travel as cleartext through the local network wey you no wan trust.

Two configurations fit cause am. The first na client wey get AllowedIPs = 0.0.0.0/0, ::/0 but no DNS = line. wg-quick dey install the default route for its own routing table and add rule with suppress_prefixlength 0. This one deliberately keep more specific local routes active, so the machine fit still reach its printer. The resolver wey client learn through DHCP, usually the router for 192.168.1.1, match one of those local routes. Your traffic dey pass through the tunnel. But the local network still dey receive the complete list of names wey you look up.

The second na split tunnel: AllowedIPs = 10.8.0.0/24 with DNS = 9.9.9.9. Because 9.9.9.9 no dey inside AllowedIPs, the client no get route to am through the tunnel. So the query go leave through the local link, exactly like for the first case.

Prove which resolver really dey answer. whoami.akamai.net na public test name wey dey answer with the IP address of the recursive resolver wey make the query. This one let you compare the answer with your server public address.

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

resolvectl status dey print one block for each link. If the block for your ethernet or wireless link still show Current DNS Server: 192.168.1.1, while the wg0 block show nothing, na the leak be that. If dig +short whoami.akamai.net return your home broadband address instead of your server address, that confirms am from the far end. The tcpdump line na the proof wey settle the matter: healthy output dey put every port 53 packet wey leave the machine on wg0. Leak dey put dem on wlan0 or enp3s0. Packets on lo to or from 127.0.0.53 na applications wey dey talk to the local systemd-resolved stub, and dem no be leak.

The fix get two parts, and you need both. Set DNS to an address wey dey inside the tunnel, and make sure say that address dey 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 dey inside 10.8.0.0/24, so the query go encrypted and sent to the server. If you insist on using public resolver for split tunnel, add am as a host route: AllowedIPs = 10.8.0.0/24, 9.9.9.9/32. The packets go then pass through the tunnel, but the local network fit still see say you choose that provider from earlier sessions. Resolver wey you run by yourself go avoid the issue.

Resolver assignment na one visible difference between hand-built WireGuard and coordinated mesh. This na part of the tradeoff for WireGuard compared with Tailscale. If you run self-hosted Headscale control server, you get that coordination without giving your key material to third party. If na that last part dey worry you, remember say Tailscale never holds the keys that encrypt your traffic, and the sharper question na wetin compromised coordination server or stolen identity account fit add to your network.

Failure three: resolvconf and systemd-resolved dey fight for Linux clients

macOS, Windows, iOS and Android clients dey apply DNS = through the official app and dem dey cause small wahala. Na Linux get the problem, because shell script dey apply the setting and e gats guess which one among different resolver managers you dey run.

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

resolvconf: command not found

wg-quick dey call resolvconf, and that binary no dey installed. For Debian 12 and Ubuntu 24.04 or later, the resolvconf command wey dey talk to systemd-resolved dey inside systemd-resolved package, so install am, then bring the interface up again. Ubuntu 22.04 no get separate package for am; for there, create the compatibility link yourself with sudo ln -s /usr/bin/resolvectl /usr/local/bin/resolvconf and skip the install line.

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

The second failure quiet, and na this one fit waste one evening. The interface comes up, resolvectl status wg0 correctly shows DNS Servers: 10.8.0.1, but lookups still dey reach the old resolver. systemd-resolved no dey ignore wg0. Name wey no match any routing domain dey go to the DNS servers for every link wey count as default route, and link wey get DNS servers but no route-only domain dey count as one by default. So query dey go to 10.8.0.1 and the router for the wireless link at the same time, and the first successful answer dey win. The router dey one hop away and e normally answers first, and the query don already leave in cleartext either way. Names under the wireless link own search domain, like lan, dey go only to that link, because the most specific routing domain dey win.

When wg-quick applies DNS = through systemd own resolvconf wrapper, e already dey request catch-all routing domain, so this failure mostly happens when another resolvconf implementation or a network manager apply the setting. Set am explicitly to remove the guesswork.

Set the resolver and claim the catch-all routing domain for the same step. %i expands to the interface name, so this block go work unchanged for 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 go write resolver state and na only one of dem go clean up afterwards. The ~. argument na the important part: e makes wg0 the route-only domain for every name, so systemd-resolved stops sending names wey no more specific domain match to the other links. Names under another link own domain, like lan, still dey go to that link, and na usually wetin you want for local hosts. Verify am.

resolvectl status wg0

Healthy output get DNS Servers: 10.8.0.1 and DNS Domain: ~.. If ~. dey missing, the resolvectl domain part no run, and wg0 don return to racing the other links. No use the default route flag as the check: e reads yes (or +DefaultRoute for the Protocols line, depending on the systemd version) even without ~., because link wey no get route-only domain dey count as default route anyway.

One more case dey worth naming. If /etc/resolv.conf na real file instead of symlink to /run/systemd/resolve/stub-resolv.conf, something else dey own am, usually NetworkManager or a container runtime. Run ls -l /etc/resolv.conf before you debug anything else, because tool wey rewrites that file every time network change happens go undo your work for the least helpful moment.

Upgrade: your own filtering resolver over the tunnel

Once queries dey reliably pass through the tunnel, resolver for the far end become control point. If you run AdGuard Home there, every connected device go get blocklist filtering and query log, without client software or per-device configuration. Official install script, wey dem check for October 2026, na one line.

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

Setup wizard dey listen on port 3000 for first run. Reach am through the tunnel for http://10.8.0.1:3000 instead of opening that port publicly. For the wizard, set both DNS listen address and admin listen address to 10.8.0.1. If unbound from failure one still dey hold the same address, stop am first with sudo systemctl disable --now unbound, because both go compete for UDP port 53 on one address and AdGuard Home go exit with listen udp 10.8.0.1:53: bind: address already in use.

Client configs no need change if dem already dey say DNS = 10.8.0.1. Query log go now show every lookup wey reach am from every peer, unless you exclude one client from logging. This na real privacy decision, no be free benefit: you move trust from your internet provider to yourself, and na you go need keep that box patched. Server wey dey exposed to internet need the basic protections in place first, and the first ten minutes for new VPS cover dem.

FAQ

Why tunnel WireGuard dey connect but names no resolve?

Tunnel dey carry packets, but e no handle names at all. So if tunnel dey work but lookup dey fail, resolver wey you point to no dey answer. Test am with dig +short @10.8.0.1 example.com from client. If communication timed out reply come back, e mean say either no resolver dey listen for that tunnel address, often because systemd-resolved stub dey bind only to 127.0.0.53, or server firewall dey drop UDP port 53 wey enter through wg0. Fix the listener first, then open the port for wg0 only.

How I fit check whether my DNS dey leak through WireGuard?

Run sudo tcpdump -ni any -c 10 port 53 for client and watch the interface column while you browse. Every packet wey comot from the machine suppose dey on wg0. Packets for lo wey dey go to or come from 127.0.0.53 na the local systemd-resolved stub, and dem dey okay. If dem show for your wireless or ethernet interface, the queries dey comot as cleartext. dig +short whoami.akamai.net go give you another confirmation, because e answers with the public address of whichever recursive resolver make the request. So if the answer no be your server address, e confirm say leak dey happen.

I need the DNS = line if I dey use split tunnel?

Yes. Resolver address must also dey inside AllowedIPs, otherwise client no get route reach am. With AllowedIPs = 10.8.0.0/24, resolver for 10.8.0.1 dey covered and query dey encrypted. Public resolver like 9.9.9.9 no dey covered, so query go comot through local link even though DNS line look correct.

Why resolvectl dey show correct server but lookup still dey go somewhere else?

systemd-resolved dey send any name wey no match routing domain to every default-route link at the same time, including wg0. E then return the first successful answer, and na usually local router dey answer first. Add PostUp = resolvectl dns %i 10.8.0.1; resolvectl domain %i ~. to client [Interface] block and remove DNS = line. resolvectl status wg0 suppose then report DNS Domain: ~., and every name wey no other link claim with more specific domain go only to wg0.

Which client I suppose fix first when several ones dey broken?

Fix one Linux client first, because na the only platform wey go show you how the mechanism dey work. resolvectl status and tcpdump go tell you which resolver answer and which interface carry the packet. Phone and desktop apps dey apply the same DNS and AllowedIPs values without showing the plumbing. So once the Linux client correct, you dey copy configuration wey you don already prove.