Skip to main content

Linux concept: DNS resolver

22 August 2026

Two people run a lookup for the same name on the same laptop, in the same minute, and get two different answers. Neither of them is wrong and neither machine is broken. On Linux, "resolving a name" is not one thing: it is a stack of four layers, each with its own configuration file, its own cache and its own opinion, and a command-line DNS tool skips most of them. Until you know which layer answered, you cannot tell whether the problem is your domain, your network, or a single line in a file you have never opened.

This article is about the client side: the machine in front of you. What /etc/resolv.conf really is on a modern system, why /etc/nsswitch.conf decides more than DNS does, what resolvectl can tell you that dig cannot, how containers resolve names differently from their host, and how to run your own resolver without becoming somebody else's attack tool.

How a Linux machine turns a name into an address, layer by layer.

Goal: after reading this you can say exactly which layer answered a lookup, and change the setting in the place where it is actually generated.

1. The Basics

The Domain Name System (DNS) is the internet's directory: it turns names into addresses, and into a dozen other kinds of information. That much is true everywhere. What is specific to Linux is how a program on your machine asks the question, because it almost never asks DNS directly.

An application calls a function in the C library. The C library consults a configuration file to decide which sources to try and in what order. One of those sources is DNS. Only then does a packet leave the machine, and on most modern systems it does not leave the machine at all at first: it goes to a local stub listening on a loopback address, which forwards it upstream.

1.1 The Four Layers on Your Machine

Four layers sit between "the browser wants a name" and "a DNS query crosses the network". Each is configured somewhere different.

LayerWhat it doesConfigured in
The application Calls getaddrinfo(). Some applications skip everything below and resolve names themselves. The application
NSS (Name Service Switch) Decides which sources to try, in order: local files, mDNS, DNS, and others. /etc/nsswitch.conf
The stub resolver Formats the actual DNS query and sends it to the servers it was given. /etc/resolv.conf, usually generated
The local caching daemon Listens on a loopback address, caches, and forwards upstream per network interface. /etc/systemd/resolved.conf, NetworkManager, DHCP

The right mental model: on Linux, DNS is the last thing consulted, not the first. Everything above it can answer, override or bypass it, which is why "what does DNS say" and "what does this machine do" are two different questions that need two different commands.

1.2 Why Two Tools Give Two Answers

Here is the disagreement, on the machine this article was written on. The name dev01 is this machine's own hostname:

$ getent hosts dev01
127.0.1.1       dev01

$ dig +search +short dev01
192.168.1.42

Two answers, both correct. getent walked the NSS list, found the name in /etc/hosts, and stopped before DNS was ever consulted. dig ignored /etc/hosts entirely and asked a name server, which appended the search domain and returned the address the network has for this machine.

Every application on the machine behaves like getent. Every DNS troubleshooting tool behaves like dig. That single fact explains most of the confusion in this subject, and it is why the two commands are both worth having in your hands.

1.3 What This Article Covers

This article is the client side: the resolver on a Linux machine, from the configuration files up to running your own caching daemon.

Two neighbouring subjects live elsewhere so that nothing is explained twice. For DNS as a system - delegation, zones, and what every record type is for - see the DNS article in the Focus On Web section. For the tooling, reading dig output block by block, tracing a delegation and comparing a cache with the source, see the dig command. Where the DNS servers on your machine came from in the first place is usually a DHCP question, covered in the DHCP article.

Every command and every piece of output below was run on Ubuntu 24.04.4 LTS with systemd 255 in August 2026. Details differ between distributions, and the article says so where they do. The only edits are cosmetic: this machine's hostname appears as dev01 and its private network addresses as 192.168.1.x.

Back to top

2. Where the Names Come From

Three filenames carry most of this subject, and each of them is an abbreviation whose expansion explains what the file is for.

NameShort forWhat that tells you
resolv.conf resolver configuration It configures the resolver library, not a server. It was never meant to describe your network
nsswitch.conf Name Service Switch A switch between name services. DNS is only one of them
gai.conf getaddrinfo configuration It configures one C library function, and only its sorting behaviour

The word stub is worth pinning down too, because it appears in every diagnostic message on a modern system. A stub resolver is a client that knows almost nothing: it asks one server and believes the answer. It does not chase referrals from the root, it does not know what an authoritative server is, and it cannot resolve anything on its own. That work belongs to a recursive resolver, which is what your ISP or 1.1.1.1 runs.

The confusing part on Linux is that systemd-resolved calls itself a "stub listener" and runs on 127.0.0.53. It is a stub from your applications' point of view, and a client from the network's point of view, and it is not resolving anything from the root. It is a forwarder with a cache.

