The DNS privacy issue

7 minute read

Travelling nowadays I’m generally a lot less worried about (online) privacy and security than I was 10 or years ago, because everything is HTTPS. No snooping, no man in the middle, it’s wonderful.

There is a big exception though, and that’s the DNS requests. A lot of hotels have a portal system asking for your room number and maintain authorization based on MAC addresses, so they could know exactly who is looking for which domain names. This may have privacy implications, and they may even sell that data, who knows.

This is actually true for your local ISP as well, and they definitely sell that data, but that’s another story.

One Solution

One way to prevent this is of course to use a VPN, like Wireguard to one of my servers, or a commercial VPN, but this may slow down my connection quite a bit, potentially change my external IP address’ detected country, and some sites detect VPN and just won’t let it happen. Also, your hotel may just allow ports 80 and 443.

Another way — which I’ll explain here — is to use DNS over HTTPS.
A few things to note:

  • You can set this up in Chrome directly, but there are other apps running on my computer, and it’s a bit of a pain to change, and I may not want this to still apply when I’m home (maybe I do, I don’t know anymore)
  • You generally still need to start with your Hotel/Hotspot’s DNS server, because it usually has the record for the portal you use to agree to the terms of use
  • This won’t work on your phone. If I find the time and patience, I will look into how to do this properly on my travel router that I bring to most places
  • Apparently Mac OS 13 has in its code the capability to do DNS over HTTPS, but there is no way to configure it.

Software & desing

So the idea is to have something listen on port 53 of localhost, and once you have passed the captive portal, just change the DNS server in Mac OS settings to be 127.0.0.1.
I wish you could specify another port, but that’s not supported, so 53 it is.

There’s plenty software out there that can help us, the most popular seems to be dnscrypt-proxy. I have played with it in the past and it works ok, but I’m just not a python person (and I have absolutely no good reason for that).

I like Go, and https://github.com/m13253/dns-over-https seems to have it all.
The doc says it doesn’t play well with the python dnscrypt-proxy server, so I’ll

  • either have to deploy the doh-server on my server,
  • or have it use Googles DNS over HTTP (see https://developers.google.com/speed/public-dns/docs/doh)

I this I could do without the doh-server side, by having doh-client directly talk to Google’s our Cloudflare’s servers.
But let’s assume that for some reason the hotel I’m in blocks requests to 8.8.8.8 and 8.8.8.4 (very unlikely, but just for the purpose of showing the full install here).

+--------------+                        +------------------------+
| Application  |                        |   Google's 8.8.8.4     |
+-------+------+                        +-----------+------------+
        |                                           |
+-------+------+                        +-----------+------------+
|              |                        |      The Internet      |
|    Mac OS    |                        +-----------+------------+
+-------+------+                                    |
        |                               +-----------+------------+
+-------+------+  +------------------+  |       My server        |
|  doh-client  +--+   The Internet   +--+ (Apache -> doh-server) |
+--------------+  +------------------+  +------------------------+

First try

Let’s try things out first:

  • launching manually doh-client on mac os, as root, in oder to bind on port 53
    • configuring it to use https://my.server.name/dns-query/ as upstream server
    • bootstrapping: well, I’m just leaving Google’s server right now (in order to resolve my.server.name but if that IP was blocked indeed, the trick is to put it in /etc/hosts and you can leave the bootstrap field empty — I haven’t tested this)
  • set up the proxy settings on my server
    • manually running doh-server on port 5380
    • configure it to use 8.8.8.8 in ‘tcp-tls’ mode

Building on the server side

Debian 11 comes with go 1.15, but this requires 1.17 or later (for modules). Go 1.17 can be found in the debian backports, but installing it doesn’t install a diversion, and go still points to 1.15. Just edit the Makefile to force that.

From

ifeq ($(GOROOT),)
GOBUILD = go build
else
GOBUILD = $(GOROOT)/bin/go build
endif

to

GO = /usr/lib/go-1.17/bin/go
ifeq ($(GOROOT),)
GOBUILD = $(GO) build
else
GOBUILD = $(GO) build
endif

Configuring the server

To use Google’s servers

upstream = [
    #"udp:1.1.1.1:53",
    #"udp:1.0.0.1:53",
    "tcp-tls:8.8.8.8:853",
    "tcp-tls:8.8.4.4:853",
]

Configuring Apache

<VirtualHost>
    [...]
    ProxyPass /dns-query http://[::1]:8053/dns-query
    ProxyPassReverse /dns-query http://[::1]:8053/dns-query
</VirtualHost>

And the command line on the server side

$ ./doh-server -conf ./doh-server.conf

And now we try

Now setting my MacOS’ network preference to use 127.0.0.1, and tadah! Everything seems to be working.

% dig @localhost A google.com

; <<>> DiG 9.10.6 <<>> @localhost A google.com
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11612
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
; CLIENT-SUBNET: 2603:7000:3240::/56/39
;; QUESTION SECTION:
;google.com.			IN	A

;; ANSWER SECTION:
google.com.		300	IN	A	142.250.80.46

;; Query time: 531 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sat Nov 26 18:11:50 EST 2022
;; MSG SIZE  rcvd: 80

Running as root

One thing I don’t like: MacOS won’t let you configure a DNS server on a port other than 53.
And I don’t want to run this as root. Why? I would just rather not. I haven’t read the code, even if I had, I’m not a security expert, it just seems that it doesn’t need to.

Potential solutions and workarounds:

  • I could just run doh-client when I’m traveling and using it, as root. That’s better that keeping it running as root all the time
  • I could run netcat at root :-) and have it redirect to doh-client running on an unprivileged port

