{"id":291,"date":"2016-07-15T06:00:34","date_gmt":"2016-07-15T14:00:34","guid":{"rendered":"http:\/\/andrewwippler.com\/?p=291"},"modified":"2016-07-15T06:17:17","modified_gmt":"2016-07-15T14:17:17","slug":"openwrt-captive-portal","status":"publish","type":"post","link":"https:\/\/andrewwippler.com\/2016\/07\/15\/openwrt-captive-portal\/","title":{"rendered":"OpenWRT Captive Portal"},"content":{"rendered":"

In a previous post<\/a>, 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.<\/p>\n

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<\/a> and here<\/a>.<\/p>\n

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<\/a> 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.<\/p>\n

<\/p>\n

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<\/a> 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.<\/p>\n

Setting up the router<\/h3>\n

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<\/em> 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.<\/p>\n

uci get uhttpd.main.index_file\nuci get uhttpd.main.index_page\nuci get uhttpd.main.cgi_prefix\nuci get uhttpd.main.home\n<\/code><\/pre>\n

uci<\/code> is the program that interfaces with OpenWRT settings. We could manually edit the config files, but using this program makes life easier.<\/p>\n

Next we need to install the two packages we need:<\/p>\n

opkg update\nopkg install nginx iptables-mod-ipopt\n<\/code><\/pre>\n

Now we are ready to configure our hotspot.<\/p>\n

Setting up the webserver<\/h3>\n

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<\/code> and have it listen on port 81.<\/p>\n

# Set up nginx.conf\ncat << EOF > \/etc\/nginx\/nginx.conf\nuser nobody nogroup;\nworker_processes  1;\n\nevents {\n    worker_connections  2048;\n}\n\nhttp {\n    include       mime.types;\n    sendfile        on;\n    keepalive_timeout  65;\n    server {\n        listen       81 default_server;\n\n        location \/ {\n            root   html;\n            index  index.html index.htm;\n            return 302 http:\/\/hotspot.localnet\/hotspot;\n        }\n   }\n}\nEOF\n<\/code><\/pre>\n

Setting up the redirection<\/h3>\n

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.<\/p>\n

cat << EOF > \/www\/cgi-bin\/hotspot\n\n#!\/bin\/sh\necho \"Content-type: text\/html\"\necho \"\"\n\nif [ \"$QUERY_STRING\" = \"\" ]; then\n#Do captive portal detection\n\tCNS=$(echo \"$HTTP_USER_AGENT\" | grep -o CaptiveNetworkSupport)\n\n\tif [ \"$CNS\" = \"CaptiveNetworkSupport\" ]; then\n\t\techo \"<html>\n\t <!--\n\t <?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\n   <WISPAccessGatewayParam xmlns:xsi=\"http:\/\/www.w3.org\/2001\/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http:\/\/www.wballiance.net\/wispr_2_0.xsd\">\n<Redirect>\n<MessageType>100<\/MessageType>\n<ResponseCode>0<\/ResponseCode>\n<VersionHigh>2.0<\/VersionHigh>\n<VersionLow>1.0<\/VersionLow>\n<AccessProcedure>1.0<\/AccessProcedure>\n<AccessLocation>Andrew Wippler is awesome<\/AccessLocation>\n<LocationName>MyOpenAP<\/LocationName>\n<LoginURL>http:\/\/hotspot.localnet\/?r=1<\/LoginURL>\n<\/Redirect>\n<\/WISPAccessGatewayParam>\n\t --><\/html>\"\n\t else\n\n\t  echo \"<html>\n\t  <head>\n\t    <title>Redirecting<\/title>\n\t    <META http-equiv=\\\"refresh\\\" content=\\\"0;URL=http:\/\/hotspot.localnet\/hotspot?r=1\\\">\n\t  <\/head> <body><\/body><\/html>\"\n\t  fi\n\nelse\n# After captive portal redirection\n\n# Check to see if accepted TOS\nPASSPHRASE=$(echo \"$QUERY_STRING\" | grep -o andrew-wippler-is-cool )\nif [ \"$PASSPHRASE\" != \"andrew-wippler-is-cool\" ]; then\n\n  \techo \"<html>\n    <head>\n      <title><\/title>\n    <\/head>\n    <body>\n      <h1>Authorization Required<\/h1>\n      <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.\n      <\/p>\n      <form method='GET' action='\/hotspot'>\n        <input type='hidden' name='security_code' value='andrew-wippler-is-cool' \/>\n        <input type='checkbox' name='checkbox1' CHECKED \/><label for='checkbox1'>I Agree to the terms<\/label><br \/>\n        <input type='submit' value='Connect' \/>\n        <\/form>\n    <\/body>\n    <\/html>\"\n\n  # User accepted TOS\n  else\n\n    # Grab the MAC address\n    MAC_ADDRESS=$(cat \/proc\/net\/arp | grep $REMOTE_ADDR | awk {'print $4'})\n\n    # Add device to the firewall\n    iptables -t mangle -A wlan0_Outgoing -m mac --mac-source \"$MAC_ADDRESS\" -j MARK --set-mark 2\n\n    sleep 5\n\n    echo \"<html>\n    <head>\n    <title><\/title>\n    <\/head>\n    <body>\n    <h1>You are now free to browse the internet.<\/h1>\n    <\/body>\n    <\/html>\"\n\n  fi\n\nfi\nEOF\n<\/code><\/pre>\n

Setting up the firewall<\/h3>\n

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.<\/p>\n

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

Final steps<\/h3>\n

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).<\/p>\n

# Set up dnsmasq\nuci set dhcp.lan.start='3'\nuci set dhcp.lan.limit='254'\nuci set dhcp.lan.leasetime='1h'\nuci set network.lan.ipaddr='192.168.24.1'\n\n# set up static DNS entry\nuci add dhcp domain\nuci add_list dhcp.@domain[0].ip=192.168.24.1\nuci add_list dhcp.@domain[0].name=hotspot.localnet\n\n# Set dns servers to be the router\nuci set dhcp.lan.dhcp_option='6,192.168.24.1'\n\n# Set WAP Name\nuci set wireless.@wifi-iface[0].ssid=MyOpenAP\n\n# Point uhttpd to our new document root\nuci set uhttpd.main.index_file='hotspot'\nuci set uhttpd.main.index_page='hotspot'\nuci set uhttpd.main.cgi_prefix='\/'\nuci set uhttpd.main.home='\/www\/cgi-bin\/'\n\n# Save to NVRAM\nuci commit\n\n# Turn on our work\n\/etc\/init.d\/nginx start\n\/etc\/init.d\/uhttpd restart\n\/etc\/init.d\/firewall restart\n<\/code><\/pre>\n

If all went well, you should be prompted with a captive portal alert on your device.<\/p>\n","protected":false},"excerpt":{"rendered":"

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 […]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"advanced_seo_description":"","jetpack_seo_html_title":"","jetpack_seo_noindex":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false}}},"categories":[6],"tags":[16,43,50,58],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack-related-posts":[],"jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"_links":{"self":[{"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/posts\/291"}],"collection":[{"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/comments?post=291"}],"version-history":[{"count":5,"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/posts\/291\/revisions"}],"predecessor-version":[{"id":305,"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/posts\/291\/revisions\/305"}],"wp:attachment":[{"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/media?parent=291"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/categories?post=291"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/andrewwippler.com\/wp-json\/wp\/v2\/tags?post=291"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}