DNS over I2P: True DNS Query Privacy
Today, terms like DoH (DNS over HTTPS), DoT (DNS over TLS), and other DNS encryption methods are commonplace. For those new to the topic, DNS (Domain Name System) is the system that translates human-readable domain names like habr.com or cia.gov into IP addresses that computers use to connect to websites. Every major organization, home internet provider, and even individuals can run their own DNS server, as they are relatively simple to set up. One key reason to run your own DNS server is privacy: the administrator of any DNS server you use can see your IP address and know which websites you visit.
The DNS protocol dates back to around 1987 and was not designed with encryption in mind. All DNS queries and responses are sent in plain text, so not only can the DNS server administrator see your activity, but so can any operator of the intermediate nodes your traffic passes through. Modern solutions like DNSCrypt and DNS over HTTPS encrypt DNS queries between the user and the server, protecting them from interception in transit. However, these protocols do not solve the problem of the DNS server itself seeing both your query and your IP address. DNSCrypt offers an additional feature to address this, but I find it overly complex. I welcome constructive criticism, but let’s avoid the suggestion that everyone needs their own personal DNS server for privacy.
DNS over the anonymous I2P network is a concept where DNS queries are encrypted, and, importantly: (1) the server administrator does not know the real IP address of the user, and (2) the user does not know the physical location of the server (which helps protect the administrator from pressure). DNS over I2P, or DoI2P (or simply DoI), is an interesting way to use hidden networks, and we’ll take a closer look at how it works.
Theory
By default, DNS servers operate on port 53 (websites typically use ports 80 and 443), but what’s more important here is the transport protocol DNS uses. This is necessary for creating the right tunnels through the I2P network.
An I2P router, which provides access to the I2P network, offers local SOCKS and HTTP proxies. Usually, these proxies are used to access the network, but for more advanced configurations, you can create custom tunnels in a configuration file. Tunnels can be server-side or client-side. Their purpose is to accept connections from the hidden network on a designated local port (for example, for a web server) or to forward client connections from a local tunnel address into the hidden network. There are two main types of tunnels: TCP and UDP.
DNS primarily uses UDP, but the standard also allows for some data to be transmitted over TCP. This means you need to create two server and two client tunnels: one for UDP and one for TCP.
Server Setup
In this example, we’ll use dnsmasq, a simple DNS server, but you can use any server you prefer. A basic configuration might look like this:
port=53 listen-address=256.257.258.259 domain-needed bogus-priv server=8.8.8.8
This configuration forwards all queries to 8.8.8.8
. While this isn’t very useful on its own, it serves as a good anonymity layer and example. The server listens on IP 256.257.258.259
, port 53
. This is a fictional IP for demonstration; you can use 127.0.0.1
and any port you like if you’re only serving users via the hidden network.
To make the DNS server accessible via I2P, you need to create server tunnels. I use i2pd on Debian 10. The tunnel configuration file is usually at /etc/i2pd/tunnels.conf
. Here’s a minimal configuration:
[DNS-TCP] type = server host = 256.257.258.259 port = 53 keys = hidden-dns.dat [DNS-UDP] type = udpserver host = 256.257.258.259 address = 256.257.258.259 port = 53 keys = hidden-dns.dat
Note the address
line in the UDP tunnel section. This is required for non-local addresses and tells i2pd which address incoming requests from the hidden network will use. You can omit it if you use 127.0.0.1
. The keys
parameter specifies the keys that form the internal network address, usually stored in /var/lib/i2pd
. If the file doesn’t exist, a new one is created.
Restart i2pd for the changes to take effect. You can see the I2P address of the tunnel in the web console under “I2P Tunnels.” For example: dnsgzxkak4zlrrs5tfh42ob57iley4xrp7srrltn2j2yl2ynbiaq.b32.i2p
.
Client Setup
On the client machine, I also use i2pd and Debian, with the tunnel file in the same location: /etc/i2pd/tunnels.conf
. The client configuration might look like this:
[DNS-CLIENT-TCP] type = client address = 127.0.0.1 port = 35353 inbound.length = 2 outbound.length = 2 destination = dnsgzxkak4zlrrs5tfh42ob57iley4xrp7srrltn2j2yl2ynbiaq.b32.i2p keys = transient-dns [DNS-CLIENT-UDP] type = udpclient address = 127.0.0.1 port = 35353 destination = dnsgzxkak4zlrrs5tfh42ob57iley4xrp7srrltn2j2yl2ynbiaq.b32.i2p keys = transient-dns
The inbound.length
and outbound.length
parameters set the length of the inbound and outbound tunnels. By default, they use three transit nodes, but I reduced them to two to minimize latency. You can use similar parameters for server tunnels. Additional parameters are only specified in the first section, as the first block defines parameters for all tunnels using the same key (in this case, transient-dns
). Keys starting with transient
are temporary—after each i2pd restart, the client will use a new internal network address to contact the server.
Restart i2pd to create the new tunnels. However, there’s one more step. On Debian 10, the DNS configuration file does not support specifying a port—only the server’s IP address. All queries are sent to port 53, but our tunnel listens on port 35353. If you set port 53 in the client tunnels, you’ll get an error and the tunnels won’t be created, because ports below 1024 are privileged and reserved for special use. Only the root user can run services on these ports, but i2pd (like most applications) runs without root privileges. Before running i2pd or any other software as root, take a deep breath and read on.
Remove any previous DNS entries from /etc/resolv.conf
so all queries go through I2P, and add a single server: nameserver 127.0.0.1
. This tells the OS to resolve names via 127.0.0.1:53
. Now, you need to ask the system kernel to redirect queries from port 53 to 35353, where your TCP/UDP tunnels are listening, and send responses back. Time for some iptables magic (sorry, not the newer netfilter—I’m old school):
iptables -t nat -A PREROUTING -i lo -p udp --dport 53 -j REDIRECT --to-port 35353 iptables -t nat -I OUTPUT -p udp -d 127.0.0.1 --dport 53 -j REDIRECT --to-port 35353 iptables -t nat -A PREROUTING -i lo -p tcp --dport 53 -j REDIRECT --to-port 35353 iptables -t nat -I OUTPUT -p tcp -d 127.0.0.1 --dport 53 -j REDIRECT --to-port 35353
Hear that? That’s the sound of fanfare! Test DNS resolution however you like; I’ll use nslookup
:
acetone@adeb:~$ nslookup habr.com Server: 127.0.0.1 Address: 127.0.0.1#53 Non-authoritative answer: Name: habr.com Address: 178.248.237.68
Afterword
During setup, I noticed that dnsmasq only listens on a UDP socket by default, but I kept the TCP tunnel for standards compliance, as DNS can use TCP for some data. In practice, the described setup works even without TCP tunnels.
With this configuration, DNS queries and responses take about 1–2 seconds on average. If that seems slow, you can reduce the length of the server and client tunnels to 2. You can even use a single transit node, or none at all, if you’re not concerned about device compromise (for example, if your DNS isn’t secret). In that case, users can build longer anonymizing tunnels on their end. The key is to be smart and not skip reading the documentation and consulting with knowledgeable people.
As a demonstration (or for regular use), you can use the DNS server with the user configs provided above.