Your phone handles airport WiFi without being asked. You join "CLT Free WiFi," a little sheet slides up, you tap Accept, and you're on. A Linux laptop joins the same network, reports a perfectly good connection, and then every tab you open spins and dies. There's no sheet, no link, no hint of what the network wants from you. The password-less network is somehow the one you can't get onto.
I sat at Charlotte Douglas with exactly that problem on October 21st, tethered to my phone so I could keep working, and decided the laptop should learn the trick the phone knows. This is how captive portals actually work, why a modern browser is the worst possible tool for finding one, and what it took to make wifimenu, the little dmenu WiFi picker on Aegix Linux, open the login page by itself.
What a captive portal actually does
The network has no idea who you are, and it doesn't care until you try to leave. Joining is free: the access point hands you an address, a gateway, and a DNS server, and NetworkManager reports "connected" in complete honesty. Then the gateway does one rude thing. Every connection you open to port 80, plain unencrypted HTTP, gets answered not by the site you asked for but by the portal's own web server, pretending to be whoever you called.
you gateway the internet
|-- GET http://x.com --->| |
|<-- "Please wait..." ---| (never forwarded) |
| 200 OK, from the | |
| portal, not x.com | |
That's the whole mechanism. The portal is a man in the middle that runs the network, so it doesn't need to hack anything. It just answers for everyone.
Why your browser never sees it
Here's the part that surprises people. The portal can only impersonate sites over plain HTTP, because over HTTPS it would have to present a certificate for x.com, and it can't. A connection to port 443 either gets refused or gets a certificate the browser rightly rejects.
Now think about what a 2026 browser does when you type an address. Brave, Chrome, and Firefox all try HTTPS first, before the first packet leaves. So the request the portal was waiting to intercept never happens. The browser knocks on a door the portal can't answer, waits, and shows you a timeout. The better your browser's security defaults, the more thoroughly it hides the one page you need.
Your phone gets around this by never trusting the browser with the job. The moment it joins a network it fires its own plain-HTTP request at a known address and compares the answer to what that address is supposed to say. Android asks connectivitycheck.gstatic.com/generate_204 and expects an empty 204. iOS asks captive.apple.com and expects a page that says "Success." If anything else comes back, the phone knows a portal is in the way and shows you whatever it got.
Teaching the laptop the same trick
NetworkManager actually does this check too. On Arch and Artix it asks ping.archlinux.org/nm-check.txt and expects the text "NetworkManager is online." When the answer is wrong it flips its connectivity state to portal, and then nothing on a minimal desktop does anything with that fact. The verdict sits there, correct and ignored.
So wifimenu now runs its own probe the moment a connection comes up. It asks that Arch endpoint, then Mozilla's detectportal.firefox.com/success.txt as a fallback, then Google's, over plain HTTP with curl, which doesn't upgrade anything. A 204 or the expected one-line body means you're online. Anything else that answers is the portal, and the answer is where the login URL hides.
The easy portals send a redirect, and curl hands you the Location header. Then there are the Aruba ones.
The page that said "Please wait"
I couldn't capture the Charlotte portal's answer while sitting on it, because the moment I'm on that network I can't talk to anything. So I did the capture on the phone's hotspot, wrote a script, switched networks, ran it, and switched back. Later I found a better trick: the portal remembers you by MAC address, so telling NetworkManager to reconnect with a random one makes the airport treat the laptop as a brand new device. You get a fresh portal on demand, without leaving your seat.
Here's what CLT sends back for every plain-HTTP request, whatever you asked for:
HTTP/1.1 200 OK
Content-type: text/html; charset=utf-8
<title>REDIR</title>
<body onLoad='redirTimer()'>Please wait...
<SCRIPT LANGUAGE='JavaScript'>
redirTime = '500';
redirURL = 'http://198.18.2.1:80/reg.php?ah_goal=index.html&ah_login=true&url=E2B8F3...';
function redirTimer() {
self.setTimeout('self.location.href = redirURL;',redirTime);
}
</script>
No redirect. No meta refresh. No link. A 200 with the login URL parked in a JavaScript variable and a half-second timer that moves you there. My first version of the extractor looked for a Location header, a meta refresh, a location.href = 'http...' literal, or an href, and this page has none of those. It fell back to opening the probe URL in the browser and hoping the portal would hijack that, which, to be fair, worked twice and failed once.
The fix is embarrassingly small: as a last resort, take the first quoted absolute URL anywhere in the page. On the Aruba page that's redirURL, and it points at 198.18.2.1, an address out of the benchmarking range that no real site uses, which happens to be the one kind of address no browser will try to upgrade to HTTPS. The portal, by accident, hands you the one URL your browser will load exactly as written.
That real page now lives in the test suite, next to a fake portal that serves every variant I could think of: redirects, meta refreshes, JavaScript assignments, a 511 Network Authentication Required, a server that hangs. Fourteen cases, all runnable on a hotspot at a gate.
What it feels like now
Pick a network in wifimenu. If the portal is there, a notification names the login URL, the URL is already on your clipboard, and Brave opens straight to the page with the Accept button. Click it, and a second notification tells you the portal let you through, because the script keeps probing in the background until the answer flips to online.
Networks you didn't join through wifimenu get the same treatment. nmcli monitor prints a line every time NetworkManager's connectivity verdict changes, so a tiny watcher started from xprofile reads that stream for the life of the session and runs the portal flow on every Connectivity is now 'portal'. Hotels that re-prompt every twenty-four hours, networks that auto-reconnect while you were getting coffee, the one you joined in nmtui out of habit: NetworkManager notices, the watcher opens the page. A stamp of the last URL keeps the watcher and the connect path from opening two tabs when they both spot the same portal within seconds of each other.
And the status bar shows a ๐ while NetworkManager says a portal is in the way. Click it and you get the login page instead of nmtui.
What it is not
It's a bash script. It leans entirely on curl, nmcli, and dmenu, all of which an Aegix install already has, and it adds nothing to the system that wasn't there. It doesn't fill in the form for you, and it doesn't pretend the portal's terms are anything other than the portal's terms. It does the one thing the phone does and the laptop didn't: it asks a question over plain HTTP and takes the answer seriously.
The whole episode is a reminder of how much of a good desktop is other people's unglamorous plumbing. NetworkManager was telling the truth the entire time. "Connected" was true, "portal" was true, and the login URL was sitting in a 200 response any curl could fetch. The only thing missing was something willing to read it.
Timothy