DNS Query Structure Explained: Anatomy of a DNS Request & Response Packet with URL Location Details


1 views

DNS (Domain Name System) operates primarily over UDP port 53, though TCP is used for larger responses. A DNS query consists of a header and question section, while responses include additional answer, authority, and additional sections.

Here's the breakdown of a standard DNS query:

+---------------------+
|        Header       |  (12 bytes)
+---------------------+
|       Question      |  (variable)
+---------------------+

The header contains:

0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

The question section contains the domain name being queried. For example, when resolving "example.com":

+---------------------+
|       7|e|x|a|m|p|l|e|  (length-prefixed labels)
+---------------------+
|       3|c|o|m|        |
+---------------------+
|       0              |  (null terminator)
+---------------------+
|      Query Type      |  (e.g., A, AAAA, MX)
+---------------------+
|     Query Class      |  (typically IN for Internet)
+---------------------+

A DNS response includes additional sections:

+---------------------+
|        Header       |
+---------------------+
|       Question      |
+---------------------+
|        Answer       |  (resource records)
+---------------------+
|      Authority      |  (name server records)
+---------------------+
|      Additional     |  (extra info)
+---------------------+

Here's how to examine DNS packets using Python's dnspython library:

import dns.message
import dns.query

# Create a DNS query
query = dns.message.make_query('example.com', 'A')

# Print raw query bytes
print("DNS Query Bytes:")
print(query.to_wire())

# Send query and get response
response = dns.query.udp(query, '8.8.8.8')

# Print response structure
print("\nDNS Response:")
print(response)
print("\nAnswer Section:")
for answer in response.answer:
    print(answer)

Examining a DNS query for "google.com" in Wireshark shows:

Domain Name System (query)
    Transaction ID: 0x2d6c
    Flags: 0x0100 Standard query
    Questions: 1
    Answer RRs: 0
    Authority RRs: 0
    Additional RRs: 0
    Queries
        google.com: type A, class IN
            Name: google.com
            Type: A (Host Address)
            Class: IN (0x0001)
  • DNS queries use a simple binary format with length-prefixed domain names
  • The queried URL appears in the Question section as series of labels
  • Responses include TTL values and resource record data
  • Packet inspection tools like Wireshark reveal the complete structure

A DNS query is transmitted as a binary UDP packet (typically on port 53) with this basic structure:

+---------------------+
|        Header       |
+---------------------+
|       Question      |
+---------------------+
|        Answer       | 
+---------------------+
|      Authority      |
+---------------------+
|      Additional     |
+---------------------+

The 12-byte header contains control flags and counters:

0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode  |AA|TC|RD|RA| Z |AD|CD|   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

The URL being resolved appears in the QNAME field as a series of labels:

; Example query for www.example.com
$ dig www.example.com +nocomments +nostats

; QUESTION SECTION:
;www.example.com.          IN  A

; Raw bytes representation (hex dump):
C0 18 00 01 00 01 00 00 00 00 03 77 77 77 07 65 
78 61 6D 70 6C 65 03 63 6F 6D 00 00 01 00 01

A typical DNS response contains these additional sections:

;; ANSWER SECTION:
www.example.com.    3600    IN  A    93.184.216.34

;; AUTHORITY SECTION:
example.com.        172800  IN  NS   a.iana-servers.net.

;; ADDITIONAL SECTION:
a.iana-servers.net. 172800  IN  A    199.43.135.53

Here's Python code to decode DNS packets:

import socket
import struct
import dnslib

def capture_dns_query():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('', 5353))  # Capture port
    
    while True:
        data, addr = sock.recvfrom(1024)
        try:
            dns_data = dnslib.DNSRecord.parse(data)
            print("Received query:")
            print(dns_data)
            
            # Extract domain from question section
            if dns_data.questions:
                qname = str(dns_data.questions[0].qname)
                print(f"Query for: {qname}")
                
        except dnslib.DNSError as e:
            print(f"Invalid DNS packet: {e}")
Field Bytes Description
ID 2 Transaction identifier
Flags 2 Control bits (QR, OPCODE, etc.)
QDCOUNT 2 Number of questions
ANCOUNT 2 Number of answers
NSCOUNT 2 Number of authority records
ARCOUNT 2 Number of additional records
  • A (1): IPv4 address
  • AAAA (28): IPv6 address
  • CNAME (5): Canonical name
  • MX (15): Mail exchange
  • TXT (16): Text strings