DNS Resolution
On this page
The Phonebook of the Internet#
Computers route traffic using numbers (104.20.23.154), but humans use names (example.com). The Domain Name System (DNS) translates between them. It is a hierarchical, massively distributed database with aggressive caching at every level, and it sits in front of almost every connection you make. When "the site is down" for some users and not others, DNS is one of the first things to check.
The DNS Resolution Journey#
When you type https://github.com into a browser:
- Browser and OS cache: The browser checks its own cache, then asks the operating system's stub resolver, which checks its cache and the
hostsfile (/etc/hosts, orC:\Windows\System32\drivers\etc\hosts). - The recursive resolver: On a miss, the OS asks its configured recursive resolver: usually your router or ISP, a corporate resolver, or a public one such as
1.1.1.1or8.8.8.8. - Root servers: If the resolver has nothing cached, it asks a root server. There are 13 root server identities (a through m), each served by many anycast instances worldwide. The root replies with a referral: "I don't know
github.com, but here are the nameservers for.com." - TLD servers: The resolver asks a
.comserver, which refers it to the nameservers listed forgithub.com. - Authoritative servers: The resolver asks one of those. It holds the actual records and returns the answer with the authoritative flag set.
- Return and cache: The resolver returns the answer to your OS and caches every step (root, TLD and final answers) for the TTL attached to each record. The next lookup for any
.comname skips the root entirely.
Recursive vs. Iterative Queries#
Your computer asks the resolver a recursive question: "find the final answer for me." The resolver performs iterative queries, following referrals from root to TLD to authoritative server itself. Recursion is the service; iteration is the mechanism. Authoritative servers normally refuse to do recursion for strangers; an open resolver that answers recursive queries for anyone on the internet gets abused for amplification attacks.
Watching It Happen with dig#
dig (from BIND tools; nslookup and PowerShell's Resolve-DnsName are alternatives on Windows) shows the whole exchange:
dig example.com A
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41806
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; ANSWER SECTION:
example.com. 95 IN A 104.20.23.154
example.com. 95 IN A 172.66.147.243
;; Query time: 14 msec
;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP)
Read it top to bottom:
status: NOERRORis the response code. Others you will meet:NXDOMAIN(name does not exist),SERVFAIL(the resolver could not get a valid answer, often a broken authoritative server or a DNSSEC validation failure),REFUSED(the server will not answer you).- Flags:
qr= this is a response,rd= recursion desired,ra= recursion available. Anaaflag means the answer came straight from an authoritative server. - The
95is the remaining TTL in the resolver's cache. Run the command again and it counts down. SERVERtells you which resolver actually answered, which matters when a VPN or corporate policy has changed it.
Useful variations:
dig +short example.com AAAA # just the answers
dig @1.1.1.1 example.com # bypass your local resolver
dig @ns1.example-dns.net example.com # ask an authoritative server directly (substitute the zone's real NS)
dig example.com NS # who is authoritative for the zone?
dig +trace example.com # do the iteration yourself, root -> TLD -> authoritative
dig -x 8.8.8.8 # reverse lookup (PTR record)
+trace is the best learning tool here: it prints the referral at every level, so you can see exactly where a broken delegation stops.
Common DNS Record Types#
| Type | Purpose | Example value |
|---|---|---|
| A | name to IPv4 address | 93.184.216.34 |
| AAAA | name to IPv6 address | 2606:2800:220:1::1 |
| CNAME | alias to another name | www.example.com. -> example.com. |
| MX | mail servers, with priority (lower preferred) | 10 mail.example.com. |
| TXT | free text: SPF, DKIM, DMARC, ownership verification | v=spf1 include:... -all |
| NS | delegates a zone to nameservers | ns1.provider.net. |
| SOA | zone metadata, including the negative-caching TTL | serial, refresh, minimum |
| SRV | host and port for a named service | 10 5 5060 sip.example.com. |
| CAA | which certificate authorities may issue for the domain | 0 issue "letsencrypt.org" |
| PTR | reverse lookup, IP to name | dns.google. |
Rules that trip people up:
- A CNAME cannot coexist with other records at the same name. That is why you cannot put a CNAME on the zone apex (
example.comitself), which must have SOA and NS records. DNS providers work around this with proprietary "ALIAS", "ANAME" or "CNAME flattening" features that resolve the target server-side and return A/AAAA records. - MX and NS records must point to names, not IP addresses, and not to CNAMEs.
- A Null MX (
MX 0 .) explicitly declares that a domain accepts no email.
Querying DNS from Python#
socket.getaddrinfo() asks the operating system, so it respects /etc/hosts, the OS cache and search domains, exactly like your application will. For inspecting specific record types and TTLs, the dnspython package talks DNS directly:
import dns.resolver
resolver = dns.resolver.Resolver()
resolver.nameservers = ["1.1.1.1"] # ask a specific recursive resolver
resolver.lifetime = 3.0 # total seconds before giving up
def lookup(name, rtype):
try:
answer = resolver.resolve(name, rtype)
print(f"{name} {rtype} ttl={answer.rrset.ttl}", [r.to_text() for r in answer])
except dns.resolver.NXDOMAIN:
print(f"{name} {rtype}: NXDOMAIN (the name does not exist)")
except dns.resolver.NoAnswer:
print(f"{name} {rtype}: NODATA (name exists, no record of this type)")
except dns.resolver.LifetimeTimeout:
print(f"{name} {rtype}: timeout (resolver unreachable or blocked)")
lookup("example.com", "A")
lookup("example.com", "MX")
lookup("www.github.com", "CNAME")
lookup("nonexistent.invalid", "A")
Typical output (addresses and TTLs will differ):
example.com A ttl=95 ['104.20.23.154', '172.66.147.243']
example.com MX ttl=61 ['0 .']
www.github.com CNAME ttl=3600 ['github.com.']
nonexistent.invalid A: NXDOMAIN (the name does not exist)
The distinction between NXDOMAIN and NODATA matters. NXDOMAIN means the name does not exist at all; NODATA means the name exists but has no record of the requested type, which is what you get when asking for an AAAA record on an IPv4-only host. Monitoring scripts that treat both as "down" raise false alarms.
TTL, Propagation and Negative Caching#
Every record carries a TTL in seconds. A TTL of 300 lets resolvers cache the answer for five minutes. After you change a record, old answers linger in caches until their TTL expires: this is what people call "propagation". Nothing is actively pushed; caches simply expire.
The standard migration playbook:
- Days before the change, lower the TTL (for example from 86400 to 300). Wait at least the old TTL so every cache picks up the short one.
- Make the change. Most clients now switch within five minutes.
- Keep the old server running for a while; some clients and libraries cache longer than they should.
- Raise the TTL again once the change is verified.
Failed lookups are cached too. Negative caching (RFC 2308) stores NXDOMAIN and NODATA answers for a period derived from the zone's SOA record. If you query a name before creating it, resolvers may keep answering "does not exist" for a while after you add it. Test new records with dig @<authoritative-server> first, not through your normal resolver.
UDP, TCP and Message Size#
Most queries use UDP port 53, one packet each way. Classic DNS limited UDP responses to 512 bytes; EDNS(0) lets clients advertise a larger buffer. If a response still does not fit, the server sets the TC (truncated) flag and the client retries over TCP port 53. Zone transfers (AXFR/IXFR) always use TCP. Firewalls that allow only UDP 53 cause baffling failures for large responses, such as DNSSEC-signed records or domains with many TXT entries.
DNSSEC#
DNS was designed without authentication, so it is vulnerable to cache poisoning: an attacker tricks a resolver into caching a forged record. Source port randomization and random query IDs make blind poisoning much harder, and DNSSEC solves it properly by signing records. A validating resolver checks a chain of signatures from the root down (DS and DNSKEY records) and returns SERVFAIL if the signatures do not verify. DNSSEC proves authenticity; it does not encrypt anything. Expired signatures or a botched key rollover make an entire domain unresolvable for validating resolvers, which is the most common DNSSEC outage. See DNS Security Deep Dive.
Encrypted DNS#
Traditional DNS travels in plaintext, visible to and modifiable by anyone on the path. DNS over TLS (DoT) runs queries over TLS on port 853; DNS over HTTPS (DoH) carries them inside HTTPS on port 443. Both protect the stub-to-resolver hop, not the resolver-to-authoritative hop. Because browsers can use DoH independently of the OS, a browser and dig on the same machine may get different answers, a frequent source of confusion when debugging split-horizon setups.
Troubleshooting Checklist#
| Symptom | Likely cause | Check |
|---|---|---|
Works with dig, fails in the app |
hosts file, OS cache, or search domain |
getent hosts name (Linux) uses the same path as apps |
| Works for you, not for others | stale caches, TTL not expired | dig @1.1.1.1 and dig @8.8.8.8, compare |
| New record "does not exist" | negative caching | query the authoritative server directly |
SERVFAIL everywhere |
broken delegation or DNSSEC failure | dig +trace, then dig +cd (checking disabled) to test DNSSEC |
| Internal names fail on VPN | split-horizon DNS, wrong resolver | SERVER line in dig, resolvectl status |
To clear local caches: resolvectl flush-caches on systemd-resolved Linux, ipconfig /flushdns on Windows, and sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder on macOS. Browsers keep their own cache as well.
Resolution happens before every new connection, so it also shapes performance: CDNs use DNS to steer users to nearby edges, and a slow resolver adds latency to every uncached lookup before the HTTP request even starts.
Practice#
Pick a domain you control (or example.com). Run dig +trace and write down each nameserver that answered. Then compare the TTL from dig @<authoritative-server> with two successive queries to 1.1.1.1, and explain why the numbers differ.
Check your understanding
4 quick questions on this lesson. Get them all right and the lesson is marked complete.