And resolvectl is simply resolve control, following the same pattern as systemctl, journalctl and hostnamectl: a control program for one systemd service.

Back to top

3. A Short History

Every layer in section 1.1 exists because the layer below it turned out to be insufficient, and none of the old layers was ever removed. Reading the history is the fastest way to understand why a modern Linux machine has four places to look.

EraWhat arrivedThe problem it solved
Until 1983 /etc/hosts, copied from a central HOSTS.TXT Nothing yet: this was name resolution
1983 DNS is published as RFC 882 and RFC 883 One file could not scale to a growing network
1986 BIND ships with 4.3BSD, bringing the resolver library and /etc/resolv.conf Programs needed a standard way to find a name server
1992 Solaris 2 introduces /etc/nsswitch.conf Names could come from NIS, files or DNS, and the order had to be configurable
1997 glibc 2.0 adopts the Name Service Switch on Linux Linux inherits the Solaris design, and still uses it
2000s nscd, dnsmasq and resolvconf appear Caching, and several programs fighting over one generated file
2004 NetworkManager Laptops move between networks, and each network has its own DNS servers
28 May 2014 systemd-resolved arrives in systemd 213 Per-interface DNS: a VPN and a Wi-Fi network need different answers
Today All of the above, still present, on the same machine -

Two things follow from that table. First, /etc/hosts is older than DNS and still wins by default, which is not a bug but a deliberate order that has never been changed. Second, every generation added a layer without deleting one, so documentation written at any point in the last forty years is still partly correct and partly dangerously out of date. Advice to "just edit /etc/resolv.conf" was excellent in 1995.

The design that keeps winning is the Solaris one. nsswitch.conf is thirty-four years old, was invented for a different operating system, and is still the file that decides what your browser resolves a name to.

Back to top

4. Simple Use Cases

Five commands answer almost every everyday question. Learn these first, because most of the time the answer is in one of them and no editing is needed at all.

4.1 What Is This Machine Actually Using?

The obvious first move is to read /etc/resolv.conf. Do it, but read what it says:

$ cat /etc/resolv.conf
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
...
nameserver 127.0.0.53
options edns0 trust-ad
search localdomain

That 127.0.0.53 is not a DNS server on the internet. It is the local stub, and the real upstream servers sit behind it. The file is telling you, in a comment, that it cannot answer your question. The command that can is resolvectl status:

$ resolvectl status
Global
           Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
    resolv.conf mode: stub
Fallback DNS Servers: 9.9.9.9

Link 3 (wlp0s20f3)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 192.168.1.1
       DNS Servers: 192.168.1.1
        DNS Domain: localdomain

Link 6 (docker0)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

Read that output as a per-interface table, because that is what it is. The Wi-Fi interface has a DNS server; docker0 has none. The minus signs are features that are switched off: no DNS over TLS, no DNSSEC validation, no multicast DNS. resolv.conf mode: stub confirms that the file you just read is the generated stub version rather than a static one you wrote.

For just the servers, without the rest:

$ resolvectl dns
Global:
Link 3 (wlp0s20f3): 192.168.1.1
Link 6 (docker0):

4.2 What Does This Machine Resolve a Name To?

resolvectl query asks the same path an application would, and then tells you things no other tool reports:

$ resolvectl query petermartin.nl
petermartin.nl: 23.88.98.40                                 -- link: wlp0s20f3
                2a01:4f8:c0c:6848::1                        -- link: wlp0s20f3

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

The last three lines are the reason to use this command. It reports which interface answered, whether the answer was DNSSEC validated, whether it travelled encrypted, and whether it came from the cache or the network. Those are four questions people normally guess at. Here the answer came partly from cache, was not validated, and travelled in clear text, which is the ordinary state of a laptop talking to a home router.

4.3 getent hosts Versus getent ahosts

getent queries the NSS databases exactly as an application would. For names it has two forms, and they are not the same:

$ getent hosts petermartin.nl
2a01:4f8:c0c:6848::1 petermartin.nl

$ getent ahosts petermartin.nl
23.88.98.40     STREAM petermartin.nl
23.88.98.40     DGRAM
23.88.98.40     RAW
2a01:4f8:c0c:6848::1 STREAM
2a01:4f8:c0c:6848::1 DGRAM
2a01:4f8:c0c:6848::1 RAW

Look at the order. hosts returned the IPv6 address; ahosts put IPv4 first. Same machine, same name, same second. Section 7.1 explains why, and it is the single most useful surprise in this article.

