back to research
rustsystemsnetworking

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.

Chaitanya

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 Servers
plain 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 = 0
plain 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
00
plain text

Responses frequently use compression pointers:

0xC0 0x0C
plain text
  • The first two bits 11 indicate 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:

  1. Select a root server
  2. Send an A record query
  3. If Answer section present → return result
  4. If referral → extract NS records from Authority section
  5. Extract glue A records from Additional section
  6. Select next nameserver
  7. 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 0c
plain 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 curl request 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.