← nunq.net

making captive portals work with dns over tls in systemd-resolved

published on 19 December 2023

I’m currently using systemd-networkd, systemd-resolved and iwd together with my ad blocking DNS-over-TLS server for my networking, and it works quite well for most networks, even the pesky 802.1x types such as eduroam. But now picture this: you’re at a hotel, a public place or in a train and you want to use the free Wi-Fi but you can’t seem to get online because your custom config using some minimal network configuration doesn’t work, and you’re on the brink of just installing NetworkManager and rolling with the defaults. Fear not! This is how to make an exception for your DNS-over-TLS config for certain networks.

But first, let’s talk about why we need to do this.

how do captive portals work?

First you need to know that Wi-Fi access points always provide connected devices with a default DNS server over DHCP, which is usually your access point itself. Using this default DNS server, the AP can block all traffic to external sites by just making every domain name resolve to the captive portal login page (it then keeps track of your login status using your device’s MAC address). Note that there are also captive portals using HTTP traffic interception instead of DNS redirection, however, I found that these aren’t that common anymore, because they have issues with HTTPS traffic, which is most traffic nowadays.

current config

Your config files probably look something like this if your setup is similar to mine:

/etc/systemd/network/20-wlan0.network
[Match]
Name=wlan0

[DHCPv4]
# dont use dns servers advertised by dhcp server (router)
UseDNS=no

[DHCPv6]
# dont use dns servers advertised by dhcp server (router)
UseDNS=no
/etc/systemd/resolved.conf
[Resolve]
# insert your dns over tls server ip and domain name below, keep the '#'
DNS=1.2.3.4#dnsovertls.resolver.tld
# only use provided dns server, no fallback
FallbackDNS=
# use dns for all domains
Domains=~.
DNSOverTLS=yes
ReadEtcHosts=yes
Cache=yes
LLMNR=no
MulticastDNS=no
# hacky way to only make resolved listen on udp 127.0.0.1:53
DNSStubListener=no
DNSStubListenerExtra=udp:127.0.0.1:53

Note that by default iwd does network configuration such as handling DHCP itself, but I chose to let systemd-networkd do this, because I’m not sure if it’s possible to do such high-level config distinctions (Wi-Fi access point names, etc.) in the iwd config. So here’s my iwd config, too:

/etc/iwd/main.conf
[General]
# dhcp is done by systemd-networkd
EnableNetworkConfiguration=false

# random mac address
AddressRandomization=once
AddressRandomizationRange=full

[Network]
NameResolvingService=systemd

needed config changes

We need to create a config file that overrides the default interface settings shown above. To do this, just create the following file:

/etc/systemd/resolved.conf
[Match]
Name=wlan0
SSID="free_wifi"

[Network]
DHCP=yes
# because the dns advertised by routers is not encrypted
DNSOverTLS=no
# i dont expect hotspots to support dnssec
DNSSEC=no
# reset options set in global systemd-resolved conf
DNS=
Domains=~.

[DHCPv4]
UseDNS=yes

[DHCPv6]
UseDNS=yes

And exclude the SSID in your default config file:

/etc/systemd/network/20-default.network
[Match]
Name=wlan0
SSID=! "free_wifi"

If you want to connect to a new open Wi-Fi, just add its SSID to the two config files, explicitly including it in one file and explicitly excluding it in the other, like so:

/etc/systemd/network/20-default.network
[Match]
Name=wlan0
SSID=! "free_wifi_2"
/etc/systemd/network/30-captiveportal.network
[Match]
Name=wlan0
SSID="free_wifi_2"

Then restart all the relevant services:

sudo systemctl restart iwd systemd-resolved systemd-networkd

And go to http://captive.apple.com, which should be intercepted by the captive portal and redirect to its login page.

You should now also be able to see that you’re using a different DNS server when you run resolvectl status.

switching back dns servers

Now that you’re authenticated to the captive portal, you should be able to change back your DNS server using this command:

sudo systemd-resolve --interface wlan0 --set-dns="1.2.3.4#dnsovertls.resolver.tld" --set-domain "~." --set-dnsovertls=true

To verify this, run sudo ngrep port 853 (853 is the port used for DNS-over-TLS) and see the encrypted DNS traffic to the server address you used.