2 · Worked example — read every step
Combine UDP + DNS parsing to build a working DNS server:
func main() {
// Static records
records := map[string]net.IP{
"web.local": net.ParseIP("10.0.0.1"),
"api.local": net.ParseIP("10.0.0.2"),
"redis.local": net.ParseIP("10.0.0.3"),
}
pc, err := net.ListenPacket("udp", ":5353")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
slog.Info("DNS server listening", "addr", ":5353")
buf := make([]byte, 512)
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil {
continue
}
go handleDNSQuery(pc, addr, buf[:n], records)
}
}
func handleDNSQuery(pc net.PacketConn, addr net.Addr, query []byte, records map[string]net.IP) {
// Parse the question
domain, _ := decodeDomain(query, 12)
ip, found := records[domain]
if !found {
// Send NXDOMAIN response
resp := buildNXDomain(query)
pc.WriteTo(resp, addr)
return
}
resp := buildResponse(query, ip)
pc.WriteTo(resp, addr)
slog.Info("resolved", "domain", domain, "ip", ip, "client", addr)
}
Test it: dig @localhost -p 5353 web.local
3 · Fill the gaps
The query handler, from memory — where does the question start, and what if the domain is unknown?
func handleDNSQuery(pc net.PacketConn, addr net.Addr, query []byte, records map[string]net.IP) {
domain, _ := decodeDomain(query, )
ip, found := records[domain]
if !<span class="gap-slot"><input class="gap-input" data-answer="found" size="7" spellcheck="false" autocomplete="off" autocapitalize="off"></span> {
pc.WriteTo(buildNXDomain(query), addr)
<span class="gap-slot"><input class="gap-input" data-answer="return" size="8" spellcheck="false" autocomplete="off" autocapitalize="off"></span>
}
pc.WriteTo(<span class="gap-slot"><input class="gap-input" data-answer="buildResponse(query, ip)" size="26" spellcheck="false" autocomplete="off" autocapitalize="off"></span>, addr)
}