Building a DNS resolver in Go from scratch - Part 2

Mostafa AhmedMostafa Ahmed
5 min read

Recap

In the last part, we went over the DNS components and outlined what the query's cycle looks like. In this part, we'll go over the format of a DNS packet and start building our toy resolver using Go & send a DNS query to the root nameserver.

Israel is committing a genocide against the people of Palestine.
Find out how you can help https://techforpalestine.org/learn-more/
Free Palestine 🇵🇸 End the occupation 🇵🇸


View the full code

DNS Packet Structure

DNS uses the same format for requests and responses. A message consists of 5 sections, a header followed by question, answer, authority & additional sections.

DNS Message Format

We’ll build the query by populating the header and question sections, the other sections will be empty, we’ll discuss them in detail later when we start parsing the response.

Header

A DNS header is 12 bytes long, it consists of 6 fields, each being 2 bytes long.

DNS Header Format

For us, the important fields and the values we’ll use are:

  • ID, an identifier for the DNS query. The server copies this to the response

  • QR, whether this message is a query or a response

  • Opcode, type of query, set to 1 for standard query

  • QDCOUNT, number of questions in message. we'll set this to 1

  • ANCOUNT, number of answer records in response, 0 in a query

  • NSCOUNT, number of authority nameserver records in response, 0 in a query

  • ARCOUNT, number of additional records in response, 0 in a query

Question section

The question section contains the queries we're asking the nameserver.

DNS Question Section Format

Its fields are:

  • QNAME, encoding of the domain name, a variable length field

  • QTYPE, type of domain name translation we’re asking for, set to A to indicate we need an IPv4 address

  • QCLASS, query class, always IN for our case

Encoding the domain name

The DNS protocol defines how to encode a domain name, because QNAME is a bit is a variable length field we need to encode it in a special format. That way whoever parses the message can tell when it ends without needing to know the length.

We can encode the domain name by doing the following:

// Pseudocode
// array of bytes
encoded_domain = []

split the domain name by `.`
    for each part
        encoded_domain += <length of part> <part as bytes>

encoded_domain += 0x00 to indicate termination

For example, if we're encoding mail.google.com, this encodes to

Encoding Example

Building the packet

Now we're ready to start building the query packet

It will be a simple utility function, that accepts a string for the domain name and returns a slice of bytes to be sent.

// dns/message.go
package dns

import (
    "strings"
)

func NewDNSMessage(query string) []byte {
    message := []byte{}

    header := []byte{
        // Transaction ID
        0xaa, 0xaa,

        // Flags
        0x00, 0x00,

        // Queries, Answers, Authority Nameservers & Additional Count
        0x00, 0x01,
        0x00, 0x00,
        0x00, 0x00,
        0x00, 0x00,
    }

    // Domain name, encoded as <PART_LENGTH><PART>
    qname := []byte{}
    parts := strings.Split(query, ".")

    for _, p := range parts {
        l := byte(len(p))

        qname = append(qname, l)
        qname = append(qname, p...)
    }

    // Termination byte
    qname = append(qname, 0x00)

    // QTYPE,  1 = A record (IPv4 address)
    qtype := []byte{0x00, 0x01}

    // QCLASS, 1 = IN (Internet)
    qclass := []byte{0x00, 0x01}

    message = append(message, header...)
    message = append(message, qname...)
    message = append(message, qtype...)
    message = append(message, qclass...)

    return message
}

We will use a dummy value for the transaction ID, this won't be a problem as we’ll only have 1 DNS query in flight at any moment.

For flags we'll use 0x0000, this translates to the following properties on the query

Flags Meaning

Ideally, the message should be serialized from a DNS struct. But for what we're building a simple function gets the job done.

Asking the root server

Typically DNS works over UDP on port 53. We can use net.Dial for this, it creates a net.Conn, with it we can send and receive UDP packets.

The following code builds a DNS query packet, sends it to the root server, waits for the response and copies it into a slice of bytes. Later we will get to parsing the response and building a DNS message from it.

// dns/resolver.go
package dns

import (
    "fmt"
    "net"
    "os"
)

func ResolveFromRoot(domain string, root string) string {
    target := root

    fmt.Printf("Asking: %s\n", target)
    message := NewDNSMessage(domain)

    c, err := net.Dial("udp", target+":53")
    if err != nil {
        fmt.Println("connection to server failed")
        os.Exit(1)
    }

    defer c.Close()
    c.Write(message)

    b := make([]byte, 1024)

    // Read response from socket to the slice of bytes
    n, _ := c.Read(b)
      fmt.Println("response bytes:", b[:5], "...", b[n-5:n])

    return "TBC"
}

Take off

Putting everything together in main.go to test what we have so far.

// main.go
package main

import (
    "fmt"

    "github.com/mostafaahmed97/rootwalk/dns"
)

var roots = map[string]string{
    "a": "198.41.0.4",
    "b": "170.247.170.2",
    "c": "192.33.4.12",
    "d": "199.7.91.13",
    "e": "192.203.230.10",
    "f": "192.5.5.241",
    "g": "192.112.36.4",
    "h": "198.97.190.53",
    "i": "192.36.148.17",
    "j": "192.58.128.30",
    "k": "193.0.14.129",
    "l": "199.7.83.42",
    "m": "202.12.27.33",
}

func main() {
    r := "a"
    domain := "google.com"

    fmt.Printf("Resolving: %s from Root Server: %s(%s)\n", domain, r, roots[r])
    ip := dns.ResolveFromRoot(domain, roots[r])
    fmt.Printf("%s is at %s\n", domain, ip)
}

and running with go run . we get the following output:

We can also see our query and the root's response to it in Wireshark

In the next part, we will parse the response received and update the resolver to recursively ask nameservers until we get our answer.

Cool docs/articles

0
Subscribe to my newsletter

Read articles from Mostafa Ahmed directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mostafa Ahmed
Mostafa Ahmed

Free Palestine, end the genocide 🇵🇸 https://techforpalestine.org/learn-more/