The short version for now: ahosts is the one that tells you what an application will actually do. Use it, and ignore hosts unless you specifically want the legacy behaviour.

4.4 Where Did That Answer Come From?

Three tools, three different layers, and knowing which is which turns a confusing bug into a one-line diagnosis:

CommandAsksUse it to answer
getent ahosts NAME The full NSS chain, like an application "What will my software actually connect to?"
resolvectl query NAME systemd-resolved, skipping /etc/hosts "What does the local resolver have, and from where?"
dig NAME A name server directly, skipping everything local "What does DNS itself say?"

When those three disagree, the disagreement is the answer. getent differing from dig means something local is intercepting the name: an /etc/hosts entry, an mDNS responder, or a search domain. resolvectl differing from dig usually means a cache, and section 4.5 clears it.

4.5 Flushing the Cache

After a record changes, the local daemon may still be holding the old answer for the rest of its TTL:

$ resolvectl flush-caches

It prints nothing and returns immediately. That is success. Note what it does not do: it clears the cache on this machine only. Your router, your ISP's resolver and your browser all have their own, and none of them is affected. If a name still resolves to the old address after this command, the stale copy is not on your machine.

There is no cache to flush on a system without a local caching daemon. Plenty of servers run that way, and the absence of a cache is why a change appears there immediately and on your laptop five minutes later.

Back to top

5. Moderate Use Cases

5.1 Reading /etc/nsswitch.conf

One line in this file decides more about name resolution than every DNS setting on the machine:

$ grep '^hosts:' /etc/nsswitch.conf
hosts:          files mdns4_minimal [NOTFOUND=return] dns mymachines

Read it left to right. That machine checks /etc/hosts first (files), then multicast DNS for .local names, then real DNS, then systemd's container names. The first source that answers wins, and everything after it is never consulted.

The bracketed [NOTFOUND=return] is an action, not a source. It says: if mDNS gives a definitive "this name does not exist", stop here instead of falling through to DNS. That is why a .local name can fail on a machine where DNS is working perfectly.

SourceWhere it looks
files /etc/hosts
dns The servers in /etc/resolv.conf
mdns4_minimal Multicast DNS on the local network, IPv4 only, .local names
myhostname Synthesised: the machine's own name, always resolves
mymachines Names of local systemd-nspawn containers
resolve systemd-resolved directly, bypassing the stub on 127.0.0.53

5.2 /etc/hosts Wins, and That Is the Point

Because files comes first, an entry in /etc/hosts overrides DNS completely for every application on the machine:

$ head -3 /etc/hosts
127.0.0.1 localhost
127.0.1.1 dev01

This is a feature and a trap in equal measure. It is the correct way to test a site on a new server before you move the DNS record, and it is the reason a developer's laptop can be the only machine in the company where a site works.

/etc/hosts is older than DNS and still outranks it. Every "why does this only happen on my machine" question in this subject has the same first answer, and it is a file you can read in one screen.

The trap has a specific shape: an entry added during a migration, never removed, that keeps pointing at a server decommissioned two years ago. Nothing warns you, because the file is doing exactly what it was told. When a name behaves differently on one machine than on every other, /etc/hosts is the first place to look and it takes five seconds.

5.3 The Search List and Single-Label Names

The search line in /etc/resolv.conf handles names with no dots in them. Ask for server1 with search localdomain in effect and the resolver tries server1.localdomain before giving up.

This is convenient inside a company network and confusing everywhere else, because it means a name can resolve for your applications and not for your DNS tool. dig does not use the search list unless you ask it to:

$ dig +short dev01          # no search list applied
127.0.1.1

$ dig +search +short dev01  # search list applied
192.168.1.42

A related option is ndots, which sets how many dots a name must contain before the resolver tries it as-is rather than appending a search domain. The default is 1. Kubernetes commonly sets it to 5, which is why a lookup inside a cluster can generate several failed queries before the successful one, and why DNS latency in Kubernetes is a subject of its own.

The reason for a local daemon at all is that one machine can be on several networks at once, and each of them has its own name server that knows things the others do not. A VPN resolves internal names; the Wi-Fi does not. Before per-link DNS, connecting to a VPN meant overwriting /etc/resolv.conf and hoping the previous contents got restored.

$ resolvectl domain
Global:
Link 3 (wlp0s20f3): localdomain
Link 6 (docker0):

A domain listed here is a search domain. A domain written with a tilde, such as ~corp.example.com, is a routing domain: it is not appended to short names, it only says "send queries for this domain to this link's server". That one character is how split DNS is expressed, and it is the setting a VPN client sets on your behalf.

5.5 Change the Setting Where It Is Generated

