Captive Portal Chat With OpenWrt

Sneha Belkhale
Nov 20, 2020 · 7 min read
Image for post
Image for post

Heya! So, lately I’ve been thinking about how the current state of media directs us to spending a lot of time focusing on global events and people. We are so connected with the entire world, that sometimes we lose sight of what is happening right outside our windows. Instead of trying to reach the largest audience possible by sharing events and ideas on social media, what if we steered our efforts towards our local communities? This could look like pinning up event posters outside, gathering for mutual aid community dinners, oor hosting a web page on an open local network…??

I had seen this idea in Berlin — Google was attempting to open a new headquarters in Neukölln and the people were not going to allow it. Alongside the protests, someone had setup a local network where Google was planning to build its office. When you connected to the network, it would pop up a page of anti-google propaganda that everyone should be aware of. It was a cool supplementary way to share information, localized to a specific area and idea.

I wanted to implement something like this here in NYC, and maybe take it a step further by including a local network chat to spark the sense of community along with some (abolitionist?) propaganda. I came up with a list of the most important features…

  • The page should pop up automatically after you connect (Captive Portal).
  • The local network should not need internet connection.
  • The router should be able to run a small server for the chat API.
  • All processes should survive a reboot.

Lets do iittt::::

Image for post
Image for post
OpenWRT — Wireless Freedom — OS that runs on the router

The Hardware

We need a portable router for this project. This could come in many different forms, I even contemplated using my Raspberry Pi, but eventually settled on the GL.iNet Slate Router for it’s simplicity and specialized functionality.

Image for post
Image for post
GL.iNet Slate Router

This cute portable router comes with an installation of OpenWRT, as well as a Admin Panel web interface. I knooww we all want to be terminal purists sometimes, but this admin panel is actually really nice and helpful to get a visual representation of the router state.

Image for post
Image for post
Router Admin Panel, for basic network configuration

To prep for this experiment, I used the Admin Panel to set up an open guest network named Starbucks WiFi…(ask me about it :p) I also enabled a captive portal for the guest network using the interface. Depending on which router you use, you may have to figure out some of those things with commands, but I have been blessed with this interface and will enjoy its benefits:)

Setting up the Captive Portal

Now that the captive portal has been enabled, when connecting to the router, I receive a popup html page! Now to swap it out with our own.

OpenWRT comes with the NoDogSplash captive portal manager. Looking through the docs, I found that the html page that it sends to newly connected users is located in /etc/nodogsplash/htdocs/splash.html.

So all we need to do is copy over our html page with (anti-capitalist?) propaganda using scp from our computer.

scp index.html root@

Wonderful! Now every time you connect to the open guest network, it should bring you to your (anti-google?) propaganda page.

Allowing Captive Portal to Work Offline

By default, NoDogSplash captive portals will not work without internet. If the router fails to resolve the DNS request for the operating systems “connectivity check” URL (mac uses, then NoDogSplash will return an error and deem the network/captive portal unusable.

I found this to be quite a blocking problem, since one of my goals was to take my local network chat to places where a WiFi connection was not guaranteed (NYC Metro, street protests, etc). Furthermore, since all data for the chat is local to the router, there really was no reason it needed WiFi..

After scrolling through some Github issues, I found a hack to basically configure NoDogSplash to resolve all DNS queries locally with the same random public address, making them seem to pass the “connectivity check” and allow NoDogSplash to continue.

(on router)uci add_list dhcp.@dnsmasq[0].address=’/#/'# saves the config edit for persistency after reboot
uci commit dhcp
service dnsmasq restart

Setting up the Chat Server

At this point, we basically have enough to recreate the Berlin anti google setup:) However I still wanted to see if it was possible to setup a simple chat server. The simplest chat ever needs a running server with one route for posting a message, one route for getting all messages, a super sophisticated .json file database, and an html page to display the chat.

The router model I have only has 128 mb of RAM.. so I thought about downloading Python-Light, but it was too light and did not have SimpleHttpServer. I decided to just pray and hope the device could withstand the vanilla Python installation.

First I installed Python on the router using the OpenWRT package manager.

opkg update
opkg install python

Then copied over my server code and new chat html page from my computer ( I wont go into describing how the chat code works here, but its pretty simple and you can check it out here if you want :)

scp root@ chat.html root@

Create an empty “database” on the router to store our messages:

echo "[]" >> /root/data.json

And run the server!

python /root/

All is well…. but if we open our chat now we can see that requests to the Python server are failing with a connection time out, meaning that the port our server is listening on is not accessible from our chat page. If we remember, NoDogSplash is in charge of the captive portal which blocks most of the ports by default, so we can adjust its settings to allow traffic directed to our server port.

uci add_list nodogsplash.@nodogsplash[0].users_to_router='allow tcp port 8989'uci commit nodogsplashservice nodogsplash restart

Now, if we run the server again and try connecting to our open network, our chat should pop up as a captive portal, happily GETting/POSTing requests to our local server :)

Survive the Reboot, the Final Boss

If you’ve gotten this far.. you should have a captive portal that automatically pops up with a chat window served by the simple Python server running on the router.

In order to reach our ideal of robustness, we need to make sure our processes can survive a reboot!

OpenWRT uses Init Scripts to configure daemons that run on startup, if we make our python server run as a daemon, it should hopefully be able to survive a reboot.

The OpenWRT docs give a pretty simple example Init Script, which we can modify for our purposes :

#!/bin/sh /etc/rc.commonSTART=98
start() {
echo "starting chat server"
(sleep 20; python /root/
stop() {
echo "stopping chat server"
killall python

Init scripts use the template /etc/rc.common, a wrapper that provides its main and default functionality, such as start, stop, restart, enable, and disable. We need to override the default start and stop functions. If you want to learn more about the details of init scripts, check out the OpenWRT docs on the subject, they are awesome.

If you noticed in the script, our start function has a lot going on in the line :

(sleep 20; python /root/

First off, the process is running in the background — indicated by the &. I noticed that if I tried running the process in the foreground, the Admin Panel wouldn’t show up! I have a hunch that init.d start functions are blocking, so whatever you need to start needs to account for that by running itself as a background process.

Another mystery is that if I just ran (python /root/ without the sleep bit, the server would not start up properly. After killing the process and starting it again manually using /etc/init.d/chat start, everything would work fine! My hunch here is that something needed for python servers to initialize is not available at boot time… would love if someone has more information on that. For now, the sleep timer works :)

We can copy this file from our computer into the designated directory of init scripts:

scp root@

And enable it! Enabling the process allows it to startup on boot.

sudo chmod +x /etc/init.d/
/etc/init.d/chat enable
/etc/init.d/chat start

Nowwww cross your fingers and lets reboot!!! To make sure your python server is running in the background after the reboot, you can run

ps | grep python

and check if the server process survived :)

Image for post
Image for post
Captive Portal Chat
Image for post
Image for post
Captive Portal Zapatista Wisdom

We made it! This article may not accurately portray the amount of mental breakdowns that occurred during this exploration, but hey thats linux so it’s implied :) I hope after an adequate amount of flipped tables you are able to get this to work too and we can spread the joy of LAN chats & local environment awareness :P

❤ Snayss

check out my other things @



insta: @snayss

The Startup

Medium's largest active publication, followed by +752K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store