Writing a DNS Resolver From Scratch (Part 1)

EDIT: I’ve discontinued this series because I’ve frankly just lost interest and am focused on other things.

If you haven’t already, I highly recommend you read “An Introduction to DNS” and “Writing Sub-Byte Data in C” before continuing. DNS was first described in RFC 882, 883, and 973, and the current DNS specification is detailed in RFC 1034 and 1035; it is also recommended you give these RFCs a read. In pursuit of a basic DNS resolver implemented in C, this writeup will be staying consistent with RFC 1034 and 1035.

Contents

Objective

The goal of this writeup is not to create a complete, robust, all-edge-cases-accounted-for implementation of a fully-fledged DNS name server. In fact, it’s not even to implement a typical DNS name server on the Internet that holds records. No, the objective of this writeup is to create a basic DNS resolver from scratch in C that will be able to perform a hardcoded DNS lookup, going through a series of iterative queries with DNS name servers on the Internet to interpret, communicate, and process data sent and received.

Why? Well, for educational purposes, of course! Our motivation today is to understand what DNS looks like from the perspective of a resolver. By implementing it from scratch, we gain a deeper-level understanding of the specification. We want our resolver to:

Note that the scope for this writeup is restricted to IPv4. This is not the case in real life.

Resolvers vs Recursive Servers

In “An Introduction to DNS”, I briefly talked about how DNS can be set up on a local host and/or network. Recall that a resolver that exists at the network level, with whom local hosts on the network communicate with through their own local host stub resolvers, is referred to as a recursive server. Thus, recursive servers are just resolvers shared by the network. At this level, I may interchangeably use the terms “recursive server,” “resolver,” and “local name server.” While there are differences later on at where this software is run, there is no meaningful difference at the level of this writeup.

Inspecting a DNS Query

With Wireshark, we can inspect DNS queries that are generated when we perform an action that utilizes DNS, such as visiting a website with a web browser or SSHing into a server using a domain. The following is a DNS query generated from opening up a browser and visiting shawnd.xyz.

DNS Format

This is consistent with RFC 1035. Both RFC 883 and 1035 reference a format for DNS messages, with RFC 1035 describing it in section 4.1:

All communications inside of the domain protocol are carried in a single format called a message. The top level format of message is divided into 5 sections (some of which are empty in certain cases) shown below:

    +---------------------+
    |        Header       |
    +---------------------+
    |       Question      | the question for the name server
    +---------------------+
    |        Answer       | RRs answering the question
    +---------------------+
    |      Authority      | RRs pointing toward an authority
    +---------------------+
    |      Additional     | RRs holding additional information
    +---------------------+

DNS Headers

What we see in the query are the header and question. The header itself is defined in RFC 1035 section 4.1.1:

The header contains the following fields:

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      ID                       |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    QDCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ANCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    NSCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ARCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Summarizing, the individual sections are:

Section Description
ID An integer associated with the query, copied by the reply to match queries to replies.
QR 0=query; 1=response
OPCODE 0=query; 1=inverse query; 2=server status request
AA 0=non-authoritative; 1=authoritative; used in responses.
TC 0=non-truncated; 1=truncated
RD 0=neutral; 1=request to use recursion; used in queries, copied in responses.
RA 0=recursion unavailable; 1=recursion available; used in responses.
Z The zero bits, always set to 0.
RCODE 0=no error; 1=format error; 2=server failure; 3=name error; 4=not implemented; 5=refused; responses only
QDCOUNT The number of questions.
ANCOUNT The number of answers.
NSCOUNT The number of authority records.
ARCOUNT The number of additional records.

DNS Questions

The question section is defined in RFC 1035 section 4.1.2:

The question section is used to carry the “question” in most queries, i.e., the parameters that define what is being asked. The section contains QDCOUNT (usually 1) entries, each of the following format:

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                     QNAME                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QTYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QCLASS                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Summarizing, the individual sections are:

Section Description
QNAME The domain name being queried as a sequence of label lengths and labels.*
QTYPE The query type, having same possible values as TYPE.
QCLASS The query class, usually IN for the Internet.

* This is in the format [1 B label length n][n B label] and always terminates with the root label (null). No padding is used.

In hexadecimal, the query was:

42 6a 01 00 00 01 00 00 00 00 00 00 06 73 68 61 77 6e 64 03 78 79 7a 00 00 01 00 01

Let’s break that down. The header section is a fixed length of 12 bytes. Therefore, our header is:

