captive-firefox: Solving Captive Portal Headaches with DNS over TLS

If you’re running systemd-resolved with DNS over TLS (like I detailed in my split DNS post), you’ve probably run into the same annoying problem I have: captive portals just don’t work properly.

The issue is straightforward but frustrating. Your system is configured to use secure DNS servers like Cloudflare (1.1.1.1) with DNS over TLS, which is great for security and privacy. But when you connect to that hotel Wi-Fi or coffee shop network, the captive portal can’t intercept your DNS queries to redirect you to their login page. Your requests bypass their DNS entirely, so you never see the portal.

The Manual Workaround (That Gets Old Fast)

The typical workaround involves giving the Wi-Fi network DNS priority and disabling DNS over TLS:

sudo resolvectl domain wlp0s20f3 "~." && sudo resolvectl dnsovertls wlp0s20f3 no
# Connect to captive portal
# Log in manually
# Revert changes
sudo resolvectl domain wlp0s20f3 "" && sudo resolvectl dnsovertls wlp0s20f3 yes

This works, but it’s annoying for two reasons: you have to remember to revert the changes, and it affects your entire system’s DNS behavior instead of just the browser you need for the captive portal.

Enter captive-firefox

I got tired of this dance and wrote a simple Bash script that handles captive portals elegantly. The idea is simple: launch Firefox in a sandbox that uses the Wi-Fi network’s DNS server directly, bypassing your system’s DNS configuration entirely.

Here’s what the script does:

  1. Auto-detects your Wi-Fi interface (or you can specify it)
  2. Extracts the DHCP-provided DNS server from NetworkManager
  3. Launches Firefox in a firejail sandbox with that specific DNS
  4. Uses a completely isolated profile – no access to your regular browsing data

The result? Firefox can see the captive portal while your system maintains its secure DNS configuration.

Zero Dependencies, Maximum Convenience

The script requires only standard Linux tools you probably already have:

  • Bash
  • firejail (for sandboxing)
  • nmcli (NetworkManager CLI)
  • iw (wireless tools)
  • Firefox

No Go binaries to compile, no complex dependencies. Just copy the script and run it.

Usage

Most of the time, it’s as simple as:

./captive-firefox.sh

The script will auto-detect everything and open Firefox pointing to the standard captive portal detection URL. For edge cases, you can specify the interface or target URL manually.

I’ve also included a .desktop file so you can launch it from your application menu when needed.

Security Considerations

The sandboxed Firefox instance:

  • Can’t access your real Firefox profile or data
  • Uses only the captive portal’s DNS (isolated from your secure setup)
  • Runs in a firejail container for additional isolation
  • Automatically cleans up when closed

Your main system’s DNS configuration remains untouched throughout the process.

Get It

The script is available on my GitHub: captive-firefox

Finally, a civilized way to deal with captive portals without compromising your DNS security setup.

Automating DNS Configurations for F5 VPN Tunnel using Systemd-resolved and NetworkManager-dispatcher

F5 VPN does not play well with split DNS configuration using systemd-resolved because it insists on trying to rewrite /etc/resolv.conf. The workaround is to make resolv.conf immutable, and configure the DNS settings for the tunnel manually. systemd-resolved does not have a mechanism for persistant per-interface configuration, and it relies on NetworkManager to configure each connection correctly. F5 VPN is not compatible with NetworkManager, and does not make it easy to configure it this way.

NetworkManager-dispatcher allows you to run scripts based on network events. In our case, we will use it to automatically add DNS configurations when the F5 VPN tunnel tun0 is up, and thus provide persistent configuration.

Here is the script:

#!/bin/bash

INTERFACE=$1
STATUS=$2

case "$STATUS" in
    'up')
        if [ "$INTERFACE" = "tun0" ]; then
            # Add your search domains here
            SEARCH_DOMAINS="~example.corp ~example.local"

            resolvectl domain "$INTERFACE" $SEARCH_DOMAINS
            resolvectl dns $INTERFACE 192.168.100.20 192.168.100.22
            resolvectl dnsovertls tun0 no
        fi
        ;;