/etc/resolv.conf is a symlink into a directory that is rebuilt at boot:

$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 39 Apr 24  2024 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf

Anything you type into it is lost the next time the machine restarts, or sooner. Several programs may be generating it: systemd-resolved, NetworkManager, a DHCP client, resolvconf, a VPN client, or a container runtime. Editing the file is a fix that survives until Tuesday.

Change it where it is produced instead:

You want toChange it in
Set the DNS servers for one Wi-Fi or wired network The NetworkManager connection profile (nmcli, or the desktop settings)
Set a global fallback for the whole machine /etc/systemd/resolved.conf, key DNS=
Set the servers for every machine on the network The DHCP server, which is where they came from
Test a change without persisting it resolvectl dns <link> <server>, undone by resolvectl revert <link>
Override one name for every program on the machine /etc/hosts

To see what NetworkManager believes, which is often the real source:

$ nmcli dev show | grep -E 'GENERAL.DEVICE|IP4.DNS|IP4.DOMAIN'
GENERAL.DEVICE:                         wlp0s20f3
IP4.DNS[1]:                             192.168.1.1
IP4.DOMAIN[1]:                          localdomain

5.6 resolvectl Reads Structured Records for You

This is the part of resolvectl almost nobody uses, and it is genuinely better than dig for a few record types because it decodes them instead of printing them.

An SRV lookup normally means reading four numbers and then resolving the target yourself. resolvectl service does both:

$ resolvectl service _sip._udp sip.voice.google.com
_sip._udp/sip.voice.google.com: sip-anycast-1.voice.google.com:5060 [priority=10, weight=1]
                                216.239.32.1                -- link: wlp0s20f3
                                sip-anycast-2.voice.google.com:5060 [priority=20, weight=1]
                                216.239.32.2                -- link: wlp0s20f3

It labelled the priority and weight, sorted by preference, and resolved each target to an address in the same output. And for DANE records, where the raw form is three unexplained integers followed by a hash:

$ resolvectl tlsa tcp mx1.freebsd.org:25
_25._tcp.mx1.freebsd.org IN TLSA 3 1 1 0a7e2f469913ea64ca98af1f31bbbcaf...
        -- Cert. usage: Domain-issued certificate
        -- Selector: SubjectPublicKeyInfo
        -- Matching type: SHA-256 -- link: wlp0s20f3

Those three comment lines are exactly what you would otherwise look up in RFC 6698. There is a matching resolvectl openpgp for OPENPGPKEY records. For everything else dig remains the better tool, because it shows you the packet rather than an interpretation of it.

Back to top

6. Advanced Use Cases

6.1 Turning On DNS over TLS and DNSSEC Validation

Classic DNS travels in plain text, so anyone on the path can read what you look up and change the answer. systemd-resolved can encrypt the hop to your upstream server and validate signatures, and on most systems both are off by default. The resolvectl status output in section 4.1 says so: -DNSOverTLS and DNSSEC=no/unsupported.

Both are set in /etc/systemd/resolved.conf. The shipped file is entirely commented out, which is a useful reference for the defaults:

$ grep -E '^#(DNS|FallbackDNS|DNSSEC|DNSOverTLS|Cache)' /etc/systemd/resolved.conf
#DNS=
#DNSSEC=no
#DNSOverTLS=no
#Cache=no-negative

To encrypt, name a server that supports it and pin the hostname its certificate must match:

[Resolve]
DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com
DNSOverTLS=yes
DNSSEC=allow-downgrade

Then systemctl restart systemd-resolved and confirm with resolvectl status that the minus signs turned into plus signs.

Two warnings worth taking seriously. DNSOverTLS=yes is strict: if the server does not support it, resolution fails rather than falling back, which is the correct behaviour and also a good way to lose all name resolution on a captive-portal Wi-Fi. And DNSSEC=yes makes a broken signature anywhere in a chain fatal, including on domains whose owners have made a mistake you cannot fix. allow-downgrade validates where it can and does not break where it cannot, which is the setting most people actually want.

6.2 Split DNS, and Why a VPN Should Not Take Over Everything

A VPN typically needs two things: internal names must resolve through the tunnel, and everything else should keep using the local network. The old approach - rewrite /etc/resolv.conf and put the VPN's server first - sends every lookup you make through the company's resolver, including your personal browsing, and breaks whatever the local network was resolving.

Routing domains are the correct mechanism. A domain prefixed with ~ is not appended to short names; it only routes queries for that suffix to that link:

# route only corp.example.com through the VPN link
$ sudo resolvectl domain tun0 '~corp.example.com'
$ sudo resolvectl dns tun0 10.0.0.53

