Building a DNS Forwarder CLI Application in Golang

Luka Piplica
3 min readJan 21, 2024

--

Golang

Introduction

In this article, we’ll explore the creation of a DNS Forwarder Command-Line Interface (CLI) application using the Go programming language. The DNS Forwarder will act as a nameserver to resolve DNS queries, offering local caching for improved performance. The project is inspired by my curiosity about how DNS servers work at a low-level. The source code can be found on GitHub.

Goal

The primary goal of the DNS Forwarder is to create a UDP server that listens for incoming DNS requests, parses the requests, and forwards them to another DNS server for resolution. Additionally, the application should implement local caching of successful DNS resolutions to enhance performance.

Setting up the UDP Server

Let’s start by setting up the UDP server. The server listens on a specified port, defaulting to port 8080 if no port is provided. The following code snippet demonstrates how to initiate the server:

// ... (imports and other code)

var cacheMutex sync.RWMutex
var dnsCache = make(map[string]dns.Msg)

func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
// ... (existing handleDNSRequest code)
}

var startCmd = &cobra.Command{
// ... (start command details)

Run: func(cmd *cobra.Command, args []string) {
// ... (existing run command code)

// Creating a UDP listener
conn, err := net.ListenUDP("udp", addr)
error.Check(err)

// Close the UDP server once Run is finished executing
defer conn.Close()

// Creating a tcp listener
l, err := net.Listen("tcp", ":2000")
error.Check(err)

// Activate and serve DNS requests
err = dns.ActivateAndServe(l, conn, dns.DefaultServeMux)
error.Check(err)
},
}

// ... (rest of the code)

Parsing DNS Requests

Next, we need to parse the incoming DNS requests. A DNS message comprises a header, questions section, answer section, authority section, and additional section. The header and question section are particularly important for our purposes. The following function extracts relevant information from the DNS request:

func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)

fmt.Println("DNS Message: ", r)

question := r.Question[0]

cacheKey := fmt.Sprintf("%s|%d", question.Name, question.Qtype)

// Check the cache for a previous response
cacheMutex.RLock()
cachedResponse, cacheExists := dnsCache[cacheKey]
cacheMutex.RUnlock()
}

This function prints a human-readable interpretation of the DNS request header and question. Additionally, it checks the cache for a previous response based on the question key.

Forwarding DNS Requests and Caching

Now, let’s implement the forwarding of DNS requests to an external DNS server and caching of successful resolutions. The following code demonstrates this process:

func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)

fmt.Println("DNS Message: ", r)

question := r.Question[0]

cacheKey := fmt.Sprintf("%s|%d", question.Name, question.Qtype)

// Check the cache for a previous response
cacheMutex.RLock()
cachedResponse, cacheExists := dnsCache[cacheKey]
cacheMutex.RUnlock()

if cacheExists {
// Serve the response from the cache
m.Answer = append(m.Answer, cachedResponse.Answer...)
} else {
// Forward DNS query to another DNS server
forwardedResponse, err := dns.Exchange(r, "8.8.8.8:53")
if err != nil {
fmt.Println("Error forwarding DNS request:", err)
m.SetRcode(r, dns.RcodeServerFailure)
w.WriteMsg(m)
return
}

// Check the query type (A record or IPv4 address)
if question.Qtype == dns.TypeA {
// Serve the response from the forwarded request
m.Answer = append(m.Answer, forwardedResponse.Answer...)

// Cache the response
cacheMutex.Lock()
dnsCache[cacheKey] = *forwardedResponse
cacheMutex.Unlock()
} else {
// Handle other query types or respond with an error
m.SetRcode(r, dns.RcodeNameError)
}

// Print that the request has been forwarded
fmt.Println("Forwarded DNS request to 8.8.8.8:53")
}

// Write the response
w.WriteMsg(m)
}

This code checks if a valid response exists in the cache. If so, it serves the response from the cache. Otherwise, it forwards the DNS request to the external DNS server, processes the response, caches it, and serves the result.

Conclusion

In this article, we’ve covered the implementation of a DNS Forwarder CLI application in Golang. The application listens for DNS requests, parses them, forwards the requests to an external DNS server, caches successful resolutions, and serves responses from the cache when available. The project can be found on GitHub.

--

--

Luka Piplica

Software Developer & Computer Science student from Toronto, Canada