Performance of Iodine over DNS-over-HTTPS

Iodine is a DNS-tunnel that can be used to send TCP traffic encapsulated in DNS queries. Sometimes, this helps to bypass captive portals or otherwise restricted networks.

In this blogpost I'll test the performance of using Iodine in combination with DoH (DNS-over-HTTPS).

For my test setup, I spun up a vanilla debian VM using vagrant and installed iodine in it. I have an iodine server running on one of my servers, but I won't cover the steps to install and configure it here, because I already wrote a post about it.

Baseline

Before we start experimenting with DNS-over-HTTPS, let's measure the baseline performance of the DNS tunnel. For the measurements I used the TCP-based network throughput testing tool iperf.

Raw mode

Iodine has a raw mode - and if I understood it correctly - it sends the DNS traffic directly to the iodine DNS server, without going through the DNS resolver. That allows for quite a high bandwidth:

As you can see, we can easily push more than 50mbit/s through the tunnel. Not too bad, I think.

Query mode

So in essence, using the raw mode is kinda cheating, because usually - at least with captive portals - a specific DNS resolver is enforced at the network level and we cannot simply bypass it.
With that limitation in place, iodine has to send a lot of small DNS queries to transfer our traffic.

As we can see below, the initialisation phase gets longer, because iodine tries to figure out the best query type and maximum fragment size to use.

The bandwidth went down from 50 mbit/s to only around 350 - 450 kbit/s. That does not sound much, but ~ 50 kb/s might still be enough to slowly download a document at an airport or check one's emails in an emergency, but won't be enough to watch YouTube videos.

DNS-over-HTTPS (DoH)

DoH (DNS-over-HTTPS) is a technique where DNS queries are not send in plaintext via UDP to DNS servers. Instead, the queries are send using TLS-encrypted HTTPS requests to special DoH API servers. For example, Cloudflare operates such a server.

You can use curl to resolve the A record for 0day.work like this:

$> curl --silent -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=0day.work&type=A' | js-beautify
{
    "Status": 0,
    "TC": false,
    "RD": true,
    "RA": true,
    "AD": false,
    "CD": false,
    "Question": [{
        "name": "0day.work.",
        "type": 1
    }],
    "Answer": [{
        "name": "0day.work.",
        "type": 1,
        "TTL": 10789,
        "data": "185.26.156.49"
    }]
}

dnscrypt-proxy

dnscrypt-proxy is a tool written in Go that can act as a local DNS resolver which transparently forwards the DNS queries to a DoH server.

The setup is quite straight forward: Simply download one of the pre-built releases packages, rename the example-dnscrypt-proxy.toml to dnscrypt-proxy.toml and then run sudo ./dnscrypt-proxy. But before that, I changed dnscrypt_servers = true to false, so that the proxy will only use DoH to resolve the DNS queries.


Now we only have to tell our system to use the dnscrypt-proxy as its resolver. For that, we comment out all lines in /etc/resolv.conf and add nameserver 127.0.0.1, because the proxy bound itself to port 53. All queries from our system will go towards our dnscrypt proxy.

A test query using the tool dig shows that the proxy works and we can resolve DNS records over DoH:

Iodine and DoH

Let's get to the interesting part: The performance of Iodine over the DoH-based DNS resolver.

Spoiler: It is terrible :-( It also took me multiple tries until iodine managed to setup the tunnel.

In the lower part of the screenshot you can see that tshark does not capture any packets going out over port 53. That means that iodine is really using the dnscrypt-proxy.

We can also see that iodine throws several errors/warnings and complains about server's replies.

In the end we have only between 30 - 90 kbit/s. I have not tried starting a SSH session or something else. Maybe mosh might somewhat work, but I do not believe that using iodine over DoH is a pleasant experience in general.

To be honest, it does not surprise me at all, because each DNS query that iodine sends is a separate HTTPS request and as you might have seen in the first dnscrypt screenshot, the average RTT is between 18ms - 78ms.

-=-