WireGuard DNS Not Working Through VPN: Correct DNS Setup on Linux and Windows

Was this helpful?

You bring up the tunnel. The handshake is clean. You can ping an IP on the far side. Then you try a hostname and everything falls apart: browsers spin, package installs fail, and your monitoring agent starts screaming about “temporary failure in name resolution.”

This is the classic WireGuard trap: the VPN is fine, your packets are fine, and DNS is quietly going somewhere else. DNS failures are rarely “a DNS problem.” They’re usually a routing, policy, or resolver-integration problem wearing a DNS hat.

Fast diagnosis playbook

If you do nothing else, do this. It’s the fastest path from “DNS is broken” to “here’s the exact component lying to me.”

1) Prove the tunnel works without DNS

  • Ping a known IP across the tunnel (a VPN-side router, a DNS server IP, or a service VIP).
  • If IP connectivity fails, stop. This is not a DNS article anymore; it’s routing/firewall/AllowedIPs.

2) Identify which resolver you’re actually using

  • On Linux: check if you’re on systemd-resolved, NetworkManager, or plain /etc/resolv.conf.
  • On Windows: check if the WireGuard adapter has DNS servers and whether NRPT is in play.

3) Watch the DNS packets

  • Run a packet capture on the WireGuard interface and the physical interface.
  • Decision: if DNS queries leave via the wrong interface, you have a routing/policy problem; if they leave correctly but get no reply, you have firewall/ACL/NAT or a server problem.

4) Confirm the VPN-side DNS server is reachable and answering

  • Query the DNS server by IP explicitly (bypass local resolver behavior).
  • Decision: if direct query works but normal name resolution doesn’t, your OS resolver integration is wrong.

5) Check for split DNS expectations versus reality

  • Are you expecting only corp.example to resolve via VPN, while public DNS stays local?
  • If yes, you need domain-based routing (Linux resolved/NetworkManager, Windows NRPT). WireGuard itself does not do domain-based policy.

One dry rule: don’t change five things at once. DNS is a liar; you need controlled experiments.

A working mental model: where DNS breaks with WireGuard

WireGuard is intentionally small and dumb (in a good way). It encrypts packets between peers. It sets up an interface. It has a concept of “what IP ranges should go into this tunnel” (AllowedIPs). That’s it.

DNS, meanwhile, lives in a messy, OS-specific world:

  • Applications call a resolver library.
  • The resolver library consults configuration (which may be a file, a daemon, a network manager, or all three arguing together).
  • Queries are sent to configured DNS servers over UDP/TCP 53 (or sometimes DoT/DoH if your system is fancy or cursed).
  • Those packets follow your routing table and policy rules like any other traffic.

So “WireGuard DNS not working” usually means one of these:

  • Your system never switched DNS servers when the tunnel came up.
  • It switched, but the DNS packets are routed outside the tunnel anyway.
  • They go into the tunnel, but the server is unreachable from that source IP or blocked.
  • Split tunnel plus corporate DNS: you need domain routing, but you configured a single global DNS server.
  • Your local resolver daemon caches garbage or refuses to send queries to the VPN DNS server due to “DNSSEC”, “privacy”, or “I know better.”

WireGuard’s DNS= line in wg-quick is not magic. It’s a convenience hook that tries to integrate with your OS. When it fails, it fails quietly, like a grown adult ghosting a meeting invite.

Interesting facts and historical context (so you stop arguing with your laptop)

  1. DNS predates modern VPNs by decades. DNS was standardized in the early 1980s; most VPN designs came much later, so “DNS integration” is bolted on, not fundamental.
  2. WireGuard intentionally avoids pushing DHCP-style options. Traditional enterprise VPNs often push DNS settings; WireGuard doesn’t have a built-in control channel for that.
  3. resolv.conf was never meant for today’s complexity. It started as a simple file; modern systems replaced it with daemons because laptops roam and interfaces come and go.
  4. systemd-resolved introduced per-link DNS routing. That’s what makes split DNS sane on Linux—when it’s configured correctly.
  5. Windows has had domain-based DNS policy for a long time. NRPT (Name Resolution Policy Table) exists largely because enterprises demanded “these domains go to these DNS servers.”
  6. DNS can ride UDP or TCP. Most people forget TCP 53 exists until UDP fragments get dropped and suddenly “only big lookups fail.”
  7. EDNS0 made DNS “bigger” and more fragile. Larger responses mean fragmentation and PMTU issues show up as “random DNS failures.”
  8. Corporate DNS often blocks recursion from “unknown” source subnets. If your tunnel gives you a new address range and the DNS ACL isn’t updated, it looks like DNS is down.

