OpenWRT Captive Portal

In a previous post, I explained how to set up a captive portal on a Raspberry Pi which was running Raspbian (Debian). If you read that article, you can skip the next paragraph.

A captive portal is a piece of software that prompts for user interaction before allowing the client to access the internet or other resources on the network. It is a combination of a firewall and a webserver. In this tutorial, I will explain how to create an open WiFi network on OpenWRT firmware. Before deploying an open WiFi network, you may want to consult a lawyer of the legality and restrictions for having one. You can also review what has been said by lawyers here and here.

To set up a captive portal on a wireless access point (WAP), you will need to have the OpenWRT firmware installed and have at least 5mb of free space. My TP-Link 1043ND had enough space and this article was tested against it. This article assumes you have OpenWRT installed without any additional addons and have plenty of space to spare.

Before you run any command on your device, be sure to check syntax and my comments of what the code does. You have the possibility of placing your WAP into a hard-to-recover state. At one point I placed my access point into a solid firewall that did not allow any traffic in or out! If you encounter this same scenario: stop, take a deep breath, and do a factory reset from failsafe mode. In my particular model, I have the broadcast packet which made it easier to reset. It is not my intention with this article to brick your device. By following this article you acknowledge that I am not liable for any damages you may encounter and will not hold me responsible for the outcome on the device.

Setting up the router

There is not much software needed after a fresh install of OpenWRT. It comes with all the bells and whistles and basically turns your $100 router into a $1,000 router. Since the commands need to run on the device, you will need to login via SSH or Telnet. You can enable SSH on the System > Administration page. Once connected, you will need to run the following 3 commands and keep the information safe. You will need these to revert back to using the LuCI admin interface.

uci get uhttpd.main.index_file
uci get uhttpd.main.index_page
uci get uhttpd.main.cgi_prefix
uci get uhttpd.main.home

uci is the program that interfaces with OpenWRT settings. We could manually edit the config files, but using this program makes life easier.

Next we need to install the two packages we need:

opkg update
opkg install nginx iptables-mod-ipopt

Now we are ready to configure our hotspot.

Setting up the webserver

The web server was the trickiest part in all of this. On one hand, you need to execute a scripting language and on the other hand, you had limited space so scripting languages were difficult to install. The micro httpd server which shipped with OpenWRT was sufficient to run the scripts needed, but I was unable to get the magic packet to work properly. My solution was to install nginx and have it listen on port 81.

# Set up nginx.conf
cat << EOF > /etc/nginx/nginx.conf
user nobody nogroup;
worker_processes  1;