$ resolvectl domain
Link 3 (wlp0s20f3): localdomain
Link 8 (tun0): ~corp.example.com

Now db.corp.example.com goes to 10.0.0.53 and everything else goes to the Wi-Fi's server. To hand every query to a link, the routing domain is ~., a tilde and a dot: the root, meaning "everything". Seeing ~. on a VPN link tells you it has claimed all your DNS, which is sometimes intended and is worth knowing about either way.

resolvectl revert tun0 undoes per-link settings made this way.

6.3 Containers Resolve Names Differently From Their Host

A container does not inherit your machine's resolver settings, and the difference bites during exactly the debugging session where you can least afford a surprise. What it gets depends on which Docker network it joined, and the two cases behave completely differently.

On the default bridge, the container is handed the host's upstream server and nothing else:

$ docker run --rm alpine cat /etc/resolv.conf
# Generated by Docker Engine.
...
nameserver 192.168.1.1

$ docker run -d --name dnsdb2 alpine sleep 60
$ docker run --rm alpine getent hosts dnsdb2
$ echo $?
2                             # nothing: container names do not resolve here

On a user-defined network, Docker inserts its own embedded resolver and container names start working:

$ docker network create dnstest
$ docker run --rm --network dnstest alpine cat /etc/resolv.conf
# Generated by Docker Engine.
...
nameserver 127.0.0.11

$ docker run -d --network dnstest --name dnsdb alpine sleep 60
$ docker run --rm --network dnstest alpine getent hosts dnsdb
172.18.0.2        dnsdb  dnsdb

That address, 127.0.0.11, is Docker's built-in DNS server, and it is what resolves one container name to another. This is why service names work in a Docker Compose stack without anyone configuring DNS: Compose creates a user-defined network for the project automatically.

Notice also that the host was using 127.0.0.53 and the container on the default bridge was given 192.168.1.1. Docker deliberately does not pass a loopback address into a container, because inside the container's network namespace it would point at the container itself.

Two practical consequences. First, when a containerised application cannot reach its database, test resolution inside the container, not on the host, because they are answering from different resolvers. Second, when names do not resolve in a stack, check which network the containers are on before you look at anything else:

$ docker exec -it mycontainer cat /etc/resolv.conf
$ docker exec -it mycontainer getent hosts database

The same idea scales up. Kubernetes runs a cluster DNS service, usually CoreDNS, and gives every service a name such as database.default.svc.cluster.local so applications can find each other without hard-coded addresses. Inside modern platforms, DNS is the service-discovery layer rather than an internet directory.

6.4 Running Your Own Resolver, Without Becoming an Open One

There are good reasons to run a real recursive resolver on Linux: caching for a whole office, your own DNSSEC validation, independence from a third party, and logs you control. unbound is the usual choice; BIND can do it too, and dnsmasq is a lighter forwarder-with-a-cache that many home routers already run.

There is one way to get this badly wrong, and it is worth stating plainly. A recursive resolver that answers anyone on the internet is an open resolver, and it will be found and abused within days.

The abuse is amplification. DNS traditionally runs over UDP, which does not verify who sent a packet, so an attacker sends small queries with the victim's address forged as the source. Your server dutifully sends much larger answers to the victim. A modest request becomes a flood, the attacker spends almost nothing, and the traffic arrives from your address rather than theirs.

Preventing it is not complicated:

  • Restrict recursion to clients you trust, meaning your own networks. This one setting removes the entire problem.
  • Keep the two roles separate. A server that publishes your zones to the world is authoritative and needs no recursion at all. A server that resolves for your staff is recursive and should not be reachable from outside. Combining them on one public address is how this usually happens by accident.
  • Turn on response rate limiting on anything that does face the internet.
  • Bind to the right interface. A resolver listening on 0.0.0.0 on a machine with a public address is exposed even if you believed it was internal.

The general principle carries beyond DNS: before exposing any server, know which role it is meant to play, because the correct configuration differs per role and the failure mode here is that somebody else uses your bandwidth to attack a stranger.

6.5 Bypassing a Layer on Purpose

When you suspect one layer, take it out of the picture rather than reasoning about it. Each of these skips something specific:

$ resolvectl query --cache=no petermartin.nl    # skip the local cache
$ resolvectl query --protocol=dns NAME          # skip mDNS and LLMNR
$ dig @192.168.1.1 petermartin.nl               # skip the local daemon entirely
$ dig @1.1.1.1 petermartin.nl                   # skip the local network's server too
$ getent ahosts NAME                            # include every layer, like an application

