Old-School Networking / 2026 ・ No.09

Old-School Networking

Part 9: DNS — looking up addresses from names, the internet's phone book

Published

The Wireshark window: a DNS list recorded at the resolver, showing the PC's query for A expenses.example, queries and answers to the root (203.0.113.1), then 203.0.113.2 and 203.0.113.3, and the answer to the PC, A 203.0.113.20. Packet 5, the root's answer, is selected; under Authoritative nameservers the details show example: type NS, ns1.example and Time to live: 86400 (1 day)
The resolver's recording. Packet 5 is the root replying “I don't know expenses.example; for .example, ask ns1.example”, with permission to remember that referral for a day (86,400 seconds) (Recorded on 25 October 2026 on a practice network the author built in the cloud and opened in Wireshark 4.2.2. The bar across the top of the window and the headings were added for the illustration.)

Up to part 8 we told curl “expenses.example is 203.0.113.20” with --resolve. This time we drop that and let the real system, DNS, look up the name.
For practice we set up three tiers of DNS servers (the root, .example and expenses.example) and a resolver playing the provider's role. We measure the walk down the name tree, how long answers are remembered (TTL), disagreements with the hosts file, and what happens when DNS stops.

1

The naming layer

AraiAbout last time's --resolve: how does a name actually turn into an address?

FuruiNo single machine knows every name in the world. Names are split up like a tree, with someone in charge of each branch. If they don't know, they say “ask over there”, and you follow that.

AraiSo it's being passed from pillar to post.

FuruiYes, very well-organised pillar to post. And once an answer has been found, it's remembered for a while.

The 12-part route map (you are here: part 9)

1 Big picture 2 Physical 3 Data link 4 IP addresses 5 Routers 6 TCP 7 UDP, sockets 8 TLS 9 DNS 10 HTTP 11 Dev networks 12 Troubleshoot You are here Overview Layer 1 Layer 2 Layer 3 Layer 4 Layers 5-6 Layer 7 Practice
  • The name tree — the root, .example, expenses.example, each tier with its own servers
  • The resolver — walks the tree for the PC: 0.166 s the first time (measured)
  • TTL and caching — 0 ms while remembered, asked again when it expires (measured)
  • Developer pitfalls — hosts vs dig, and what a DNS outage looks like (measured)

In TCP/IP terms DNS is an application-layer (layer 7) system, but since it always runs before the application talks, it belongs here. The practice network puts the PC and the provider's resolver (Unbound) on the near side, and the three tiers of DNS servers (NSD) plus the expenses server beyond the mischief wire, 20 ms each way.

2

The name tree

Names are split into a tree, with DNS servers in charge of each branch

. root com jp example our TLD net … expenses the expenses zone www mail ns1 root server 203.0.113.1 .example server 203.0.113.2 expenses.example's 203.0.113.3 Read right to left: www . expenses . example . (last dot = root)

Read www.expenses.example from the right. The invisible dot at the far right is the root; below it is example, then expenses, then www. The root's DNS servers know who handles example, and example's know who handles expenses. Handing a branch to someone else like this is called delegation.

On the real internet the root has 13 names, a to m, run by 12 organisations; in practice more than a thousand copies with identical contents sit around the world, and the nearest one answers. For this practice the author made the root too (a.root.lab, 203.0.113.1).

3

The resolver's path

The PC doesn't walk the tree itself. It asks a resolver (a caching DNS server) provided by the home router or the provider, once: “what's the address of expenses.example?” Here's the recording at the resolver.

# Recorded at the resolver: for the PC's single question, it asked three places in turn (20 ms each way)
  0.000                PC → resolver          A expenses.example
  0.000          resolver → root              NS <Root>
  0.041              root → resolver          NS <Root> NS a.root.lab A 203.0.113.1
  0.042          resolver → root              A expenses.example
  0.082              root → resolver          A expenses.example NS ns1.example A 203.0.113.2
  0.083          resolver → .example          A expenses.example
  0.124          .example → resolver          A expenses.example NS ns1.expenses.example A 203.0.113.3
  0.124          resolver → expenses.example  A expenses.example
  0.165  expenses.example → resolver          A expenses.example A 203.0.113.20 NS ns1.expenses.example A 203.0.113.3
  0.166          resolver → PC                A expenses.example A 203.0.113.20

