dns null record tunneling
on this page
dns null record tunneling achieves the highest performance among dns covert channels by exploiting null records (rfc 1035) that allow up to 65,535 octets of binary data per response.
technical description
null records (type 10) were designed for experimental use and can carry arbitrary binary data. unlike txt records limited to 255 octets, null records support large payloads making them ideal for high-bandwidth tunneling.
the technique works by:
- encoding data in dns query subdomains
- server responds with null records containing downstream data
- automatic fragmentation for large payloads
- base32/64/128 encoding for compatibility
implementation: iodine
overview
iodine (https://github.com/yarrick/iodine) is the primary implementation achieving:
- downstream: 585.4 kbit/s on wired connections
- upstream: 100-200 kbit/s typical
- fragment size: 1,174 bytes with edns0
- latency: 10-30ms overhead
installation
# debian/ubuntu
apt install iodine
# from source
git clone https://github.com/yarrick/iodine.git
cd iodine
make
make install
server setup
# basic server
iodined -f 10.0.0.1 tunnel.example.com
# with password
iodined -f -P password 10.0.0.1 tunnel.example.com
# specify dns port
iodined -f -p 5353 10.0.0.1 tunnel.example.com
# debug mode
iodined -DD -f 10.0.0.1 tunnel.example.com
dns configuration for tunnel.example.com:
tunnel.example.com. IN NS ns.tunnel.example.com.
ns.tunnel.example.com. IN A server.public.ip
client connection
# connect to server
iodine -f tunnel.example.com
# with password
iodine -f -P password tunnel.example.com
# force encoding
iodine -f -T null -O base64 tunnel.example.com
# specify nameserver
iodine -f -n 8.8.8.8 tunnel.example.com
# lazy mode (slower but more compatible)
iodine -f -L tunnel.example.com
performance tuning
# optimal settings for lan
iodine -f -m 1472 -M 1472 -T null -O raw tunnel.example.com
# through restrictive firewall
iodine -f -m 220 -T txt -O base32 -L tunnel.example.com
# auto-detect optimal settings
iodine -f -I 1 tunnel.example.com
encoding options
encoding | efficiency | compatibility | use case |
---|---|---|---|
raw | 100% | lowest | direct connection |
base128 | 87.5% | medium | most networks |
base64 | 75% | high | standard dns |
base32 | 62.5% | highest | restrictive filters |
traffic characteristics
query pattern
client.tunnel.example.com: type=NULL
response: NULL record with encoded data
# example query
aGVsbG8td29ybGQtZGF0YS0x.tunnel.example.com
packet analysis
# scapy analysis
from scapy.all import *
packets = rdpcap('iodine_traffic.pcap')
dns_packets = [p for p in packets if p.haslayer(DNS)]
for p in dns_packets:
if p[DNS].qd and p[DNS].qd.qtype == 10: # NULL record
print(f"null query: {p[DNS].qd.qname}")
if p[DNS].an and p[DNS].an.type == 10:
print(f"null response size: {len(p[DNS].an.rdata)}")
detection methods
traffic indicators
- high volume of null record queries (rare in normal traffic)
- queries to single domain with random subdomains
- large dns response sizes (>512 bytes typical)
- consistent query/response pattern
- unusual ttl values (often 0)
detection rules
# suricata rule
alert dns any any -> any any (
msg:"possible iodine dns tunnel - null records";
dns.query;
content:"|00 00 0a 00 01|"; # NULL query
threshold: type limit, track by_src, seconds 60, count 50;
sid:1000001;
)
statistical detection
# entropy analysis for subdomain detection
import math
from collections import Counter
def calculate_entropy(domain):
subdomain = domain.split('.')[0]
counts = Counter(subdomain)
probs = [count/len(subdomain) for count in counts.values()]
return -sum(p * math.log2(p) for p in probs)
# high entropy indicates encoding
threshold = 4.0 # typical for base64
if calculate_entropy(query) > threshold:
print("possible tunnel detected")
countermeasures
network level
# block null records at firewall
iptables -A FORWARD -p udp --dport 53 -m string --hex-string "|00 00 0a|" -j DROP
# rate limit dns queries
iptables -A FORWARD -p udp --dport 53 -m recent --update --seconds 1 --hitcount 10 -j DROP
dns server configuration
# bind9 - block null records
zone "." {
type master;
file "null.zone";
check-names ignore;
deny-answer-type { NULL; };
};
monitoring
# monitor null record queries
tcpdump -i eth0 -w null_records.pcap 'udp port 53' &
tshark -r null_records.pcap -Y "dns.qry.type == 10" -T fields -e dns.qry.name
advantages and limitations
advantages
- highest bandwidth among dns tunnels
- automatic fragmentation handling
- multiple encoding options
- edns0 support for larger packets
- built-in compression
limitations
- null records often blocked by security appliances
- easily detected due to rarity in normal traffic
- requires dns server control
- performance degrades with packet loss
alternative implementations
dns2tcp
less efficient but more compatible:
# server
dns2tcpd -f dns2tcp.conf
# client
dns2tcpc -z tunnel.example.com server.ip
ozymandns
older perl implementation:
# historically significant but less maintained
perl ozymandns_server.pl
real-world usage
documented cases:
- 2013: iodine used in targeted attacks
- 2016: apt group dns tunneling campaigns
- 2019: cryptocurrency mining via dns tunnels
testing setup
local testing
# setup local dns server
apt install bind9
echo "zone tunnel.local { type master; file \"/etc/bind/tunnel.local\"; };" >> /etc/bind/named.conf.local
# start iodine server
iodined -f -c -P test123 10.99.0.1 tunnel.local
# test client connection
iodine -f -P test123 tunnel.local
# verify tunnel
ping 10.99.0.1
performance testing
# bandwidth test
iperf3 -s & # on server side of tunnel
iperf3 -c 10.99.0.1 -t 60 # on client
# latency test
ping -c 100 10.99.0.1 | grep avg
references
- rfc 1035: domain names - implementation and specification (null records)
- “dns tunneling with iodine” - erik hjelmvik, netresec
- “detecting dns tunneling” - sans reading room
- iodine documentation: https://github.com/yarrick/iodine/wiki