Networking for beginners. Part 12: DNS

All parts of the series “Networking for beginners”

Everything we have covered so far worked with addresses: 203.0.113.10. People do not use addresses; they type www.example.com. Someone has to translate one into the other, and that is DNS — the Domain Name System, RFC 1034 and 1035, 1987. DNS is the first thing a browser does when opening a page, the first thing to break when a network is misconfigured, and the largest distributed database in the world: several hundred million names, not a single central server, and an answer within tens of milliseconds from anywhere on the planet.

As in the previous parts, we are interested not in "DNS turns names into addresses" but in exactly how: who stores, who asks, what travels in the packet.


The name tree

The name www.example.com. is a path in a tree from the root to a leaf, written right to left:

                        . (root)
          ┌───────────────┼───────────────┐
         com             org              ru
     ┌────┴────┐          │
  example   google     wikipedia
     │
    www
  • The root is an empty label, denoted in a full name by the trailing dot: www.example.com.. Such a name is called an FQDN (fully qualified); without the dot, the resolver may try appending a search domain to the name.
  • The first level is the TLD: com, org, ru, by and several hundred others.
  • Each label is up to 63 bytes, the whole name up to 253; case does not matter: WWW.Example.COM and www.example.com are the same name.

Nobody stores the tree in full. It is cut into zones — pieces, each the responsibility of its own organisation:

  • the root zone — the list of TLDs, held by IANA and served by 13 "root servers" a.root-servers.net … m.root-servers.net (in reality more than one and a half thousand machines around the world sharing those 13 addresses; more on that below);
  • the com. zone — the list of all second-level domains in .com, held by the Verisign registry;
  • the example.com. zone — held by the domain owner on their own servers or at a hosting provider.

The boundary between zones is a delegation: the com. zone has no records about www.example.com; it only has "such-and-such servers are responsible for example.com". That is why a domain owner can change anything inside their zone without touching the registry.


Three kinds of participants

Three different programs take part in resolving a name, and they are the ones most often confused.

The stub resolver is part of the operating system. It does not look anything up itself: it takes a name from an application, sends a query to the recursive server from its configuration and returns the answer. The configuration is /etc/resolv.conf on Linux (or systemd-resolved, which listens on 127.0.0.53 and goes further itself), the DNS addresses from the DHCP of the previous part, or ones set by hand in the Windows adapter properties. Before querying it checks /etc/hosts: a name found there never goes out to the network.

The recursive server (resolver) is the one that does all the work: it walks the tree from the root down until it finds the answer, and caches everything it learns. This is your ISP's DNS server, Google's 8.8.8.8, Cloudflare's 1.1.1.1, or your home router, which looks nothing up itself but forwards queries to the ISP (this mode is called forwarding).

The authoritative server holds a zone and answers only for it. It looks nothing up and caches nothing: whatever is in the zone is what it serves, with the "authoritative answer" flag. Every zone has at least two such servers — the primary, on which the zone is edited, and secondaries, which fetch a copy from it (a zone transfer, AXFR in full or IXFR as changes, over TCP) and answer on an equal footing.


The path of a query

A browser wants www.example.com. The recursive server's cache is empty:

 stub                 recursive server R              root          com.          example.com.
  │                          │                            │            │               │
  ├── A? www.example.com ───►│                            │            │               │
  │  (RD = 1, "do it all")   │                            │            │               │
  │                          ├── A? www.example.com ─────►│            │               │
  │                          │◄─ "don't know; for com.: ──┤            │               │
  │                          │   NS a.gtld-servers.net    │            │               │
  │                          │   + addresses (glue)"      │            │               │
  │                          ├── A? www.example.com ──────────────────►│               │
  │                          │◄── "for example.com.: NS ───────────────┤               │
  │                          │    ns1.example.com + glue"              │               │
  │                          ├── A? www.example.com ──────────────────────────────────►│
  │                          │◄── A 203.0.113.10, TTL 3600, AA = 1 ────────────────────┤
  │◄── A 203.0.113.10 ───────┤                                                         │
  │                          │  cached: NS com. (2 days), NS example.com. (2 days),    │
  │                          │          www.example.com A (1 hour)                     │
  1. The stub sends a query with the RD (recursion desired) flag: "find it yourself". This is a recursive query.
  2. R asks a root server. The root server does not know www.example.com, but it knows who is responsible for com. and sends a referral: the NS records of the com. servers and their addresses. This is an iterative query: "tell me what you know, I'll take it from there".
  3. R asks a com. server. It refers it to the example.com. servers.
  4. R asks ns1.example.com and gets an authoritative answer with the AA flag.
  5. R gives the answer to the stub and caches every step with its own TTL.

