iodine: IP-over-DNS, or the internet where only DNS passes
The scene is familiar: the conference hotel, the fast-food hotspot, one of those captive portals hatching everywhere since Wi-Fi became a service. You connect, a page opens, you are supposed to pay or type a room code and everything that is not HTTP on port 80 falls into the void. No SSH, no direct SMTP, no visible VPN. A filter that only lets the web through, which is almost nothing.
Except the portal has a technical obligation: it must keep name resolution working. A captive portal that does not resolve DNS is a portal you cannot even connect to, since its own home page has a name to resolve. So the network resolver resolves, for everyone, authenticated or not and it forwards to the outside, otherwise it would be useless.
That is where everything happens. The DNS protocol is a transport protocol that does not know it is one. A query is a domain name and a domain name can carry data in its labels. A response is a record and a record can carry just as much. If the network only leaves you DNS, you do not have almost nothing: you have a bidirectional channel. And a channel can be routed.
iodine does exactly that: IPv4 tunneling over DNS. First public release on June 24 (0.3.0, Linux, FreeBSD, OpenBSD), 0.3.1 this month to add Mac OS X. Two authors, Erik Ekman and Björn Andersson, ISC license, two binaries (client and server), one non-negotiable requirement: a TUN/TAP device. And a name that is a coat of arms: IOD for IP Over DNS and iodine, element number 53 in the periodic table, for the DNS port 53. Could not be clearer.
The preliminary test: one question for dig
The captive portal gives you DNS, therefore it gives you the internet. It still needs checking, because not all resolvers are equal. Some relays refuse to resolve anything that is not public, some truncate, some switch everything to lowercase. The test fits in one command and it goes BEFORE touching anything (dig got its article the month before ):
$ dig -t NS foo123.tunnel.mydomain.com +short
If an answer comes back (even a cryptic one), the portal’s resolver forwarded your query to the name server of YOUR domain. And that name server is you. The path is open. If the query dies in a timeout, the portal filters relayed DNS too and this whole article is theory: go back to the portal’s home page, there is no shame in paying for the room code.
Why it works: delegation. The portal’s resolver does not know tunnel.mydomain.com, so it does what every resolver has done since RFC 1035: it walks up the DNS tree until it finds the authoritative server and that server is yours. The portal is just doing normal DNS, it cannot do anything else.
The delegation: two lines in the zone
Three things are already owned: a domain of your own, a server with a public IP (the weekend dedibox does the job nicely) and ten minutes. The zone’s DNS server gains two lines:
tunnel IN NS ns.tunnel.mydomain.com.
ns.tunnel IN A <ip.of.the.dedibox>
Note the trailing dot on the NS line, it is mandatory and forgetting it is the leading cause of broken zones in DNS history. These two lines tell the world: everything ending in tunnel.mydomain.com must be asked of ns.tunnel.mydomain.com, whose address follows. That is all. Every DNS query for a name in that subdomain will now land on the dedibox, wherever the asker sits, including from a captive portal resolver.
Two details that matter for throughput: the subdomain must be short (every character saved on the name is a character gained for data) and the NS target must be a name with an A record, not a CNAME.
iodined, the server
$ ./iodined -f 10.0.0.1 tunnel.mydomain.com
The first argument is the server end’s tunnel IP (a range you do not use yet, the classic 10.0.0.1/10.0.0.2 pair works), the second is the delegated subdomain. iodined opens a tun interface and listens on port 53/udp. If a DNS server already runs on the box, the manpage explains how to move its listening port and let iodined forward; on a virgin dedibox, the port is free.
The client, behind the portal
$ ./iodine resolver.of.the.portal tunnel.mydomain.com
The first argument is the relay resolver, that is, the portal’s (the one cat /etc/resolv.conf shows you), the second is your subdomain. iodine opens its tun interface and the client takes the next IP in the range: 10.0.0.2. The verification ritual, as is:
$ ping 10.0.0.1
If the ping answers, you have a working IP interface inside a network that only offered you a web filter. Version 0.3.1 does not yet consult /etc/resolv.conf on its own: pass the portal’s resolver as the first argument, that is what it is for.
What goes through the pipe
The asymmetric direction is the signature of DNS tunneling. Upstream, data is gzipped, encoded in base32 and split across the labels of the query name: 63 characters per label, 255 per name, one query per packet. That is the slow direction and it always will be: DNS was not designed for this.
Downstream, iodine answers queries with NULL-type records, which have no imposed format and therefore no encoding: each DNS response can stuff several hundred bytes of payload. That is the fast direction, the one your web traffic takes.
The tunnel is NOT encrypted and the README says so itself: tunneling on top is mandatory. The winning pair:
$ ssh -D 1080 user@dedibox
A local SOCKS at the end of the tunnel and the hotspot’s web exits through your own egress. The purist variant: default route through the tunnel (a host route to the portal’s resolver first, otherwise the tunnel itself becomes unreachable, then the default via 10.0.0.1) and NAT on the dedibox. The web-only variant: a Privoxy on the server and the browser pointed at it.
The limits, honestly
No authentication. Version 0.3.x knows no password: whoever discovers your subdomain can plug into your tunnel. The only current barriers are the obscurity of the name plus an iptables rule on the source IP if your client does not move. Authentication is the first open request on the project; until then, do not publish the subdomain and treat the tunnel as an open pipe.
Relays destroy tunnels. Some resolvers switch long names to lowercase (death of the case-sensitive codec), some refuse NULL-type queries, some answer so slowly that throughput collapses. It is the number one risk of the domain and it can only be tested in real conditions, hence the preliminary dig.
It is visible. A DNS tunnel is a continuous stream of queries toward a single domain, with 255-character hostnames of near-random shape. An admin watching the resolver logs sees it in a minute. Here the exercise is getting out of a hotel captive portal, not smuggling a corporate network; for the latter we would discuss policy-blocked port 53 and that would be another article.
The hotspot’s terms of service. One more line nobody reads, the one saying tunneling is not covered. The exercise stays academic: understanding that a filter letting one protocol through lets through everything that protocol can carry.
The kit
Screen to keep the tunnel open in a terminal that survives logout, dig for every test, netcat to check that whatever sits at the far end answers, ssh -D for the encrypted exit, tcpdump or ethereal to watch the packets when it fails. Nothing exotic: the weekend kit.
The captive portal gives you DNS. DNS is a transport protocol. Therefore the captive portal gives you the internet. The rest is encoding.
[edit: 2026] Twenty years later, iodine still runs. Version 0.4.0 “Run Home” (March 2007) brought the password authentication this article asked for, 0.5.2 “WifiFree” (June 2009) confessed the use case in its own codename, 0.6.0 added the TXT/SRV/MX/CNAME fallbacks and raw UDP mode for networks where port 53 goes out directly, 0.7.0 “Kryptonite” (2014) added a PRIVATE record type in the private-use range for servers speaking RFC 3597 and fixed an authentication flaw reported by Oscar Reparaz. 0.8.0 shipped in 2023, the project is in every distribution and in the Kali livecd.