Brief
Every request your code has ever made started with a question nobody taught you to ask: "what address is this name?" You are going to answer it yourself — bytes, not libraries. Pack a DNS query by hand, throw it at the internet over UDP, decode what comes back, and then do what your OS's resolver does all day: start at a root server that knows almost nothing and follow referrals until someone authoritative answers. Then put the packet capture on the table and read every byte of it back.
Instructions
The query — bytes you packed yourself
Build the query per RFC 1035 §4, in network byte order (big-endian), no DNS library:
- Header (12 bytes): a random 16-bit ID; the flags word (QR=0, OPCODE=0, and RD as a parameter — you'll want it on for milestone 1's stub query, off for the walk); QDCOUNT=1; the other counts 0.
- Question: the QNAME as length-prefixed labels —
example.combecomes\x07example\x03com\x00— followed by QTYPE (A=1) and QCLASS (IN=1). Labels are ≤ 63 bytes (the top two bits of a length byte are reserved — that fact returns in the next milestone). - Send it with a UDP socket (
sendto/recvfromor your language's thin wrapper) to a public resolver (8.8.8.8:53or1.1.1.1:53), and hexdump both the query and the reply.
The parser — including the pointer trick
Decode the reply fully: header (check the ID matches and QR=1; read RCODE), the echoed question, then each resource record — name, type, class, TTL, RDLENGTH, RDATA (render A records as dotted quads).
Names in replies use compression (RFC 1035 §4.1.4): a length byte with the top two
bits set (0xC0) means the next 14 bits are an offset from the start of the message
where the name continues. Your name reader must:
- follow pointers, including a pointer landing on a name that itself ends in a pointer;
- bound the chase — a malicious (or buggy) message can point in a loop; cap hops or reject revisited offsets;
- resume parsing after the pointer at the right place (two bytes consumed, not the pointed-to length).
Verify against dig for the same names: same addresses, same TTL ballpark.
The walk — root to authoritative
Resolution without anyone else's recursion: RD=0 throughout.
- Start at a root server — the IPs are published ("root hints");
198.41.0.4(a.root-servers.net) is fine to hard-code. - The root won't answer for
example.com— it refers you: NS records in the AUTHORITY section, and (when you're lucky) their addresses as A records in the ADDITIONAL section ("glue"). - Pick a referred server, ask again; repeat down the chain — root → TLD → authoritative — until a reply carries your answer in the ANSWER section.
- The no-glue case: a referral may name an NS whose address you don't have (out-of-bailiwick nameservers carry no glue). Resolve the NS name itself with a fresh walk, then continue — this recursion is the resolver's one subtle control path; bound its depth.
Print the journey as you go (asking 198.41.0.4 about example.com → referred to l.gtld-servers.net …) — the trace is part of the deliverable.
Records of the real world
- CNAME: when the answer is an alias, follow the chain (the target may be in the same reply or need its own walk). Bound it — CNAME loops exist.
- AAAA and TXT: query type becomes a parameter; TXT shows RDATA isn't always an address.
- Cache: key on (name, type); store records for their TTL against an injected clock; a repeat lookup within TTL answers from cache with no packets sent (assert that in a test). Expired entries walk again.
The wire, annotated
Two captures, both committed alongside the code:
- Your resolver's walk:
tcpdump -wwhile resolving a fresh (uncached) name from the root; open in Wireshark and annotate each exchange — who was asked, what they returned, where the referral/glue/answer sits in each message. - One full HTTPS request to a real site (a fresh connection, e.g.
curl https://example.comwith the capture running): annotate the phases — DNS lookup, TCP three-way handshake (SYN / SYN-ACK / ACK, note the port numbers and sequence-number relationship), TLS handshake (ClientHello with SNI, ServerHello, where the certificate travels and why you can't read it under TLS 1.3), application data, teardown (FIN/ACK exchange).
Annotations are prose in a markdown file keyed to packet numbers — written so a reader with the capture open can follow every arrow.
Constraints
- No DNS libraries, and no
getaddrinfo/gethostbyname/language resolver APIs anywhere in the resolution path — UDP sockets and hand-packed bytes only.digand the language resolver may serve as oracles in tests. - Network byte order handled explicitly — if your language makes you think about endianness, that's the exercise working.
- Every pointer/CNAME/NS chase is bounded; a malformed reply must produce an error, not a hang or a crash.
- The walk queries with RD=0; leaning on a recursive resolver for milestones 3–4 defeats the exercise.
Acceptance
- Milestone 1 (query): the hexdumped query matches the RFC layout byte-for-byte (verifiable in Wireshark's decoder); a reply arrives with the same ID and QR=1.
- Milestone 2 (parser): replies for at least three real domains parse fully;
compressed names resolve correctly (a name under
.comwhose reply uses pointers); a crafted pointer-loop fixture is rejected, not chased. Addresses agree withdig. - Milestone 3 (walk): a real domain resolves starting from
198.41.0.4with RD=0, printing the full referral chain; a no-glue delegation (find one — they're common) resolves via the nested walk. - Milestone 4 (records): a CNAME chain resolves to its final address; AAAA and TXT queries work; the cache test proves a repeat lookup sends zero packets and an expired entry re-walks.
- Milestone 5 (captures): both annotated captures are complete per the instructions — every packet in the resolver walk attributed, every HTTPS phase labeled with the packets that realize it.
Related
- Build a DNS resolver — the exercise note this is the subject of.
- Sources: Implement DNS in a Weekend (the guided original this subject adapts and extends), RFC 1035 (message format §4, compression §4.1.4), and the root hints file.