42 6a 01 00 00 01 00 00 00 00 00 00
ID........... 0x426a
QR........... 0 (query)
OPCODE....... 0 (standard query)
AA........... 0 (N/A, 0 because this is not a response)
TC........... 0 (this message was not truncated)
RD........... 1 (request the name server to use recursion)
RA........... 0 (N/A, 0 because this is not a response)
Z............ 0
RCODE........ 0 (N/A, 0 because this is not a response)
QDCOUNT...... 1 (we have 1 question)
ANCOUNT...... 0 (N/A, 0 because this is not a response)
NSCOUNT...... 0 (N/A, 0 because this is not a response)
ARCOUNT...... 0 (N/A, 0 because this is not a response)

The question section is the remainder of our message. Therefore, our question is:

06 73 68 61 77 6e 64 03 78 79 7a 00 00 01 00 01

We first start with the QNAME until we reach the root label. 06 denotes a label length of 6, and 73 68 61 77 6e 64 is just hex for shawnd. The next label has a length of 03 and is 78 79 7a (xyz). The next label has a length of 00, meaning that we’ve hit the root. Therefore, our label is shawnd.xyz..

QNAME....... shawnd.xyz.
QTYPE....... 1 (IPv4)
QCLASS...... 1 (Internet)

QTYPE values are described in RFC 1035 sections 3.2.2 and 3.2.3. QCLASS values are described in RFC 1035 sections 3.2.4 and 3.2.5.

Cool, now we know the format for a typical DNS query! Note that all of this happens over UDP. At this point, we can start writing queries in C.

Creating a DNS Query From Scratch

We first start off with our includes and definitions. We use <arpa/inet.h> for all things networking and <string.h> for some string operations when creating the body later on. We additionally need <stdlib.h> for some memory operations. As always, we’ll use <stdio.h> for input and output:

#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define FQDN "shawnd.xyz."
#define ROOT "198.41.0.4"

/* Header options. */
enum
{
	ID      = 0x426a,
	QR      = 0,
	OPCODE  = 0,
	AA      = 0,
	TC      = 0,
	RD      = 1,
	RA      = 0,
	Z       = 0,
	RCODE   = 0,
	QDCOUNT = 1,
	ANCOUNT = 0,
	NSCOUNT = 0,
	ARCOUNT = 0,
};

ROOT is a root DNS name server. How does the resolver know the root name server IP? Simple: it’s pre-communicated. You can find a full list of root servers here: IANA.

Creating the Header

Now we can write our header:

/* Create a DNS query message header. */
char *create_header(int id, char qr, char opcode, char aa, char tc, char rd, char ra,
	char z, char rcode, int qdcount, int ancount, int nscount, int arcount)
{
	char *header = (char *)calloc(12, sizeof(char));

	/* ID. */
	header[0] = (id & 0xFF00) >> 8;
	header[1] = id & 0x00FF;

	/* QR, OPCODE, AA, TC, RD. */
	header[2] = qr;
	header[2] = (header[2] << 4) | opcode;
	header[2] = (header[2] << 1) | aa;
	header[2] = (header[2] << 1) | tc;
	header[2] = (header[2] << 1) | rd;

	/* RA, Z, RCODE. */
	header[3] = ra;
	header[3] = (header[3] << 3) | z;
	header[3] = (header[3] << 4) | rcode;

	/* QDCOUNT. */
	header[4] = (qdcount & 0xFF00) >> 8;
	header[5] = qdcount & 0x00FF;

	/* ANCOUNT. */
	header[6] = (ancount & 0xFF00) >> 8;
	header[7] = ancount & 0x00FF;

	/* NSCOUNT. */
	header[8] = (nscount & 0xFF00) >> 8;
	header[9] = nscount & 0x00FF;

	/* ARCOUNT. */
	header[10] = (arcount & 0xFF00) >> 8;
	header[11] = arcount & 0x00FF;

	return header;
}

If you’re confused, I’d recommend you give “Writing Sub-Byte Data in C” a read.

Creating the Body

The next step is to write our body. Writing the body is going to be a little bit trickier.

/* Create a DNS query message body. */
char *create_body(char *fqdn, int length, int qtype, int qclass)
{
	int n = 0;
	char *body = NULL;

	/* Body size should be enough for the labels, lengths, QTYPE, and QCLASS. */
	n = length+5;

	/* Allocate the body. */
	body = (char *)calloc(n, sizeof(char));

    /* QTYPE, QCLASS. */
	body[n-4] = (qtype & 0xFF00) >> 8;
	body[n-3] = qtype & 0x00FF;
	body[n-2] = (qclass & 0xFF00) >> 8;
	body[n-1] = qclass & 0x00FF;

	/* Make all labels and their lengths. */
	for (int i = 0, n = 0; i < length; i++)
	{
		if (fqdn[i] == '.')
		{
			body[n] = i-n;
			memcpy(body+n+1, fqdn+n, i-n);
			n = i+1;
		}
	}

	return body;
}

