DEV Community

Cover image for Creating a Captive Page to Sign into any Public Network
Ingo Steinke, web developer
Ingo Steinke, web developer

Posted on • Updated on • Originally published at

Creating a Captive Page to Sign into any Public Network

What is a captive page and how to create one?

My issue: the expected "Sign into Wi-Fi network" prompt does not always appear automatically when I try to log into a public network that requires a login, like "WiFi on ICE" in Deutsche Bahn's intercity trains. Sometimes computers fail or refuse to display that page, making it hard to use the internet while travelling or working in a café.

Cannot reach public WIFI login page in Ubuntu, Captive Portal Issue, a post on, has been viewed 23k times! How can so many people visit that page if they don't have internet? 🤔
Screenshot of quoted post on

What is happening here?

Many systems like Android, ChromiumOS, Apple's MacOS, or Microsoft Windows seem to have a built-in sign-in process that calls an external "captive page" like

Ubuntu Linux doesn't, which might be one of the few disadvantages of using Linux, although technically speaking, we could rather say the captive portals hijack people's first internet requests, which is more easy to achieve on Windows and Apple devices than on Linux which mostly prioritizes security over convenience.

I tried to understand what is actually happening here and found it is not that simple and straightforward as it might seem on a first glance. Check Wikipedia and Chromium for more details.

A captive portal usually tries to capture your first website request using its DNS server, and if it doesn't, your operating system's connection manager will try to do so, at least on most operating system except Linux.

Even without setting up an automatic sign-in process, we can mostly achieve the same result by manually visiting one of the popular captive pages like which will return a simple, unstyled, page with a "success" message without enforcing an encryted HTTPS connection.

There is no Internet

Screenshot of Deutsche Bahn WiFi on ICE internet portal page and an error message that there is no internet

If it doesn't work, you might see another vintage style website telling you that there is no internet. I can tell you as I'm living in Germany. Despite the material wealth of our country, we are quite poor in other aspects of life, maybe emotionally but surely technologically, as Germany is infamous for its slow and unskillful adoption of new technology and its relatively slow and unreliable internet connections.

Understanding Captive Pages and their Limitations

Wikipedia lists different kinds of issues that might break the automatic portal process:

Captive portals often require the use of a web browser; this is usually the first application that users start after connected to the Internet, but users who first use an email client or other application that relies on the Internet may find the connection not working without explanation, and will then need to open a web browser to validate. [...] A similar problem can occur if the client uses AJAX or joins the network with pages already loaded into its web browser. Similarly, as HTTPS connections cannot be redirected (at least not without triggering security warnings), a web browser that only attempts to access secure websites before being authorized by the captive portal will see those attempts fail without explanation (the usual symptom is that the intended website appears to be down or inaccessible).

Building a Captive Page of our own

Wouldn't it be cool to have our own captive.localhost or even a service like ready to go on any device?

Screenshot of Open Mind Culture captive page with network headers shown in the browser's developer tools

While the actual captive mechanisms can do more or less complicated things like access control and intercepting network traffic, all that we want to build is a page that will trigger this mechanism.

So "creating a captive page" that we can call in our web browser to trigger the actual captive mechanism, is in fact nothing but a page that accepts an unencrypted HTTP request and returns a 204 or 200 status code without redirecting to HTTPS.

Using PHP to generate a Simple Page and Set HTTP Headers

One possible way to return a website and control its HTTP headers is by using PHP, which might be the simplest way if you already have a PHP-based application like WordPress up and running on a web server. If you haven't, you might prefer to deploy a node server to a cloud infrastructure or configure a "serverless" service.

Let's assume we have a PHP server ready, we can create a new directory and configure a new subdomain that points to that folder where we will then put a file called index.php that sets the appropriate response headers, like preventing our browser or web proxies to cache the page.

Sending any Successful 2xx HTTP Status

As Apple's implementation shows, we don't have to send a 204 No Content status. Many portals, like ICEportal of German long distance trains, use their captive page to display a consent or login form, advertise their services (like entertainment content stored on a local server) or display status information (like the current train line and upcoming station). But we don't have to bother putting too much content either, as we expect to get redirected to the real captive page once the external network intercepts our call and hijacks our first request anyway. Well, some do and some don't, so let's put some minimal content at least.


    if (!headers_sent()) {
        header('Status: 200 OK');
        header('Cache-Control: no-cache');
        header('Content-Type: text/html');
?><!DOCTYPE html>
<html lang="en">
More content here...
Enter fullscreen mode Exit fullscreen mode

Nice! Now we have to prove that it actually works and does its job, right? Here's the page, let's see it in action:

Everything seems to work as expected. No error, no redirect, no https in the first place, and an HTTP header to prevent caching.

Test, Discussion, and German Blogpost

Now all I have to do is test it "in the wild", taking my Linux laptop outside and try to use it in a public network known to require a captive page like Deutsche Bahn's WiFiOnICE or WiFi@DB.

This article has bee cross-posted as "There is no internet". A German version will be coming up in my Open Mind Culture webblog.

If you have any issues or feedback about my captive page and explanations, feel free to share your experience in the comments!

Top comments (4)

ingosteinke profile image
Ingo Steinke, web developer • Edited
ingosteinke profile image
Ingo Steinke, web developer • Edited

If you still can't connect using German railway Deutsche Bahn's WiFi on ICE network from a Ubuntu / Linux system, the reason might be that you're a dev an using Docker with the standard configuration which has an address conflict with WIFIonICE at Thanks to German UbuntuUsers forum: Probleme mit dem WIFIonICE for pointing out!

A quick workaround, unless you actually have to use Docker while working on a train (of course you do), you can stop Docker, optionally also prune its networks and try to explicitly clear using the br- address that you can find out using the ifconfig command. Thus:

ip a to check if you are actually connected to any network.

ip a | grep docker to check if Docker is a potential problem and find out its name to use for shutting it down.

$ ip a | grep docker
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    inet brd scope global docker0
$ sudo ifconfig docker0 down
$ docker network prune
WARNING! This will remove all custom networks not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Networks:
Enter fullscreen mode Exit fullscreen mode

Last but not least, if needed, use ifconfig to find out the conflicting broadcast address.

$ ifconfig -a | grep br
br-2abd62f2e2aa: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet  netmask  broadcast
br-7eeffe1d5437: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet  netmask  broadcast
br-a5a3fb8bd4be: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet  netmask  broadcast
        inet  netmask  broadcast
        inet  netmask  broadcast
$ sudo ip -4 addr del dev br-2abd62f2e2aa
RTNETLINK answers: Cannot assign requested address
Enter fullscreen mode Exit fullscreen mode

Then reload the captive page or

See the forum post (or any other helpful documentation) on how to permanently reconfigure Docker to use an alternative network address, for example this post on ServerFault: Configuring Docker to not use the range:

Edit or create /etc/docker/daemon.json config file for docker daemon
using the alternate address pool suggested in the comments to avoid another address collision conflict

Enter fullscreen mode Exit fullscreen mode

Restart dockerd:
service docker restart

ingosteinke profile image
Ingo Steinke, web developer • Edited

Tested today on FlixTrain: my HTTP page works and gets redirected to their portal page, which offers entertainment content, but no checkbox to accept the terms in my desktop browser, even after clearing cache and cookies, and no internet apart from their own page and Google. 🤔
Takeaway/reminder: always make as much data available offline as possible. Use a mail client software that stores emails and schedules to send them when online again.

ingosteinke profile image
Ingo Steinke, web developer