DNS Resolver (Sockets, Packets & Python)
Learning Goal: Develop a custom DNS resolver from scratch to master low-level network socket programming, bitwise operations, and binary packet parsing using Python. By the end of this curriculum, you will understand how DNS queries are structured, transported over UDP, parsed byte-by-byte according to RFC 1035, and recursively resolved.
Prerequisites
- Intermediate Python programming skills (functions, classes, basic exception handling).
- Basic familiarity with command-line interfaces (terminal/command prompt).
Estimated Study Time
15 Hours (including video lectures, reading RFC documentation, and hands-on coding).
Module 1: Network Basics & The Domain Name System
This module covers the core concepts of computer networks. You will explore how machines locate each other using IP addresses, the functional trade-offs between the TCP and UDP transport protocols, and how the Domain Name System (DNS) operates as the distributed telephone directory of the internet.
Recommended Videos
Why this video is valuable: Computerphile provides a clear, high-level structural explanation of the Domain Name System. It breaks down the hierarchical nature of DNS records and how a recursive lookup moves from the root servers down to top-level domains (TLDs) and authoritative servers.
Why this video is valuable: Because DNS predominantly runs over UDP for standard queries, understanding why UDP is chosen over TCP is critical. This video compares transport protocols, explaining concepts like connection states, head-of-line blocking, and packet overhead.
Why this video is valuable: This video provides an overview of IP addresses (both IPv4 and IPv6) and highlights how DNS maps human-readable names to these numerical routes.
Knowledge Checkpoint
- What is the difference between a recursive DNS resolver and an authoritative DNS name server?
- Why does standard DNS primarily use UDP port 53 instead of TCP?
- What is the structural difference in address space between IPv4 (32-bit) and IPv6 (128-bit)?
- What happens when a DNS query packet is lost in transport over UDP?
Module 2: Network Socket Programming with Python
This module transitions from theory to execution. You will learn to use Python's built-in socket module to create a low-level UDP server and client. You will learn how to bind sockets to ports, listen for inbound datagrams, parse the sender's metadata, and return network responses.
Recommended Videos
Why this video is valuable: This practical coding tutorial demonstrates the mechanics of writing a clean UDP client and server in Python. You will learn how to initialize socket objects, configure bindings, and handle network data blocks.
Why this video is valuable: A quick, targeted overview of how UDP servers track incoming payloads using recvfrom(), which captures both the raw data bytes and the sender's IP/Port tuple needed for routing responses back via sendto().
Knowledge Checkpoint
- What parameter flags must be passed to
socket.socket()to instantiate a UDP socket instead of a TCP socket in Python? - What is the purpose of the
bind()method in a network socket server? - Why does
recvfrom(1024)return a tuple instead of just a raw byte array? - How do you convert a Python string to a raw byte array before transmitting it over a socket, and vice versa?
Module 3: Binary Representation and Python's Struct Module
To parse and construct network packets, you must be able to read and write raw binary configurations. This module focuses on binary, hexadecimal, bitwise operators, and Python's built-in struct module, which maps binary buffers to Python data types.
Curriculum Gap Alert: The video pool lacks a dedicated, comprehensive tutorial on using the Python
structmodule specifically for DNS parsing. To address this, study the recommended real-world video below on medical packet parsing, and thoroughly read the structured walk-through provided in the section below.
Recommended Videos
Why this video is valuable: An excellent academic grounding in bits, bytes, and words. It explains why hexadecimal notation is used in system programming to represent binary sequences concisely.
Why this video is valuable: This video shows a developer parsing raw binary network packets in Python. It demonstrates using struct.unpack() with big-endian designators (like >H) to translate raw network bytes into readable integers.
Why this video is valuable: Explains how Python processes integers at the bit level. It covers the logic behind bitwise shift operators (>>, <<) and bitwise logical masks (&, |), which are critical for unpacking packet flags.
Python struct and Bitwise Operations Reference Guide
Since DNS uses big-endian byte order (also known as Network Byte Order), you must prefix format strings with > when packing or unpacking.
import struct
Parsing a 12-byte standard DNS Header
Format: 6 unsigned short integers (16-bit each -> 2 bytes * 6 = 12 bytes)
'>' specifies big-endian network byte order. 'H' specifies a 16-bit unsigned short.
dns_header_format = ">HHHHHH"
raw_bytes = b'\xaa\xbb\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00' tx_id, flags, q_count, ans_count, auth_count, add_count = struct.unpack(dns_header_format, raw_bytes)
print(f"Transaction ID: {hex(tx_id)}") # Output: 0xaabb print(f"Flags Field: {bin(flags)}") # Output: 0b100000000
Parsing Sub-byte Flags
In the 16-bit DNS flags field:
- QR (Query/Response) is 1 bit at position 15 (most significant bit).
- Opcode is 4 bits at positions 11–14.
- AA (Authoritative Answer) is 1 bit at position 10.
- TC (TrunCation) is 1 bit at position 9.
- RD (Recursion Desired) is 1 bit at position 8.
- RCODE (Response Code) is 4 bits at positions 0–3.
To extract these in Python, use bitwise shifting and logical masking:
To read Recursion Desired (RD) - Bit 8
rd = (flags >> 8) & 1
To read Response Code (RCODE) - Bits 0-3
rcode = flags & 0x000F
Knowledge Checkpoint
- What does
>represent in a Python struct format string, and why is it essential for network applications? - How many bytes of data are represented by the format string
>HHI? - Write a Python expression using bitwise operators to check if the most significant bit (bit 15) of a 16-bit flag integer is set to 1.
- How do you convert the decimal value
254into hex, binary, and a raw byte representation in Python?
Module 4: DNS Packet Structure & RFC Spec
This module covers the layout of a DNS packet as specified in RFC 1035. You will analyze how queries are constructed under the hood, parsing headers, questions, answers, authority zones, and additional records.
Curriculum Gap Alert: Standard tutorials online often rely on high-level libraries (like
dnspythonorscapy) or Wireshark captures. To build a resolver from scratch, you must understand the exact wire format. Study the videos below to understand the physical layer capture, and use the detailed breakdown below to map these layouts directly to code.
Recommended Videos
Why this video is valuable: Wireshark acts as our window into the network wire. This walkthrough breaks down captured DNS frames, allowing you to match raw binary fields directly to the high-level application values decoded by Wireshark.
Why this video is valuable: Shows how to analyze raw DNS queries using command-line sniffing tools. It focuses on the transaction ID, query types (such as "A" records), and response field counts.
Why this video is valuable: Highly technical visualization of the DNS packet payload, highlighting transaction IDs and recursion request flags.
The DNS Packet Wire Format (RFC 1035)
Every standard DNS packet (both request and response) has a fixed 12-byte header, followed by variable-length payload fields.
+---------------------------------------------------+ | Header (Fixed 12 Bytes) | +---------------------------------------------------+ | Question Section (Variable Length) | +---------------------------------------------------+ | Answer Section (Variable Length) | +---------------------------------------------------+ | Authority Section (Variable Length) | +---------------------------------------------------+ | Additional Section (Variable Length) | +---------------------------------------------------+
Detailed Header Layout
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ID | (Transaction ID) +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode |AA|TC|RD|RA| Z | RCODE | (Flags) +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QDCOUNT | (Number of Questions) +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ANCOUNT | (Number of Answers) +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | NSCOUNT | (Number of Authority RRs) +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ARCOUNT | (Number of Additional RRs) +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
The Question Section
The Question Section format is:
- QNAME: A variable-length field that encodes domain names as a sequence of "labels". For example,
google.comis encoded as:\x06g o o g l e \x03c o m \x00(length-prefixed strings ending with a null byte0x00). - QTYPE: A 2-byte integer specifying the query type (e.g., Type A =
0x0001). - QCLASS: A 2-byte integer specifying class (usually Internet, IN =
0x0001).
Parsing DNS Name Pointers (Compression)
To reduce packet size, DNS responses often use compression pointers. If the first two bits of a length byte are 11 (binary), it indicates a pointer.
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | 1 1| OFFSET | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
If you encounter 0xC00C (which is 11000000 00001100 in binary), you strip the first two bits (0xC000) to find the remaining value of 12. This means you should jump to offset 12 in the raw packet to read the domain name.
Knowledge Checkpoint
- Draw the layout of a DNS packet header and list the size of each field in bits.
- How is the domain name
mail.yahoo.comformatted in a raw DNS question payload? (Write out the hexadecimal bytes). - What is the purpose of the compression schema in DNS, and how do you identify a compressed pointer byte?
- What integer code represents a standard IPv4 host address lookup (A record) in the QTYPE field?
Module 5: Building a Custom DNS Resolver from Scratch
In this module, you will write a fully functional custom DNS recursive resolver in Python from scratch. Your program will listen on a UDP port for incoming client queries, parse the domains, resolve them recursively by querying official DNS root servers, extract the answers, and compile a binary response payload back to the client.
Curriculum Gap Alert: Most videos on building a DNS server in Python focus on installing or configuring third-party products (like Pi-hole, pfSense, or BIND9). To help you build a programmatic resolver from scratch, use the guides below alongside the coding fundamentals videos.
Recommended Videos
Why this video is valuable: This video focuses on writing a UDP socket listener in Python specifically designed to capture DNS frames and prepare them for parsing.
Why this video is valuable: While this tutorial uses high-level Python libraries to resolve queries, it is useful for understanding resolving architecture.
Step-by-Step Code Architecture Guide
Your custom DNS resolver should follow this architecture:
+-----------------------------------+
| 1. Listen on Port 5353 |
| (Python UDP Socket) |
+-----------------+-----------------+
|
v (Inbound Client Query)
+-----------------+-----------------+
| 2. Parse Binary Header |
| (Extract Query Domain) |
+-----------------+-----------------+
|
v
+-----------------+-----------------+
| 3. Recursive Resolve Loop |
| Query Root Name Server (198.41.0.4)|
+-----------------+-----------------+
|
+-----------------------+-----------------------+
| | |
v (Recv CNAME/NS Record)| v (Recv IP Address)
+-----------+-----------+ | +-----------+-----------+ | Query Name Server IP | | | Return Final Answer | | returned in Authority | | +-----------------------+ +-----------+-----------+ | ^ | | | +-----------------------+
Core Classes to Implement
To organize your code, implement these class structures in your resolver script:
class DNSHeader: def init(self, tx_id, flags, num_questions, num_answers, num_authorities, num_additionals): self.tx_id = tx_id self.flags = flags self.num_questions = num_questions self.num_answers = num_answers self.num_authorities = num_authorities self.num_additionals = num_additionals
@classmethod
def parse(cls, data):
# Unpacks 12 bytes using struct
pass
def to_bytes(self):
# Pack fields into 12 big-endian bytes
pass
class DNSQuestion: def init(self, name, qtype, qclass): self.name = name self.qtype = qtype self.qclass = qclass
@classmethod
def parse(cls, data, offset):
# Parses the name and unpacks QTYPE and QCLASS
pass
class DNSRecord: def init(self, name, rtype, rclass, ttl, rdata): self.name = name self.rtype = rtype self.rclass = rclass self.ttl = ttl self.rdata = rdata # IP Address bytes or authoritative Domain bytes
The Resolution Loop Engine
To resolve a query from scratch:
- Extract the requested domain name from the inbound client request (e.g.,
example.com). - Query a Root Name Server (such as Verisign's standard root server
198.41.0.4) on Port 53 with your structured query bytes. - Parse the response. Look at the
Answerscount. If it contains an "A" record matching your domain, return it. - If there is no answer but you receive
Authority Records(NS name servers) andAdditional Records(IP addresses of those name servers), extract those IP addresses. - Re-run your query step using one of those newly parsed authoritative IP addresses (this resolves the TLD and domain authoritative level).
- Loop recursively until you receive an IP answer, then send this result back to the original client.
Knowledge Checkpoint
- Implement the
DNSQuestion.parsemethod in Python to successfully extract a query domain name, resolving any pointers. - Write a function that takes a query domain name string and returns the raw bytes formatted as length-prefixed labels ending in a null byte.
- Build a functional recursive resolver routing query requests from root IP
198.41.0.4down to authoritative servers. - Test your resolver locally using the command-line tool:
dig @127.0.0.1 -p 5353 example.com. Verify that the correct IP address is returned.
Course Map
Key People Index
- Computerphile (@Computerphile): A popular computer science educational channel. Known for breaking down complex systems like routing tables and protocol designs into accessible concepts.
- NeuralNine: A software developer and educator specializing in low-level socket abstractions, multi-threading architectures, and secure client-server design.
- PieterExplainsTech: A hardware and networking systems engineer known for clear technical comparisons of transport architectures.
Final Self-Assessment
Complete this comprehensive self-assessment to verify that you have mastered this curriculum:
- Explain the differences between TCP and UDP, and why UDP is the standard transport protocol for DNS query resolutions.
- Write Python code to instantiate, bind, receive data from, and respond to a standard UDP socket.
- Convert any arbitrary integer value into its corresponding hex representation, binary bit pattern, and raw byte array in Python without using third-party modules.
- Explain the role of the format characters
>,B,H, andIin Python'sstructmodule when parsing network frames. - Map all 12 bytes of a standard DNS packet header to their specific variables according to the RFC 1035 specification.
- Write a helper function in Python to read sub-byte flags from a 16-bit integer using bitwise shift and mask operations.
- Parse length-prefixed domain labels from a raw binary stream and convert them back into standard dot-notation format (e.g., converting
\x07example\x03com\x00intoexample.com). - Identify compression pointer prefixes (
0xC0) within a DNS response, extract the offset, and retrieve the domain name from the target address. - Write a functional recursive lookup routine in Python that starts from a root DNS server IP (e.g.,
198.41.0.4) and traverses TLD and authoritative NS records to resolve a domain name. - Launch your custom Python resolver, bind it to a local port, and successfully resolve domain queries using client tools like
nslookupordig.












