Skip to content

DNS Resolution

Turning a name into an address on Linux passes through nsswitch.conf, /etc/hosts, the resolver configuration and often a local stub resolver before any DNS server is asked. Knowing that path, and which tools skip parts of it, separates a client-side problem from a server-side one.

Track: Core · Interview weight: High


Must-Know Facts

Fact Value Verify with
Lookup order hosts: line in /etc/nsswitch.conf; files = /etc/hosts, dns = resolvers in /etc/resolv.conf grep ^hosts /etc/nsswitch.conf
RHEL hosts: files dns myhostname; /etc/resolv.conf written by NetworkManager head -1 /etc/resolv.conf
Ubuntu systemd-resolved stub at 127.0.0.53; /etc/resolv.conf is a symlink to stub-resolv.conf ls -l /etc/resolv.conf, resolvectl status
Application view getent hosts <name> follows nsswitch like applications getent ahostsv4 <name>
DNS view dig, host, nslookup query DNS only and ignore /etc/hosts (the resolved stub serves hosts entries itself) dig @<server> <name>
resolv.conf limits Up to 3 nameserver lines; search domains; options timeout:N attempts:N cat /etc/resolv.conf
Search list Short names get each search domain appended getent hosts db
Status codes NOERROR, NXDOMAIN (name does not exist), SERVFAIL (server failed), REFUSED (policy) status: line of dig
aa flag Authoritative answer from the zone's own server dig @<server> <name>
TTL Seconds a resolver may cache the record; counts down in cached answers dig +noall +answer twice
Record types A, AAAA, CNAME, MX, NS, TXT, SOA, PTR (reverse, dig -x), SRV dig <name> <type>
Transport UDP 53, TCP 53 for large answers and zone transfers ss -ulpn 'sport = :53'
Split DNS resolved routing domain Domains=~zone sends only that zone to a server resolvectl domain
Cache control resolvectl flush-caches, resolvectl statistics sudo resolvectl statistics

The Resolution Path

flowchart LR
    app["Application<br/>getaddrinfo()"] --> nss["/etc/nsswitch.conf<br/>hosts: files dns"]
    nss --> files["/etc/hosts"]
    nss --> rc["/etc/resolv.conf"]
    rc --> stub["systemd-resolved<br/>127.0.0.53 (Ubuntu)"]
    rc --> up
    stub --> up["Upstream DNS servers"]
grep ^hosts /etc/nsswitch.conf
cat /etc/resolv.conf
systemctl is-active systemd-resolved

Output:

hosts:      files  dns myhostname
# Generated by NetworkManager
nameserver 1.1.1.1
inactive
grep ^hosts /etc/nsswitch.conf
ls -l /etc/resolv.conf
grep -v '^#' /etc/resolv.conf | grep .

Output:

hosts:          files dns
lrwxrwxrwx 1 root root 39 Sep 17 11:55 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search lab.internal

myhostname on RHEL resolves the machine's own hostname even when nothing else knows it. On Ubuntu, applications talk to the stub, and resolvectl status shows the real upstream servers, per interface.

/etc/hosts wins over DNS

With files first, a stale /etc/hosts line silently overrides the zone. It is the first file to check when one host resolves a name differently from the others.


Querying DNS with dig

The lab zone shop.internal is served by BIND on web (172.16.1.3), which does not recurse for other names:

dig @172.16.1.3 api.shop.internal

Output:

; <<>> DiG 9.18.39-0ubuntu0.24.04.7-Ubuntu <<>> @172.16.1.3 api.shop.internal
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54385
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: d6fcc48743c63d08010000006aabd642b57f7060a2385dc2 (good)
;; QUESTION SECTION:
;api.shop.internal.     IN  A

;; ANSWER SECTION:
api.shop.internal.  300 IN  A   172.16.1.3

;; Query time: 0 msec
;; SERVER: 172.16.1.3#53(172.16.1.3) (UDP)
;; WHEN: Thu Sep 17 12:00:02 UTC 2026
;; MSG SIZE  rcvd: 90
Part Meaning
status: NOERROR The query succeeded (an empty answer is still NOERROR)
flags: qr aa rd Response, authoritative answer, recursion desired by the client
WARNING: recursion requested... The server has recursion no; ra is missing from the flags
ANSWER SECTION Name, TTL in seconds, class, type, value
SERVER Which server answered; with no @, the first nameserver in resolv.conf
dig @172.16.1.3 shop.internal MX +short
dig @172.16.1.3 shop.internal NS +short
dig @172.16.1.3 shop.internal TXT +short
dig @172.16.1.3 -x 172.16.1.10 +short
dig @172.16.1.3 www.shop.internal +noall +answer
host api.shop.internal
nslookup db.shop.internal