Work down that list and the first command whose answer changes tells you which layer was responsible. It is faster than reading configuration files, and it works on a machine whose configuration you have never seen.

Back to top

7. Something Most Users Do Not Know

7.1 getent hosts and getent ahosts Call Different Functions

Section 4.3 showed the two commands returning the same addresses in a different order. That is not cosmetic. They use different C library calls, and the man page says so:

  • getent hosts NAME calls gethostbyname2(), the legacy interface from the 1980s.
  • getent ahosts NAME calls getaddrinfo() with AF_UNSPEC, which is what every modern application uses.

Only getaddrinfo() applies destination address selection, the rules that decide whether to try IPv4 or IPv6 first. So getent ahosts returns the addresses in the order your software will actually attempt them, and getent hosts returns them in whatever order the lookup produced.

This matters when you are debugging a connection that fails for some users. The question is never "does the name resolve", it is "which address is tried first", and only one of these two commands answers it.

7.2 /etc/gai.conf Decides IPv4 or IPv6, and It Is Entirely Commented Out

The ordering rules that getaddrinfo() applies come from a policy table, and that table lives in a file most administrators have never opened:

$ grep -vE '^\s*#|^\s*$' /etc/gai.conf
                              # nothing: every line is a comment

An empty result does not mean the file does nothing. It means glibc is using its compiled-in defaults, which prefer IPv6 over IPv4. The file exists so you can override them, and the shipped comments tell you exactly how:

#precedence  2002::/16     30
#precedence ::/96          20
#precedence ::ffff:0:0/96  10
#
#    For sites which prefer IPv4 connections change the last line to
#
#precedence ::ffff:0:0/96  100

Uncomment that last line and the machine prefers IPv4 everywhere, for every program, without touching DNS at all. ::ffff:0:0/96 is the IPv4-mapped address range, and raising its precedence above the IPv6 entries flips the order.

The order in which a machine tries IPv4 and IPv6 is not a DNS setting, not a routing setting and not an application setting. It is a policy table in a glibc configuration file, and on a stock system every line in it is a comment.

This is the real fix for the classic "the site is slow to load but fast once it loads" complaint on a network with broken IPv6. The machine tries the AAAA address, waits for a timeout, and only then falls back. People reach for removing the AAAA record, which is the wrong end of the problem and affects every visitor in the world; the right end is one line on the affected machine.

7.3 The Cache Is There, and You Cannot Read It Without Root

systemd-resolved has commands to show you its cache contents and its hit statistics. Run them as an ordinary user and you get this:

$ resolvectl show-cache
Failed to connect to query monitoring service
/run/systemd/resolve/io.systemd.Resolve.Monitor: Permission denied

The monitoring socket is privileged, because the contents of a DNS cache are a record of everything every user on the machine has looked at. The same applies to resolvectl statistics and resolvectl monitor, which streams queries live and is the closest thing Linux has to watching name resolution happen. All three work under sudo.

Being denied is itself informative: it confirms a caching daemon is running and reachable. On a machine with no local cache the failure looks completely different.

7.4 Negative Answers Are Not Cached By Default

The default is Cache=no-negative, and the manual is explicit about what that means: only positive answers are cached.

So "this name does not exist" is asked again every single time. Publish a record that was previously missing and it works immediately on this machine, with no cache to flush, while a positive answer you changed keeps returning the old value for its full TTL. Two changes, two completely different waiting times, from one setting nobody knew was there.

There is a second default in the same file worth knowing: CacheFromLocalhost=no. If your upstream server is on 127.0.0.1 - a local unbound or dnsmasq - systemd-resolved deliberately does not cache its answers, because that server already has a cache and two of them would only add confusion.

7.5 A Browser Can Ignore All of This

Everything in this article describes what happens when a program asks the operating system. A modern browser does not have to. With DNS over HTTPS enabled in its own settings, it sends lookups to a resolver of its own choosing over port 443, bypassing nsswitch.conf, systemd-resolved, your /etc/hosts file and your VPN's routing domains at the same time.

This produces a genuinely confusing failure: getent, dig and resolvectl all agree, and the browser still loads something else. Or you add a line to /etc/hosts to test a new server, every command-line tool honours it, and the browser does not.

When a name behaves differently in one browser than everywhere else on the same machine, check that browser's own DNS setting before you check anything belonging to the operating system.

7.6 Knowing Where the Client Side Stops

Part of expertise is knowing which layer a problem belongs to. This table is the one worth keeping:

