Setup

VPN Not Working on Linux: systemd-resolved, Split DNS and the Leak Nobody Notices (2026)

9 min read

Linux is the platform where a VPN most often works perfectly and still does the wrong thing. The tunnel comes up, the routes are correct, traffic flows — and every name you look up goes to your provider's resolver, because DNS on a modern Ubuntu is not a file any more. It is a per-interface routing problem, and it needs to be diagnosed as one.

Everything below applies to systems using systemd-resolved, which means Ubuntu 18.04 onwards, including 24.04 and 26.04 LTS, and most current Fedora and Debian desktop installs.

Stop reading /etc/resolv.conf. It has not been the answer for years

On these systems /etc/resolv.conf is a symlink to a stub, and it says 127.0.0.53 no matter what is really happening. Every guide that tells you to edit it is describing a machine from 2015. The two commands that do tell you the truth:

  1. 1resolvectl status — resolvers per interface, plus which domains each one is authoritative for. This is the whole picture on one screen.
  2. 2resolvectl query example.com — shows which interface answered a specific lookup. If it names your Ethernet or Wi-Fi device while the VPN is up, you have found the leak.
  3. 3resolvectl statistics — useful only to confirm that cached answers, not the network, are what you are actually testing.
The line to look for in resolvectl status is DNS Domain: ~. on the tunnel interface. That single dot means “send everything here”. Without it the tunnel resolver only handles the domains explicitly routed to it, and everything else goes to whichever interface has one — which is your ISP.

Split DNS: the design, and where it goes wrong

systemd-resolved deliberately scopes resolvers to interfaces so a corporate VPN can answer for internal.company while everything else keeps using your normal resolver. That is a feature, and it is precisely what makes a privacy VPN leak: unless the client claims the whole namespace with ~., the split works exactly as designed and against you.

  1. 1Check what the tunnel claimed: resolvectl domain.
  2. 2Claim everything for the tunnel by hand while testing: sudo resolvectl domain tun0 '~.' — substituting your interface, which ip -brief link will name.
  3. 3Point the tunnel interface at the VPN's resolver: sudo resolvectl dns tun0 10.8.0.1.
  4. 4Re-test with resolvectl query example.com and confirm the answering interface changed.

Both of those are runtime settings and vanish on reconnect. If they fix it, the durable answer is a client that integrates with NetworkManager properly, or an OpenVPN setup using update-systemd-resolved with dhcp-option DOMAIN-ROUTE . so the same thing happens automatically.

The leak that appears after you disconnect

A less obvious failure, and a genuinely dangerous one. A client that exits uncleanly — crash, kill, suspend — leaves its per-interface DNS assignment behind. The tunnel is gone. The resolver entry pointing into it is not. Lookups then go to an address that no longer routes anywhere, which reads as “the internet is broken” and gets fixed by rebooting, so nobody ever learns what happened.

  1. 1resolvectl status and look for an interface that no longer exists, or a tunnel device with stale servers.
  2. 2Clear it: sudo resolvectl revert tun0, or sudo resolvectl flush-caches for cached answers only.
  3. 3As a last resort sudo systemctl restart systemd-resolved, which rebuilds every scope from scratch.

WireGuard: reading the handshake instead of guessing

WireGuard tells you exactly what is wrong if you ask it. sudo wg show prints a latest handshake line per peer, and its absence or age is the entire diagnosis.

  • No handshake at all — your packets are not reaching the peer, or its replies are not coming back. Firewall, wrong endpoint, or a network that drops UDP.
  • A handshake, then nothing, then silence — usually MTU. The handshake is small and fits; real traffic does not.
  • Transfer counters rising on sent only — you are talking, nobody is answering. Wrong key on the far side, or an endpoint that moved.
  • Handshakes every two minutes and traffic in both directions — WireGuard is healthy, and your problem is DNS or routing, not the tunnel.

When UDP turns out to be blocked outright — common on corporate and hotel networks — no WireGuard setting will help, because the protocol is the thing being filtered. The reasoning, and what to use instead, is in why QUIC and UDP get blocked and what VLESS Reality is.

Routing: which packets are actually going where

  1. 1ip route get 1.1.1.1 — the single most useful routing command on Linux. It tells you the interface a real packet would take, rather than making you interpret a table.
  2. 2ip -brief addr — confirms the tunnel got an address at all.
  3. 3ip rule list — WireGuard's wg-quick uses policy routing with fwmark, and a leftover rule from a previous session sends traffic to a tunnel that no longer exists.

A kill switch that survives a crash

Client-level kill switches stop working at exactly the moment they are needed, because they live inside the process that just died. A firewall rule does not. On a modern Ubuntu that means nftables, and the shape is simple: permit the tunnel interface and the VPN endpoint, refuse everything else on the physical one.

Two things to get right before you lock yourself out. Allow the VPN server's address and port explicitly, or the client can never reconnect. And allow DNS to the tunnel resolver only — a kill switch that blocks all DNS looks identical to a broken network, and you will spend an hour on the wrong problem. What such a switch does and does not protect is set out in the kill switch article.

Frequently asked

Why does my Linux VPN leak DNS even though the tunnel works?

Because systemd-resolved scopes resolvers per interface by design, and unless the VPN client claims the whole namespace the split works against you. Run resolvectl status and check whether the tunnel interface holds DNS Domain: ~. — the single dot means everything resolves through the tunnel. Without it, only explicitly routed domains do.

Why is editing /etc/resolv.conf useless on Ubuntu?

It is a symlink to a stub that always points at 127.0.0.53, which is systemd-resolved itself. Editing it changes nothing about which upstream server is used, and on the next network event it is regenerated anyway. Use resolvectl status and resolvectl dns instead.

The internet broke after my VPN crashed. Why?

The client left a per-interface DNS assignment behind, pointing at a resolver inside a tunnel that no longer exists. Lookups go nowhere and everything appears offline. sudo resolvectl revert tun0 clears it, or restart systemd-resolved to rebuild every scope.

What does “handshake did not complete” mean in WireGuard?

That the single UDP exchange never finished — a reachability problem, not a cryptographic one. Work through it in order: blocked UDP, wrong endpoint or port, mismatched keys, NAT, clock skew, MTU. sudo wg show narrows it immediately: no handshake at all points at the network, while a handshake followed by silence points at MTU.

Should I use NetworkManager or wg-quick?

NetworkManager if the desktop is what you use, because it integrates with systemd-resolved and cleans up after itself on disconnect — which is exactly the failure that leaves stale resolvers behind. wg-quick is fine on a server, where it is the only thing touching the routing table and nothing else needs to be told the tunnel came up.

Did this help?

Nothing follows from the answer; only we see it.

Send this to someone

Useful to anyone with the same problem.