Author: Zhen TONG 120090694
The DNS Header is the beginning of DNS messages (both queries and responses). It has:
Query ID: This is a 16-bit identifier assigned by the DNS resolver (client) to match queries with responses. The query ID helps ensure that the response matches the corresponding query.
Flags: The Flags field is a 16-bit field that contains several subfields or flags, including:
QR (Query/Response): A 1-bit flag indicating whether the message is a query (0) or a response (1).
OpCode: A 4-bit field that specifies the type of query. Common values include standard query (0), inverse query (1), server status request (2), etc.
AA (Authoritative Answer): A 1-bit flag that indicates whether the responding DNS server is authoritative for the queried domain.
TC (Truncated): A 1-bit flag that indicates if the message was truncated during transmission due to its size exceeding the maximum allowed for the transport protocol (usually UDP).
RD (Recursion Desired): A 1-bit flag that tells the DNS server whether the client wants the server to perform recursive DNS resolution on its behalf.
RA (Recursion Available): A 1-bit flag indicating whether the DNS server supports recursive queries.
Z (Zero): A 3-bit reserved field set to zero.
Response Code: A 4-bit field indicating the result of the DNS query (e.g., NoError, NameError, ServerFailure, etc.).
Counts: The Counts field includes the following counters:
Question Count (QDCount): A 16-bit field that specifies the number of DNS query questions in the message.
Answer Count (ANCOUNT): A 16-bit field indicating the number of resource records in the Answer section of the message.
Authority Count (NSCOUNT): A 16-bit field specifying the number of resource records in the Authority section.
Additional Count (ARCOUNT): A 16-bit field indicating the number of resource records in the Additional section
class DNSHeader:
query_id:int # 16-bit
flags:int # 16-bit
question_count:int # 16-bit
answer_count:int # 16-bit
authority_count:int # 16-bit
additional_count:int # 16-bit
The purpose of header_to_bytes()
method is to convert the attributes of the class instance into a binary representation (a byte sequence)
A tuple named field_tuple
is created. This tuple contains values extracted from the instance's attributes, specifically query_id
, flags
, question_count
, answer_count
, authority_count
, and additional_count
. These attributes seem to represent fields in a DNS header.
return struct.pack("!HHHHHH", *field_tuple)
: This line uses the struct
module's pack
function to convert the field_tuple
into a binary representation. Here's what each part of the format string "!HHHHHH"
means:
"!"
specifies the network byte order (big-endian), ensuring that the data is correctly formatted for network transmission.
"H"
indicates a 16-bit unsigned short integer (2 bytes). Since there are six "H"
format specifiers, this means that the method is expected to pack six 16-bit values.
xxxxxxxxxx
def header_to_bytes(self):
field_tuple = (self.query_id, self.flags, self.question_count, self.answer_count, self.authority_count, self.additional_count)
return struct.pack("!HHHHHH", *field_tuple) # H is 2-byte = 16-bit, ! is using big-endian
A DNS question consists of the following components:
Name: The name field represents the domain name being queried, such as "example.com." The size of the name field can vary depending on the length of the domain name. It is encoded using a variable-length format called DNS name compression. In the maximum case, a domain name can be up to 255 bytes in length, which is equivalent to 2040 bits (255 bytes * 8 bits/byte). However, in most practical cases, domain names are much shorter.
Type: The type field is 16 bits and specifies the type of DNS record being queried. Type field specifies the particular type of information that the DNS resolver (client) is requesting for a given domain name, or say what is being asked. Common types include
A (IPv4 address)
AAAA (IPv6 address)
MX (mail exchange)
TXT (Text)
NS (Name Server)
Class: The class field specifies the DNS class of the query. The Class field indicates the protocol family or namespace to which the DNS query belongs. In practice, the DNS Class field is almost always set to the Internet class (IN), which has a numeric value of 1.
xxxxxxxxxx
class DNSQuestion:
domain_name:bytes # less than 255 bytes
question_type:int # 16-bit
question_class:int # 16-bit
The name in question should follow the name encoding format, for example the encoding of "www.example.com" would be b"\x03www\x07example\x03com\x00"
. Each part of the domain name is with a byte called Length Prefix that represents the length of that part. "\x00"
is the null terminator, indicating the end of the domain name.
Combine the binary representations of the DNS header and DNS question into a single binary message, which is the complete DNS query message that can be used for DNS resolution.
Sending a DNS query using a UDP socket to the Google Public DNS server (8.8.8.8) on port 53 (the standard DNS port), and then receiving the response. Here's a breakdown of the implementation:
Import the socket
module: This code relies on the Python socket
module to create and manage network sockets.
Create a UDP socket: The socket.socket()
function is called to create a socket. It specifies the address family (socket.AF_INET
for IPv4) and socket type (socket.SOCK_DGRAM
for UDP, which is commonly used for DNS queries).
Send a DNS query: The sock.sendto()
method is used to send a DNS query (query
) to the IP address "8.8.8.8" and port 53, which is the address for the Google Public DNS server.
Receive the response: The sock.recvfrom()
method is used to receive the DNS response, which is stored in the response
variable. The maximum size of the response that can be received is limited to 1024 bytes. The _
variable is used to ignore the sender's address information since it's not needed in this code.
Header: If we want to ask for a public server for an domain name, we need to set the flags to RECURSION_DESIRED = 1 << 8
, if we want to ask for a root server, then set it to 0.
parse_header(reader)
is designed to parse a DNS header from a binary data stream using the struct
module. The header is expected to be 12 bytes long, and each of the 6 fields in the header is assumed to be a 2-byte (16-bit) integer. The function reads the binary data from the reader
object and returns a DNSHeader
object.
The DNS Response includes 5 fields:
Domain Name: In the answer section, the DNS uses a pointer to point to the name in the query section
Record Type: According to the question type
Class: Also according to the question class
TTL: Time to Live (TTL) value associated with the DNS record. The TTL indicates how long the DNS information should be cached by DNS resolvers.
Data Length: Takes 4 bytes in the DNS ip response
IP Address: The following 4 bytes.
Here we mainly focus on 3 types of records parsing, NS (Name Server), A (Address), and CNAME (Canonical Name) records.
xxxxxxxxxx
try:
self.record_type, self.record_class, self.ttl, self.data_len = struct.unpack("!HHIH", self.data)
if self.record_type == TYPE_NS:
# print("TYPE_NS")
self.data = decode_domain_name(self.response).decode("utf-8", errors='ignore')
elif self.record_type == TYPE_A:
# print("TYPE_A")
self.data = self.response.read(self.data_len)
self.ip = bytes_2_string(self.data)
elif self.record_type == TYPE_CNAME:
# print("TYPE_CNAME")
self.data = decode_domain_name(self.response).decode("utf-8", errors='ignore')
except:
# print("wrong")
pass
A Record (TYPE_A):
The code checks if the DNS record type is TYPE_A=1
(Address).
If it is, it reads the IP address data from the response based on the data length specified in the DNS record. This data represents an IPv4 address.
The code then converts the binary IP address data into a human-readable string format (e.g., "192.168.1.1") using the bytes_2_string
function.
The IP address is stored in the ip
variable.
NS Record (TYPE_NS):
The code checks if the DNS record type is TYPE_NS=2
(Name Server).
If it is, it decodes the domain name from the response and converts it to a UTF-8 encoded string. The resulting string represents the name server responsible for the domain.
CNAME Record (TYPE_CNAME):
The code checks if the DNS record type is TYPE_CNAME=5
(Canonical Name).
If it is, it decodes the canonical domain name from the response and converts it to a UTF-8 encoded string. The canonical name is an alias for the original domain.
DNS compression is used to reduce the size of DNS response messages when multiple domain names share common parts. This compression technique is specified in the DNS protocol to improve efficiency.RFC 1035, section 4.1.4
In a DNS response message, when you encounter a length field (e.g., 192) that starts with the bits 11
, (11000000 in binary), it signifies a compression pointer. These pointers are used to refer back to previously occurring domain name parts within the same DNS message. The actual domain name data is not repeated; instead, it's referenced by the pointer.
Here's a simplified example to illustrate this concept:
Suppose you have a DNS response message like this:
xxxxxxxxxx
03www06example03com00
03
represents the length of the first part "www."
06
represents the length of the second part "example."
03
represents the length of the third part "com."
00
is the null terminator, indicating the end of the domain name.
Now, imagine you have another domain name in the same DNS response that is the same as the first one, like this:
xxxxxxxxxx
03www06example03com00
To avoid repeating the same domain name, DNS compression is used. In the second occurrence of the domain name, instead of repeating "03www06example03com00," a compression pointer is used:
xxxxxxxxxx
C00A
C00A
is a compression pointer, where C0
indicates that it's a pointer, and 0A
represents an offset to the first occurrence of the domain name in the DNS message (in this case, it means go back 10 bytes to the start of the first occurrence).
xxxxxxxxxx
# RECURSION
dnsq = DNSQuestion("www.baidu.com", TYPE_A, CLASS_IN)
print("dnsq.encode_domain_name()", dnsq.encode_domain_name())
dnsh = DNSHeader(random.randint(0, 65535), question_count=1, flags=RECURSION)
query = build_query(dnsh, dnsq)
print("query", query)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(query, ("8.8.8.8", 53))
response, _ = sock.recvfrom(1024)
# PARSE
print("#"*100)
print(type(response))
print(response)
print(bytes_2_string(response))
reader = BytesIO(response)
print("reader type", type(reader))
dns_packet = DNSPacket(response)
dns_packet.show()
The code uses an iterative process to resolve a domain name to its IP address:
The resolver was initialized with a root name ROOT_SERVER = '198.41.0.4'
. Then it send the query to the name server and waiting for the response. After that, the response is packed into a DNSPacket
using a parsing rule mentioned previously.
If we successfully get a response, we will first look into the answers in the answers list. If there are some answers as TYPE_A, we will report the IP
If there is a TYPE_CNAME server, we will change the domain name to the new domain name, because they are the same IP. And then, we will go on search the ip for the new domain name
If there is a TYPE_A in the additionals records, we will set the name server as the additionals record ip and iteratively ask the new name server for the domain name ip
If there is a TYPE_NS in the authorities records, we will need to recursively ask for the ip of the name server, because there is only a name for the next name server to ask for. We will first find out what is the ip for the name server, and then we will set the next name server .
Finally, we get a IP response for the domain.
First, activate the server program with running the python3 code.
xxxxxxxxxx
python3 server.py
Then enter the instruction for query. YOU DON"T NEED TO TYPE dig
IT IS GIVEN TO YOU!!!
Ask Public Server for www.example.com
xxxxxxxxxx
www.example.com @127.0.0.1 -p 1234 -f 0
Ask Root Server for www.example.com
xxxxxxxxxx
dig www.example.com @127.0.0.1 -p 1234 -f 1
Ask Public Server for www.baidu.com
xxxxxxxxxx
dig www.baidu.com @127.0.0.1 -p 1234 -f 0
Ask Root Server for www.baidu.com
xxxxxxxxxx
dig www.baidu.com @127.0.0.1 -p 1234 -f 1
Cache: If you query the domain you have been queried before.
xxxxxxxxxx
dig www.example.com @127.0.0.1 -p 1234 -f 1
Reset
xxxxxxxxxx
dig reset
exit
xxxxxxxxxx
dig exit