Take a moment to read through the logic of that. The logic is actually easier to follow than it seems at first glance.

With both these functions created, we can now integrate them into the main function to form the full message.

int main()
{
	char *header = create_header(ID, QR, OPCODE, AA, TC, RD, RA, Z, RCODE,
		QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT);
	char *body = create_body(FQDN, strlen(FQDN), 1, 1);
	char *message = (char *)calloc(12+strlen(FQDN)+5, sizeof(char));

	memcpy(message, header, 12);
	memcpy(message+12, body, strlen(FQDN)+5);

	return 0;
}

C Sockets

The next part scares most people, but I promise it’s actually a lot easier than it seems: sockets. Sockets in C can be daunting at first but they’re actually fairly intuitive once you get the hang of it. We first start off by making some structures, a descriptor, and a length buffer:

struct sockaddr_in ns;    // Address structure for the name server.
struct timeval timeout;   // Time structure for the timeout.
int sockfd  = 0;          // Socket file descriptor.
int socklen = 0;          // Socket length (used later).

Then, we define our destination:

ns.sin_family       = AF_INET;           // IPv4.
ns.sin_addr.s_addr  = inet_addr(ROOT);   // Turn "198.41.0.4" into an Internet address.
ns.sin_port         = htons(53);         // htons - host to network short; 53 is the port.

Now, we can define the length of the socket and timeout parameters. Let’s go for a 60-second timeout:

socklen = sizeof(ns);   // This will get used later.

timeout.tv_sec  = 60;
timeout.tv_usec =  0;

At this point, we can create the socket and set the socket options:

sockfd = socket(AF_INET, SOCK_DGRAM, 0);
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));

The specification for socket is:

int socket(int domain, int type, int protocol);

We have created an IPv4 UDP (AF_INET, SOCK_DREAM, respectively) socket. As protocol is 0, the system will select the default protocol for the domain/type.

The specification for setsockopt is:

int setsockopt(int socket, int level, int optname, const void *optval, int optlen);

The specification for setsockopt is a tad bit more complex, but not by much. We are setting the options for the socket at sockfd, with SOL_SOCKET meaning that the options will be set at the socket level, SO_RCVTIMEO meaning that we’re setting the timeout, and the last two arguments are obviously the timeout and the length of the timeout. This, well, sets the timeout.

At this point, we have all the logic necessary to send with sendto, whose specification is:

ssize_t sendto(int socket, const void *message, size_t length, int flags,
	const struct sockaddr *dest_addr, socklen_t dest_len);

Thus, we’d write:

sendto(sockfd, message, 12+strlen(FQDN)+5, 0, (struct sockaddr *)&ns, socklen);

If it returns a negative value then that signifies an error. We’ll also have to receive the response with recvfrom, whose specification is:

ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags,
	struct sockaddr *restrict address, socklen_t *restrict address_len);

Thus, we’d write:

recvfrom(sockfd, buff, 1024, 0, (struct sockaddr *)&ns, &socklen);

We’d of course have to create a new buffer for this, which is no problem at all. At this point, we can integrate it into the main function and test it, inspecting the traffic with Wireshark. We won’t receive in C just yet – only observe from Wireshark:

int main()
{
	/* Socket. */
	struct sockaddr_in ns;
	struct timeval timeout;
	int sockfd  = 0;
	int socklen = 0;

	/* Message. */
	char *header = create_header(ID, QR, OPCODE, AA, TC, RD, RA, Z, RCODE,
		QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT);
	char *body = create_body(FQDN, strlen(FQDN), 1, 1);
	char *message = (char *)calloc(12+strlen(FQDN)+5, sizeof(char));

	/* Buffers. */
	char buffer[1024] = {0};

	/* Copy the header and body into the message. */
	memcpy(message, header, 12);
	memcpy(message+12, body, strlen(FQDN)+5);

	/* Define the destination. */
	ns.sin_family       = AF_INET;
	ns.sin_addr.s_addr  = inet_addr(ROOT);
	ns.sin_port         = htons(53);

	/* Define the socket length and timeout. */
	socklen = sizeof(ns);
	timeout.tv_sec  = 60;
	timeout.tv_usec =  0;

	/* Create the socket and set options. */
	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));

	/* Send the query. */
	if (sendto(sockfd, message, 12+strlen(FQDN)+5, 0, (struct sockaddr *)&ns, socklen) < 0)
	{
		puts("Error encountered when sending query!");
		return -1;
	}

	return 0;
}

Response Observation

The query is successful, and we observe the response:

It goes on for quite a bit…

Part 2: Interpreting Responses

At this point, we’ve successfully created a single DNS query that receives a DNS response. In the next installment in this series, we will be interpreting DNS responses.

Happy hacking!