One quote, because it fits operations work better than motivational posters:

“Hope is not a strategy.” — attributed to Vince Lombardi

Common failure modes: what “DNS not working” actually means

Mode A: DNS never changes after tunnel up

Typical symptoms: you can resolve public domains but not internal ones; or you keep resolving internal names to public addresses (or nothing).

Why it happens: wg-quick didn’t apply DNS settings because your distro uses a resolver manager that wg-quick can’t talk to (or it tried and failed). Or the system overwrote DNS right back via NetworkManager/DHCP.

Mode B: DNS changes, but queries go out the wrong interface

Typical symptoms: the system shows VPN DNS server configured, but packet capture shows queries leaving the Wi‑Fi interface.

Why it happens: your route to the DNS server IP points to the physical gateway, not the tunnel; or policy routing sends UDP/53 out the default table.

Mode C: Queries go into the tunnel, server doesn’t answer

Typical symptoms: tcpdump shows outbound DNS packets on wg0, but no replies.

Why it happens: firewall rules, missing NAT on the VPN server, DNS server ACL not allowing your tunnel subnet, or wrong source IP because of policy routing.

Mode D: Split tunnel expectations without split DNS

Typical symptoms: everything becomes slow or breaks when you set VPN DNS globally; SaaS logins fail; internal works but external feels haunted.

Why it happens: you pointed all DNS to a corporate resolver that blocks or filters external domains, or it’s only reachable through the tunnel and you’re split-tunneling traffic.

Mode E: MTU/fragmentation causes “random” DNS failures

Typical symptoms: small queries work, big ones fail; internal zones with lots of records fail; DNSSEC causes intermittent pain.

Why it happens: UDP fragmentation or PMTU black holes across the tunnel path; WireGuard adds overhead; your MTU is too optimistic.

Joke #1: DNS is like office gossip—fast, unreliable, and it always takes the most dramatic route possible.

Practical tasks (with commands, outputs, and decisions)

These are real checks you can run in prod and on laptops. Each task includes: command, representative output, what it means, and what you decide next. Run them in order of suspicion, not in order of vanity.

Task 1 (Linux): Verify WireGuard interface state and peer handshake

cr0x@server:~$ sudo wg show
interface: wg0
  public key: O3bY...redacted...
  private key: (hidden)
  listening port: 51820

peer: q7m2...redacted...
  endpoint: 203.0.113.10:51820
  allowed ips: 10.50.0.0/24, 10.60.0.0/16
  latest handshake: 36 seconds ago
  transfer: 28.31 MiB received, 51.22 MiB sent
  persistent keepalive: every 25 seconds

What it means: Handshake is fresh; traffic flows. DNS issues are now likely resolver/routing, not crypto.

Decision: If handshake is “never,” fix connectivity/keys/endpoint before touching DNS.

Task 2 (Linux): Confirm you can reach the VPN DNS server by IP

cr0x@server:~$ ping -c 2 10.50.0.53
PING 10.50.0.53 (10.50.0.53) 56(84) bytes of data.
64 bytes from 10.50.0.53: icmp_seq=1 ttl=63 time=22.1 ms
64 bytes from 10.50.0.53: icmp_seq=2 ttl=63 time=21.7 ms

--- 10.50.0.53 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 21.741/21.913/22.086/0.172 ms

What it means: Layer 3 path exists to DNS server. If DNS still fails, suspect UDP/53 filtering, resolver config, or split DNS policy.

Decision: If ping fails, check routing/AllowedIPs/firewall first.

Task 3 (Linux): Query the DNS server directly (bypass local resolver)

cr0x@server:~$ dig @10.50.0.53 internal.service.corp A +time=2 +tries=1

; <<>> DiG 9.18.24 <<>> @10.50.0.53 internal.service.corp A +time=2 +tries=1
; (1 server found)
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22141
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; ANSWER SECTION:
internal.service.corp. 60 IN A 10.60.12.34

