DNS Tunneling with iodine on Linux: Bypass Firewalls and Access Networks via DNS Protocol

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

You’re at a conference hotel, you’ve paid for “internet access,” and every port except 80, 443, and 53 is blocked. SSH on port 22? Dead. VPN? Not connecting. But DNS queries sail right through — because blocking DNS breaks everything, so nobody touches it.

That’s exactly where iodine shines. I hit this scenario at a client site during an authorized network audit and had full connectivity within ten minutes of starting the tunnel. Since then I’ve used this approach on a mix of corporate intranets and hotel captive portals — it’s held up every time, even when Tailscale, OpenVPN, and plain SSH all failed.

Here’s the complete setup: server, client, routing, and the specific gotchas that’ll trip you up the first time.

Quick Start — Get iodine Running in 5 Minutes

What You Need

  • A VPS with a public IP (Ubuntu/Debian/CentOS)
  • A domain you control (e.g., example.com)
  • DNS access to create an NS record

Step 1: DNS Record Setup

Create two DNS records at your domain registrar:

  • An A record: vps.example.com → YOUR_VPS_IP
  • An NS record: tunnel.example.com → vps.example.com

That NS record is the critical piece — it delegates all queries for *.tunnel.example.com to your VPS. iodine encodes IP packets inside DNS query hostnames using this subdomain namespace.

Step 2: Install iodine

On your VPS (server side):

# Ubuntu/Debian
sudo apt update && sudo apt install iodine -y

# CentOS/RHEL
sudo yum install iodine -y

# From source (if not in repos)
git clone https://github.com/yarrick/iodine.git
cd iodine && make && sudo make install

Install the same package on your client machine.

Step 3: Start the Server (iodined)

sudo iodined -f -c -P yourpassword 10.0.0.1 tunnel.example.com

Breaking down the flags:

  • -f: Run in foreground (drop this for daemon mode)
  • -c: Disable client IP check — useful when your VPS is behind NAT
  • -P yourpassword: Shared password for authentication
  • 10.0.0.1: IP assigned to the server’s TUN interface
  • tunnel.example.com: Your NS-delegated subdomain

Step 4: Connect from the Client

sudo iodine -f -P yourpassword tunnel.example.com

A successful connection looks like this:

Opened dns0
Opened IPv4 UDP socket
Sending DNS queries for tunnel.example.com to 8.8.8.8
Autodetecting DNS query type (use -T to override).
Using EDNS0 extension
...
Server tunnel IP is 10.0.0.1
Sending handshake...
Connection setup complete, transmitting data.

Your client now has a dns0 TUN interface with IP 10.0.0.2. Verify with ping 10.0.0.1.

Deep Dive — How DNS Tunneling Actually Works

The Protocol Mechanics

iodine encodes IP packets as hostnames in DNS query strings. A packet leaving your client gets chunked, base32-encoded, and sent as a lookup request like:

aabbccddee.tunnel.example.com → DNS type NULL query

Your VPS receives those queries through the NS delegation. It decodes the data, forwards the actual IP packet onward, encodes the response, and ships it back as a DNS answer. To any firewall sitting in the middle, this looks like ordinary DNS traffic.

Query Types and Encoding

iodine auto-negotiates the best DNS record type at startup:

  • NULL records: Best throughput, but some DNS relays strip them
  • TXT records: Good compatibility, slightly less efficient
  • CNAME/A records: Fallback for highly restrictive environments

You can force a specific type if auto-detection picks something suboptimal:

sudo iodine -f -T TXT -P yourpassword tunnel.example.com

In my experience, NULL works best on corporate networks, while TXT is more reliable on hotel captive portals that proxy DNS traffic through their own resolvers.

Understanding the TUN Interface

Once connected, check what iodine created:

ip addr show dns0
# 4: dns0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1130 qdisc fq_codel
#     inet 10.0.0.2/27 scope global dns0

