How to write a simple DNS client (resolver) with C#
First off, to write a Domain Name System client we first need to know what DNS actually does, it’s an integral part of the internet, yet not that many people are familiar with it.
To put simply DNS is responsible for resolving human-readable domain names into IP addresses of the server you’re trying to reach.
So when you are trying to go to Facebook or Google websites your request is first handled by DNS in order to reach the appropriate server. To view this in action you can open your terminal and type $ tracert google.com on Windows machine or $ traceroute google.com for Linux machine.
The Ip address in brackets is the actual address of the server you’re connecting to.
Writing a client
“The client side of the DNS is called a DNS resolver. A resolver is responsible for initiating and sequencing the queries that ultimately lead to a full resolution (translation) of the resource sought” As described in Wikipedia.
We are going to write a simple solution using C# sockets to send a query for domain address and receive an answer parsing it into human-readable format.
Note that the Ip address is different from the previous example of trace route command. The reason behind this is that there are many DNS servers available that could resolve domain names differently. On my machine, I use 1.1.1.1 Cloudfare DNS, but for this implementation, we’re going to use 8.8.8.8 Google’s Public DNS.
Opening a socket
First thing’s first we need to create a UDP socket and “Connect” to it (Note: C# sockets use method Connect, but in UDP case it’s a bit inaccurate since it mostly just established remote endpoint)
Constructing the header and query
This part is a bit harder if you’re unfamiliar working with ASCII and constructing commands as byte arrays.
Let’s first write the code and then describe each part:
Note 0x00 values describe integers written in hexadecimal
You can find a simplified version of the official documentation about DNS formats here
This code specifies ID of query as 0x46 0x62, specifies that the message is a query and that recursion is desired. Then it specifies the hostname we wish to refer to. We send this query to the socked established previously.
Reading response
After sending the query we should receive a response with the data desired now all that’s left is parsing the data back into human-readable form.
First things first we need to read the header and get response information.
The variables that are important here are queCount- Question Count and ansCount -Answer Count. To properly read Answers and Questions we will need to create their structures.
With these structures established we can move forward to parsing data into them.
Here we use byteCount as an index of where in the response byte array we are reading. We have already read the header which contains 12 bytes so we skip 12 bytes and start reading questions if there are any.
Questions usually contain domain names already asked in the query, but we are still going to parse it, it always leads with a length of the string to the next dot.
Then we simply read the question type and class which follows after the name and both are 2 bytes long.
Next, we move onto reading the Answers.
Reading answers are a bit easier since their structure is mostly fixed. First two bytes contain a pointer to the domain we are trying to get, this usually points to the question’s Name to save space.
The next 2 bytes are the type of the answer, usually, we are expecting to receive either type 1 or type 5.
Next 2 bytes tell us what class the answer is, we are expecting class 1 since it means we are receiving an Internet Address.
TTL is 4-byte unsigned integer which tells us how long in seconds the message we received is valid.
rdLength is a 2-byte integer of the length of the following data.
Once all the answers are read and put in structure let’s parse and output the data to the user.
First for each answer we check its type.
If the type is 5 it means we need to read data in ASCII format then concat the string with dots as separators and then write the name to the console.
If the type is 1 it means we are reading IPv4 address so each byte after the first, which is the length of data, is a number separated by dots.
That’s it! Don’t forget to close the socket!
Now you can check any domain’s IP address with the program we have written.
For full code check out my Github!
For further research check out rfc1035