;; Query time: 28 msec
;; SERVER: 10.50.0.53#53(10.50.0.53) (UDP)
;; WHEN: Sat Dec 27 10:12:11 UTC 2025
;; MSG SIZE  rcvd: 74

What it means: The VPN DNS server works. If normal resolution fails, your OS is not using it (or routing to it is wrong for normal queries).

Decision: Move up the stack: resolver manager config, per-link DNS, and route/policy checks.

Task 4 (Linux): See what resolver your system is using

cr0x@server:~$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 39 Nov 12 09:01 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf

What it means: You’re on systemd-resolved stub resolver. Your real DNS servers are managed elsewhere.

Decision: Use resolvectl to inspect per-interface DNS; don’t “fix” DNS by editing the stub file.

Task 5 (Linux): Inspect per-link DNS with systemd-resolved

cr0x@server:~$ resolvectl status
Global
       Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub

Link 2 (eth0)
    Current Scopes: DNS
         Protocols: +DefaultRoute
Current DNS Server: 192.168.1.1
       DNS Servers: 192.168.1.1

Link 7 (wg0)
    Current Scopes: DNS
         Protocols: -DefaultRoute
       DNS Servers: 10.50.0.53
        DNS Domain: ~corp

What it means: Split DNS is configured: ~corp goes to 10.50.0.53. Everything else goes via eth0.

Decision: If wg0 has no DNS server or no domain route, fix your wg-quick PostUp or your NetworkManager profile.

Task 6 (Linux): Verify routing to the DNS server IP uses wg0

cr0x@server:~$ ip route get 10.50.0.53
10.50.0.53 dev wg0 src 10.50.0.2 uid 1000
    cache

What it means: The kernel will route traffic to the DNS server through the tunnel interface.

Decision: If it shows dev eth0, you have a route leak: add the DNS server IP to AllowedIPs and/or fix policy routing.

Task 7 (Linux): Confirm which DNS server your libc is asking (quick smoke test)

cr0x@server:~$ getent hosts internal.service.corp
10.60.12.34     internal.service.corp

What it means: Your system-level resolver path can resolve the name. This is closer to “what apps will do” than dig in many environments.

Decision: If getent fails but dig @10.50.0.53 works, the OS resolver integration is wrong.

Task 8 (Linux): Capture DNS traffic on wg0 and on the physical interface

cr0x@server:~$ sudo tcpdump -ni wg0 udp port 53 -c 5
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wg0, link-type RAW (Raw IP), snapshot length 262144 bytes
10:13:02.115693 IP 10.50.0.2.53244 > 10.50.0.53.53: 41249+ A? internal.service.corp. (39)
10:13:02.141220 IP 10.50.0.53.53 > 10.50.0.2.53244: 41249 1/0/0 A 10.60.12.34 (55)

What it means: DNS queries and replies traverse wg0. Routing is correct, and the server answers.

Decision: If you see queries on eth0 instead, fix routes; if you see queries on wg0 but no replies, check firewall/ACL/NAT on the far end.

Task 9 (Linux): Check if wg-quick installed special routing rules (policy routing)

cr0x@server:~$ ip rule show
0:      from all lookup local
32764:  from all lookup main
32765:  from all lookup default

What it means: No extra policy routing rules are present. That’s fine for simple “route by destination” setups, but split tunneling sometimes needs more.

Decision: If you’re trying to do advanced split routing (like “only DNS via VPN”), consider explicit rules or per-link DNS routing via systemd-resolved.

Task 10 (Linux): Look for MTU issues that can break larger DNS replies

cr0x@server:~$ ip link show dev wg0
7: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/none

What it means: MTU is 1420, a common safe-ish value. If you’re on PPPoE, LTE, or weird clouds, you might need lower.

Decision: If big DNS replies fail, test with a smaller MTU (e.g., 1380) and re-check behavior.

Task 11 (Windows): Verify adapter DNS settings via PowerShell

cr0x@server:~$ powershell -NoProfile -Command "Get-DnsClientServerAddress -AddressFamily IPv4 | Format-Table -AutoSize"
InterfaceAlias        ServerAddresses
--------------        ---------------
Wi-Fi                 {192.168.1.1}
WireGuard Tunnel       {10.50.0.53}