The next query for mail.example.com goes straight to step 4: the example.com servers are already in the cache. A query for www.google.com goes to step 3: the com. servers are in the cache. A good resolver contacts the root servers once every two days, which is why 13 addresses can withstand the load of the whole planet. The rest is done by anycast: one address 198.41.0.4 is announced from hundreds of locations, and routing brings you to the nearest one.

How does R know the root servers' addresses if there is nobody to ask? From a file built into the server (root hints), and that is the only thing that has to be configured by hand.

Delegation and glue

At step 2 the root server sent not only the names of the com. servers but also their addresses. Otherwise R would have to ask "what is the address of a.gtld-servers.net?" — and ask whom? The net. servers, whose addresses also need to be found out. The records with server addresses that the parent zone keeps alongside the NS records are called glue, and without them a delegation inside the zone itself (example.com served by ns1.example.com) would loop. When a domain's NS servers are changed, the glue at the registrar must be changed too — people forget, and the domain keeps answering from the old servers as long as they are alive.


The message

DNS travels in UDP, port 53, one message per datagram. Queries and responses have the same format:

 ┌───────────────────────────────┐
 │ Header, 12 bytes              │  ID, flags, four counters
 ├───────────────────────────────┤
 │ Question                      │  what was asked: name, type, class
 ├───────────────────────────────┤
 │ Answer                        │  answer records
 ├───────────────────────────────┤
 │ Authority                     │  the zone's NS records (in referrals) or SOA (in negative answers)
 ├───────────────────────────────┤
 │ Additional                    │  glue, OPT and other useful things
 └───────────────────────────────┘

The header:

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
┌───────────────────────────────┐
│              ID               │  16 bits, chosen by the client; the response carries the same one
├─┬───────┬─┬─┬─┬─┬─────┬───────┤
│Q│Opcode │A│T│R│R│  Z  │ RCODE │
│R│       │A│C│D│A│     │       │
├─┴───────┴─┴─┴─┴─┴─────┴───────┤
│            QDCOUNT            │  number of questions (always 1)
│            ANCOUNT            │  records in Answer
│            NSCOUNT            │  in Authority
│            ARCOUNT            │  in Additional
└───────────────────────────────┘
  • QR — 0 query, 1 response. AA — the answer is authoritative. TC — the response is truncated, it did not fit in the datagram. RD — the client asks for recursion. RA — the server supports it.
  • RCODE: 0 NOERROR, 2 SERVFAIL (the server could not: the authoritative servers do not respond, a DNSSEC error), 3 NXDOMAIN (no such name), 5 REFUSED (the server refused: you are not its client). A separate case is NODATA: the name exists, but there are no records of that type; this is NOERROR with an empty Answer.

Names are encoded not as a string but as a chain of labels each preceded by its length: www.example.com is 3 w w w 7 e x a m p l e 3 c o m 0. The zero is the root. To avoid repeating the same name in every record of a response, there is compression: a pointer to the place in the message where that name already appeared.

A record (RR, resource record) in the response sections is name, type, class (always IN), TTL, length, data. This is what a response looks like in tcpdump:

$ sudo tcpdump -i eth0 -n udp port 53
IP 192.168.1.10.40321 > 192.168.1.1.53: 12345+ A? www.example.com. (33)
IP 192.168.1.1.53 > 192.168.1.10.40321: 12345 1/0/0 A 203.0.113.10 (49)

12345 is the ID, + is RD, A? is the question, 1/0/0 means one record in Answer and none in Authority and Additional.


Record types

Type What is in the data What for
A an IPv4 address name → address; a name can have several, the resolver returns them all, the client takes any
AAAA an IPv6 address the same for IPv6 (part 13); a browser asks for A and AAAA simultaneously
CNAME another name an alias: "www.example.com is really example.hosting.net"; a server that meets a CNAME keeps looking for the requested type at the target and returns the whole chain. A name with a CNAME cannot have any other records, which is why a CNAME cannot be placed at the zone apex, where SOA and NS must exist
NS a server name who is authoritative for the zone; it sits both in the parent zone (the delegation) and in the zone itself
SOA primary, admin email, serial, refresh, retry, expire, minimum the zone's passport: the serial increases with every change, which is how secondaries know it is time to fetch a copy; refresh/retry/expire — how often to check and how long to live without the primary; minimum — the TTL of negative answers
MX priority, server name where to send mail for the domain: the sender's mail server (which speaks SMTP — the protocol servers use to pass mail to each other) asks for MX, takes the one with the lowest priority, and asks for its A record
PTR a name reverse resolution: address → name, through the special in-addr.arpa tree: for 203.0.113.10 you ask for 10.113.0.203.in-addr.arpa (the octets reversed, because the tree goes from general to specific). Mail servers check senders using PTR
TXT arbitrary text domain ownership verification, mail policies: SPF (who may send on behalf of the domain), DKIM (mail signing keys)
SRV priority, weight, port, name where to find a service: _ldap._tcp.example.com → the server and port on which the LDAP directory answers (the protocol for accessing a directory of users and computers); this is how machines in a Windows domain find the domain controllers
CAA a certificate authority who is allowed to issue certificates for the domain

TTL and caching

Every record has a TTL in seconds — how long it may be kept in a cache. The zone owner sets it, and all recursive servers honour it, as, partly, do stubs and browsers. An authoritative answer comes with the full TTL, one from a cache with what remains, and in dig you can see a second query showing a smaller number.

Hence "DNS propagation" that takes "up to 48 hours": nothing propagates anywhere; caches around the world are simply holding the old record until its TTL runs out. The right way to move a server: a day before the move, lower the record's TTL to 5 minutes, wait for the old TTL to expire everywhere, change the address, restore the TTL.

Negative answers are cached too: an NXDOMAIN is remembered for as long as the minimum field of the zone's SOA says (usually an hour). That is why "I just created a record and it's not there" happens — someone asked first and created it afterwards, and the resolver will keep answering "no" for an hour.

Stubs cache as well: systemd-resolved and Windows keep their own cache, and resolvectl flush-caches and ipconfig /flushdns clear it.


UDP, TCP, EDNS

The historical limit of a DNS datagram is 512 bytes. A response that does not fit is truncated by the server, which sets the TC flag, and the client repeats the query over TCP on the same port 53: a handshake, a query preceded by a two-byte length, a response of any size. TCP is also needed for zone transfers.

To avoid going to TCP for every response with several addresses and signatures, EDNS(0) was invented: the client adds an OPT pseudo-record to Additional with the datagram size it is willing to accept. Today it is customary to announce 1232 bytes — that is guaranteed to get through any reasonable MTU without fragmentation, and fragmented datagrams, as we know from the sixth part, get lost and filtered. OPT also carries extensions such as the client subnet (so that a CDN returns the nearest address) and the DO flag for DNSSEC.

What to allow in a firewall: UDP and TCP on 53 in both directions. The rule "DNS is UDP" that blocks TCP breaks large responses, DNSSEC and zone transfers.


Delegation in practice: dig +trace

You can walk the whole path of a query yourself:

$ dig +trace www.example.com

.                       518400  IN  NS  a.root-servers.net.
.                       518400  IN  NS  b.root-servers.net.
...
;; Received 525 bytes from 192.168.1.1#53(192.168.1.1) in 2 ms

com.                    172800  IN  NS  a.gtld-servers.net.
com.                    172800  IN  NS  b.gtld-servers.net.
...
;; Received 1170 bytes from 198.41.0.4#53(a.root-servers.net) in 15 ms

example.com.            172800  IN  NS  ns1.example.com.
example.com.            172800  IN  NS  ns2.example.com.
;; Received 220 bytes from 192.5.6.30#53(a.gtld-servers.net) in 20 ms

www.example.com.        3600    IN  A   203.0.113.10
;; Received 60 bytes from 192.0.2.53#53(ns1.example.com) in 25 ms

