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):
- A name that matches a link's routing domain goes to that link's servers. If more than one link has the same best match, the query goes to all of them.
- A name that matches no routing domain goes to the servers on every link marked as a default route, and to the global servers.
So the classic leaks are:
- DHCP overrides you. You configured a resolver globally, but the router handed out its own (often the ISP's) by DHCP. That server sits on the Wi-Fi link and gets queries too.
- The VPN does not take over DNS. The tunnel link has the VPN's resolver, but the Wi-Fi link still has its DHCP servers and is still a default route, so lookups go out both ways.
- Something bypasses the resolver.
/etc/resolv.confis a plain file listing other servers, or a program has its own DNS client. Firefox and Chrome with DNS-over-HTTPS turned on send lookups straight to the browser's chosen resolver instead ofsystemd-resolved, so a browser-based leak test describes the browser, not the system.
Step 1: see which servers are configured, per link
resolvectl status
resolvectl dns
resolvectl domain
ls -l /etc/resolv.confRead resolvectl status section by section: Global first, then one Link block per interface. In each block:
Current DNS ServerandDNS Serversare the addresses that link will use. A server written1.1.1.1#cloudflare-dns.comcarries a TLS hostname for DNS-over-TLS.- The
Protocols:line shows+DefaultRouteor-DefaultRoute, plusDNSOverTLS=andDNSSEC=modes. A link with+DefaultRouteand its own servers will receive ordinary lookups. resolv.conf mode: stubmeans programs that read/etc/resolv.confare sent to the local stub at127.0.0.53, and so throughsystemd-resolved.
/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: networkWith 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.
- Packets on port 53 on the physical interface are plaintext DNS. If you expect DNS-over-TLS or a VPN to carry your lookups, that is a leak, and the destination address shows which server got it.
- Packets on port 853 are DNS-over-TLS.
- With a full-tunnel VPN up, you should see neither on the physical interface, only the VPN's own encrypted traffic.
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 statisticsexample.comis signed, so the answer should end withData is authenticated: yes. That line meanssystemd-resolvedvalidated it locally.dnssec-failed.orgis deliberately misconfigured. With validation on,resolvectlreportsDNSSEC validation failed(the reason after it can vary), and plaindigreturns nothing.dig +cdsets "checking disabled". If that returns an address while the plain query returns nothing, the failure is DNSSEC, not a dead domain.resolvectl statisticscounts Secure, Insecure and Bogus verdicts. If they stay at zero after lookups through the resolver, nothing is being validated.
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
resolvectl statusshowsDNSOverTLS=yesorDNSOverTLS=opportunisticon the relevantProtocols:line, and the server is written with a#hostname.resolvectl query --cache=noreportsData was acquired via local or encrypted transport: yes.- The capture above shows port 853 traffic to your resolver and no port 53.
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-resolvedDomains=~. 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.confDo 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
- systemd-resolved.service(8): protocols and routing,
/etc/resolv.confmodes - resolved.conf(5):
DNS=,FallbackDNS=,Domains=,DNSSEC=,DNSOverTLS=, drop-ins - resolvectl(1):
query,--cache=,statistics - nm-settings-nmcli(5):
ipv4.ignore-auto-dns,ipv4.dns-priority,ipv4.dns - tcpdump(1)