What it means: The WireGuard adapter has the DNS server set. That’s necessary, not sufficient.

Decision: If the WireGuard adapter has no DNS server, fix the WireGuard Windows profile (or use NRPT for split DNS).

Task 12 (Windows): See which DNS server Windows actually used for a query

cr0x@server:~$ powershell -NoProfile -Command "Resolve-DnsName internal.service.corp -Type A -DnsOnly | Format-List"
Name       : internal.service.corp
Type       : A
TTL        : 60
Section    : Answer
IPAddress  : 10.60.12.34
Server     : 10.50.0.53

What it means: Windows queried the expected DNS server.

Decision: If Server shows your Wi‑Fi router or a public DNS, you have interface metric/NRPT/suffix issues.

Task 13 (Windows): Check interface metrics that influence DNS selection

cr0x@server:~$ powershell -NoProfile -Command "Get-NetIPInterface -AddressFamily IPv4 | Sort-Object InterfaceMetric | Select-Object -First 6 | Format-Table -AutoSize"
ifIndex InterfaceAlias     AddressFamily NlMtu(Bytes) InterfaceMetric Dhcp
------- --------------     ------------- ------------ --------------- ----
25      WireGuard Tunnel   IPv4          1420         5               Disabled
12      Wi-Fi              IPv4          1500         35              Enabled

What it means: Lower metric tends to win. Here, the WireGuard interface is preferred, which is often what you want for full-tunnel DNS.

Decision: If WireGuard metric is higher and you expect VPN DNS to be used, lower it—or use NRPT so domain routing doesn’t depend on metrics.

Task 14 (Windows): Inspect NRPT rules (split DNS sanity)

cr0x@server:~$ powershell -NoProfile -Command "Get-DnsClientNrptRule | Select-Object Namespace,NameServers,DirectAccessDnsServers | Format-Table -AutoSize"
Namespace   NameServers
---------   ----------
.corp       {10.50.0.53}

What it means: Queries for *.corp are directed to the VPN DNS server regardless of default DNS behavior.

Decision: If you need split DNS in Windows, NRPT is the grown-up approach. Configure it intentionally and test with Resolve-DnsName.

Correct DNS setup on Linux

Linux DNS configuration isn’t one system. It’s an ecosystem of polite suggestions and dueling daemons. Your goal is to pick one authority and make it deterministic.

Linux reality check: choose your resolver path

In practice you’re in one of these camps:

  • systemd-resolved (common on Ubuntu, Debian variants, Fedora, etc.). Best option for split DNS when used correctly.
  • NetworkManager managing DNS (often integrates with resolved; sometimes it doesn’t).
  • Openresolv/resolvconf writing /etc/resolv.conf. Still common in minimal servers and containers.
  • Static /etc/resolv.conf. Great until you roam networks; then it becomes a crime scene.

What wg-quick actually does with DNS=

wg-quick is a shell script. When you put DNS=10.50.0.53 in the config, it tries to call tools like resolvconf or interact with resolved via resolvectl depending on distro. If those tools aren’t present, DNS may simply not change.

Opinion: don’t rely on “maybe” integration. For laptops, integrate with your platform’s network manager or resolved explicitly. For servers, prefer a deterministic resolver config that survives reboots and service restarts.

Correct setups (pick one)

Setup 1: systemd-resolved split DNS (recommended for corp domains)

This is what you want if your requirement is: internal domains resolve via VPN DNS; everything else resolves via local network (or public resolver).

WireGuard itself can’t do “domain-based AllowedIPs.” But resolved can do domain-based DNS server selection. You combine them: route the DNS server IP into the tunnel, and teach resolved which domain goes to it.

WireGuard config example:

cr0x@server:~$ sudo sed -n '1,120p' /etc/wireguard/wg0.conf
[Interface]
Address = 10.50.0.2/24
PrivateKey = (hidden)
ListenPort = 51820

PostUp = resolvectl dns %i 10.50.0.53; resolvectl domain %i ~corp
PostDown = resolvectl revert %i

[Peer]
PublicKey = (redacted)
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.50.0.0/24, 10.60.0.0/16, 10.50.0.53/32
PersistentKeepalive = 25

