Bash and Linux

Read DNS Answers

easyNetworking and connectivity

Problem statement

Read saved dig output and pull out the answer: the status, the chain of names, the IP addresses and how long each answer may be cached. Then check whether the server's /etc/hosts file overrides DNS for that name. "It's always DNS" is a running joke because so many outages start there, and reading dig output is how you check.

dig.txt (from dig api.example.com)

◈ DIAGRAM
; <<>> DiG 9.18.28 <<>> api.example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4242
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; QUESTION SECTION:
;api.example.com. IN A
;; ANSWER SECTION:
api.example.com. 300 IN CNAME lb.example.net.
lb.example.net. 60 IN A 203.0.113.10
lb.example.net. 60 IN A 203.0.113.11
;; Query time: 12 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)

hosts (a copy of the server's /etc/hosts)

TEXT
127.0.0.1 localhost
10.0.0.50 api.example.com
  1. Print the status of the answer.
  2. Print each record in the answer section: name, type, value and TTL.
  3. Print just the IP addresses, as dig +short would show at the end.
  4. Say whether /etc/hosts overrides the name, and which address the server would really use.

Expected output:

TEXT
== status ==
status: NOERROR
== answer records ==
api.example.com. CNAME lb.example.net. ttl 300s
lb.example.net. A 203.0.113.10 ttl 60s
lb.example.net. A 203.0.113.11 ttl 60s
== just the addresses (like dig +short) ==
203.0.113.10
203.0.113.11
== does /etc/hosts override it? ==
yes: hosts file says 10.0.0.50, and it is checked before DNS

Hints

Hint 1: The status is in the header line after status:. The answer records are the lines after ;; ANSWER SECTION: up to the next empty line.

Approach

Optimal: dig answer section with awk

Covers: DNS records (A, AAAA, CNAME), TTL, dig output sections, status: (NOERROR, NXDOMAIN, SERVFAIL), dig +short, /etc/hosts and the lookup order, getent hosts, awk flags for "inside a section".

What a DNS lookup returns. You ask for a name, and DNS answers with records:

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart TB Q["api.example.com"]:::blue --> C["CNAME
lb.example.net"]:::purple C --> A1["A
203.0.113.10"]:::green C --> A2["A
203.0.113.11"]:::green classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
Type Means Example
A an IPv4 address 203.0.113.10
AAAA an IPv6 address 2001:db8::10
CNAME "this name is another name", follow it lb.example.net.
MX mail server
TXT text, often for verification

Here api.example.com is a CNAME for lb.example.net, which has two A records. Clients pick one, which is a simple form of load balancing.

TTL is how long to cache. The number after the name is the TTL (time to live) in seconds. Resolvers keep the answer that long before asking again. A 60-second TTL means a change takes up to a minute to reach everyone; a TTL of a day means a mistake lasts a day. Lower the TTL before a planned move.

The status line.

Status Means
NOERROR the name exists (the answer may still be empty for this type)
NXDOMAIN the name does not exist
SERVFAIL the DNS server failed to answer, often a broken zone or DNSSEC problem
REFUSED the server will not answer you

/etc/hosts can win. On most Linux systems, /etc/nsswitch.conf says hosts: files dns, which means the hosts file is checked first. An old line in /etc/hosts silently beats DNS, so dig (which asks DNS directly) shows one address while the app connects to another. getent hosts api.example.com shows what the system really uses.

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart TB APP{{"app looks up a name"}}:::purple --> H{{"in /etc/hosts?"}}:::yellow H --> HY["use the hosts entry
10.0.0.50"]:::red H --> D(["ask DNS"]):::blue D --> DA["203.0.113.10 or .11"]:::green classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px

Reading a section with awk. The answer lines come after a marker and end at an empty line. A flag handles that: /^;; ANSWER SECTION:/ { f = 1; next } turns it on, /^$/ { f = 0 } turns it off, and the pattern f means "only while the flag is on".

Walking through the code. The # Setup: lines only save the two sample files, so skip past them.

  1. grep -o 'status: [A-Z]*' pulls the status out of the header.
  2. The awk prints name, type, value and TTL for each answer line.
  3. The same flag with $4 == "A" prints only the addresses.
  4. A second awk looks for the name in the hosts file and reports the override.

Edge cases. Names in dig end with a dot, the root of DNS; strip it if you compare names. dig is in the dnsutils or bind-utils package and may be missing; nslookup, host and getent hosts are alternatives.

# Setup: save sample dig output and a hosts file in a fresh temporary folder
cd "$(mktemp -d)"
cat > dig.txt << 'OUT'
; <<>> DiG 9.18.28 <<>> api.example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4242
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; QUESTION SECTION:
;api.example.com.               IN      A

;; ANSWER SECTION:
api.example.com.        300     IN      CNAME   lb.example.net.
lb.example.net.         60      IN      A       203.0.113.10
lb.example.net.         60      IN      A       203.0.113.11

;; Query time: 12 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
OUT
printf '127.0.0.1   localhost\n10.0.0.50   api.example.com\n' > hosts
name=api.example.com

echo "== status =="
grep -o 'status: [A-Z]*' dig.txt

echo "== answer records =="
awk '/^;; ANSWER SECTION:/ { f = 1; next } /^$/ { f = 0 }
     f { printf "%-17s %-6s %-16s ttl %ss\n", $1, $4, $5, $2 }' dig.txt

echo "== just the addresses (like dig +short) =="
awk '/^;; ANSWER SECTION:/ { f = 1; next } /^$/ { f = 0 } f && $4 == "A" { print $5 }' dig.txt

echo "== does /etc/hosts override it? =="
awk -v n="$name" '$2 == n { print "yes: hosts file says " $1 ", and it is checked before DNS"; found = 1 }
                  END { if (!found) print "no: DNS answer is used" }' hosts

Interview follow-ups

  • Check that every name in a list resolves, and print the ones that do not.

    Loop over the names and use getent hosts, which follows the same order the system uses: while read -r n; do getent hosts "$n" > /dev/null || echo "FAILED: $n"; done < names.txt. getent exits non-zero when the name cannot be resolved. For checking DNS itself, ignoring the hosts file, use dig +short "$n" and treat empty output as a failure.

Frequently asked questions

Three usual causes. The hosts file has an old entry, which getent hosts name reveals. A cache somewhere still holds the old answer until its TTL runs out: the local resolver (resolvectl flush-caches on systemd-resolved), the app itself (Java caches DNS by default), or a proxy. Or dig is asking a different DNS server than the system uses; dig shows the one it asked on the SERVER: line, and /etc/resolv.conf shows what the system uses.

dig @8.8.8.8 api.example.com asks Google's public resolver instead of your default, which is the quickest way to tell "broken everywhere" from "broken on my resolver". dig +trace api.example.com starts at the root servers and follows the delegation down, showing where an answer goes wrong. dig api.example.com AAAA or MX asks for other record types. +short prints only the values.

NXDOMAIN means the authoritative servers say the name does not exist: a typo, a deleted record, or a zone that was not delegated. SERVFAIL means the resolver could not get a good answer at all: the authoritative servers are down or unreachable, the zone is broken, or DNSSEC checks failed. With SERVFAIL, try dig +trace and another resolver to find which step fails.