A DNS server implementation written in Rust, following RFC 1035. It is a fully functional DNS server that can handle DNS queries, serve records from an in-memory cache, and fall back to upstream DNS servers for unknown domains. The implementation follows RFC 1035 standards with a modular, trait-based architecture for extensibility.
Have a working standalone DNS server that can handle requests and deliver reliable responses.
For educational purposes, this project aims to use as little external dependencies as possible, implementing core DNS functionalities from scratch.
- DNS Message Handling: Encodes and decodes DNS messages following RFC 1035
- Multi-threaded Server: Handles concurrent DNS queries using thread-per-request model
- UDP and TCP Transport: Full support for both UDP (port 53) and TCP (port 53) protocols
- Smart Caching: Two-tier storage with in-memory cache and upstream DNS fallback
- Upstream DNS Integration: Automatically queries upstream DNS (e.g., 8.8.8.8) for unknown domains
- Resource Record Support: A, AAAA, TXT, CNAME, NS, MX, and PTR records fully implemented
- EDNS(0) Support: Extension Mechanisms for DNS (RFC 6891) with OPT pseudo-record handling
- Response Truncation: Automatic truncation of responses exceeding UDP size limits (512 bytes standard, 4096 bytes with EDNS)
- RFC 1035 Compliant: Proper handling of DNS headers, questions, and resource records
- Domain Name Compression: Efficient domain name encoding with label compression support
The project is organized into clear, modular components:
src/
├── common/ # Core DNS data structures
├── decoder/ # Binary to DNS message parsing
├── encoder/ # DNS message to binary serialization
├── storage/ # Record storage backends
├── server.rs # UDP and TCP server on port 53
├── transport.rs # Transport constants
├── utils.rs # Bit manipulation utilities
└── main.rs # Application entry point
- Trait-Based Architecture:
Decoder,Encoder, andResourceRecordRepositorytraits enable dependency injection and testing - Generic Storage: Storage layer is generic over decoder/encoder implementations
- Safe Concurrent Access: Arc<Mutex<>> pattern for thread-safe storage operations
- Error Propagation: Result types throughout for proper error handling
| Type | Code | Status |
|---|---|---|
| A | 1 | ✅ Fully implemented (IPv4 addresses) |
| AAAA | 28 | ✅ Fully implemented (IPv6 addresses, RFC 3596) |
| TXT | 16 | ✅ Fully implemented (with RFC 1035 character-string format) |
| CNAME | 5 | ✅ Fully implemented (canonical name alias) |
| NS | 2 | ✅ Fully implemented (authoritative name server) |
| MX | 15 | ✅ Fully implemented (mail exchange with preference) |
| PTR | 12 | ✅ Fully implemented (reverse DNS pointer) |
| OPT | 41 | ✅ Fully implemented (EDNS(0) pseudo-record, RFC 6891) |
| SOA | 6 |
Plus 14 additional record types (HINFO, MINFO, WKS, SVCB, HTTPS, etc.)
# Run the server (may require sudo for port 53)
sudo cargo run
# Or run the compile the project and run the binary
cargo build --release
sudo ./target/release/do-not-sneezecargo test# Query A record (UDP)
dig @127.0.0.1 google.com A
# Query AAAA record (IPv6)
dig @127.0.0.1 google.com AAAA
# Query TXT record
dig @127.0.0.1 google.com TXT
# Query CNAME record
dig @127.0.0.1 www.example.com CNAME
# Query NS record
dig @127.0.0.1 example.com NS
# Query MX record
dig @127.0.0.1 example.com MX
# Query PTR record (reverse DNS)
dig @127.0.0.1 -x 192.168.0.1
# Force TCP transport
dig @127.0.0.1 google.com A +tcp
# Force EDNS(0) support (server responds with OPT record)
dig @127.0.0.1 google.com A +edns=0
# Disable EDNS if needed
dig @127.0.0.1 google.com A +noednsNote: The server fully supports EDNS(0). When a client sends an OPT record, the server will respond with an OPT record indicating support for larger UDP payloads (4096 bytes). TCP transport is also fully supported with proper message framing (2-byte length prefix per RFC 1035).
- Listen: UDP or TCP socket receives DNS query on port 53
- UDP: Direct datagram reception
- TCP: Read 2-byte length prefix, then read message body
- Decode: Binary message parsed into structured DNS Message
- Query Storage:
- Check in-memory cache first
- If not found, query upstream DNS (8.8.8.8)
- Cache upstream responses for future queries
- Format Response: Original message converted to response with answers
- Encode: DNS response serialized back to binary format
- Check Size (UDP only): Verify response fits within UDP size limits
- Standard DNS: 512 bytes (RFC 1035)
- With EDNS(0): Up to 4096 bytes (configurable via OPT record)
- If too large: Truncate response (set TC flag, clear all answer/authority/additional sections)
- Send: Response sent back to client
- UDP: Direct datagram transmission
- TCP: Send 2-byte length prefix followed by message body
When a DNS response exceeds the maximum allowed UDP message size, the server automatically truncates it according to RFC 1035:
- Standard Mode (no EDNS): Max 512 bytes
- EDNS(0) Mode: Max 4096 bytes (or client-specified size)
Truncated responses:
- Set the
TC(Truncated) flag in the header - Keep the question section intact
- Remove all answer, authority, and additional records
- Signal to the client to retry over TCP for the complete response
common/: Shared data structures (no business logic)decoder/: Only handles binary → struct conversionencoder/: Only handles struct → binary conversionstorage/: Manages record persistence and retrievalserver.rs: Orchestrates request/response cycle
- EDNS(0) support
- TCP support (RFC 1035 compliant with 2-byte length framing)
- Additional record type implementations (MX, NS, SOA, PTR, etc.)
- TTL-based cache expiration
This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License - see the LICENSE file for details.
You may NOT use this software for commercial purposes.