What matters here:

  • AllowedIPs includes the internal networks you actually need and the DNS server IP (10.50.0.53/32) so the query path is unambiguous.
  • resolvectl dns %i sets link DNS on interface wg0.
  • resolvectl domain %i ~corp makes it “routing-only” for that domain, not a global default route.
  • revert cleans up on down. You want cleanup. Future-you wants cleanup even more.

Test it: after bringing up the interface, resolvectl status should show wg0 with DNS Domain: ~corp. Then verify traffic with tcpdump.

Setup 2: systemd-resolved full-tunnel DNS (simple, sometimes correct)

If you want: “when VPN is up, all DNS queries go to VPN DNS,” then you can set wg0 as default route for DNS by not using a routing-only domain. You can still keep split tunnel for traffic, but be careful: if your DNS server is only reachable via VPN and your DNS is global, you just made DNS a hard dependency for everything.

In resolved terms: set the DNS server on wg0 and allow it to be a default DNS route (or set global DNS). The cleanest approach varies by distro integration, but a reliable pattern is still PostUp/Down with resolvectl, just without the ~corp domain route. You may set a “.” domain route, but that can be too aggressive.

Opinion: full-tunnel DNS on laptops often creates weird SaaS failures when corporate resolvers filter. Use split DNS when you can.

Setup 3: resolvconf/openresolv (common on small servers)

If your system doesn’t use resolved, wg-quick with DNS= can work if resolvconf exists. If it doesn’t, you’ll watch DNS never change and wonder why you’re paying for electricity.

Install and verify tooling:

cr0x@server:~$ command -v resolvconf || echo "resolvconf not found"
resolvconf not found

Decision: If you want to rely on wg-quick DNS, install resolvconf/openresolv. If you want deterministic behavior, manage /etc/resolv.conf yourself or move to resolved.

Example wg-quick config using DNS=:

cr0x@server:~$ sudo sed -n '1,80p' /etc/wireguard/wg0.conf
[Interface]
Address = 10.50.0.2/24
PrivateKey = (hidden)
DNS = 10.50.0.53

[Peer]
PublicKey = (redacted)
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.50.0.0/24, 10.60.0.0/16, 10.50.0.53/32
PersistentKeepalive = 25

Linux gotchas that cause “DNS doesn’t work”

  • Stub resolv.conf confusion: editing /etc/resolv.conf when it’s a symlink to a stub won’t do what you think.
  • NetworkManager fights you: it may overwrite DNS per connection. Decide whether NM owns DNS; if yes, configure it there.
  • Local caching resolvers: unbound or dnsmasq may be running and forwarding to the wrong place.
  • VPN DNS reachable only through VPN: if route to DNS server isn’t in AllowedIPs, the system will try to reach it locally and fail.
  • Firewall output rules: local firewall can block UDP/53 on wg0 because someone thought that was “hardening.”

Correct DNS setup on Windows

Windows is simultaneously better and worse here. Better because it has first-class policy controls (NRPT). Worse because it will “help” you with interface metrics, DNS suffix search lists, and caching behavior that looks like gaslighting.

What the WireGuard Windows client really changes

The WireGuard Windows app creates a virtual adapter. When you set DNS in the tunnel profile, it configures that adapter’s DNS servers. Whether Windows uses that DNS for a given query depends on:

  • Which interface is chosen for name resolution (metrics and binding order).
  • Whether you’re using split DNS via NRPT.
  • Suffix search lists and which names are considered “in-domain.”
  • Whether the query is performed by an app using system DNS APIs (most do) or shipping its own resolver (some do, and those are a special joy).

Two sane patterns on Windows

Pattern 1: Full-tunnel DNS (simple)

If you want “all DNS goes to VPN DNS,” do this:

  • Set the WireGuard adapter DNS server(s) in the profile.
  • Ensure the WireGuard adapter has a low interface metric so it’s preferred.
  • Ensure the VPN DNS server IP is routed through the tunnel (AllowedIPs includes it).

This is the least moving parts. It’s also the most likely to annoy users if corporate DNS blocks external lookups or returns “helpful” walled-garden answers.

Pattern 2: Split DNS with NRPT (recommended for corp domains)

If you want “only corp zones use VPN DNS,” use NRPT. NRPT can target namespaces (domains) and force specific resolvers for those names. It removes the dependency on interface metrics and reduces random behavior when Wi‑Fi reconnects.

