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.Messageerr = m.Unpack(buf)
In case you are curious how a DNS message looks like:
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 WriteToUDP
should 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!