The PC asks once; the resolver does the walking (measured times)

PC resolver root .example expenses root check ask .example ask expenses 203.0.113.20 answer 0 166 The PC asks once; the resolver follows the chain (3 RTTs + 1 initial check)

The resolver first checks that the root is alive (the root's NS) and then asks the root. The root's answer is a referral: “I don't know. For .example, ask ns1.example (203.0.113.2)” (the cover photo). .example refers it on to expenses.example in the same way, and finally expenses.example's DNS server answers 203.0.113.20. Four round trips on the wire: 0.166 s.

dig +trace walks the same path itself from the PC so you can watch it.

# dig +trace: the PC follows the chain itself from the root (← marks who answered)
pc$ dig +trace expenses.example
    .  86368  IN  NS  a.root.lab.
    ← 198.51.100.53 (198.51.100.53)
    example.  86400  IN  NS  ns1.example.
    ← a.root.lab (203.0.113.1)
    expenses.example.  3600  IN  NS  ns1.expenses.example.
    ← ns1.example (203.0.113.2)
    expenses.example.  30  IN  A  203.0.113.20
    expenses.example.  3600  IN  NS  ns1.expenses.example.
    ← ns1.expenses.example (203.0.113.3)
NOTE

Many real resolvers show each server only as much of the name as it needs: just “example” to the root, just “expenses.example” to .example (QNAME minimisation). It's switched off here to keep the flow easy to read.

4

Remembering: TTL and caching

Spending 0.166 s walking the tree every time would make the web unbearably slow, so the resolver remembers answers. How long is set by the answer's TTL in seconds. For practice, expenses.example's address has a TTL of 30.

# PC to the resolver (198.51.100.53): the answer's TTL (seconds left to remember it) and time taken
pc$ dig expenses.example @198.51.100.53
    first time            expenses.example.  30  IN  A  203.0.113.20    168 ms
    1 s later             expenses.example.  29  IN  A  203.0.113.20      0 ms
    10 s later            expenses.example.  19  IN  A  203.0.113.20      0 ms
    20 s later            expenses.example.  10  IN  A  203.0.113.20      0 ms
    30 s later (expired)  expenses.example.  30  IN  A  203.0.113.20     44 ms

The TTL counts down, and the resolver asks again when it expires (measured)

0 s 10 s 20 s 30 s 168 ms TTL 30 0 ms TTL 19 0 ms TTL 10 44 ms TTL 30 Blue line = seconds left to remember the answer (TTL) Red dots: the resolver asked again. Blue dots: it answered from memory After expiry only expenses.example is asked; referrals are kept longer
Two Wireshark packet lists stacked. Top: the first query, where the resolver asks the root, 203.0.113.2 and 203.0.113.3 in turn and answers the PC at 0.165845 s. Bottom: 30 seconds later, the resolver asks only 203.0.113.3 and answers at 0.041326 s
Top, the first query; bottom, a query after the answer's 30-second lifetime ran out. The root's and .example's referrals were still remembered, so the second time only one place was asked (Recorded on 25 October 2026 on a practice network the author built in the cloud and opened in Wireshark 4.2.2. The bar across the top of the window and the headings were added for the illustration.)

While remembered, answers took 0 ms and the TTL counted down 29, 19, 10. When it expired at 30 s the resolver asked again, but it still remembered the root's and .example's referrals (TTLs of a day and an hour), so it asked only expenses.example: 0.044 s.

POINT

When you move a server to a new address, the old answer lingers in resolvers around the world until its TTL runs out. That's what “waiting for DNS to propagate” really means. The standard move is to shorten the TTL a few days before the move.

5

Record types

DNS answers more than addresses. Each line attached to a name is a record, and there are several types.

# Various records for expenses.example (the number is the TTL in seconds)
    expenses.example.  29  IN  A  203.0.113.20
    expenses.example.  30  IN  AAAA  2001:db8::20
    www.expenses.example.  300  IN  CNAME  expenses.example.
    expenses.example.  3600  IN  MX  10 mail.expenses.example.
    expenses.example.  3600  IN  TXT  "kiji practice zone for part 9"
    expenses.example.  3600  IN  NS  ns1.expenses.example.
TypeMeaningWhere developers meet it
A / AAAAIPv4 / IPv6 addressregistering a server's address
CNAMEalias: “this name is the same as that one”pointing www at the domain's apex or at a cloud service
MXmail server (the number is the priority)receiving mail on your own domain
TXTfree textproving domain ownership, sender checks (SPF etc.)
NSthe DNS servers in charge of this zonechoosing a DNS service after buying a domain
SOAbasic zone information (including how long “doesn't exist” lasts)next section

A name with a CNAME can't have any other records. The apex of a domain (here, expenses.example itself) always has NS and SOA records, so the apex can't be a CNAME, a rule worth knowing.

6

Names that don't exist

# Asking for a name that doesn't exist
pc$ dig nosuch.expenses.example
    status: NXDOMAIN
    expenses.example.  60  IN  SOA  ns1.expenses.example.  admin.expenses.example.  2026102501  1800  900  604800  60
# The “doesn't exist” answer is remembered too, for the SOA's last number (60 s)
    1st: 40 ms    2nd: 0 ms

Ask for a name that doesn't exist and the answer is NXDOMAIN (no such name). It comes with the SOA, whose last number (60 s) is how long the “doesn't exist” answer may be remembered. The second time took 0 ms. Mistyped names don't send the resolver down the tree every time. Conversely, look a new name up just before you register it, and the “doesn't exist” can stick around for a while.

7

When an app looks up a name

Finally, curl sends the submission without --resolve: twice in a row, starting with an empty resolver cache.

# An HTTPS submission with curl (no --resolve), twice, starting with an empty resolver cache
    1st  namelookup 0.1667  connect 0.208  tls 0.252  total 0.294  (201)
    2nd  namelookup 0.0006  connect 0.041  tls 0.087  total 0.144  (201)
Two Wireshark packet lists stacked, both recorded at the PC: a DNS query and answer, followed by SYN and Client Hello to port 3443 on 203.0.113.20. The answer came at 0.166132 s in the top list and 0.000145 s in the bottom one
Two HTTPS submissions with curl. The first time the resolver walked the name tree (0.166 s); the second time it answered from memory (0.0001 s), so the connection started at once (Recorded on 25 October 2026 on a practice network the author built in the cloud and opened in Wireshark 4.2.2. The bar across the top of the window and the headings were added for the illustration.)

The first time, the lookup took 0.167 s, followed by part 6's TCP and part 8's TLS. The second time it took 0.0006 s, and the whole request dropped from 0.294 s to 0.144 s. That's why a site you haven't visited is a little slower the first time.

Name lookups search from nearest to farthest

Browser cache OS cache hosts file Resolver cache DNS servers walk the tree Searched left to right, stopping at the first answer on your PC across the network An old address left in hosts wins over DNS dig ignores hosts, so it can disagree with curl or a browser The second curl got its answer from the resolver's cache: 0.0006 s

Applications such as curl and browsers check nearer places before asking DNS, and the hosts file is checked before DNS. A line left over from development does this:

# If the PC's hosts file still has an old address, 203.0.113.21
    /etc/hosts:  203.0.113.21  expenses.example
# The OS's name lookup (checks hosts first)
pc$ getent ahostsv4 expenses.example
    203.0.113.21  STREAM  expenses.example
# dig (asks DNS only)
pc$ dig +short expenses.example
    203.0.113.20
# curl (uses the OS's lookup)
    curl: (28) Failed to connect to expenses.example port 3443 after 3002 ms: Timeout was reached

dig correctly answers 203.0.113.20, but curl went to the old address in hosts, 203.0.113.21, and timed out after 3 seconds. dig asks DNS only and ignores hosts. When “dig is right but the app can't connect”, suspect hosts and the OS's and browser's caches.

8

When DNS stops

# The resolver program is stopped (ICMP says nobody is there)
    curl: (6) Could not resolve host: expenses.example
    total 0.0001 s
# The resolver can't be reached (the link is down)
    curl: (6) Could not resolve host: expenses.example
    total 6.023 s

With only the resolver program stopped, the OS reports “nobody there” (part 7's ICMP) and curl failed at once with Could not resolve host. When the resolver couldn't be reached at all, the same error came only after 6 seconds; here, the PC asked for the resolver's MAC six times with ARP. If queries vanish further along the way, Linux's defaults (timeout 5 s and attempts 2 in resolv.conf) can make the wait even longer.

POINT

When DNS stops, things addressed by number keep working while everything addressed by name stops. To users it looks like “the internet is down” or “it's slow”. The first step in diagnosis is to try both the name and the address (part 12).

9

Hiding DNS

Everything recorded in this part was DNS on UDP port 53, with the names asked for readable as is. Part 8 noted that HTTPS still shows the name (SNI); DNS queries reveal it too.

Ways to wrap DNS queries

PC Resolver UDP 53 Plain DNS: names in plain view everything captured in this part PC Resolver DoT 853 DNS over TLS wrapped in part 8's TLS PC Resolver DoH 443 DNS over HTTPS blends in with web traffic The resolver itself still sees the names: choose one you trust Together with part 8's ECH, “where you're going” is hidden from the path

So DNS queries can now be wrapped in TLS (DoT, port 853) or HTTPS (DoH, port 443). Major browsers and operating systems can be set to use DoH. The resolver itself still sees the names, though, so which resolver to trust remains a separate question. Large answers can also make DNS retry over TCP instead of UDP (part 7's 1232 bytes), so TCP port 53 is DNS too.

10

Checking your own PC

To seeMacWindowsLinux
Look up a namedig example.comResolve-DnsName example.comdig example.com
Walk the pathdig +trace example.com(step by step with nslookup)dig +trace example.com
The OS's answer (with hosts)dscacheutil -q host -a name example.comping -n 1 example.comgetent ahosts example.com
Which resolver you usescutil --dnsipconfig /allresolvectl status
Flush the OS cachesudo killall -HUP mDNSResponderipconfig /flushdnsresolvectl flush-caches

The second column of a dig answer is the TTL. Look the same name up a few times and watch it count down.

§ — On one page, and next time

  1. Names form a tree read right to left: root → .example → expenses.example, each tier with its own servers (delegation).
  2. The PC asks the resolver once; the resolver follows the referrals. The first time took four round trips: 0.166 s.
  3. Answers are remembered for their TTL: 0 ms while remembered; after expiry only what's needed is asked again (0.044 s).
  4. A, AAAA, CNAME, MX, TXT, NS, SOA. The apex can't be a CNAME.
  5. “Doesn't exist” (NXDOMAIN) is remembered too, for the SOA's number of seconds.
  6. Apps check hosts before DNS; dig ignores hosts, so they can disagree.
  7. If the resolver can't be reached, everything addressed by name waits and then fails. DoT and DoH hide the queries.

AraiWe know the name, we're connected, and it's locked. Finally it's the submission's turn.

FuruiNext time: HTTP. We'll read the POST /api/expenses we've been sending since part 1, line by line.

References

The recordings were made on 25 October 2026 on a practice network the author built with network namespaces on Linux (Ubuntu 24.04, kernel 6.18) in the cloud. The resolver is Unbound 1.19.2 (DNSSEC validation and QNAME minimisation off); the root, .example and expenses.example DNS servers are NSD 4.8.0, and every name and address is either reserved for documentation or made up for the practice. Between the resolver and the DNS servers runs part 6's mischief wire (20 ms each way). Every number in the text is a real value read from those recordings and the commands' output. Root server figures are from root-servers.org. The characters are fictional.

Old-School Networking, part 9 — DNS / 2026.10

← Back to all posts