open atlas
↑ Back to track
Linux, the operating system LIN · 09 · 03

DNS resolution

Name resolution follows the nsswitch hosts: line — files then dns by default. /etc/hosts is checked first; /etc/resolv.conf (or systemd-resolved at 127.0.0.53) provides the DNS server. getent follows nsswitch; dig bypasses it and talks directly to a nameserver.

LIN Middle ◷ 20 min
Level
FoundationsJuniorMiddleSenior

dig google.com resolves fine, but curl https://api.internal fails with “Name or service not known.” Or the reverse: your /etc/hosts entry is being ignored. These failures are almost always about not knowing the full resolution path: the kernel does not query DNS directly — it asks the C library (glibc), which follows a policy file (nsswitch.conf), which may say “check /etc/hosts first, then DNS.” And “DNS” on a modern Ubuntu system means a local stub at 127.0.0.53 run by systemd-resolved, not the nameserver in /etc/resolv.conf directly. Knowing where each piece lives and what order they fire in turns a mysterious “name not found” into a two-minute diagnosis.

Goal

After this lesson you can trace the full name resolution path on a Linux host, read /etc/nsswitch.conf to understand resolution order, check and override /etc/resolv.conf, understand systemd-resolved’s role as a stub at 127.0.0.53, use getent hosts to test resolution as the application sees it, and use dig to test DNS directly.

1

The resolution path starts at nsswitch.conf — not DNS.

/etc/nsswitch.conf is the policy file that tells glibc which sources to consult for name resolution, and in what order.

grep ^hosts /etc/nsswitch.conf
# hosts:    files dns

# Meaning:
# 1. files  → check /etc/hosts first
# 2. dns    → then query DNS (via /etc/resolv.conf or systemd-resolved)

# On Ubuntu 22.04+ with systemd-resolved:
# hosts:    files mdns4_minimal [NOTFOUND=return] dns
# mdns4_minimal handles .local names (Avahi/mDNS)
# [NOTFOUND=return] means: if mdns says "not found", stop — do NOT fall through to dns
# This is why *.local names never reach your DNS server

The application calls getaddrinfo("api.internal"). glibc reads nsswitch.conf and calls each source in order until one returns a result or an authoritative “not found.”

2

/etc/hosts — the first source in the chain.

/etc/hosts is a static file checked before any network query (when files appears first in nsswitch.conf). Each line maps an IP to one or more names.

cat /etc/hosts
# 127.0.0.1   localhost
# 127.0.1.1   host-a
# ::1         localhost ip6-localhost ip6-loopback
# 10.0.0.20   db-primary db-primary.internal

# Add a temporary override (useful for testing before DNS propagates):
echo '10.0.0.100  staging-api.internal' | sudo tee -a /etc/hosts

# Test it immediately (getent follows nsswitch, so /etc/hosts wins):
getent hosts staging-api.internal
# 10.0.0.100  staging-api.internal

# Remove the override when done — /etc/hosts is not DNS, stale entries confuse everyone

/etc/hosts is the fastest possible override for a name. The OS reads it on every lookup — no cache to flush. The risk: entries accumulate and conflict with real DNS records months later.

3

/etc/resolv.conf — which DNS server to use.

When the dns source fires, glibc reads /etc/resolv.conf to find the nameserver address.

cat /etc/resolv.conf
# nameserver 127.0.0.53          ← systemd-resolved stub (Ubuntu default)
# options edns0 trust-ad
# search internal.example.com    ← appended to unqualified names

# Or on a Debian system without systemd-resolved:
# nameserver 10.0.0.1            ← the router/DNS relay

# The search directive:
# If you query "db-primary" (no dots), the resolver appends the search domain:
# → tries "db-primary.internal.example.com" first
# If that fails, falls back to "db-primary" as-is

# Check what resolver is actually in use:
resolvectl status   # on systemd-resolved systems
# or
cat /run/systemd/resolve/resolv.conf  # the real upstream config

A common gotcha: /etc/resolv.conf is a symlink (lrwxrwxrwx /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf) on Ubuntu. If a tool like dhclient or openvpn overwrites it with a plain file, you bypass systemd-resolved and lose its caching and split-DNS features without noticing.

4

systemd-resolved: the stub at 127.0.0.53.

On Ubuntu 18.04+ and Debian 12+, systemd-resolved runs a local DNS stub that listens on 127.0.0.53:53. All DNS queries from glibc go there; resolved handles caching, DNSSEC, and split-DNS routing to different upstreams per interface.

# Check if systemd-resolved is running:
systemctl status systemd-resolved

# Query the stub directly:
dig @127.0.0.53 example.com