Create an NRPT rule (example):

cr0x@server:~$ powershell -NoProfile -Command "Add-DnsClientNrptRule -Namespace '.corp' -NameServers '10.50.0.53'"

Verify it:

cr0x@server:~$ powershell -NoProfile -Command "Get-DnsClientNrptRule | Where-Object Namespace -eq '.corp' | Format-List"
Namespace   : .corp
NameServers : {10.50.0.53}

Test resolution and confirm which server answered:

cr0x@server:~$ powershell -NoProfile -Command "Resolve-DnsName internal.service.corp -Type A -DnsOnly | Select-Object Name,IPAddress,Server | Format-Table -AutoSize"
Name                 IPAddress    Server
----                 ---------    ------
internal.service.corp 10.60.12.34  10.50.0.53

Decision: If this works reliably, keep it. If users need multiple internal namespaces, add them explicitly. Don’t try to be clever with wildcards; you’ll lose.

Windows-specific failure modes

  • Interface metric wars: Windows may decide Wi‑Fi is “better” and use its DNS for everything. Lower the WireGuard metric or use NRPT.
  • Multi-homed name resolution: Windows can try multiple interfaces; results can be inconsistent when one DNS server returns NXDOMAIN and the other returns an answer.
  • Suffix search list mismatch: Users type internal and expect internal.corp. Without the right suffix, they get nothing (or worse, public DNS results).
  • Apps that ignore system DNS: Some browsers and agents do their own DNS (DoH). Your VPN DNS settings won’t matter until you disable or manage that behavior.

Joke #2: Windows DNS troubleshooting is a great career move because it guarantees job security—mostly for whoever inherits your machine after the reboot.

Common mistakes: symptoms → root cause → fix

These are the ones I see over and over. They waste hours because they “look” like DNS while actually being routing or resolver selection.

1) “Handshake is up, but internal names don’t resolve”

Symptom: wg show looks good. ping 10.60.12.34 works. getent hosts internal.service.corp fails.

Root cause: OS isn’t using the VPN DNS server; wg-quick DNS integration didn’t apply.

Fix: On Linux, configure systemd-resolved via resolvectl dns/domain hooks or configure NetworkManager. On Windows, set adapter DNS or use NRPT.

2) “DNS server is set to 10.50.0.53, but it times out”

Symptom: Resolver points to VPN DNS, but dig shows timeouts.

Root cause: Route to DNS server IP is not via wg0; AllowedIPs missing the DNS server address or the internal subnet containing it.

Fix: Add 10.50.0.53/32 (or the DNS subnet) to AllowedIPs. Verify with ip route get 10.50.0.53.

3) “Only big DNS answers fail (or only some domains)”

Symptom: Small lookups work, DNSSEC-heavy zones fail, or TXT queries die.

Root cause: MTU/fragmentation/PMTU issues in the tunnel path.

Fix: Lower WireGuard MTU (e.g., 1380) and retest. If needed, force TCP DNS for troubleshooting with tools, and inspect packet captures for fragments.

4) “External browsing breaks when VPN is connected, but internal works”

Symptom: VPN up; corp apps work; public sites stall or resolve to odd addresses.

Root cause: You set corporate DNS globally, and that resolver filters or hijacks external lookups; or external DNS queries now hairpin through VPN and get rate-limited.

Fix: Use split DNS (resolved routing domains on Linux; NRPT on Windows). Keep public DNS local or use a VPN-accessible recursive resolver designed for it.

5) “It works on CLI tools but not in browsers”

Symptom: dig and getent work, but the browser can’t load internal hostnames.

Root cause: Browser uses DNS-over-HTTPS or its own resolver; it bypasses OS resolver selection.

Fix: Disable DoH for corporate environments or configure enterprise policies so internal domains resolve via system DNS.

6) “It worked yesterday, now it doesn’t, and reboot ‘fixes’ it”

Symptom: Intermittent resolution; reboot clears it.

Root cause: Caching resolver state mismatch (systemd-resolved cache, Windows DNS cache) or interface bouncing causing wrong DNS server selection.

Fix: Flush caches as a diagnostic; then fix deterministic DNS selection (per-link DNS, NRPT, proper metrics). Stop treating reboot as a solution.