Output:

10 mail.shop.internal.
ns1.shop.internal.
"v=spf1 -all"
db.shop.internal.
www.shop.internal.  300 IN  CNAME   api.shop.internal.
api.shop.internal.  300 IN  A   172.16.1.3
api.shop.internal has address 172.16.1.3
Server:     127.0.0.53
Address:    127.0.0.53#53

Non-authoritative answer:
Name:   db.shop.internal
Address: 172.16.1.10

-x builds the reverse name (10.1.16.172.in-addr.arpa) and asks for its PTR. The CNAME answer includes the target's A record. host and nslookup went through the stub, which answered Non-authoritative because it relayed a cached or forwarded answer.

Status Codes and Failures

dig @172.16.1.3 one.one.one.one | grep -E 'status|WARNING'
dig @172.16.1.3 nosuch.shop.internal | grep status
dig @172.16.1.99 api.shop.internal +tries=1 +time=2 | head -3

Output:

;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 981
;; WARNING: recursion requested but not available
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 9502
;; communications error to 172.16.1.99#53: timed out

; <<>> DiG 9.18.39-0ubuntu0.24.04.7-Ubuntu <<>> @172.16.1.99 api.shop.internal +tries=1 +time=2
Result Likely cause
NXDOMAIN The name does not exist in the zone that was asked (or the wrong zone was asked)
REFUSED The server will not answer this client or this name (ACL, no recursion)
SERVFAIL The server tried and failed: zone not loaded, upstream down, DNSSEC validation failure
timed out Nothing listening, or UDP 53 blocked on the path
connection refused Host reachable, no DNS service on port 53

With named stopped on web, the stub on client turns the failed upstream into SERVFAIL:

dig nothere2.shop.internal | grep -E 'status|SERVER'

Output:

;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 1749
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)

systemd-resolved

Ubuntu's stub caches answers. A second query five seconds later shows the TTL counting down:

sudo resolvectl flush-caches
dig +noall +answer api.shop.internal
sleep 5; dig +noall +answer api.shop.internal
sudo resolvectl statistics | grep -iE 'cache|hits|misses'

Output:

api.shop.internal.  300 IN  A   172.16.1.3
api.shop.internal.  294 IN  A   172.16.1.3
Cache                                        
                         Current Cache Size:  1
                                 Cache Hits: 19
                               Cache Misses: 33

A routing domain sends only one zone to an internal server and leaves public names on the link's servers. The drop-in /etc/systemd/resolved.conf.d/shop.conf holds DNS=172.16.1.3 and Domains=~shop.internal:

resolvectl dns
resolvectl domain

Output:

Global: 172.16.1.3
Link 2 (eth0): 1.1.1.1 8.8.8.8
Global: ~shop.internal
Link 2 (eth0): lab.internal

lab.internal without ~ is a search domain; ~shop.internal only routes queries. Adding shop.internal as a search domain at runtime makes the short name work:

getent hosts db
sudo resolvectl domain eth0 lab.internal shop.internal
getent hosts db
grep ^search /etc/resolv.conf

Output:

172.16.1.10     db.shop.internal
search lab.internal shop.internal

The first getent printed nothing: db.lab.internal does not exist. sudo resolvectl revert eth0 drops the runtime change.

Resolve through the same path as the application

resolvectl query <name> and getent hosts <name> use the stub and /etc/hosts; dig @<server> bypasses both. Compare the two before deciding where the fault is.


Several Servers on RHEL

With NetworkManager, the servers and options come from the profile:

sudo nmcli connection modify dmz ipv4.dns "172.16.1.3 1.1.1.1" ipv4.dns-search lab.internal ipv4.dns-options "timeout:2 attempts:2"
sudo nmcli device reapply eth1 >/dev/null
cat /etc/resolv.conf
getent hosts api.shop.internal
getent hosts one.one.one.one

Output:

# Generated by NetworkManager
search lab.internal
nameserver 172.16.1.3
nameserver 1.1.1.1
options timeout:2 attempts:2
172.16.1.3      api.shop.internal
2606:4700:4700::1111 one.one.one.one
2606:4700:4700::1001 one.one.one.one

The glibc resolver asks the servers in order and moves on after a timeout or a REFUSED, so the public name still resolved through 1.1.1.1. glibc has no routing domains: every name goes to the first server first, which is why split DNS on RHEL uses systemd-resolved or dnsmasq.


Common Errors

curl: (6) Could not resolve host: api.shop.internal

Cause: no source in nsswitch.conf knows the name, often because the internal zone's server is not configured on the client.

Fix: getent hosts <name>, then resolvectl status or /etc/resolv.conf, then dig @<internal server> <name>. See DNS Not Resolving.

;; communications error to 172.16.1.99#53: timed out

Cause: no DNS server at that address, or a firewall drops port 53.

Fix: ping the server, nc -vzu <server> 53, check security groups and the server's firewall.

;; WARNING: recursion requested but not available

Cause: the server is authoritative only and refuses names outside its zones.

Fix: point clients at a recursive resolver, or route only the zone to this server.

ping: api.shop.internal: Name or service not known

Cause: the same lookup failure as above, reported by getaddrinfo().

Fix: as for Could not resolve host.


Interview Checkpoints

L1: What happens when a program looks up a hostname on Linux?

Say first: glibc reads the hosts: line in nsswitch.conf, checks /etc/hosts, then asks the servers in /etc/resolv.conf, which on Ubuntu is the local systemd-resolved stub.

Proof: grep ^hosts /etc/nsswitch.conf; cat /etc/resolv.conf; resolvectl status.

Follow-up: Which tools skip /etc/hosts?

L1: What is the difference between NXDOMAIN and SERVFAIL?

Say first: NXDOMAIN is a definite answer that the name does not exist; SERVFAIL means the server could not produce an answer.

Proof: dig @172.16.1.3 nosuch.shop.internal gives NXDOMAIN; with named stopped, the resolved stub answers SERVFAIL for the same zone.

Follow-up: Which one is cached, and for how long? (Negative answers, for the SOA minimum.)

L2: Query the MX and TXT records of a domain from a specific server.

Say first: dig @server domain MX and TXT, with +short for the values.

Proof: dig @172.16.1.3 shop.internal MX +short prints 10 mail.shop.internal.

Follow-up: How do you find the authoritative servers for a public domain? (dig NS, dig +trace.)

L2: Find the hostname for 172.16.1.10.

Say first: a reverse lookup of the PTR record.

Proof: dig -x 172.16.1.10 +short; getent hosts 172.16.1.10.

Follow-up: Who controls reverse zones for public addresses?

L2: Read this: dig shows TTL 294 on a second query. What does it tell you?

Say first: the answer came from a cache, six seconds into a 300-second TTL.

Proof: dig +noall +answer twice through 127.0.0.53; sudo resolvectl statistics shows cache hits.

Follow-up: Why lower TTLs before migrating a service?

L3: dig returns the right address but the application connects to an old one. Why?

Say first: the application uses nsswitch, so /etc/hosts, a local cache (resolved, nscd, the application's own) or a different resolver answers for it.

Proof: getent hosts <name> versus dig @<server> <name>; grep <name> /etc/hosts; resolvectl flush-caches.

Follow-up: How does the JVM cache DNS answers?

L3: Internal names stopped resolving after a VPN client was installed. What do you check?

Say first: which servers and routing domains are active now, and whether the VPN replaced them.

Proof: resolvectl status; ls -l /etc/resolv.conf; resolvectl query <internal name>.

Follow-up: How do routing domains avoid sending every query through the VPN?

L3: Name resolution is slow, about five seconds per lookup, but succeeds. What is the usual cause?

Say first: the first nameserver does not answer, so glibc waits for the timeout (5 s by default) before trying the next.

Proof: cat /etc/resolv.conf; dig @<first server> <name> times out; time getent hosts <name>.

Follow-up: Which options shorten the wait? (timeout:, attempts:, rotate.)


Captured on Ubuntu 24.04.4 (systemd 255, dig 9.18.39) and Rocky Linux 10.2 (NetworkManager 1.56, BIND 9.18.33) on iximiuz Labs FlexBox microVMs, kernel 6.1.167, 2026-09.