A minimal, educational DNS server implemented from scratch in Go. It parses DNS queries, handles basic A-record lookups from an in-memory map, and sends DNS responses over UDP.
This is meant for learning: the code is heavily commented to explain what each piece is doing and why.
- Parses DNS header, names (with compression), and questions
- Answers A (IPv4) queries for a small in-memory zone
- Sends valid DNS responses over UDP
- Clear, well-documented code paths for parsing and serialization
- Listens on UDP port 5353 instead of 53 (no admin rights required)
- Only answers Type A, Class IN
- No EDNS(0), DNSSEC, TCP fallback, or name compression on serialization
- Very small in-memory zone (edit
zoneAinmain.go)
dns.go— Core DNS types (Header, Question, ResourceRecord, Message)parse.go— Safe readers, header parsing, name parsing (with compression), question parsing, message parsingserialize.go— Big-endian writers, name encoder (no compression), message serialization, response helpersmain.go— UDP server loop, minimal query handler, in-memory zone, and response builder
Use Go to build and run the server. It listens on UDP :5353.
# From the project root
go run .You should see:
DNS server listening on :5353
nslookup example.com 127.0.0.1:5353
nslookup localhost 127.0.0.1:5353dig @127.0.0.1 -p 5353 example.com A
dig @127.0.0.1 -p 5353 localhost AExpected responses (default zone):
example.com→93.184.216.34localhost→127.0.0.1- Unknown names → NXDOMAIN
Edit the in-memory zone in main.go:
var zoneA = map[string]string{
"example.com": "93.184.216.34",
"localhost": "127.0.0.1",
// add more here
}Keys are case-insensitive at query time. Trailing dots are ignored when matching.
- Parsing follows RFC 1035: fixed 12-byte header, then question/answer sections.
- Names on the wire are label sequences; compression pointers are supported when parsing.
- Serialization writes header + sections back to bytes (big-endian). For simplicity, we do not compress names when sending.
- Response flags set QR=1, copy OPCODE and RD from the query, set AA, and set RCODE appropriately.
- Nothing returns: ensure you’re querying port 5353, not 53.
- Permission denied on port 53: use 5353 or run with elevated privileges (not recommended for learning).
- NXDOMAIN for known name: check you added it to
zoneA, and you queriedA(IPv4) records. - Mixed case or trailing dot: server normalizes lookups; try
dig @127.0.0.1 -p 5353 ExAmPlE.CoM. A.
- Add AAAA (IPv6), CNAME, NS records
- Implement name compression during serialization
- Add unit tests for parsing/serialization helpers
- Add a config or file-backed zone loader
Have fun exploring DNS internals! This server aims to be a clear, hackable starting point rather than production-ready.