Your /etc/resolv.conf Is a Liar (and That’s Fine)
You cat /etc/resolv.conf, see nameserver 127.0.0.53, and immediately think something’s broken. Maybe a previous sysadmin hacked it. Maybe it’s a VM artifact. Maybe you should just replace it with 8.8.8.8 and call it a day.
Don’t. That file is a symlink pointing to a stub resolver that’s quietly doing more useful work than most people ever configure it for. systemd-resolved has been the default DNS resolver on Ubuntu since 16.10, Fedora since forever, and it’s baked into any modern distro running systemd — which is basically all of them.
Here’s the thing: most people treat it like a middleman to disable. That’s backwards. Used right, it’s one of the better pieces of the systemd puzzle.
What systemd-resolved Actually Is
systemd-resolved is a caching stub resolver that runs as a system daemon (systemd-resolved.service). It listens on 127.0.0.53:53 and acts as the local DNS endpoint for everything on your machine.
That’s the stub part. “Stub” means it doesn’t actually resolve anything recursively — it forwards upstream to real DNS servers (your router, your ISP, Cloudflare, whatever) and caches the results locally.
What makes it interesting is everything layered on top:
- Per-link DNS routing — different interfaces can use different DNS servers
- Split DNS — VPN traffic goes to corporate DNS, everything else goes to your normal upstream
- DNSSEC validation — cryptographic verification of DNS responses
- DNS over TLS (DoT) — encrypted queries to upstream resolvers
- mDNS and LLMNR — local network name resolution (
.localhostnames, Windows network names)
The stub at 127.0.0.53 handles all of this transparently. Your application asks for example.com, the stub figures out which DNS server to ask based on what network interface you’re on, validates the response if DNSSEC is configured, and hands the answer back.
The /etc/resolv.conf Symlink, Explained
When systemd-resolved is running, /etc/resolv.conf is a symlink — usually to one of these:
/run/systemd/resolve/stub-resolv.conf → nameserver 127.0.0.53 (recommended)/run/systemd/resolve/resolv.conf → actual upstream DNS servers (bypass mode)The stub-resolv.conf version is what you want. It points everything at the stub, which handles routing. The resolv.conf version bypasses the stub entirely and talks directly to upstream — you lose caching, split DNS, and DNSSEC.
If your /etc/resolv.conf isn’t a symlink at all — maybe something wrote a static file over it — you can fix it:
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.confSome tools (Docker, NetworkManager, VPN clients) will overwrite this. That’s a common source of “why did my DNS break after I connected to the VPN” complaints.
resolvectl: Your New Best Friend
resolvectl is the CLI for interacting with systemd-resolved. If you’ve been using systemd-resolve (the old name), it still works — it’s just an alias.
Check Current Status
resolvectl statusGlobal Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupportedresolv.conf mode: stub DNS Servers: 192.168.1.1 DNS Domain: ~.
Link 2 (eth0) Current Scopes: DNS Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported DNS Servers: 192.168.1.1Read this output carefully. It tells you:
- What DNS servers are configured globally vs per-link
- Whether DNSSEC is active (and if it’s actually working or just “unsupported” by upstream)
- Whether DNS over TLS is on
- Which interface is the default route for DNS
Query DNS Directly
resolvectl query example.comresolvectl query -t MX gmail.comresolvectl query -t TXT _dmarc.example.comThis bypasses your local applications and asks systemd-resolved directly. Useful for confirming the resolver sees what you expect.
Flush the Cache
resolvectl flush-cachesSimple. Kills the cache. Use this when you’ve updated DNS records and dig still shows the old answer.
Set Per-Link DNS (Temporary)
resolvectl dns eth0 1.1.1.1 1.0.0.1resolvectl domain eth0 ~example.comThe ~ prefix on the domain means “route DNS queries for this domain over this link.” This is how split DNS works — more on that below.
Check Statistics
resolvectl statisticsTransactionsCurrent Transactions: 0 Total Transactions: 1423
Cache Current Cache Size: 87 Cache Hits: 934 Cache Misses: 489If your cache hit rate looks terrible, you’re either flushing too often or TTLs are very short (looking at you, AWS).
Split DNS: The Reason VPNs Break Your DNS
Split DNS is the killer feature most people don’t know they’re getting. The idea: route DNS for specific domains through specific interfaces.
When you connect to a corporate VPN, the VPN pushes a DNS server for corp.example.com. systemd-resolved picks this up automatically (if your VPN client is well-behaved) and routes queries for corp.example.com through the VPN interface, while everything else continues going to your normal DNS.
You can verify this with resolvectl status — look for the per-link DNS configuration and domain routing rules.
For manual split DNS (say, you’re managing a homelab with a separate DNS for .home.arpa):
resolvectl dns eth0 10.0.0.53resolvectl domain eth0 ~home.arpaNow all .home.arpa queries go to your local DNS server on that link. Queries for anything else still go to your regular upstream.
Configuring systemd-resolved Persistently
The main config file is /etc/systemd/resolved.conf. Drop-in files go in /etc/systemd/resolved.conf.d/.
[Resolve]DNS=1.1.1.1 1.0.0.1FallbackDNS=9.9.9.9 149.112.112.112Domains=~.DNSSEC=allow-downgradeDNSOverTLS=opportunisticCache=yesReload after editing:
sudo systemctl restart systemd-resolvedKey options:
DNS=— upstream DNS servers. SupportsIP#hostnameformat for DoT (1.1.1.1#cloudflare-dns.com)FallbackDNS=— used when primary servers are unreachableDomains=~.— the~.means “use this for all domains” (makes it the default route)DNSSEC=—yes(enforce),allow-downgrade(verify when possible),no(off)DNSOverTLS=—yes(require),opportunistic(try, don’t fail),no(off)
DNS over TLS Setup
DoT encrypts your DNS queries so your ISP can’t log every hostname you visit. With Cloudflare:
[Resolve]DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.comDNSOverTLS=yesThe #hostname suffix is required for TLS certificate validation — without it, systemd-resolved can’t verify it’s talking to the right server.
Verify it’s working:
resolvectl status | grep DNSOverTLSsystemd-networkd .network Drop-ins
If you’re using systemd-networkd (common on servers, minimal installs, containers), you configure per-link DNS in .network files under /etc/systemd/network/.
[Match]Name=eth0
[Network]DHCP=yesDNS=10.0.0.53Domains=~corp.internal
[DHCP]UseDNS=falseUseDNS=false prevents DHCP from overwriting your DNS setting. Critical if your router pushes garbage DNS and you want to override it.
For static DNS without DHCP:
[Match]Name=eth0
[Network]Address=192.168.1.100/24Gateway=192.168.1.1DNS=1.1.1.1 1.0.0.1Reload networkd after changes:
sudo networkctl reloadresolvectl statusNetworkManager Integration
Most desktop distros use NetworkManager, not systemd-networkd. NM has its own DNS plugin architecture, and the interaction with systemd-resolved trips people up.
Check how NM is handling DNS:
grep -r dns= /etc/NetworkManager/You want to see dns=systemd-resolved in /etc/NetworkManager/NetworkManager.conf or a drop-in:
[main]dns=systemd-resolvedWith this set, NM pushes per-connection DNS config into systemd-resolved automatically. Connecting to a VPN? NM tells resolved about the VPN’s DNS servers and domain routing. Disconnecting? NM removes them. It’s clean.
Without this config, NM manages /etc/resolv.conf itself and writes directly over the symlink. Then systemd-resolved is running but ignored. Your queries bypass the stub, you lose caching, split DNS doesn’t work, and you get confused when things break inconsistently.
Common Breakage Patterns
Tailscale MagicDNS
Tailscale’s MagicDNS pushes a 100.100.100.100 DNS server and registers your tailnet hostnames. When it’s working right, systemd-resolved routes *.ts.net queries to Tailscale’s resolver while everything else goes to your normal DNS.
When it breaks, usually it’s because Tailscale set itself as the default route DNS (overriding the ~. domain) and now all your DNS is going through Tailscale. Check:
resolvectl status | grep -A5 tailscaleIf you see ~. on the Tailscale interface, that’s why. Either configure Tailscale to not be the default route, or set a lower priority explicitly.
Docker Bridge DNS
Docker creates a docker0 bridge interface. By default, containers use Docker’s internal DNS (usually 127.0.0.11 inside the container network). But on the host side, if systemd-resolved is trying to route queries for container hostnames and the Docker interface shows up in resolvectl status, you can get weird resolution failures.
The fix: make sure Docker’s DNS is isolated. In /etc/docker/daemon.json:
{ "dns": ["1.1.1.1", "8.8.8.8"]}This explicitly sets DNS for containers and stops Docker from interfering with the host resolver.
If you’re running dnsmasq alongside Docker and systemd-resolved, you’re in for a bad time. Pick one path and stick with it.
Corporate VPN with Custom CA
If your corporate DNS requires queries to go through a VPN that uses a custom certificate authority, DoT validation will fail because the CA isn’t in your system trust store. You’ll see errors like:
DNSSEC validation failed for question corp.example.comOptions:
- Add the corporate CA cert to your system trust store
- Use
DNSSEC=allow-downgradeso it degrades gracefully instead of hard-failing - Set
DNSSEC=nofor that specific link (addDNSSEC=nounder[Network]in the networkd.networkfile)
LLMNR and mDNS
systemd-resolved handles two local network discovery protocols:
- mDNS (Multicast DNS) — resolves
.localhostnames. Your Raspberry Pi registered aspi.local? That’s mDNS. - LLMNR (Link-Local Multicast Name Resolution) — Microsoft protocol for resolving hostnames on local networks without a DNS server.
Out of the box, systemd’s per-link default is LLMNR yes and mDNS no — so LLMNR is usually on (and yes, it’s a known poisoning attack vector you may want off), while mDNS needs turning on. If you need .local resolution:
[Resolve]MulticastDNS=resolveLLMNR=noresolve means “answer queries but don’t register your own hostname.” Safer than yes, which registers you as a responder.
When to Disable systemd-resolved
Honestly? Rarely. The common “just disable it” advice usually trades one problem (confusion about the stub) for a worse one (no caching, no split DNS, broken VPN routing).
If you genuinely need to disable it — maybe you’re running dnsmasq or unbound as a proper local resolver — do it cleanly:
sudo systemctl disable --now systemd-resolvedsudo rm /etc/resolv.confecho "nameserver 127.0.0.1" | sudo tee /etc/resolv.confThen configure your replacement resolver to listen on 127.0.0.1:53.
If your problem is just DNSSEC validation breaking on a flaky upstream:
[Resolve]DNSSEC=noIf your problem is a VPN clobbering all DNS:
resolvectl default-route eth0 trueresolvectl default-route tun0 falseThere’s almost always a scalpel. You don’t need the sledgehammer.
Quick Reference
| Task | Command |
|---|---|
| Check current DNS config | resolvectl status |
| Query a hostname | resolvectl query hostname |
| Flush DNS cache | resolvectl flush-caches |
| Set per-link DNS | resolvectl dns eth0 1.1.1.1 |
| Set per-link domain routing | resolvectl domain eth0 ~example.com |
| Set default route interface | resolvectl default-route eth0 true |
| Check cache stats | resolvectl statistics |
| Restart resolved | sudo systemctl restart systemd-resolved |
The stub at 127.0.0.53 isn’t a mistake. It’s a feature. Once you understand what’s sitting behind it — the per-link routing, the split DNS, the DoT support, the DNSSEC validation — you’ll stop fighting it and start using it.
Your 2 AM self, debugging VPN DNS failures by staring at a static /etc/resolv.conf, will thank you.