Notice the MTU is 1130, not the standard 1500. DNS records have strict size limits, so iodine fragments packets to fit. This is the main reason DNS tunneling is slow — each IP packet needs multiple DNS round-trips to reassemble.

Advanced Usage

Route All Traffic Through the Tunnel

At this point you have a working tunnel, but your default route still exits through the restricted network. To push all traffic through iodine instead:

# Save the current gateway and DNS server IP
CURRENT_GW=$(ip route show default | awk '/default/ {print $3}')
DNS_SERVER="8.8.8.8"  # replace with the restricted network's DNS

# Critical: keep DNS queries routing through the local interface
# so iodine itself doesn't loop
sudo ip route add $DNS_SERVER/32 via $CURRENT_GW

# Now redirect everything else through the tunnel
sudo ip route del default
sudo ip route add default via 10.0.0.1 dev dns0

On the server side, enable NAT to forward client traffic to the internet:

# Enable IP forwarding
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward

# NAT tunnel clients to the internet (replace eth0 with your interface)
sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/27 -o eth0 -j MASQUERADE
sudo iptables -A FORWARD -i dns0 -o eth0 -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o dns0 -m state --state RELATED,ESTABLISHED -j ACCEPT

SOCKS Proxy Over the Tunnel

If you only need browser or application-level access, SSH SOCKS proxying over the tunnel is more efficient than full routing:

# Open a SOCKS5 proxy through the tunnel server
ssh -D 1080 -N [email protected]

# Then use it
curl --socks5 127.0.0.1:1080 https://example.com

Running iodined as a systemd Service

In production — or anywhere you need this to survive reboots — set up iodined as a systemd service:

sudo nano /etc/systemd/system/iodined.service
[Unit]
Description=iodine DNS Tunnel Server
After=network.target

[Service]
ExecStart=/usr/sbin/iodined -c -P yourpassword 10.0.0.1 tunnel.example.com
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable iodined
sudo systemctl start iodined
sudo systemctl status iodined

Practical Tips

Realistic Performance Expectations

DNS tunneling is slow by design — set expectations before deploying:

  • Upstream (client → server): 3–10 KB/s
  • Downstream (server → client): 5–20 KB/s
  • Latency: 200–800ms per DNS round-trip

SSH sessions, Git over HTTPS, and light API calls work fine. Video streaming or large file transfers will be painful. For emergency access when every other option is blocked, this throughput is absolutely worth it.

Troubleshooting Common Issues

“No downstream data received” — Your DNS relay is probably filtering or mangling responses. Switch query type:

sudo iodine -f -T TXT -P yourpassword tunnel.example.com

Connection established but ping fails — IP forwarding or NAT rules are missing on the server:

cat /proc/sys/net/ipv4/ip_forward       # Should output: 1
sudo iptables -t nat -L POSTROUTING    # Check MASQUERADE rule exists

NS delegation not resolving — DNS propagation can take up to 48 hours. Verify the record is live:

dig NS tunnel.example.com
# Expected: tunnel.example.com. IN NS vps.example.com.

Fix TCP Performance with MSS Clamping

Low MTU causes TCP fragmentation that quietly throttles throughput. One iptables rule on the server fixes it:

sudo iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

It tells TCP to cap segment sizes to what actually fits through the tunnel. Add this and SSH starts feeling responsive again — HTTP transfers speed up noticeably too.

Security Notes

iodine authenticates with a shared password, but traffic is encoded, not encrypted. For anything sensitive, layer SSH or WireGuard on top — the DNS tunnel gives you connectivity, encryption is a separate concern.

Worth knowing: DNS tunneling produces distinctive traffic patterns — unusually high query volume, long subdomain strings — that network monitoring tools will flag. Use it only on networks you’re authorized to test.

My iodined server has been running for over a year, quietly waiting as a backup access method. When a client’s primary VPN went down mid-maintenance-window and the team lost access to their servers, this tunnel was what got them back in — within minutes, not hours. Pre-configuring it means you’re never completely locked out. That peace of mind is worth the fifteen minutes.

Share: