Writing a simple DNS client capable of resolving the IP address of a hostname. This can be done by sending a request to one of the root nameservers.
RFC
RFC-1035 details the implementation and specification of DNS.
In section 4 it details the format:
Header
The header contains the following fields:
I’m not going to copy the description of those as the RFC has that information.
For generating a query we want:
ID: any 16 bit number
QR: 0 because it’s a query
OPCODE: 0 because it’s a standard query
AA: 0, not used for query
TC: 0 because message won’t be truncated
RD: 1 enable recursion. If the DNS that we’re querying doesn’t have the answer it will recursively find it for us
RA: 0, used on responses
Z: 0, reserved for future use
RCODE: 0, used for response
QDCOUNT: how many questions we’re asking. It will be 1.
ANCOUNT: 0
NSCOUNT: 0
ARCOUNT: 0
Creating the header
This take care of creating the header.
Question
The question section has this format:
For querying marcelofernandes.dev this section has the following value in hexadecimal:
Which can be translated to:
Hex
Translation
Comment
10
16
The length of the label
6D
m
Start of the label: marcelofernandes
61
a
72
r
63
c
65
e
6C
l
6F
o
66
f
65
e
72
r
6E
n
64
d
65
e
73
s
End of the label: marcelofernandes
03
3
The length of the upcoming label
64
d
Start of the label: dev
65
e
76
v
End of the label: dev
00
0
End of QNAME
00
0
QTYPE is 16 bit, first half is 0 in this case
01
1
QTYPE = Internet
00
0
QCLASS is 16 bit, first half is 0 in this case
01
1
QCLASS = A (hostname)
Creating the question
Making the UDP request
To make a request we can send this query to root namespace server. To be more precise, we pick a root server from https://root-servers.org/ and sent a UDP request to it on port 53.
In my case I’m using Google’s server: 8.8.8.8. The complete code is:
Then we can use Wireshark for looking at the query and response/answer:
Query:
Answer:
Parsing response
First thing we need to grab the response:
Understanding the encoding of the response
Translating the response we have:
Hex
Translation
Comment
002A
42
The ID we provided in our query
81
1000 0001
This part is better visualized in binary. This sets the QR bit to 1 (response) and RD (recursion desired)
80
1000 0000
RA (1) means recursion available
0001
1
Question count was 1
0002
2
Answer count was 2
0000
0
0 NSCOUNT on response
0000
0
0 ARCOUNT
106D6…010001
This was our query
From here the answer part starts:
Hex
Translation
Comment
C00C
11 00000000001100
It starts with 11, meaning it’s a pointer. The rest of the bits (which represent 12) is an offset. Applying this offset to the message leave us at 16marcelofernandes. I won’t go over details. The RFC explains this compression well.
0001
1
TYPE = Internet
0001
1
A (hostname)
00000014
20
TTL in seconds. This is 20s
0004
4
The length in bytes of RDATA which is what comes next
34436156
52.67.97.86
Each byte is a part of the IP
C00C
11 00000000001100
Same as above. It’s a pointer to marcelofernandes.dev
0001
1
TYPE = Internet
0001
1
A (hostname)
00000014
20
TTL in seconds. This is 20s
0004
4
The length in bytes of RDATA which is what comes next
B147C3FF
177.71.195.255
A second IP
00
End
Nothing else on the message
Getting to the answer bytes
Can just skip the header which has a fixed width, the question by watching the NUL symbol, and skip QTYPE and QCLASS that are also fixed.
Parsing the answer
The parsing will be hand made for this case. It can be expanded to handle other cases later.
Identifying the pointer
To verify if the answer is a pointer we can do:
Having that we need to join the other bits into a number:
Finally, ignore the first 2 bits and get the rest.
Now we need to get the response and skip 12 bytes:
Decoding the name
Decoding the name is a little trickier.
Parsing the rest
Conclusion
Far from perfect, but I just wanted to mess around with DNS. I wasn’t religious with naming things, haven’t parsed the TTL, and the second answer provided by the server.
However, this gets the job done in resolving a hostname to an IP address.