published on 19th 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.
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.
Your config files probably look something like this if your setup is similar to mine:
/etc/systemd/network/20-wlan0.network
copy 1[Match]
2Name=wlan0
3
4[DHCPv4]
5# dont use dns servers advertised by dhcp server (router)
6UseDNS=no
7
8[DHCPv6]
9# dont use dns servers advertised by dhcp server (router)
10UseDNS=no/etc/systemd/resolved.conf
copy 1[Resolve]
2# insert your dns over tls server ip and domain name below, keep the '#'
3DNS=1.2.3.4#dnsovertls.resolver.tld
4# only use provided dns server, no fallback
5FallbackDNS=
6# use dns for all domains
7Domains=~.
8DNSOverTLS=yes
9ReadEtcHosts=yes
10Cache=yes
11LLMNR=no
12MulticastDNS=no
13# hacky way to only make resolved listen on udp 127.0.0.1:53
14DNSStubListener=no
15DNSStubListenerExtra=udp:127.0.0.1:53Note 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
copy 1[General]
2# dhcp is done by systemd-networkd
3EnableNetworkConfiguration=false
4
5# random mac address
6AddressRandomization=once
7AddressRandomizationRange=full
8
9[Network]
10NameResolvingService=systemdWe 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
copy 1[Match]
2Name=wlan0
3SSID="free_wifi"
4
5[Network]
6DHCP=yes
7# because the dns advertised by routers is not encrypted
8DNSOverTLS=no
9# i dont expect hotspots to support dnssec
10DNSSEC=no
11# reset options set in global systemd-resolved conf
12DNS=
13Domains=~.
14
15[DHCPv4]
16UseDNS=yes
17
18[DHCPv6]
19UseDNS=yesAnd exclude the SSID in your default config file:
/etc/systemd/network/20-default.network
copy1[Match]
2Name=wlan0
3SSID=! "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
copy1[Match]
2Name=wlan0
3SSID=! "free_wifi_2"/etc/systemd/network/30-captiveportal.network
copy1[Match]
2Name=wlan0
3SSID="free_wifi_2"Then restart all the relevant services:
1sudo systemctl restart iwd systemd-resolved systemd-networkdAnd 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.
Now that you’re authenticated to the captive portal, you should be able to change back your DNS server using this command:
1sudo systemd-resolve --interface wlan0 --set-dns="1.2.3.4#dnsovertls.resolver.tld" --set-domain "~." --set-dnsovertls=trueTo 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.