Verify it's running

# Service status
systemctl status systemd-resolved

# What's in /etc/resolv.conf?
ls -la /etc/resolv.conf
# /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf

cat /etc/resolv.conf
# nameserver 127.0.0.53      <-- the resolved stub
# options edns0 trust-ad
# search ...

# What does resolved actually do for queries?
resolvectl status
# Global settings + per-link DNS

If /etc/resolv.conf is not a symlink to ../run/systemd/resolve/stub-resolv.conf, resolved isn't the active resolver. NetworkManager / systemd-networkd / cloud-init might be writing a static file. Verify what's actually answering before debugging further.

Configure global upstreams + DoT

Edit /etc/systemd/resolved.conf:

[Resolve]
# Upstream DNS servers (fall-through if per-link DNS doesn't provide)
DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 9.9.9.9#dns.quad9.net

# Fallback if the above are unreachable
FallbackDNS=8.8.8.8

# DNS-over-TLS to the configured DNS servers
DNSOverTLS=yes

# DNSSEC validation
DNSSEC=yes

# Cache
Cache=yes
CacheFromLocalhost=no

# mDNS for *.local resolution (handled by Avahi if installed; see /tutorials/mdns-avahi-zero-config-naming.html)
MulticastDNS=resolve
LLMNR=resolve

# Reject queries for domains in private IP space coming from public sources
DNSStubListenerExtra=
sudo systemctl restart systemd-resolved
resolvectl status

Now every DNS query the local machine makes goes through TLS to Cloudflare / Quad9; resolved caches answers in-process; DNSSEC validation is on.

Per-link DNS

The killer feature: each network interface can have its own DNS upstream + search domain. When the corporate VPN comes up, resolved learns the VPN's pushed DNS servers + search domain (e.g. internal.example.com); queries for *.internal.example.com go to the VPN's resolver while everything else uses the global upstream.

resolvectl status

# Look for per-interface sections:
# Link 3 (tun0)
#       Current Scopes: DNS LLMNR/IPv4
#        Protocols: +DefaultRoute -LLMNR -mDNS DNSOverTLS=opportunistic DNSSEC=no/unsupported
#  Current DNS Server: 10.0.5.1
#         DNS Servers: 10.0.5.1
#          DNS Domain: ~internal.example.com

The ~ prefix means "this DNS server handles only queries for this domain." Without ~ (just DNS Domain: internal.example.com), it's a search suffix.

To set per-link DNS manually:

sudo resolvectl dns tun0 10.0.5.1
sudo resolvectl domain tun0 '~internal.example.com'

NetworkManager normally manages this automatically; manual setting is for testing.

Query individual zones / show DNS server selection

# Resolve a name
resolvectl query example.com
# example.com: 93.184.216.34
#              -- link: eth0
#              -- protocol: dns
#              -- server: 1.1.1.1

# Force a specific interface
resolvectl query -i tun0 internal.example.com

# DNSSEC validation status
resolvectl query --validate=yes example.com

# Reverse
resolvectl query 93.184.216.34

The output shows which interface the name was resolved through — useful for "why is this internal name failing? Which DNS server did it actually go to?"

Inspect cache + statistics

resolvectl statistics
# Transactions
#       Current Transactions: 0
#       Total Transactions: 12345
# Cache
#       Current Cache Size: 142
#       Cache Hits: 5678
#       Cache Misses: 89
# DNSSEC Verdicts
#       Secure: 0
#       Insecure: 5678
#       Bogus: 0
#       Indeterminate: 0

# Flush the cache (after DNS changes)
sudo resolvectl flush-caches

Cache hit rate is the easiest indicator of resolver performance; on a desktop, expect 70-90% after warm-up.

When resolved is the wrong tool

  • Pi-hole / Unbound on the LAN. If you're running a recursive resolver on the network (see that tutorial), point resolved at it as the upstream — or, if you don't want resolved's per-link logic, disable it and write /etc/resolv.conf directly.
  • Containers. systemd-resolved doesn't run inside most containers; container runtimes have their own resolvers (Docker uses an embedded DNS forwarder; Podman defaults to whatever's in the host's resolv.conf).
  • nsswitch.conf order matters. If you've configured Avahi for mDNS (see that tutorial) and resolved is also handling mDNS, the two can race. Pick one for .local resolution; disable mDNS in the other.

Disable it cleanly

To go back to old-school /etc/resolv.conf:

sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
# Write a plain file
echo -e "nameserver 1.1.1.1\nnameserver 9.9.9.9" | sudo tee /etc/resolv.conf

# Make sure NetworkManager doesn't overwrite it
# In /etc/NetworkManager/NetworkManager.conf:
[main]
dns=none

For a server that only needs upstream-DNS-from-a-static-file, this is fine. For laptops crossing networks (corporate VPN + home + coffee shop), resolved's per-link awareness is worth the complexity.

Worth knowing

  • NSS configuration. /etc/nsswitch.conf has a hosts: line that picks the resolution order: files dns means /etc/hosts first then DNS. With resolved, the order is usually files mymachines resolve [!UNAVAIL=return] dns; resolve calls into resolved over D-Bus before falling back to plain DNS.
  • D-Bus interface. Applications can talk to resolved via D-Bus, getting richer answers than the simple getaddrinfo path — multi-record returns with TTLs, DNSSEC results, per-link source identification.
  • resolvectl monitor — live stream of resolution events for debugging "what's resolving right now and where to?"