Why split DNS between interfaces?
When a machine plugs into a VPN it usually needs to look up hostnames that live only inside that tunnel.
If you let the whole system talk to the same nameserver, two things can go wrong:
- The VPN’s DNS might not be reachable until the tunnel is up, so internal lookups fail.
- Public resolvers get handed internal queries, leaking sensitive names to the outside world.
Binding a dedicated DNS server to the VPN link keeps the resolver stack tidy, cuts the attack surface, and stops accidental leaks.
Systemd‑resolved makes this painless. Below is a hands‑on walk‑through that shows how to give a VPN interface its own DNS server while the rest of the machine continues to use the normal public resolver.
Prerequisites
- A recent distro that ships with systemd‑resolved (almost every mainstream one).
- A VPN client that creates its own link (OpenVPN, WireGuard, NetworkManager‑VPN, etc.).
- Root or
sudoaccess.
1. Find the VPN interface
After you connect, just list the tun/tap/wg devices:
ip link show | grep -E 'tun|tap|wg'
Typical names you’ll see:
| Device | Client |
|---|---|
tun0 |
OpenVPN |
wg0 |
WireGuard |
vpn0 |
NetworkManager |
Write that name down – you’ll need it later.
2. Make sure systemd‑resolved is running
systemctl status systemd-resolved
If it’s not active, bring it up:
sudo systemctl enable --now systemd-resolved
Check what the global resolver is using:
resolvectl status
You’ll see a “Global” section with the nameservers that the system uses by default.
3. Drop a per‑link config
Systemd‑resolved reads per‑link settings from /etc/systemd/resolved.conf.d/.
Create a drop‑in for the VPN link:
sudo mkdir -p /etc/systemd/resolved.conf.d
sudo nano /etc/systemd/resolved.conf.d/vpn.conf
Paste:
[Link]
Name=wg0
DNS=10.0.0.1
Domains=~.vpn.internal
Name=must match the interface name exactly. If you want to cover several similar interfaces, use a pattern (Name=wg*).DNS=is the VPN’s nameserver.Domains=limits that DNS to the specified domain(s). The~prefix tells systemd‑resolved to use it only for queries that match the domain. OmitDomains=if you want a full split‑tunnel (every query on that link goes to the VPN DNS).
Security note – Pointing the VPN link to its own DNS prevents leaks that could expose internal hostnames to the public resolver. It also keeps all DNS traffic encrypted inside the tunnel.
4. Reload systemd‑resolved
sudo systemctl restart systemd-resolved
Verify the new per‑link settings:
resolvectl status wg0
You should see the DNS and Domains you just set.
5. Test the split
5.1 Public domain
dig +short example.com @127.0.0.53
You should get a reply from your normal resolver (ISP, 1.1.1.1, etc.).
5.2 Internal domain
dig +short host.vpn.internal @127.0.0.53
The answer must come from 10.0.0.1. If it times out, the VPN isn’t up or the DNS server is unreachable.
6. Multiple VPNs
If you run more than one VPN, just drop another file:
# /etc/systemd/resolved.conf.d/openvpn.conf
[Link]
Name=tun0
DNS=10.8.0.1
Domains=~.openvpn.internal
Systemd‑resolved merges these per‑link settings. The global resolver stays untouched.
7. Keep the default route from leaking DNS
A full tunnel VPN pushes all traffic (including DNS) through the tunnel. If you want a split tunnel—only VPN traffic goes through the tunnel—make sure the system’s default DNS remains the public resolver.
Some VPN clients automatically replace /etc/resolv.conf with the VPN’s nameserver. To keep systemd‑resolved in charge:
-
Disable the client’s DNS injection.
OpenVPN: addpull-filter ignore redirect-gatewayandpull-filter ignore dhcp-optionto the client config. -
Make sure
/etc/resolv.confpoints to the systemd stub:sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
dig host.vpn.internal times out |
VPN not up or DNS unreachable | Check ip addr show wg0 and ping the DNS server. |
| Public DNS goes through VPN | VPN client overrides /etc/resolv.conf |
Re‑enable the systemd stub and disable client DNS injection. |
resolvectl status shows no per‑link config |
Drop‑in file missing or syntax error | Inspect /etc/systemd/resolved.conf.d/ and run systemd-analyze verify. |
| DNS fails after reconnecting VPN | Interface name changed | Use a pattern (Name=wg*) or pin the name in /etc/systemd/network/. |
9. Trade‑offs and alternatives
| Approach | Pros | Cons |
|---|---|---|
| systemd‑resolved per‑link | Native, no extra daemon, fine‑grained | Needs systemd (not on very old distros) |
| NetworkManager DNS | GUI friendly, works with many plugins | Can override systemd‑resolved; less transparent |
| dnsmasq + custom routing | Full cache & filtering control | Extra service, more maintenance |
| iptables DNAT for DNS | Works on any kernel | Complex, can clash with other rules |
For most setups the per‑link method is the cleanest and most secure.
10. Security checklist
- Validate the VPN DNS – Make sure it’s trustworthy. If the VPN supports DoH, use that instead of plain UDP.
- Disable plain UDP DNS on the VPN link – If you’re using DoH, set
DNS=noneand run a DoH client; this eliminates spoofing risk. - Monitor DNS traffic –
tcpdump -i wg0 port 53will show you exactly which server is queried. - Keep systemd‑resolved updated – Regular patches. On Debian/Ubuntu:
sudo apt update && sudo apt upgrade systemd.
11. Real‑world use cases
- Enterprise VPN – Employees need to resolve internal services without leaking names to the ISP. Split DNS keeps those queries private.
- Home lab with multiple VPNs – A dev box might use a corporate VPN for source control and another for cloud resources. Per‑link DNS keeps each environment isolated.
- Privacy‑focused users – Forcing internal queries through the VPN’s nameserver stops your ISP from seeing those hostnames.
12. Quick reference
# Create per‑link config
sudo mkdir -p /etc/systemd/resolved.conf.d
sudo tee /etc/systemd/resolved.conf.d/vpn.conf <<EOF
[Link]
Name=wg0
DNS=10.0.0.1
Domains=~.vpn.internal
EOF
# Reload systemd‑resolved
sudo systemctl restart systemd-resolved
# Verify
resolvectl status wg0
See also
- Recovering from emergency mode after a bad /etc/fstab entry on Ubuntu 24.04: a single‑command root shell solution
- How to Give the Backup User Read‑Only Access to /var/log via ACLs Without Granting sudo
- Rootless Podman on a Raspberry Pi – Solving the “Cannot Bind Mount /home/user/data” Permission Issue
- Finding the Most Frequent Syslog Lines in /var/log/syslog with awk, sort, and head
- Preventing Disk Space Woes on a Home NAS by Configuring Logrotate for Docker Container Logs