Three corporate mini-stories from the trenches

Mini-story 1: The incident caused by a wrong assumption

They rolled out WireGuard to replace an aging remote-access VPN. The migration plan was clean: the tunnel comes up, users get access to internal services, done. The team even tested connectivity with pings and a couple of curl requests to IPs. Everything looked green.

Monday morning arrived and ticket volume spiked. “VPN connected but nothing works.” The on-call checked the WireGuard server. Handshakes were happening. Traffic counters moved. The tunnel wasn’t down; it was indifferent.

The assumption was the killer: someone believed that adding DNS=10.50.0.53 to the client config meant the OS would reliably switch resolvers. On some Linux laptops, wg-quick couldn’t talk to the local resolver setup. On Windows, the adapter had DNS set, but name resolution still preferred Wi‑Fi DNS because the interface metric flipped after a driver update.

They fixed it by making DNS selection explicit: systemd-resolved per-link config on Linux, NRPT rules on Windows for internal namespaces. The postmortem wasn’t about WireGuard; it was about “we tested connectivity but didn’t test name resolution like an application.” That’s the real lesson. Apps don’t connect to IPs you paste into Slack; they connect to names.

Mini-story 2: The optimization that backfired

A security team wanted to reduce DNS leaks. Reasonable goal. They decided: “force all DNS through the VPN.” They pointed all clients to a corporate recursive resolver reachable only inside the tunnel, and they considered the job done.

The first problem: latency. Remote staff on distant networks now sent every DNS query across the tunnel to a resolver that wasn’t tuned for global users. Browsing felt sluggish, because modern web pages trigger a small stampede of DNS lookups. The second problem: reliability. The resolver did occasional filtering, and when it was slow, everything was slow.

Then the real backfire: some SaaS authentication flows broke intermittently. Not because the SaaS was down, but because the corporate resolver’s filtering and caching behavior didn’t match public DNS. A few edge CDNs returned different answers than expected, and apps behaved differently. The team had “optimized” for leak prevention and created a single point of failure for the entire browsing experience.

The fix was more boring: split DNS for internal zones, plus sensible egress rules for DNS leak prevention (blocking direct UDP/53 to the internet on managed devices where appropriate, while allowing DoH policies for public). The corporate resolver stayed in the loop only for domains it actually owned. Users stopped complaining, and the security team still got the control they wanted—without turning DNS into a choke point.

Mini-story 3: The boring but correct practice that saved the day

A different organization did something unglamorous: they wrote a one-page “VPN DNS contract.” It specified: internal namespaces, authoritative resolvers, recursion behavior, expected response sizes, and exactly how Linux and Windows clients should route those queries when connected. No hand-waving. No “should.”

When they deployed WireGuard, they didn’t just ship configs. They shipped tests. A small script validated: tunnel up, route to DNS server via wg, direct dig @dns works, system resolver uses the right server for corp domains, and packet capture shows no leakage for internal domains.

Months later, a network change introduced a new internal zone with larger DNS responses. Some users started seeing intermittent resolution failures for that zone only. Because they had a contract and a test suite, the on-call quickly reproduced it and saw fragmentation. The MTU was fine for most traffic but marginal for EDNS-heavy replies.

The resolution was mechanical: adjust MTU and, for the internal recursive resolvers, tune response behavior to avoid oversized UDP answers where possible. The “boring practice” wasn’t the MTU tweak—it was the fact they could detect and localize the failure in minutes. No heroics. No all-hands call. Just disciplined, observable behavior.

Checklists / step-by-step plan

Checklist A: Full-tunnel DNS that actually works (Linux + Windows)

  1. Pick a VPN DNS server that is reachable from the tunnel subnet and allows recursion from it.
  2. Add the DNS server IP to AllowedIPs (at least as /32) so routing is deterministic.
  3. Linux:
    • If using systemd-resolved, set wg link DNS via resolvectl dns wg0 ... and ensure it can be used for default.
    • If not using resolved, ensure resolvconf exists or manage /etc/resolv.conf explicitly.
  4. Windows:
    • Set WireGuard adapter DNS in the tunnel profile.
    • Ensure interface metric prefers WireGuard if you want it to be default.
  5. Verify with packet capture that DNS queries go into the tunnel.
  6. Verify with Resolve-DnsName / getent that apps see correct results.