# See what upstreams resolved is using per interface:
resolvectl status
# Global
#   DNS Servers: 10.0.0.1
# Link 2 (ens3)
#   Current DNS Server: 10.0.0.1
#   DNS Servers: 10.0.0.1
#   DNS Domain: internal.example.com

# Flush the resolved cache (useful when a DNS record changed):
sudo resolvectl flush-caches

# Check if a name resolves and where it came from:
resolvectl query db-primary.internal
# db-primary.internal: 10.0.0.20
#   -- Information acquired via protocol DNS ...

Resolved is not just a proxy — it maintains a per-link DNS routing table. VPN tunnels can inject DNS servers for .vpn.internal without overriding the global resolver, which is why resolvectl status shows different DNS per interface.

5

getent hosts vs dig: the critical difference.

# getent hosts follows the full nsswitch chain — same path as your application
getent hosts db-primary
# 10.0.0.20  db-primary

# dig talks directly to a nameserver — bypasses /etc/hosts and nsswitch entirely
dig db-primary
# ;; ANSWER SECTION:
# db-primary.    0  IN  A  10.0.0.20
# (or NXDOMAIN if it is only in /etc/hosts — dig will not find it)

# Practical rule:
# Use getent hosts to test what the application will see
# Use dig to test pure DNS (bypassing local overrides)

# dig with explicit server:
dig @127.0.0.53 db-primary.internal      # test systemd-resolved stub
dig @10.0.0.1 db-primary.internal        # test upstream nameserver directly

# Query type (default is A/AAAA):
dig MX example.com
dig TXT example.com
dig AAAA example.com

The classic mismatch: dig api.internal returns NXDOMAIN, but the app works because the name is in /etc/hosts. Or the reverse: dig api.internal returns an IP, but the app gets NXDOMAIN because a stale /etc/hosts entry shadows the DNS record and points nowhere.

Worked example

Diagnosing “Name or service not known” for an internal host.

App on host-a (10.0.0.10) cannot reach db-primary.internal. Error: getaddrinfo: Name or service not known.

# Step 1: does getent resolve it? (tests the full nsswitch path)
getent hosts db-primary.internal
# (empty — not found via files or dns)

# Step 2: is it in /etc/hosts?
grep db-primary /etc/hosts
# (empty)

# Step 3: does dig resolve it? (pure DNS test)
dig @127.0.0.53 db-primary.internal
# ;; ANSWER SECTION:
# db-primary.internal.  60  IN  A  10.0.0.20
# DNS says yes — the name exists in DNS

# Step 4: but getent failed — why? Check nsswitch
grep ^hosts /etc/nsswitch.conf
# hosts:  files mdns4_minimal [NOTFOUND=return] dns
# mdns4_minimal with [NOTFOUND=return] is blocking .internal before dns fires
# .internal is not an mDNS domain, mdns4_minimal says NOTFOUND, and the
# [NOTFOUND=return] action stops the chain — dns is never reached

# Fix: edit nsswitch.conf to remove the blocking action for non-.local names
# or add db-primary.internal to /etc/hosts as a workaround:
echo '10.0.0.20  db-primary.internal' | sudo tee -a /etc/hosts

getent hosts db-primary.internal
# 10.0.0.20  db-primary.internal — now works
Common mistake

/etc/resolv.conf gets overwritten silently. Tools like dhclient, openvpn, resolvconf, and NetworkManager all want to manage /etc/resolv.conf. On a system that expects it to be a symlink to systemd-resolved’s stub, any of these tools can replace the symlink with a regular file containing a different nameserver — and you lose caching, DNSSEC, and split-DNS without any error message. Check with ls -la /etc/resolv.conf: if it is a regular file instead of a symlink, something replaced it. Restore with sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf.

Check yourself
Quiz

You run dig api.internal and get a valid A record. Then you run getent hosts api.internal and get nothing. What is the most likely explanation?

Recap

Name resolution on Linux is not just DNS. getaddrinfo() follows the nsswitch.conf hosts: line, checking files (/etc/hosts) before dns. DNS on modern Ubuntu/Debian means the systemd-resolved stub at 127.0.0.53, which caches, validates DNSSEC, and routes queries to per-interface upstreams. /etc/resolv.conf tells glibc where the DNS resolver is — on Ubuntu it is a symlink to the stub; overwriting it with a plain file breaks resolved silently. getent hosts tests the full resolution chain as the application sees it. dig bypasses nsswitch entirely and talks directly to a nameserver — use it to test pure DNS. The [NOTFOUND=return] trap in nsswitch is the most common reason why DNS works but getent/the application does not.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources1
expand
  1. 01

Trademarks belong to their respective owners. Editorial reference only.