DNS (or Domain Name Service) is the prime mechanism for translating human-readable web domains to IP addresses. netprivacypro.com for example can become 217.160.0.183. IP addresses are what routing protocols in local networks or on the internet use to establish connections between hosts. Since memorizing IP addresses is cumbersome and error prone (and they can change frequently), DNS servers were introduced. These servers (if public) usually receive a domain name and, if they know it, return the respective IP address. This mechanism is hidden inside your operating system’s network stack and you will almost never have to interact with it directly. If you are a developer seeking to implement this mechanism though, you need to know how to utilize the DNS protocol. In this article, I will show you how you can implement dependency-free DNS name resolution in C.
Of course you can rely on your OS to perform this name resolution for you, and in most cases this is the best choice. Implementing it yourself is a great way to understand the underlying protocol though, and this way you can configure exactly how you want the DNS query to behave. Deciding on the exact DNS server to use can be a crucial security feature too, as DNS servers can keep track of which IP client requested which domain to be resolved, and when. From a privacy point of view, this can be problematic as your browsing behavior is then tied to your IP identity. There are servers that specifically do not record your activity (like Cloudflare’s WARP service), but that’s by far not true for the vast majority of DNS servers. Use this article as a guide to how DNS works from a user perspective, and how you can manually make your applications perform the right queries. You can find the entire source code covered here in this GitHub repository, too.
Understanding the DNS Protocol
The DNS protocol has a number of different options you can configure when asking name servers to resolve a domain. The sheer number of different things to set might look daunting at first, but upon closer inspection, they all just require a sensible setting for your purpose: Resolving exactly one domain name.
struct DNSHeader {
unsigned short id; ///< Identification number
unsigned char rd :1; ///< Recursion desired
unsigned char tc :1; ///< Truncated message
unsigned char aa :1; ///< Authoritative answer
unsigned char opcode :4; ///< Purpose of message
unsigned char qr :1; ///< Query/Response flag
unsigned char rcode :4; ///< Response code
unsigned char cd :1; ///< Checking disabled
unsigned char ad :1; ///< Authenticated data
unsigned char z :1; ///< Reserved
unsigned char ra :1; ///< Recursion available
unsigned short q_count; ///< Number of question entries
unsigned short ans_count; ///< Number of answer entries
unsigned short auth_count; ///< Number of authority entries
unsigned short add_count; ///< Number of resource entries
};
These sensible settings are shown below, alongside some explanatory comments. These settings are also what we will be using later on in the resolver application.
dns->id = (unsigned short) htons(getpid());
dns->qr = 0; // This is a query
dns->opcode = 0; // Standard query
dns->aa = 0; // Not authoritative
dns->tc = 0; // This message is not truncated
dns->rd = 1; // Recursion Desired
dns->ra = 0; // Recursion not available
dns->z = 0;
dns->ad = 0;
dns->cd = 0;
dns->rcode = 0;
dns->q_count = htons(1); // We have only 1 question
As you can see, there’s nothing fancy about the request header. While it is long, most values go by a 0 or 1 default value for many use-cases. The second important data structure we need to take care of is the request payload we actually send to the domain name server. It consists of two fields, the query type and the class, as shown below.
struct DNSQuestion {
unsigned short qtype; ///< Query type
unsigned short qclass; ///< Query class
};
Again, the values to put here are pretty simple for our use-case:
qinfo->qtype = htons(DNS_QUERY_TYPE_A); // Type A query
qinfo->qclass = htons(DNS_QUERY_CLASS_IN); // Internet class
These two structures make up the data packet we want to send to a DNS server. There is a great in-detail explanation of the header structures and their memory layout here. For this tutorial, we will stick with the above and make good use of it in the next section: Sending it to a DNS server.
Querying a DNS Server from C
Now that we know what data structures we want to send to the DNS server, we need to format a few more values for proper processing. For example, domain names need to be transfered in specific data chunks. While in your browser bar the domain netprivacypro.com appears as one string, DNS servers need to see them separated by “.“, becoming two elements: netprivacy and com. This format breaks the hostname into its constituent labels and prepends each label with its length. The data is terminated by a 0-length marker. That means, the above URL becomes this series of data bytes:
13 'n' 'e' 't' 'p' 'r' 'i' 'v' 'a' 'c' 'y' 'p' 'r' 'o' 3 'c' 'o' 'm' 0
This DNS format is required for forming DNS queries. It is commonly called length-prefixed format or DNS label format. We will be using the below function to format our domain names accordingly:
void hostname_to_dns_format(unsigned char* dns, unsigned char* host) {
int32_t lock = 0, i;
strcat((char*)host, ".");
for(i = 0; i < strlen((char*)host); i++) {
if(host[i] == '.') {
*dns++ = i - lock;
for(; lock < i; lock++) {
*dns++ = host[lock];
}
lock++;
}
}
*dns++ = '\0';
}
Since the DNS label format is how DNS servers communicate domains back and forth, that’s also what it returns. In the request headers above, we set dns->q_count to 1 because we want to resolve one hostname at a time. The DNS server will respond with the IP address of said hostname, accompanied by the original domain name we wanted to resolve. This is the case because we can ask for multiple domains to be resolved in the same query and need to be able to distinguish them afterwards.
Now when we get our query answered, we need a way to format the DNS label format entries back into domain name strings. For that purpose, we use the below function that essentially does the opposite of what hostname_to_dns_format from above does:
unsigned char* read_name(unsigned char* reader, unsigned char* buffer, int* count) {
unsigned char *name;
unsigned int p = 0, jumped = 0, offset;
int i , j;
*count = 1;
name = (unsigned char*)malloc(256);
name[0]='\0';
while(*reader != 0) {
if(*reader >= 192) {
offset = (*reader) * 256 + *(reader + 1) - 49152;
reader = buffer + offset - 1;
jumped = 1;
} else {
name[p++] = *reader;
}
reader = reader + 1;
if(jumped == 0) {
*count = *count + 1;
}
}
name[p]='\0';
if(jumped == 1) {
*count = *count + 1;
}
for(i = 0; i < (int)strlen((const char*)name); i++) {
p = name[i];
for(j = 0; j < (int)p; j++) {
name[i] = name[i + 1];
i = i + 1;
}
name[i] = '.';
}
name[i-1]='\0';
return name;
}
Now that we have those utility functions in place, we can make an actual request to the DNS server. By putting together the functions from above and adding some socket logic, we can utilize sendto and recvfrom for the actual data transfers:
// Set up the DNS header
dns = (struct DNSHeader *)&buf;
dns->id = (unsigned short) htons(getpid());
dns->qr = 0; // This is a query
dns->opcode = 0; // Standard query
dns->aa = 0; // Not authoritative
dns->tc = 0; // This message is not truncated
dns->rd = 1; // Recursion Desired
dns->ra = 0; // Recursion not available
dns->z = 0;
dns->ad = 0;
dns->cd = 0;
dns->rcode = 0;
dns->q_count = htons(1); // We have only 1 question
// Point to the query portion
qname = (unsigned char*)&buf[sizeof(struct DNSHeader)];
// Convert hostname to DNS format
hostname_to_dns_format(qname, (unsigned char*)argv[1]);
printf("Resolving %s\n", qname);
// Set up the question structure
qinfo = (struct DNSQuestion*)&buf[sizeof(struct DNSHeader) + (strlen((const char*)qname) + 1)];
qinfo->qtype = htons(DNS_QUERY_TYPE_A); // Type A query
qinfo->qclass = htons(DNS_QUERY_CLASS_IN); // Internet class
// Send the query
if(sendto(s, (char*)buf, sizeof(struct DNSHeader) + (strlen((const char*)qname)+1) + sizeof(struct DNSQuestion), 0, (struct sockaddr*)&dest, sizeof(dest)) < 0) {
perror("Sendto failed");
return 1;
}
// Receive the response
int i = sizeof(dest);
if(recvfrom(s, (char*)buf, 65536, 0, (struct sockaddr*)&dest, (socklen_t*)&i) < 0) {
perror("Recvfrom failed");
return 1;
}
The buf variable defined at the top in the snippet above is what holds all the relevant data in the end. Proper length definition for how much data to send is included in the sendto call, and the recvfrom call will retrieve the DNS server’s response. Now that we got the communication with the server in place, we will perform the correct setup and parsing in the next section: Putting it all together into a runnable application. Then, we will finalize our dependency-free DNS name resolution in C.
Tieing it all together: Writing a Command Line DNS Resolver
With all the functions and structures in place, we define a main function below that performs the whole process of querying a DNS server, and processing its response. We will allow two command line arguments:
- The domain name to resolve: This is the main portion of what we’re interested in. We want the IP address of whatever domain we pass into the application here.
- The DNS server to use: This is one of the main points of why you would want to implement the name resolution yourself. Deciding on what exact DNS server to use can greatly add to connection privacy.
So the final command line we use will look like this:
./dns_resolver netprivacypro.com 8.8.8.8
Where netprivacypro.com is the domain we want to resolve, and 8.8.8.8 is the DNS server we would like to query for the IP. You can find the whole compilable code on GitHub as well. Here is the main function:
int main(int argc, char *argv[]) {
unsigned char buf[65536], *qname, *reader;
struct sockaddr_in a;
struct DNSHeader *dns = NULL;
struct DNSQuestion *qinfo = NULL;
struct sockaddr_in dest;
if(argc < 3) {
printf("Usage: %s <hostname> <dns-server>\n", argv[0]);
return 1;
}
// Create a socket
int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(s < 0) {
perror("Socket creation failed");
return 1;
}
dest.sin_family = AF_INET;
dest.sin_port = htons(DNS_PORT);
dest.sin_addr.s_addr = inet_addr(argv[2]); // DNS server IP address
// Set up the DNS header
dns = (struct DNSHeader *)&buf;
dns->id = (unsigned short) htons(getpid());
dns->qr = 0; // This is a query
dns->opcode = 0; // Standard query
dns->aa = 0; // Not authoritative
dns->tc = 0; // This message is not truncated
dns->rd = 1; // Recursion Desired
dns->ra = 0; // Recursion not available
dns->z = 0;
dns->ad = 0;
dns->cd = 0;
dns->rcode = 0;
dns->q_count = htons(1); // We have only 1 question
// Point to the query portion
qname = (unsigned char*)&buf[sizeof(struct DNSHeader)];
// Convert hostname to DNS format
hostname_to_dns_format(qname, (unsigned char*)argv[1]);
// Set up the question structure
qinfo = (struct DNSQuestion*)&buf[sizeof(struct DNSHeader) + (strlen((const char*)qname) + 1)];
qinfo->qtype = htons(DNS_QUERY_TYPE_A); // Type A query
qinfo->qclass = htons(DNS_QUERY_CLASS_IN); // Internet class
// Send the query
if(sendto(s, (char*)buf, sizeof(struct DNSHeader) + (strlen((const char*)qname)+1) + sizeof(struct DNSQuestion), 0, (struct sockaddr*)&dest, sizeof(dest)) < 0) {
perror("Sendto failed");
return 1;
}
// Receive the response
int i = sizeof(dest);
if(recvfrom(s, (char*)buf, 65536, 0, (struct sockaddr*)&dest, (socklen_t*)&i) < 0) {
perror("Recvfrom failed");
return 1;
}
// Move ahead of the DNS header and the query section
reader = &buf[sizeof(struct DNSHeader) + (strlen((const char*)qname) + 1) + sizeof(struct DNSQuestion)];
// Read the answers
for(i = 0; i < ntohs(dns->ans_count); i++) {
// Read the name
reader = reader + 2; // Move past the name
reader = reader + 10; // Move past the type, class, TTL, and data length fields
struct sockaddr_in result;
memcpy(&result.sin_addr.s_addr, reader, sizeof(result.sin_addr.s_addr));
reader = reader + 4; // Move past the IP address
// Print the IP address
printf("%s resolved to %s\n", argv[1], inet_ntoa(result.sin_addr));
}
close(s);
return 0;
}
And that’s it. The whole resolver is free of external library dependencies (except for standard libraries). You can now go on and compile the application, as described in the next section. The complete, compilable source code for this DNS resolver application can be found on GitHub.
Compiling the Resolver
To compile the DNS resolver, we use the simple Makefile below:
# Compiler
CC = gcc
# Compiler Flags
CFLAGS = -Wall -Wextra -pedantic -std=c11
# Target Executable
TARGET = dns_resolver
# Source Files
SRCS = dns_resolver.c
# Object Files
OBJS = $(SRCS:.c=.o)
# Default Target
all: $(TARGET)
# Build the target executable
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $(TARGET) $(OBJS)
# Compile source files into object files
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Clean up build files
clean:
rm -f $(TARGET) $(OBJS)
# Phony targets
.PHONY: all clean
Save this as Makefile in the same folder as your source code (expected to be in dns_resolver.c) and run:
make
The compilation should only take a brief moment. Now you should have a binary in the same folder by the name dns_resolver.
Running the Finished DNS Resolver
Now that we have our application handy, we can run the actual request:
./dns_resolver netprivacypro.com 8.8.8.8
The reply will show the current IP address of the domain passed in, as far as the DNS server 8.8.8.8 knows.
Conclusion
Congratulations, you just implemented your own dependency-free DNS name resolution in C! This code allows you to make simple queries to DNS servers of your choice, making sure that the request is sent to the right server. With this knowledge handy you have control over how hostnames are resolved exactly and won’t need to rely on external libraries if that’s a concern for you.
There is a multitude of more advanced topics to cover for DNS though. Encrypted DNS queries over HTTPS (using secure sockets) are an important aspect, as well as DNS over Oblivious HTTP. These topics will be covered in a follow-up article. In te meantime, make sure you use a VPN to hide your DNS traffic from your ISP!
If you liked this article and want to share your own thoughts and experiences, comment below to get the conversation started!