This blog post will explain, in detail, how I made my own DNS server in 2020.
You will need a few things. Some are optional, some are not:
- printout or PDF of the DNS specification (optional)
- a printout or PDF quick guide on DNS that I found online
- nodejs LTS installed (npm is optional)
- dig installed (
- a VM for testing (optional; I used the free one on Google Cloud)
UDP and nodejs
The first thing I decided to tackle was UDP on nodejs. I’ve worked with TCP and HTTP a lot (I am a web developer after all) but never with UDP. So a quick google away I was able to make a quick UDP server using the built-in
This was enough to see something on my screen when I fired up
dig with a request:
The thing you will see displayed in your terminal will be something like this:
My initial reaction was to just try and convert this to string
d.toString() but I got garbage out with my domain visible. Success! At least for now. Without understanding the DNS protocol I couldn’t proceed.
Optional: formating the buffer
This helped me a bunch! I found a nice lib gagle/node-hex that I used to format the buffers while I worked. If you pass in the same buffer in it:
console.log(hex(d)); you get something like this:
Crash course in DNS
I recommend you read thru the documentation I linked. What we will need to understand before we start is how the DNS packet is constructed. In the formatted dump you can see hex sorted by two characters - this we call a byte! And two of them are called an octet. Let’s dissect the first part!
I’ve underlined the first part we are going to parse. This we call the header of the request. It consits of multiple fields all 2 bytes long.
Understanding this is not really needed but we need a few things from this table to move on. Those fields are marked with
id field is a uniq ID to the request and we need it so we can send it back in the response. The
QDCOUNT fields is the number of questions we have. This tutorial and the server will only work by assuming you are asking it only one question.
Next up is body. I’ve underlined it in the dump:
Now it’s important to understand how DNS sends ASCII text over the line. The format is really simple, and on our example, decoded, it looks like this:
From this we can observe that the body will end with a null char
00 (to here we parse the body) and every dot (
.) is preceeded with how many letters are in front of it.
3. More flags
The last to octects are the TYPE and CLASS of our request. For all intensive purposes we are going to asume we are only interested in
A records and
IN class. Reade more about the available records here.
Decoding and parsing the request
We already conculded that the
data we get from the
message event is a Buffer, but we have to parse it to understand it. My initial reponse to this was a
toString but that didn’t work. But if we remember that we can convert hex numbers to decimal we can try converting this to a regular array!
And going from there we can just follow the table in headers. I used splice here because it’s a bit faster and easier than slice (thanks Kuki!).
We will only really use a few things from here but we stored it anyways for future :D. Next up is the body. If you go back and see that the actual body ends with
0x00 we can do something like this:
What we do here is look at what index is
0x00 and we include it as well (
+1). What we still have to do is
CLASS. So our decoded question would look something like this:
So at this point all that is left we have to parse the body to a string.
Parsing the body
Let’s look at the body again:
So the idea is to iterate thru that array and go forward the number of places we are told. So for the example the first iteration should be 6 chars forward, then we place a dot, and 3 chars forward.
You might wonder why we have another
Array.from there. Remember! that
shift are modifying the array so we make a copy. The
46 you can see at the push part is the ascii code for
., and that’s the dot in our domain name.
We have to splice once again to remove the trailing dot. Play around with this function so it makes sense. We can finally store the request domain name:
Encoding the response
Let’s start preparing the response. It’s very similar as the request, and we’ll include stuff from the request while constructing it. Take a moment and study this dump. See what’s the same and what’s different.
We hardcoded the flags to make this a response, and we hardcoded the
ANCOUNT. This is the number of answers we have. Other fields we copy over. And now the finale - we have to send out the IP!
I’ve created a simple json file with only one record:
Parsing this to a an array of numbers is failry easy now:
I’ve added a check here if there is no
ip. This is to ensure that if we don’t know of an address we can respond with an empty field.
We also have a bunch of fields I decided to hardcode. If you want to know what they do read up; but I opted out on just following along the document linked above.
Let’s construct the response as a buffer:
And that’s it! 🎉🎉 We can send it out.
Sending the response
The second argument of the
message event from above contains the request IP and port where we can send out the response.
And that’s it - now you have a working but a very basic DNS server.
You can find the code at my repo: