dnstt-server requires you to specify a single DNS zone for the tunnel (e.g. t.example.com). Only queries that are under the zone are processed for tunnel data; all others are discarded. Let’s discuss the possibility of letting dnstt-server process queries under any zone. That would make it possible to set up “aliases” for a single dnstt-server instance, under different DNS names. Such aliases could be set up by anyone, not only by the person who runs the dnstt-server.
I was thinking about this during the Kazakhstan shutdown. It was discovered that dnstt in plaintext UDP port 53 mode could get through the shutdown. The problem is that plaintext UDP port 53 mode is not covert. A censor can inspect the contents of DNS queries, and block the ones that are for a subdomain of a known dnstt server.
For example, I could run an instance of dnstt-server and set up DNS records pointing to it:
- A tns.tunnel.example points to 203.0.113.2
- NS t.tunnel.example is managed by tns.tunnel.example
If I posted publicly, “there is a dnstt-server running at t.tunnel.example,” any censor could read the post and add t.tunnel.example to its blocking rules. But if dnstt-server were modified to respond to queries for any domain, then anyone could set up their own DNS records, under a domain they control, pointing to the same IP addresses:
- A tns.rhinoceros.example points to 203.0.113.2
- NS t.rhinoceros.example is managed by tns.rhinoceros.example
They could then privately use dnstt-client with the DNS zone t.rhinoceros.example, a DNS name that the censor does not know about. If an alias gets discovered and blocked, you can make a new one, without needing to relocate the dnstt-server.
For this idea to work, you need to be using a recursive resolver located outside the censor’s area of control—otherwise the censor can just block the dnstt-server’s IP address. In the case of the Kazakhstan shutdown, 18.104.22.168 would have worked.
The reason the idea might work at all is that DNS queries do not contain the IP address of the eventual authoritative resolver, only its DNS name. A censor could block this scheme by working through the name abstraction, looking up NS records for DNS queries it observes, then matching them against a database of known DNS tunnel servers. In the example above, having seen a query for xxx.t.rhinoceros.example, a censor could look up the NS for rhinoceros.example, then ask that NS for the NS of t.rhinoceros.example, then resolve tns.rhinoceros.example to find the IP address 203.0.113.2. The censor could then match the IP address against a database of known tunnel proxies. These dynamic lookups are probably awkward to do at line rate for every query, but they could be made more practical by testing domains in batches, or probabilistically.
A tunnel that uses plaintext DNS, even with a DNS zone that is unknown to the censor, is still probably identifiable anyway, because of the distinctive kinds of DNS messages it uses.
I normally don’t like this kind of circumvention design, where success depends on the censor’s being ignorant. But in an extreme case like an Internet shutdown, where it’s difficult to get any kind of access, I think it’s justified to try things even if they are theoretically unsound.
Supposing we do permit dnstt-server to respond to queries for any domain, what should the user interface look like? Currently, the DNS zone is part of the command-line interface:
dnstt-client -udp HOST:PORT -pubkey-file FILENAME DOMAIN LOCALADDR:LOCALPORT
One way would be to make the DOMAIN argument optional. If dnstt-server gets only 1 non-option argument, it will not restrict the domain of incoming queries:
dnstt-client -udp HOST:PORT -pubkey-file FILENAME LOCALADDR:LOCALPORT
Another way would be to make the DOMAIN argument a wildcard. You could pass * to permit any domain.
dnstt-client -udp HOST:PORT -pubkey-file FILENAME * LOCALADDR:LOCALPORT
Or, we could add a new command line option that means to ignore the DOMAIN argument.