events {
    worker_connections  2048;

http {
    include       mime.types;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen       81 default_server;

        location / {
            root   html;
            index  index.html index.htm;
            return 302 http://hotspot.localnet/hotspot;

Setting up the redirection

I had to change the form method from a POST to a GET. This puts my passphrase (so I know they read and acknowledged the TOS) in the URL. There might be a better way to do this. This line of code will set up the hotspot file that will serve my content.

cat << EOF > /www/cgi-bin/hotspot

echo "Content-type: text/html"
echo ""

if [ "$QUERY_STRING" = "" ]; then
#Do captive portal detection
	CNS=$(echo "$HTTP_USER_AGENT" | grep -o CaptiveNetworkSupport)

	if [ "$CNS" = "CaptiveNetworkSupport" ]; then
		echo "<html>
	 <?xml version=\"1.0\" encoding=\"UTF-8\"?>
   <WISPAccessGatewayParam xmlns:xsi="" xsi:noNamespaceSchemaLocation="">
<AccessLocation>Andrew Wippler is awesome</AccessLocation>

	  echo "<html>
	    <META http-equiv=\"refresh\" content=\"0;URL=http://hotspot.localnet/hotspot?r=1\">
	  </head> <body></body></html>"

# After captive portal redirection

# Check to see if accepted TOS
PASSPHRASE=$(echo "$QUERY_STRING" | grep -o andrew-wippler-is-cool )
if [ "$PASSPHRASE" != "andrew-wippler-is-cool" ]; then

  	echo "<html>
      <h1>Authorization Required</h1>
      <p>Before continuing, you must first agree to the <a href="#">Terms of Service</a> and be of the legal age to do that in your selective country or have Parental Consent.
      <form method='GET' action='/hotspot'>
        <input type='hidden' name='security_code' value='andrew-wippler-is-cool' />
        <input type='checkbox' name='checkbox1' CHECKED /><label for='checkbox1'>I Agree to the terms</label><br />
        <input type='submit' value='Connect' />

  # User accepted TOS

    # Grab the MAC address
    MAC_ADDRESS=$(cat /proc/net/arp | grep $REMOTE_ADDR | awk {'print $4'})

    # Add device to the firewall
    iptables -t mangle -A wlan0_Outgoing -m mac --mac-source "$MAC_ADDRESS" -j MARK --set-mark 2

    sleep 5

    echo "<html>
    <h1>You are now free to browse the internet.</h1>



Setting up the firewall

The following commands will set up the firewall to be just like the one in my other post. The difference here is that we are adding them to our custom firewall file which is preserved through reboots.

cat << EOF >> /etc/firewall.user
iptables -t mangle -N wlan0_Trusted
iptables -t mangle -N wlan0_Outgoing
iptables -t mangle -N wlan0_Incoming
iptables -t mangle -I PREROUTING 1 -i br-lan -j wlan0_Outgoing
iptables -t mangle -I PREROUTING 1 -i br-lan -j wlan0_Trusted
iptables -t mangle -I POSTROUTING 1 -o br-lan -j wlan0_Incoming
iptables -t nat -N wlan0_Outgoing
iptables -t nat -N wlan0_Router
iptables -t nat -N wlan0_Internet
iptables -t nat -N wlan0_Global
iptables -t nat -N wlan0_Unknown
iptables -t nat -N wlan0_AuthServers
iptables -t nat -A PREROUTING -i br-lan -j wlan0_Outgoing
iptables -t nat -A wlan0_Outgoing -d -j wlan0_Router
iptables -t nat -A wlan0_Router -j ACCEPT
iptables -t nat -A wlan0_Outgoing -j wlan0_Internet
iptables -t nat -A wlan0_Internet -m mark --mark 0x2 -j ACCEPT
iptables -t nat -A wlan0_Internet -j wlan0_Unknown
iptables -t nat -A wlan0_Unknown -j wlan0_AuthServers
iptables -t nat -A wlan0_Unknown -j wlan0_Global
iptables -t nat -A wlan0_Unknown -p tcp --dport 80 -j DNAT --to-destination
iptables -t filter -N wlan0_Internet
iptables -t filter -N wlan0_AuthServers
iptables -t filter -N wlan0_Global
iptables -t filter -N wlan0_Known
iptables -t filter -N wlan0_Unknown
iptables -t filter -I FORWARD -i br-lan -j wlan0_Internet
iptables -t filter -A wlan0_Internet -m state --state INVALID -j DROP
iptables -t filter -A wlan0_Internet -o eth0.2 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
iptables -t filter -A wlan0_Internet -j wlan0_AuthServers
iptables -t filter -A wlan0_AuthServers -d -j ACCEPT
iptables -t filter -A wlan0_Internet -j wlan0_Global
iptables -t filter -A wlan0_Global -d -j ACCEPT
iptables -t filter -A wlan0_Internet -m mark --mark 0x2 -j wlan0_Known
iptables -t filter -A wlan0_Known -d -j ACCEPT
iptables -t filter -A wlan0_Internet -j wlan0_Unknown
iptables -t filter -A wlan0_Unknown -d -p udp --dport 53 -j ACCEPT
iptables -t filter -A wlan0_Unknown -d -p tcp --dport 53 -j ACCEPT
iptables -t filter -A wlan0_Unknown -d -p udp --dport 67 -j ACCEPT
iptables -t filter -A wlan0_Unknown -d -p tcp --dport 67 -j ACCEPT
iptables -t filter -A wlan0_Unknown -j REJECT --reject-with icmp-port-unreachable
iptables -t nat -A POSTROUTING -o eth0.2 -j MASQUERADE

Final steps

Now that we are done with the preliminary work, we are ready to switch from the LuCI admin website to our own captive portal solution. The below commands will get you in that state. Once you do these commands, the admin section will be disabled (unless you reverse the four commands I told you to write down the values for).

# Set up dnsmasq
uci set dhcp.lan.start='3'
uci set dhcp.lan.limit='254'
uci set dhcp.lan.leasetime='1h'
uci set network.lan.ipaddr=''

# set up static DNS entry
uci add dhcp domain
uci add_list dhcp.@domain[0].ip=
uci add_list dhcp.@domain[0].name=hotspot.localnet

# Set dns servers to be the router
uci set dhcp.lan.dhcp_option='6,'

# Set WAP Name
uci set wireless.@wifi-iface[0].ssid=MyOpenAP

# Point uhttpd to our new document root
uci set uhttpd.main.index_file='hotspot'
uci set uhttpd.main.index_page='hotspot'
uci set uhttpd.main.cgi_prefix='/'
uci set uhttpd.main.home='/www/cgi-bin/'

# Save to NVRAM
uci commit

# Turn on our work
/etc/init.d/nginx start
/etc/init.d/uhttpd restart
/etc/init.d/firewall restart

If all went well, you should be prompted with a captive portal alert on your device.


  1. Thanks, great article. The main thing is simple and effective ๐Ÿ™‚

  2. Hello, I read this post with interest because I have a similar situation that should be sorted out.

    I have an OpenWRT router that already has a lan network/interface and a guest network/interface.

    I would like to add a captive portal, but to the guest network only: the lan network should keep on working just as it is now. Alternatively, I could set up one more network/interface – say, cportal – so that both lan and guest keep on working as they have been for quite some time now. Do you have any pointer or advice on how to do that without disrupting the current firewall configuration?

Comments are closed.