Build a DNS server in Golang

“This site can’t be reached”

Requirement: A local DNS server that can forward and cache DNS queries
Extra 1: Give it an interface for records management (HTTP handlers)
Extra 2: Give it a name


A few things about a DNS server:

  • DNS server translates names to IPs
  • DNS primarily uses UDP protocol on port 53
  • DNS message max length is 512 bytes, longer must use EDNS

Ingredients we need:

  • UDP
  • DNS message parser
  • Forwarding
  • Caching
  • HTTP handlers

The recipe:

  • UDP: supported with std net package
  • DNS message parser: handling packet from the wire following specific protocol will require some work, for a speedy implementation, we will use golang.org/x/net/dns/dnsmessage
  • Forwarding: any but let’s use Cloudflare public resolver 1.1.1.1
  • Caching: memory and persistent, for persistent write, we will encode data using std gob package
  • HTTP handlers: should do create, read, update and delete DNS records. No config file needed.

Open a UDP socket listening on port 53, this will receive incoming DNS queries. Notice that UDP only needs 1 socket to handle multiple “connections”, meanwhile TCP is 1 socket per connection. So we will reuse conn throughout the program.

conn, _ = net.ListenUDP("udp", &net.UDPAddr{Port: 53})
defer conn.Close()
for {
    buf := make([]byte, 512)
    _, addr, _ := conn.ReadFromUDP(buf)
    ...
}

Parse a packet to see if it’s a DNS message.

var m dnsmessage.Message
err = m.Unpack(buf)

In case you are curious how a DNS message looks like:

Source: https://www.inacon.de/ph/data/DNS/DNS-Message-Format_OS_RFC-1035.htm

Forwarding message to public resolver
// re-pack
packed, err = m.Pack()
resolver := net.UDPAddr{IP: net.IP{1, 1, 1, 1}, Port: 53}
_, err = conn.WriteToUDP(packed, &resolver)

Resolver will reply back with answer, we will grab that message and give it to client

if m.Header.Response {
packed, err = m.Pack()
    _, err = conn.WriteToUDP(packed, &addr)
}

Also conn is safe for concurrent use, so those WriteToUDPshould be in a goroutine.


Memorize the answers.

We will use map, simply keying answers by question, it makes lookup very easy, also don’t forget RWMutex, map is not safe for concurrent use. Notice that theoretically there could be multiple questions in a DNS query, but most of DNS servers only accept 1 question.

func questionToString(q dnsmessage.Question) string {
...
}
type store struct {
sync.RWMutex
data map[string][]dnsmessage.Resource
}
q := m.Questions[0]
var s store
s.Lock()
s.data[questionToString(q)] = m.Answers
s.Unlock()
Persistent cache.

We will need to write s.data to file and retrieve it later. To have that without custom parsing, we will use std gob

f, err := os.Create(filepath.Join("path", "file"))
enc := gob.NewEncoder(f)
err = enc.Encode(s.data)

Notice gob needs to know data type before encoding:

func init() {
gob.Register(&dnsmessage.AResource{})
...
}

Records managements

This is rather easy, a Create handler could look like this

type request struct {
Host string
TTL uint32
Type string
Data string
}
func toResource(req request) (dnsmessage.Resource, error) {
...
}
// POST handler
err = json.NewDecoder(r.Body).Decode(&req)
// transform req to a dnsmessage.Resource
r, err := toResource(req)
// write r to the store

And that should be it!

The full source code is here. I named it rind (REST interface name domain).

This is a demonstration of simplicity when implementing a network program in Go.

Any feedback are welcome. Happy coding, gophers!