Panoptes › DNS leak test

DNS leak test on Linux: which DNS server am I actually using?

You set a resolver, maybe turned on DNS-over-TLS, maybe connected a VPN, and a leak-test site still shows your ISP. On a systemd-resolved system the answer is rarely one setting. Every network link can bring its own DNS servers. This page shows how to see which server really answered, how to watch the wire for plaintext DNS, how to confirm DNSSEC and DNS-over-TLS, and how to fix each kind of leak.

How DNS leaks happen with systemd-resolved

On distributions that use systemd-resolved, such as Fedora and Ubuntu, names are resolved through it, fed by NetworkManager. The resolver does not have one DNS server. It has a global list from /etc/systemd/resolved.conf plus a separate list on every link: your Wi-Fi, your Ethernet, your VPN tunnel. Per systemd-resolved.service(8):

So the classic leaks are:

Step 1: see which servers are configured, per link

resolvectl status
resolvectl dns
resolvectl domain
ls -l /etc/resolv.conf

Read resolvectl status section by section: Global first, then one Link block per interface. In each block:

/etc/resolv.conf should be a symlink to /run/systemd/resolve/stub-resolv.conf. If it is a regular file listing other nameserver lines, programs that read it skip systemd-resolved entirely.

Step 2: see which link actually answered

resolvectl query --cache=no example.com

--cache=no sends the lookup to the network instead of answering from cache. Recent versions of resolvectl mark each address with the link the answer came through (-- link: wlan0) and end with lines like these:

-- Information acquired via protocol DNS in 359.2ms.
-- Data is authenticated: yes; Data was acquired via local or encrypted transport: yes
-- Data from: network

With a VPN up, answers should come through the tunnel interface, not your Wi-Fi or Ethernet link. Older systemd versions print only Data is authenticated.

Step 3: watch the wire for plaintext DNS

Configuration tells you what should happen. A packet capture tells you what does. Capture on the physical interface, not on any, so the stub's traffic on loopback does not confuse things:

sudo tcpdump -ni wlan0 'port 53'
sudo tcpdump -ni wlan0 'port 853'

Leave one running and, in another terminal, run resolvectl query --cache=no example.org.

Check that DNSSEC is really validating

resolvectl query example.com
resolvectl query dnssec-failed.org
dig +short dnssec-failed.org
dig +short +cd dnssec-failed.org
resolvectl statistics

One caveat: many public resolvers validate DNSSEC themselves and refuse bogus answers. A failed dnssec-failed.org lookup proves validation is happening somewhere, but only Data is authenticated: yes proves your own machine is doing it.

The setting is DNSSEC= in resolved.conf. The default is allow-downgrade, which turns validation off when a server does not support DNSSEC. resolved.conf(5) warns that this allows downgrade attacks. yes makes invalid answers fail.

Check that DNS-over-TLS is really in use

Opportunistic is not enforced. With DNSOverTLS=opportunistic, systemd-resolved tries TLS but falls back to plain UDP if the server, or anything in between, does not cooperate, and it cannot authenticate the server. DNSOverTLS=yes encrypts every query and fails if TLS is not available. Give each server its TLS hostname (#dns.quad9.net) so the certificate is checked against a name instead of an IP address.

Fixes

Stop DHCP from handing you its DNS servers

nmcli connection show
sudo nmcli connection modify "NAME" ipv4.ignore-auto-dns yes ipv6.ignore-auto-dns yes
sudo nmcli connection up "NAME"

With ignore-auto-dns, NetworkManager drops the name servers and search domains from DHCP and uses only the ones set on the profile, if any. Make sure you have set servers somewhere first, either in ipv4.dns on the profile or globally (next step). The cost: captive portals that redirect through their own DNS stop working on that network.

Set an encrypted, validating resolver globally

sudo mkdir -p /etc/systemd/resolved.conf.d
printf '[Resolve]\nDNS=9.9.9.9#dns.quad9.net 1.1.1.1#cloudflare-dns.com\nDNSOverTLS=yes\nDNSSEC=yes\nDomains=~.\n' | sudo tee /etc/systemd/resolved.conf.d/90-encrypted-dns.conf
sudo systemctl restart systemd-resolved

Domains=~. makes these servers a match for every name. On its own it does not stop per-link servers being asked too: NetworkManager adds ~. to the connection that holds the default route, and an equal match is sent to every server that has it. Removing the DHCP servers from the link, as above, is what leaves the global servers as the only candidates. Also note that FallbackDNS= is not a failover. It is used only when no other server is known, so put every resolver you want to fail over to in DNS=.

Make a NetworkManager VPN's DNS exclusive

sudo nmcli connection modify "VPN-NAME" ipv4.dns-priority -50 ipv6.dns-priority -50
sudo nmcli connection up "VPN-NAME"

Lower values win. VPN profiles default to 50 and other connections to 100. A negative value excludes every configuration with a higher number, so only the VPN's servers are used while it is up. nm-settings-nmcli(5) describes setting the profile you want to the most negative value as the way to avoid DNS leaks. VPNs that NetworkManager does not run (wg-quick, a bare openvpn) set DNS from their own config. Look for DNS = in WireGuard configs and dhcp-option DNS or an update-systemd-resolved hook in OpenVPN configs.

Put /etc/resolv.conf back on the stub

sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

Do this only if nothing else (resolvconf, openresolv, a VPN script) is meant to own the file.

Automated: sudo audit-dns.sh from Panoptes prints, read-only, every place on the machine that can set a resolver: the active resolver services, where /etc/resolv.conf points and what it lists, resolvectl's servers and DoT/DNSSEC modes, NetworkManager drop-ins and per-connection ignore-auto-dns, systemd-networkd and dhclient overrides, resolved.conf with its drop-ins and chattr lock, and DNS directives in OpenVPN and WireGuard configs. Run it as root, because VPN configs are usually mode 600. checkdns shows resolvectl status with each AdGuard, Cloudflare and Quad9 address labelled by provider, alongside WARP and Avahi state. harden-dns.sh --dry-run previews the fix: DoT with hostname pinning, DNSSEC, Domains=~., ignore-auto-dns on every connection and an immutable resolved.conf. Its default resolvers are AdGuard with Quad9 and Cloudflare as fallback, and you can change them with the PRIMARY_DNS and FALLBACK_DNS environment variables.

FAQ

How do I see which DNS server Linux is using?

Run resolvectl status. It lists the global servers and, separately, the servers on each network link, with the one in use shown as Current DNS Server. To see which link answered a particular lookup, run resolvectl query --cache=no example.com.

Why does my VPN leak DNS on Linux?

systemd-resolved keeps DNS servers per link. If your Wi-Fi link still has DHCP-provided servers and is still a default route, lookups go to it as well as to the VPN. For a NetworkManager VPN, set ipv4.dns-priority and ipv6.dns-priority to a negative value such as -50 on the VPN profile and reconnect.

Is FallbackDNS a backup if my DNS server goes down?

No. resolved.conf(5) says FallbackDNS is only used if no other DNS server information is known. If you want failover, list the extra servers in DNS= itself.

How do I know DNSSEC is actually validating?

Run resolvectl query example.com and look for Data is authenticated: yes. Then query dnssec-failed.org, which is deliberately broken: with validation on, it fails with DNSSEC validation failed.

Does DNSOverTLS=opportunistic protect me?

Only against passive snooping, and only when TLS works. In opportunistic mode systemd-resolved falls back to plain DNS if TLS fails and cannot authenticate the server. Use DNSOverTLS=yes with a #hostname on each server if you need it enforced.

Sources