esac

The script checks if the interface is tun0 and if the current action is up. If so, it uses resolvectl to configure search domains and local DNS servers. Lastly, DNS over TLS is disabled, as the corporate DNS servers do not support them.

To make this script work, install in the /etc/NetworkManager/dispatcher.d/ directory with the name f5-vpn. Make sure it’s executable and only writable by root. NetworkManager-dispatcher will run this script whenever a network interface goes up, automatically setting the DNS configurations for F5 VPN tunnel.

Split DNS using systemd-resolved

Many corporate environments have internal DNS servers that are required to resolve internal resources. However, you might prefer a different DNS server for external resources, for example 1.1.1.1 or 8.8.8.8. This allows you to use more secure DNS features like DNS over TLS (DoT). The solution is to set up systemd-resolved as your DNS resolver, and configure it for split DNS resolving.

Starting with systemd 251, Debian ships systemd-resolved as a separate package. If it isn’t installed, go ahead and install it.

$ sudo apt install systemd-resolved
$ sudo systemctl enable --now systemd-resolved.service

Create the following configuration file under /etc/systemd/resolved.conf.d/99-split.conf:

[Resolve]
DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com

Domains=~.
DNSOverTLS=opportunistic

Domains=~. gives priority to the global DNS (1.1.1.1 in our case) over the link-local DNS configurations which are pushed through DHCP (like internal DNS servers).

DNSOverTLS=opportunistic defaults to DNS over TLS but allows fallback to regular DNS. This is useful when corporate DNS doesn’t support DNS over TLS and you still want to resolve corporate internal domains.

Restart systemd-resolved to reload the configuration:

$ sudo systemctl restart systemd-resolved

The final step is to redirect programs relying on /etc/resolv.conf (possibly through the glibc API) to the systemd-resolved resolver. The recommended way according to the systemd-resolved man page is to symlink it to /run/systemd/resolv/stup-resolv.conf.

$ sudo ln -rsf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

F5 VPN

F5 VPN doesn’t play well with the above configuration. First, F5 VPN tries to overwrite the DNS configuration in /etc/resolv.conf, by removing the existing file and replacing it with its own (pushing corporate DNS server configuration through it). The solution is to prevent F5 VPN from deleting the /etc/resolv.conf, by setting it to immutable. However, we cannot chattr +i a symlink. We have to resort to copying the configuration statically, and then protect it.

$ sudo cp /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
$ sudo chattr +i /etc/resolv.conf

Finally, because now F5 VPN can’t update the DNS configuration, we would have to manually configure the corporate DNS servers and the search domains.

$ sudo resolvectl dns tun0 192.168.100.20 192.168.100.22
$ sudo resolvectl domain tun0 ~example.corp ~example.local

Update: See Automating DNS Configurations for F5 VPN Tunnel using Systemd-resolved and NetworkManager-dispatcher for a script that automates the configuration.

Setting SPF Record

SPF allows one to specify which SMTP servers can send emails on behalf of a domain. The SPF record is defined as a DNS TXT record and specifies the list of allowed senders for the domain.

"v=spf1 a ip4:134.209.224.112 include:_spf.mx.cloudflare.net include:_spf.google.com ~all"
  • v=spf1 specifies that this TXT record is indeed an SPF record.
  • a include the IP addresses of the domain, as returned by the A or AAAA records. This is required to let your own server send emails.
  • ip4:134.209.224.112 allows a specific server by IP address to send emails. You can also use netmasks (ie /20) to allow ranges.
  • include:_spf.mx.cloudflare.net allows Cloudflare Email Routing to forward emails for your domain.
  • include:_spf.google.com allow sending emails via Google Workspace/Gmail.
  • ~all This marks every other server not listed so far as insecure/spam, but the email will be accepted. This can be replaced by -all which would tell the recipient to reject emails from unauthorized servers.

References