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.

`xdg-open` fails when using Firefox under Wayland

Recently I noticed xdg-open started failing opening links in Firefox. Giving me the following error:

Firefox is already running, but is not responding. To open a new window, you must first close the existing Firefox process, or restart your system.

Firefox is already running, but is not responding. To open a new window, you must first close the existing Firefox process, or restart your system.

It happened while I had Firefox running and responding to everything else. I’m running the latest stable Firefox (74 as I’m writing this) on Wayland. Wayland brings a lot of good things, but also a lot of interoperability problems, so I suspected it had something to do with it. Thanks to Martin Stransky I found out that the solution is to set the MOZ_DBUS_REMOTE environment variable prior to launching Firefox. If you are using a desktop file to launch Firefox, you can set the variable in the Exec line like this:

[Desktop Entry]
Type=Application
Name=Firefox
Exec=env MOZ_DBUS_REMOTE=1 MOZ_ENABLE_WAYLAND=1 /home/guyru/.local/firefox/firefox %u
X-MultipleArgs=false
Icon=firefox-esr
Categories=Network;WebBrowser;
Terminal=false
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/vnd.mozilla.xul+xml;application/rss+xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;

You will need to restart Firefox before the fix will take affect.

Yubikey doesn’t work on Firefox installed via Snap

Installing Firefox via Snap is an easy way to get the latest Firefox version on your favorite distro, regardless of the version the distro ships with. However, due to Snap’s security model, Yubikeys, or any other FIDO tokens do not work out of the box. To enable U2F devices, like Yubikeys, you need to give the Firefox package the necessary permissions manually:

$ snap connect firefox:u2f-devices

Installing Firefox Quantum on Debian Stretch

Debian only provides the ESR (Extended Support Release) line of Firefox. As a result, currently, the latest version of Firefox available for Debian Stretch is Firefox 52, which is pretty old. Lately, Firefox 57, also known as Quantum, was released as Beta. It provides many improvements over older Firefox releases, including both security and performance.

Begin by downloading the latest beta (for Firefox 57) and extract it to your home directory:


$ wget -O firefox-beta.tar.bz2 "https://download.mozilla.org/?product=firefox-beta-latest&os=linux64&lang=en-US"
$ tar -C ~/.local/ -xvf firefox-beta.tar.bz2

This installs Firefox to your current user. Because Firefox is installed in a user-specific location (and without root-priveleges), Firefox will also auto-update when new versions are released.

If you prefer using the stable version of firefox, simply replace the first step by


$ wget -O firefox-stable.tar.bz2 "https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US"

Next, we take care of desktop integration. Put the following in ~/.local/share/applications/firefox-beta.desktop:


[Desktop Entry]
Type=Application
Name=Firefox Beta
Exec=/home/guyru/.local/firefox/firefox %u
X-MultipleArgs=false
Icon=firefox-esr
Categories=Network;WebBrowser;
Terminal=false
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/vnd.mozilla.xul+xml;application/rss+xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;

Building CookieJar out of Firefox’s cookies.sqlite

Firefox 3 started to store it’s cookies in a SQLite database instead of the old plain-text cookie.txt. While Python’s cookielib module could read the old cookie.txt file, it doesn’t handle the new format. The following python snippet takes a CookieJar object and the path to Firefox cookies.sqlite (or a copy of it) and fills the CookieJar with the cookies from cookies.sqlite.

import sqlite3
import cookielib

def get_cookies(cj, ff_cookies):
    con = sqlite3.connect(ff_cookies)
    cur = con.cursor()
    cur.execute("SELECT host, path, isSecure, expiry, name, value FROM moz_cookies")
    for item in cur.fetchall():
        c = cookielib.Cookie(0, item[4], item[5],
            None, False,
            item[0], item[0].startswith('.'), item[0].startswith('.'),
            item[1], False,
            item[2],
            item[3], item[3]=="",
            None, None, {})
        print c
        cj.set_cookie(c)

It works well for me, except that apperantly Firefox doesn’t save session cookies to the disk at all.