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 authentication10.0.0.1: IP assigned to the server’s TUN interfacetunnel.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.

