What it is and why
/etc/resolv.conf used to be a plain text file. You wrote nameserver lines into it
by hand or through a DHCP client. The old approach had problems:
- Three nameservers maximum (per RFC).
- Every interface shared one list. There was no per-link DNS, which matters for VPN.
- No caching, so every query went out to the network.
- No DNSSEC or DoT (DNS-over-TLS) out of the box.
systemd-resolved is a stub daemon that solves this. It listens on the address
127.0.0.53:53 (the last octet is 53, the DNS port) and:
- Accepts queries from local applications through
/etc/resolv.conf. - Keeps separate DNS-server configs for each network interface
(
enp0s3goes through the ISP,wg0through the VPN, so each name resolves where it should). - Caches answers.
- Supports DNSSEC, DoT, LLMNR, and mDNS.
- Receives upstream DNS from NetworkManager, systemd-networkd, or DHCP.
The basic flow:
curl example.com
↓ (libc gethostbyname)
/etc/resolv.conf → nameserver 127.0.0.53
↓ (UDP to 127.0.0.53:53)
systemd-resolved
↓ (picks an interface by routing, sends upstream)
External DNS (ISP / 8.8.8.8 / 1.1.1.1)
/etc/resolv.conf is a symlink
On current Ubuntu 22+, Fedora, and RHEL 9+, the file is a symlink:
ls -l /etc/resolv.conf
▸../run/systemd/resolve/stub-resolv.conf
It contains only nameserver 127.0.0.53 plus the search domains. The real
upstream DNS shows up through resolvectl, not in this file.
The symlink can point at one of three targets:
| Symlink target | Behavior |
|---|---|
/run/systemd/resolve/stub-resolv.conf | through 127.0.0.53 (default) |
/run/systemd/resolve/resolv.conf | straight to upstream, no stub |
your own static /etc/resolv.conf | bypass systemd-resolved fully |
If you need to turn resolved off and go back to the old way:
sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
echo 'nameserver 1.1.1.1' | sudo tee /etc/resolv.conf
# But NetworkManager may overwrite this, so disable its management
resolvectl, the main tool
resolvectl status # all links plus upstream DNS on each one
resolvectl query example.com # resolve through resolved (like dig, but via the stub)
resolvectl statistics # cache hits/misses
resolvectl flush-caches # drop the cache
resolvectl dns enp0s3 1.1.1.1 9.9.9.9 # set DNS on a specific interface
resolvectl domain wg0 '~corp.local' # split-DNS: only corp.local queries go through wg0
A ~ prefix on a domain means routing-only (use this link only for
those domains). Without ~, it is a regular search domain.
Configuration: /etc/systemd/resolved.conf
Global settings live here. Do not edit the file itself. Use a drop-in:
# /etc/systemd/resolved.conf.d/dns.conf
[Resolve]
DNS=1.1.1.1#cloudflare-dns.com 9.9.9.9
FallbackDNS=8.8.8.8
Domains=~. # all queries through DNS= (not per-link)
DNSSEC=allow-downgrade
DNSOverTLS=opportunistic
Cache=yes
After editing:
sudo systemctl restart systemd-resolved
resolvectl status # check that it took effect
A drop-in (see systemd-drop-ins) matters because package updates
can overwrite /etc/systemd/resolved.conf.
/etc/nsswitch.conf: the resolver order
Name resolution runs through the chain in nsswitch.conf:
hosts: files myhostname mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns
Left to right:
files→/etc/hosts(static entries)myhostname→ the host's own name plus localhostmdns4_minimal→ Avahi/mDNS for the local networkresolve→ systemd-resolved over D-Bus (fast, supports DNSSEC)dns→ fallback to the classic glibc resolver through/etc/resolv.conf
If you do not need the cross-check and want only resolved, leave
files resolve [!UNAVAIL=return] dns. You rarely have to change nsswitch,
but it is the first thing you look at when "host pings but dig fails" (or the other way around).
DNSSEC, DoT, LLMNR: what to enable
- DNSSEC validates DNS-zone signatures.
DNSSEC=allow-downgradeis a sensible default: it tries, but it does not break resolution when upstream cannot do it. - DNSOverTLS encrypts DNS traffic to the upstream server.
opportunistictries TLS and falls back to plain. For real guarantees you need an upstream that actually supports it (1.1.1.1, 9.9.9.9). - LLMNR is link-local multicast, a WINS replacement, meant for home networks.
It is noisy and you can turn it off (
LLMNR=no). - MulticastDNS resolves
.localnames on the LAN. If Avahi is installed, turn it off in resolved (MulticastDNS=no) so the two do not overlap.
Debugging: why a name does not resolve
resolvectl status # which upstreams on which link
resolvectl query --cache=no example.com # bypass the cache
journalctl -u systemd-resolved -f # stream the resolver logs
sudo resolvectl log-level debug # verbose logging
ss -ulnp 'sport = :53' # who actually listens on 53
dig @127.0.0.53 example.com # hit the stub directly
Common problems:
- Empty DNS Servers in
resolvectl status: NetworkManager did not hand any over. Checknmcli dev show | grep DNS. - It resolves through
digbut not through the application: the application does not use libc. It reads/etc/resolv.confitself and sees127.0.0.53, but the glibc cache is empty. Restart the application. - VPN broke DNS: the VPN client overwrote
/etc/resolv.confon top of the symlink. Useresolvectl domain wg0 '~.'instead of swapping the file out.