dns null record tunneling

published: August 12, 2025

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:

  1. encoding data in dns query subdomains
  2. server responds with null records containing downstream data
  3. automatic fragmentation for large payloads
  4. 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

encodingefficiencycompatibilityuse case
raw100%lowestdirect connection
base12887.5%mediummost networks
base6475%highstandard dns
base3262.5%highestrestrictive 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
on this page