Checklist B: Split DNS for corporate zones (recommended)

  1. Identify internal namespaces (corp, internal.corp, etc.). Be explicit.
  2. Linux: configure systemd-resolved routing domains on wg0:
    • resolvectl dns wg0 10.50.0.53
    • resolvectl domain wg0 ~corp (and additional namespaces as needed)
  3. Windows: create NRPT rules per namespace:
    • Add-DnsClientNrptRule -Namespace '.corp' -NameServers '10.50.0.53'
  4. Keep public DNS on local network or a dedicated public resolver, not the corporate internal resolver.
  5. Test:
    • Internal name resolves via VPN DNS.
    • Public name resolves without going through VPN (unless you intentionally want otherwise).

Checklist C: When “DNS not working” is really MTU

  1. Reproduce with a “big” record (TXT, DNSSEC, or an internal name with many A/AAAA records).
  2. Capture DNS packets and look for missing responses or fragments.
  3. Lower MTU on wg interface (try 1380) and retest.
  4. If lowering MTU fixes it, keep the smaller MTU and document why. Also check upstream path MTU if you can.

FAQ

1) Why does WireGuard connect but DNS fails?

Because the tunnel can be healthy while your OS keeps using the old DNS servers, or routes DNS queries outside the tunnel. WireGuard doesn’t inherently “own” DNS; it just moves packets.

2) Is DNS= in wg-quick enough?

Sometimes. It depends on whether your system has the resolver integration tooling wg-quick expects. On systemd-resolved systems, explicit resolvectl hooks are more deterministic.

3) Why does dig @10.50.0.53 work but normal lookups fail?

Because direct dig bypasses your OS resolver selection and routing quirks. Your system resolver is still pointed elsewhere, or it’s applying split DNS rules you didn’t intend.

4) Do I need to add the DNS server IP to AllowedIPs?

If the DNS server lives behind the tunnel, yes. Add it explicitly as /32 if needed. Otherwise your system may try to reach it via the default route and time out.

5) What’s the cleanest way to do split DNS on Linux?

systemd-resolved routing domains: set DNS on wg0 and assign ~corp domains to it. This avoids global DNS changes and keeps external resolution local.

6) What’s the cleanest way to do split DNS on Windows?

NRPT rules per namespace. They force the DNS server for those domains, regardless of interface metrics or Wi‑Fi reconnections.

7) Why do some apps still leak DNS even if I configured VPN DNS?

Some apps use DNS-over-HTTPS or custom resolvers and bypass OS DNS settings. Fixing that is app policy management, not WireGuard configuration.

8) How do I tell if DNS queries are leaving outside the tunnel?

Capture packets on both interfaces. If you see UDP/53 on Wi‑Fi/eth0 instead of wg0, it’s routing/policy. Don’t guess; observe.

9) Why is DNS slower over VPN even when it “works”?

Latency, resolver location, and caching behavior. A corporate resolver far from the user turns every web page into a latency tax. Split DNS reduces that tax.

10) Should I run a local caching resolver on the client?

It can help performance, but it adds another layer that can cache the wrong upstream choice. If you do it, configure upstream selection carefully and test split DNS behavior.

Conclusion: next steps that actually stick

If you’re stuck in “WireGuard DNS not working” land, stop poking at random knobs and follow a disciplined path:

  1. Prove L3 connectivity first. Handshake and IP reachability to the DNS server are your base facts.
  2. Observe where DNS packets go. Packet capture will end debates instantly.
  3. Make DNS selection deterministic. On Linux, prefer systemd-resolved per-link DNS with routing domains for split DNS. On Windows, use NRPT for split DNS and metrics only when you truly want “default everything.”
  4. Route the DNS server through the tunnel. Include it in AllowedIPs so it doesn’t accidentally hairpin via the local gateway.
  5. Document the intended behavior. Full-tunnel DNS and split DNS are different products. Pick one and write it down.

Do those, and DNS stops being “mysteriously broken.” It becomes a system with inputs, outputs, and a paper trail. Which is the only kind of magic operations teams should tolerate.

← Previous
WordPress “max_input_vars”: Why Menus and Forms Break, and How to Fix It
Next →
Docker Restart Policies: Stop Creating Infinite Crash Loops

Leave a comment