The first block is the root servers from your resolver's cache; the second is the root's answer: a referral to com.; the third is the com. answer: a referral to example.com.; the fourth is the authoritative answer. If at some step a referral leads to servers that do not respond, or respond with something else — that is where the delegation is broken.


Miscellany you will run into

Search domains. The stub appends the domains from search in resolv.conf or DHCP option 119 to short names: ping server → server.example.com. The ndots rule decides whether to try the name as is first; in Kubernetes with its ndots:5, every external query first goes through four internal domains and only then as is, hence extra queries and odd delays.

Split-horizon. The same www.example.com resolves to 192.168.1.50 from inside the network and to 203.0.113.10 from outside: the internal DNS server keeps its own version of the zone. A normal practice, until a laptop brings home a cache with the internal address.

Round-robin. Several A records for one name — the server shuffles their order in every response, and the load gets spread. This is not load balancing: a dead server does not disappear from the list.

Security. Forging a response, knowing the ID (16 bits) and the query's port, was easy until resolvers started picking a random source port for every query: this is cache poisoning, and since the Kaminsky attack of 2008 everyone does this. The real protection is DNSSEC: the zone owner signs the records with a private key (RRSIG records), publishes the public one (DNSKEY), and puts a hash of the public key into the parent zone (DS) — this builds a chain of trust from the root to the zone, and a resolver that validates signatures will not accept a forgery. DNSSEC protects authenticity but not privacy: queries still travel in the clear. For privacy there is DoT — the same DNS inside a TLS connection on port 853, and DoH — DNS messages as HTTPS requests (HTTP, the protocol a browser uses to fetch pages, inside the same TLS) on port 443, indistinguishable from web traffic; both hide queries from the ISP, but not from the resolver itself.


Seeing it for yourself

The main tool is dig:

$ dig www.example.com

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.example.com.               IN      A

;; ANSWER SECTION:
www.example.com.        3600    IN      A       203.0.113.10

;; Query time: 12 msec
;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP)
;; MSG SIZE  rcvd: 60

Reading it: status: NOERROR; the flags qr rd ra — a response, recursion was requested, the server supports it, no aa — the answer came from a cache or via recursion, not from an authoritative server; EDNS with 1232; a TTL of 3600 is the full one, so the resolver has only just fetched the record. Repeat in a minute and the TTL will be 3540.

Useful variants:

$ dig @ns1.example.com www.example.com        # ask a specific server (the answer will have aa)
$ dig example.com MX                          # another type
$ dig -x 203.0.113.10                         # PTR
$ dig +short www.example.com                  # data only
$ dig +norecurse @192.168.1.1 www.example.com # is it cached: without RD the server won't go looking
$ dig example.com SOA                         # serial and the TTL of negative answers

What the stub is configured with: resolvectl status (systemd), cat /etc/resolv.conf, on Windows ipconfig /all and Resolve-DnsName www.example.com in PowerShell, which also shows the CNAME chain.

And tcpdump on udp port 53 when it is unclear whether the query goes out at all and where the response comes from: half of all "DNS doesn't work" cases are queries going to the wrong server.


Key takeaways

  • The name tree is cut into zones; each is served by its own authoritative servers, and the parent zone holds the delegation (NS) and glue (addresses).
  • Three participants: the stub in the OS asks for recursion, the recursive server walks the tree from the root and caches, the authoritative server answers only for its own zone.
  • The message: a 12-byte header with an ID and flags (QR, AA, TC, RD, RA, RCODE), the Question, Answer, Authority and Additional sections. NXDOMAIN means no such name, NODATA means no records of that type.
  • Records: A/AAAA addresses, CNAME an alias (not at the zone apex), NS and SOA the structure of the zone, MX mail, PTR reverse, TXT, SRV, CAA.
  • TTL governs the cache: "propagation" is old records expiring; negative answers are cached for the SOA minimum.
  • UDP 53 with EDNS 1232, and on TC — TCP 53; both must be open. DNSSEC is authenticity, DoT/DoH is privacy.

This concludes the IPv4 service protocols. Next — Part 13: IPv6 — addresses and header: 128-bit addresses, how they are written and their types, why an interface always has several addresses, what was removed from the header compared with IPv4 and why, and why there is no broadcast, no fragmentation at routers and no NAT.

Only registered users can leave a comment.

Sign in or register to leave a comment.