Build a DNS server in Golang

Image for post
Image for post
“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

Ingredients we need:

  • UDP

The recipe:

  • UDP: supported with std net package

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.Messageerr = m.Unpack(buf)

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

Image for post
Image for post
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 stores.Lock()s.data[questionToString(q)] = m.Answerss.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!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store