An interesting lead: fiddling around launchd, there is apparently a way to run as a user and still bind to a low port.
Unfortunately, the way this works is that launchd opens the socket for you, and then the listening program has to get it by calling launch_activate_socket which this project does not support.

This project called launch_socket_server however may give a way.
Essentially it will do the “netcat” part I need.
It seems to only accept TCP connections. I mean I could try to change that in the plist file, but I’m not sure it will work.

This looks promising however: https://github.com/x13a/init-proxyd

Buidling the init-proxyd project

Building it on my Mac I get:

package github.com/x13a/init-proxyd
	imports github.com/x13a/init-proxyd/sockets
	imports github.com/x13a/go-launch: build constraints exclude all Go files in /Users/henri/Documents/Dev/go-workspace/pkg/mod/github.com/x13a/go-launch@v0.0.0-20220817124818-5e84cd966569

A bit of Googling brings me to https://github.com/golang/go/issues/24068, which has me look at the CGO_ENABLED environment variable.

CGO_ENABLED=1 make

Woohoo, it works! make install works great.

$ sudo make install
$ sudo launchctl load /Library/LaunchDaemons/me.lucky.init-proxyd.plist
$ ps ax | grep init-proxy
14386   ??  Ss     0:00.05 /usr/local/sbin/init-proxyd -d :5380

The doc says it may not work with nobody/nogroup, so originally I edited the provided sample plist file to run “as me”, which I don’t love either (but is better than root I guess?), but actually nobody/nogroup works fine, and the default example binds on port 53 (localhost, ipv4), both tcp and udp, so make install works out of the box.

Running doh-client as an agent

Now, have the doh client run automatically. Modify the sample plist so that it will run as me, not as root.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<dict>
		<key>Label</key>
		<string>org.eu.starlab.doh.client</string>
		<key>ProgramArguments</key>
		<array>
			<string>/usr/local/bin/doh-logger</string>
			<string>doh-client</string>
			<string>/usr/local/bin/doh-client</string>
			<string>-conf</string>
			<string>/usr/local/etc/dns-over-https/doh-client.conf</string>
		</array>
		<key>RunAtLoad</key>
		<true/>
		<key>KeepAlive</key>
		<dict>
			<key>SuccessfulExit</key>
			<false/>
		</dict>
		<key>ThrottleInterval</key>
		<integer>5</integer>
	</dict>
</plist>

Installing the server side as a service

Good surprise, on the server side make install works well.
I’m seeing that DynamicUser=True is set, which is good, dog-server will not run as root.

Thing that could be better

  • On the Mac OS side, redirecting to a tcp socket isn’t ideal - another process could bind to it before the dns-over-http client does - though honestly in that case it means my computer is compromised already.
    • One way would be to implement support for Unix Socket in the client.
    • Still, there would still be 2 processes on the Mac OS side. A solution to this is to implement launch_activate_socket.
      Maybe I’ll do that :-)
  • The client process currently runs as “me” - I should see if I can have it run as a nobody or the MacOS equivalent

Not running an open server

Well, that one is pretty easy. Instead of configuring everything for /dns-query', configure it for /super-secret/dns-query`.
It’s simple, but if this is just for your own use, it works great.

Location, location, location

Mac OS Ventura has removed the “location” UI. While for in 10+ years using MacOS I never found the use for that feature, now that it’s gone, I see it. Locations is the proper way (or at least one way) to switch to the “forced” DNS setting.

To create a new location (in this example called Traveling) just use the following. command line

$ networksetup -createlocation Traveling populate

And just like that, “location” is back in the Apple menu

The location submenu is back