SymptomIs it this machine?Where to look
One machine resolves a name differently from all the others Yes /etc/hosts, then the search list
A name works in the shell and not in a browser Yes The browser's own DNS over HTTPS setting
A name works on the host and not in a container Yes Which Docker network the container is on
Slow first connection, fast afterwards Often IPv6 timing out; see /etc/gai.conf in 7.2
Settings revert after a reboot Yes You edited /etc/resolv.conf; change the generator instead
Internal names stopped resolving after connecting a VPN Yes Routing domains: a link may have claimed ~.
A record you changed still returns the old value everywhere No The TTL. Nothing on this machine will help
The domain resolves nowhere at all, suddenly No The domain: expiry, DNSSEC, or a dead delegation
Mail is rejected or lands in spam No The zone's MX, SPF, DKIM and DMARC records

The last three rows are the boundary. When the answer is the same everywhere and simply wrong, no amount of local configuration will change it, and the question has moved from your machine to the domain and its records.

Back to top

8. Best Practices

  • Never edit /etc/resolv.conf. On most systems it is a generated symlink. Change the setting where it is produced: the NetworkManager connection profile, /etc/systemd/resolved.conf, or the DHCP server that handed it out.
  • Use resolvectl status to find the real servers. The nameserver line in resolv.conf is almost always 127.0.0.53, which tells you nothing about the network.
  • Prefer getent ahosts over getent hosts. It calls getaddrinfo() and returns addresses in the order applications will actually try them.
  • Use the right tool for the question. getent for "what does this machine do", resolvectl query for "what does the local resolver have and from where", dig for "what does DNS itself say". When they disagree, that disagreement is the diagnosis.
  • Check /etc/hosts first when one machine disagrees with all the others. It costs five seconds and it is the answer more often than anything else.
  • Remove hosts-file entries when the migration is over. An override pointing at a decommissioned server produces a failure nobody can explain because nothing is wrong with the domain.
  • Test container DNS inside the container. A container answers from a different resolver than its host, and container names only resolve on a user-defined network, not on the default bridge.
  • Use routing domains for a VPN, not a global override. ~corp.example.com on the tunnel link routes only what belongs there. A link holding ~. has taken every lookup you make.
  • Prefer DNSSEC=allow-downgrade to DNSSEC=yes unless you are prepared for a stranger's broken signature to break your resolution.
  • Test DNSOverTLS=yes before you rely on it. It does not fall back, which is correct and will also cut you off on a captive-portal network.
  • Never expose a recursive resolver to the internet. Restrict recursion to your own networks, keep authoritative and recursive roles on separate servers, and check what your resolver is bound to rather than what you assume.
  • Remember that flushing the local cache flushes only the local cache. Your router, your ISP and your browser hold their own copies and none of them is affected.
  • Read the documentation on the machine. man resolved.conf lists every setting with its default, man nsswitch.conf explains the action syntax in brackets, man gai.conf covers address ordering, and resolvectl --help lists twenty subcommands most people never discover.
Back to top

9. Common Mistakes

9.1 Myth versus Reality

MythReality
"Set your DNS servers in /etc/resolv.conf." It is a generated symlink on most modern systems. Your edit disappears at the next reboot, or sooner.
"127.0.0.53 means something is broken." It is the normal systemd-resolved stub listener. The real servers are behind it, per interface.
"dig shows what my machine will do." dig talks to a name server directly. It never reads /etc/hosts and does not use the search list unless you pass +search.
"getent hosts and getent ahosts are the same thing." Different C library calls. Only ahosts uses getaddrinfo() and returns addresses in the order applications will try them.
"DNS is the first thing Linux consults for a name." It is nearly the last. /etc/nsswitch.conf puts files ahead of it, and mDNS can stop the search before DNS is reached.
"Flushing the cache makes a changed record appear." It clears this machine only. Your router, your ISP's resolver and your browser each hold their own copy.
"A negative answer is cached like any other." Not by systemd-resolved: Cache=no-negative is the default, so "does not exist" is re-asked every time.
"IPv6 preference is a DNS setting." It is /etc/gai.conf, a glibc policy table that has nothing to do with DNS and is shipped entirely commented out.
"Connecting to the VPN broke my DNS." The VPN link probably claimed the routing domain ~. and is now answering every query, including ones its server knows nothing about.
"The container uses the host's DNS settings." It gets its own resolv.conf, and on a user-defined Docker network that is 127.0.0.11, an entirely different resolver.
"If every tool on the machine agrees, the browser will too." Not with DNS over HTTPS enabled in the browser, which bypasses the operating system completely.

