Building a DNS Resolver from Scratch in Rust
A deep dive into implementing a DNS resolver using Rust, covering UDP sockets, DNS packet parsing, and recursive resolution.

Source Code: https://github.com/cb7chaitanya/networking/tree/main/dns-resolver
DNS is often treated as background infrastructure, something that “just works.”
But behind every HTTP request, TLS handshake, and API call lies a distributed protocol that translates human-readable names into IP addresses.
This post walks through the implementation of a fully functional iterative DNS resolver written in Rust, including:
- Manual DNS packet construction and parsing
- Iterative resolution across root, TLD, and authoritative servers
- Name compression decoding
- EDNS0 support
- Positive and negative caching
- Failure handling (NXDOMAIN, SERVFAIL, timeouts)
- Packet-level verification using Wireshark
- Performance benchmarking (cold vs warm cache)
- Concurrency scaling analysis
This is not a wrapper around libc.
It is a ground-up implementation of the DNS protocol.
Why Build a Resolver?
Most applications delegate DNS to recursive resolvers like Google Public DNS or Cloudflare.
Those systems perform:
- Root lookup
- TLD resolution
- Authoritative resolution
- Caching
- Retry logic
In this project, the resolver performs iterative resolution itself.
That means:
- Recursion Desired (RD) flag is disabled
- The resolver follows referrals manually
- Authority and Additional sections must be parsed correctly
- Glue records must be extracted
- Delegation must be handled explicitly
This makes the protocol transparent.
DNS Architecture Overview
DNS is a hierarchical distributed system:
Client ↓ Root Servers ↓ TLD Servers (.com, .org, etc.) ↓ Authoritative Name Serversplain text
The 13 logical root servers are coordinated by IANA under the oversight of ICANN.
A resolver must:
- Query a root server
- Receive a referral to a TLD server
- Query the TLD server
- Receive referral to authoritative server
- Query authoritative server
- Extract final answer
Each step requires correct parsing of the DNS message format.
DNS Message Format (Wire-Level Understanding)
A DNS message consists of:
- Header (12 bytes)
- Question section
- Answer section
- Authority section
- Additional section
Header Structure
| Field | Size | | -------------- | ------- | | Transaction ID | 16 bits | | Flags | 16 bits | | QDCOUNT | 16 bits | | ANCOUNT | 16 bits | | NSCOUNT | 16 bits | | ARCOUNT | 16 bits |plain text
Important flags:
- QR → Query/Response
- RD → Recursion Desired
- RA → Recursion Available
- AA → Authoritative Answer
- RCODE → Result Code
Because this resolver is iterative:
RD = 0plain text
Constructing DNS Queries
The resolver manually constructs:
- DNS header
- Question section
- EDNS0 OPT record in the Additional section
EDNS0 extends UDP payload size beyond the traditional 512-byte limit.
Without EDNS0:
- Large responses are truncated
- TCP fallback is required
With EDNS0:
- UDP payload up to 4096 bytes
- Avoids unnecessary TCP fallback
Name Encoding and Compression
DNS encodes domain names as:
[length][label][length][label]...[0]plain text
Example:
07 example 03 com 00plain text
Responses frequently use compression pointers:
0xC0 0x0Cplain text
- The first two bits
11indicate a pointer - The remaining 14 bits represent the offset in the packet
Correct pointer decoding is critical to avoid:
- Infinite loops
- Incorrect label resolution
- Offset corruption
This resolver handles nested pointers safely and enforces bounds to ensure correctness.
Iterative Resolution Logic
Resolution proceeds as follows:
- Select a root server
- Send an A record query
- If Answer section present → return result
- If referral → extract NS records from Authority section
- Extract glue A records from Additional section
- Select next nameserver
- Repeat
Unlike recursive resolvers, this implementation must explicitly manage delegation.
That includes:
- Parsing NS records
- Handling glue
- Rotating across multiple nameservers
- Retrying when necessary
Packet-Level Verification (Wireshark Inspection)
To validate protocol correctness, DNS traffic was captured using Wireshark.
Root Query Packet
Key fields:
- RD = 0
- One question
- One Additional record (OPT for EDNS0)
- UDP port 53
This confirms:
- Iterative resolution
- EDNS0 support
- Correct header construction
Root Referral Response
Typical response:
- Answer RRs: 0
- Authority RRs: multiple NS records for
.com - Additional RRs: glue A records for TLD servers
The resolver correctly:
- Parses NS records
- Extracts glue IPs
- Selects next hop
Authoritative Response
Final authoritative response includes:
- AA flag set
- Answer section populated
- TTL value
The resolver extracts:
- IP address
- TTL
- Expiration timestamp
Packet capture confirms wire-level compliance.
Annotated Real DNS Response
Example: A example.com
Header
- QR = 1 (response)
- RCODE = 0 (success)
- ANCOUNT = 1
Compression Pointer
A pointer such as:
c0 0cplain text
Indicates reuse of the name starting at byte offset 12.
Answer Section
- Type = A
- Class = IN
- TTL = 300
- RDLENGTH = 4
- RDATA = 93.184.216.34
This confirms:
- Proper network-byte-order parsing
- Compression decoding correctness
- Accurate TTL extraction
Caching Design
The resolver implements:
- Positive caching (A records)
- Negative caching (NXDOMAIN)
- TTL-based expiration
Data structure:
HashMap<Domain, CacheEntry>plain text
Each cache entry includes:
- Result (IP or NXDOMAIN marker)
- Expiration timestamp
Cache lookups avoid unnecessary network traversal and drastically reduce latency.
Failure Case Analysis
DNS failures are normal in distributed systems.
NXDOMAIN (RCODE = 3)
Indicates the domain does not exist.
Resolver behavior:
- Stop resolution
- Extract SOA record
- Cache negative result
- Respect negative TTL (RFC 2308)
This prevents repeated unnecessary queries.
SERVFAIL (RCODE = 2)
Indicates temporary failure.
Resolver behavior:
- Attempt alternate nameservers
- Retry with backoff
- Avoid aggressive caching
Critical distinction:
- NXDOMAIN → permanent
- SERVFAIL → transient
Timeout Handling
If a UDP query receives no response:
- Retry with exponential backoff
- Rotate across available nameservers
- Cap maximum retries
Prevents infinite resolution loops.
Performance Benchmarks
Test environment:
- Local resolver
- Residential broadband
- 100–10,000 parallel queries
Cold Cache Resolution
Includes:
- Root query
- TLD query
- Authoritative query
Measured latency:
110–180 ms
Dominated by network RTT.
Warm Cache Resolution
All responses served locally.
Measured latency:
< 1 ms
Improvement:
~100x–150x faster
This illustrates why caching is fundamental to DNS scalability.
Concurrency Benchmarks
Concurrency implemented via:
Arc<RwLock<HashMap<...>>>plain text
Cold Cache (500 parallel queries)
- Total time: ~350–600 ms
- Avg latency: ~150 ms
Network-bound workload.
Warm Cache (5000 parallel queries)
- Total time: ~15–25 ms
- Avg latency: < 1 ms
Now limited by lock contention.
Throughput
Warm cache throughput:
~150k–250k queries/sec (local process)
Reveals:
- DNS parsing is lightweight
- Network traversal dominates latency
- Lock design impacts scalability
Possible optimizations:
- Sharded caches
- Lock-free structures
- Async runtime (Tokio)
What This Project Reveals
Implementing a DNS resolver from scratch demonstrates:
- Backward-compatible protocol evolution (EDNS0 layered over RFC 1035)
- Compression as a space optimization with parsing complexity
- Delegation as a scalability mechanism
- Caching as the primary performance amplifier
- Failure handling as a first-class systems concern
DNS is not just name lookup.
It is:
- A distributed system
- A caching problem
- A wire protocol exercise
- A concurrency design challenge
Closing Thoughts
This resolver is not intended to replace production-grade systems such as BIND or Unbound.
It is designed to make the protocol transparent rather than abstract.
After implementing DNS manually:
- Every
curlrequest has visible mechanics - Every TLS handshake begins with a system you understand
- Every latency spike has a traceable root
DNS stops being invisible infrastructure.
It becomes a system you can reason about byte by byte.