9.2 Traps to Avoid

  • Editing the symlink target under /run. It is a temporary filesystem. The file is gone at the next boot and you will have forgotten you changed it.
  • Replacing the symlink with a static file "to make it stick". It does stick, and it also switches off per-interface DNS, so your VPN's internal names stop resolving and nothing explains why.
  • Leaving a test entry in /etc/hosts. Six months later the server is gone and one machine in the company cannot reach a site that works everywhere else.
  • Debugging container name resolution from the host. They are answering from different resolvers, so the host proves nothing about the container.
  • Assuming a name that fails in a container is a DNS problem. On the default bridge, container names are not resolvable at all; that is the network's design, not a fault.
  • Setting DNSSEC=yes on a laptop. A validation failure in somebody else's zone becomes your outage, and captive portals routinely break validation on purpose.
  • Trusting a single ping to prove resolution works. ping follows NSS, so it may be answering from /etc/hosts or mDNS while DNS is completely broken.
  • Forgetting that dig does not apply the search list. A single-label name that works for every application will look like it does not exist.
  • Running a resolver bound to 0.0.0.0 on a machine with a public address. That is an open resolver, and it will be found and abused within days.
  • Concluding "it is DNS" from a slow page. Resolution happens once and is then cached. A slow page is almost always the application or the network path.
Back to top

10. Summary

On Linux, resolving a name is a stack, not a lookup. Almost everything confusing about it comes from asking one layer a question that another layer answered.

  • Four layers sit between an application and the network: the application itself, NSS (/etc/nsswitch.conf), the stub resolver (/etc/resolv.conf), and a local caching daemon such as systemd-resolved.
  • DNS is nearly the last source consulted, not the first. files comes before dns in nsswitch.conf, so /etc/hosts wins for every application on the machine.
  • /etc/resolv.conf is a generated symlink containing 127.0.0.53, the local stub. It cannot tell you which servers the machine really uses; resolvectl status can, per interface.
  • Three tools ask three different layers. getent ahosts behaves like an application, resolvectl query asks the local daemon and reports where the answer came from, and dig asks a name server directly. Their disagreement is the diagnosis.
  • getent hosts calls gethostbyname2(); getent ahosts calls getaddrinfo(). Only the second returns addresses in the order software will actually try them.
  • /etc/gai.conf decides IPv4 versus IPv6 preference for every program on the machine. It ships entirely commented out, and one uncommented line fixes the slow-first-connection problem caused by broken IPv6.
  • Per-interface DNS is why the daemon exists. A routing domain written ~corp.example.com routes only that suffix through a link; ~. routes everything, which is what a VPN that "broke DNS" has usually done.
  • Containers resolve differently. The default bridge gives the host's upstream server and no container names; a user-defined network gives Docker's own resolver on 127.0.0.11, and names work.
  • systemd-resolved caches positive answers only (Cache=no-negative), and its cache is readable only as root.
  • A recursive resolver exposed to the internet is an open resolver and becomes an amplification weapon within days. Restrict recursion, and keep authoritative and recursive roles apart.
  • A browser with DNS over HTTPS ignores all of this, which is why it can disagree with every command-line tool on the same machine.

The commands worth keeping within reach:

WHAT IS THIS MACHINE USING
  resolvectl status              per-link servers, DNSSEC and DoT state
  resolvectl dns                 just the servers
  resolvectl domain              search domains, and ~routing domains
  nmcli dev show | grep DNS      what NetworkManager handed over
  grep '^hosts:' /etc/nsswitch.conf    the order that decides everything

WHAT DOES A NAME RESOLVE TO
  getent ahosts NAME             like an application, correct ordering
  resolvectl query NAME          + which link, cached or not, validated or not
  dig NAME                       DNS itself, ignoring every local layer
  dig +search NAME               with the search list, like an application

NARROWING IT DOWN
  resolvectl query --cache=no NAME     skip the local cache
  dig @192.168.1.1 NAME                skip the local daemon
  dig @1.1.1.1 NAME                    skip the local network
  sudo resolvectl monitor              watch queries as they happen

CHANGING IT, WHERE IT IS GENERATED
  nmcli connection modify ...          per network profile
  /etc/systemd/resolved.conf           DNS=, DNSSEC=, DNSOverTLS=, Cache=
  /etc/hosts                           override one name for everything
  resolvectl dns LINK SERVER           temporary; resolvectl revert LINK

DOCUMENTATION
  man resolved.conf   man nsswitch.conf   man gai.conf   resolvectl --help

And when a name resolves correctly on every machine but one, the cause is almost never the domain. It is a line somebody added to /etc/hosts during a migration that finished a long time ago.

Back to top
Linux concept: DNS resolver
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.