/------> | | jkaptive - a simple captive portal | | Author: Jan Kandziora <jjj@gmx.de> | Version: 1.12 | \-----------------< LICENSE ======= I, Jan Kandziora, the author of jkaptive, grant you the right to use, copy, distribute and modify this software under the terms of the GNU General Public License(GPL), Version 2 (see file COPYING). OVERVIEW ======== A captive portal is a software which intercepts the transfer of all HTTP traffic through a router and presents a login page to the user's web browser instead. Once the user has supplied a token (a special string), transfers aren't intercepted anymore until the lease the token offered expires. There is a variety of captive portal software out there, some of them offer tight security by using a RADIUS server for authentication, while others don't bother with hackers at all and only intercept traffic from 'john doe'. Jkaptive belongs to the latter group. The reason behind this is because a lot of site administrators don't need tight security - the site is just a café which offers free internet access on an unsecured WLAN access point connected to the internet and they need a ticketing system to make it cumbersome for average people to use this offering without actually buying a single coffee. FEATURES ======== Aiming at the simple goal above, jkaptive is dead-simple, too. Jkaptive itself just presents the login page and checks the token. The blocking of unticketed traffic is done through Linux' netfilter. As no proxy server is involved, jkaptive has no performance penalty, nor does it create problems with non-HTTP traffic. Once the token is accepted, jkaptive is out of the way of any network packets completely. For presenting the login page, jkaptive has a built-in webserver, so no additional webserver application is needed. Filtering --------- With the rules shown in section NETFILTER CONFIGURATION all unticketed HTTP traffic is redirected to jkaptive's login page, while all other unticketed traffic is rejected. Jkaptive adds a netfilter rule which bypasses redirection and rejection for any single remote user host once a valid token has been given, and it automatically revokes that rule once the token expires. Instead of rejection, all other kinds of netfilter rules can be applied, e.g. rate-limiting rules, giving full internet access to anyone, but full-speed only to paying customers. The example rules feature only rejection, though. Jkaptive can be configured to work on output traffic, too, so it's an easy way to add ticketing to a stand-alone internet terminal. User's view ----------- From user's point of view, when he wants to read his favourite web comic, he clicks on a bookmark in his browser, then it tries to load http://www.sinfest.net/archive_page.php?comicID=2784 Instead of showing the desired page, the browser presents a login page. As soon the user has entered the token into the login field and that token is valid, the browser is redirected to the page of the original request and showing the comic. In the background, full internet access has been allowed for the user's computer until the token expires. Then, the login page is given again. Tokens ------ Valid tokens are created using a simple algorithm by putting their expiry date together with some salt and checksum, permutate it and make a human-typeable form of it. No communication is needed between the program creating the token and jkaptive: all needed data is encoded into the token. A simple implementation of a token generator is supplied with the jkaptive package, so it can be called by another application or have the algorithm copied into. The output is intended to be printed on a restaurant bill or similar. The same token can't be used simultaneously by more than one user's computer as jkaptive keeps track of all currently used tokens and the ip address of the user's computer they belong to. IMPORTANT: the token algorithm has changed with jkaptive-1.12. If you have written your own token generator, please do a code review of your generator and the jkaptive-token script to apply the changes. Security -------- Jkaptive tries to be secure in the way not compromising the host it runs on. To change netfilter rules it has to run as the root user but to avoid exposing root access to a remote user, the built-in httpd used by jkaptive for serving the login page is run as a non-priviledged user (e.g. nobody). Both processes communicate via pipes and do only exchange tokens and status codes. Starting with jkaptive-1.2, the webserver starts a worker process for each file served and terminates it automatically after a while, so simple starvation/DOS attacks on it don't work. To work against bruteforce resource hoggers this isn't enough, though, so you have to create some site-specific netfilter rules and ressource limits throwing away such packets at netfilter level. Your Linux distribution might have documentation and templates for it. Starting with jkaptive-1.3, the webserver protects the token salt against bruteforcing - a pause of at least 500ms per request for the login page means that a brute force attack would take hours to succeed. Each simultaneous connection to the login page from the same ip gets an extra penalty of 500ms. Note this applies only to the login HTML page. Any other pages or images, styles and scripts loaded from that or any other page are unaffected. The whole thing is somewhat against the original idea of jkaptive to let hackers pass if they really try, but it's only a few lines of code, so why not? LIMITATIONS =========== Jkaptive is not a secure captive portal. It can easily be circumvented by advanced users (aka "hackers") by sniffing for a ticketed IP address and then mimicing this address on their own computer. If you need a secure captive portal, look for one featuring RADIUS authentication; don't bother with others which claim to be "secure" without using RADIUS. Second, the token algorithm is not cryptographically secure. It doesn't have to be because no hacker would attack the token algorithm if he can just sniff the network for some packets in the air. To say once again, JKAPTIVE IS NOT A SECURE CAPTIVE PORTAL! But if you refuse to install steel doors into a traditional japanese house, jkaptive is a perfect fit. Another limitation is the storage of accepted tokens within the jkaptive process only. So if that process is terminated (E.g. by power-cycling the server), the list of accepted tokens is empty on next start: all users have to authenticate again. They may use their previous token once again, though, as long as it hasn't expired. PREREQUISITES ============= jkaptive is written in the Tcl scripting language so it obviously needs that interpreter to run. In addition it needs tclx and some sub-packages from the tcllib. For jkaptive-1.9, the packages are: * tcl >= 8.5 * tclx (8.4 is known to work) * tcllib (1.11.1 is known to work) Starting with jkaptive-1.9, ipsets may be used instead of a netfilter chain. If your site has a great number of ticketed users at any time, the linear parsing of netfilter rules inside the jkaptive chain for ticketed ip addresses may take some more time that you want. With ipsets, the kernel uses a hash instead of a linear list, which should give you more performance. To use it, you need * ipset binary * ipset-aware kernel (CONFIG_IP_SET=y or m and CONFIG_IP_SET_HASH_IP=y or m). This is completely optional, though. See CONFIGURATION below. CONFIGURATION ============= Very little configuration has to be supplied, as useful defaults apply. One single configuration item - a site specific salt - *has to be supplied* however. Jkaptive will refuse to start if it is not configured. The configuration file has ini style, one item per line. Please ensure the items are in the correct section. Lines starting with ; are comments. [general] ;loglevel=4 ;salt=mysalt [httpd] ;user=nobody ;group=nogroup ;port=8088 ;timeout=10 ;webroot=/usr/share/jkaptive/webroot ;loginpage=login.html ;tokenfailpage=tokenfail.html ;tokenoccupiedpage=tokenoccupied.html ;tokenexpiredpage=tokenexpired.html ;error404page=error404.html ;error500page=error500.html [netfilterd] ;backend=iptables ;iptables=/usr/sbin/iptables ;ipset=/usr/sbin/ipset ;chain=jkaptive ;mark=0x00200000 Loglevel is 0=no log, 1=critical only, 2=critical and error, etc. Loglevel 6 and above means all log messages are printed. The default is 4 (down to "notice") which won't clutter your syslog with unneeded messages. The salt has to be supplied! Please don't use "mysalt" but a site-specific string. It doesn't need to have more than four characters as it gets shrunk to a 16-Bit value anyway. The salt has to be the same you use for the token generator. user and group of the httpd have to be names of user and group of a least-priviledged account on your machine. port is the port number the built-in webserver of jkaptive should run. It has to be an unused unpriviledged port. timeout is a length in seconds which a httpd worker process may be present before it gets terminated by the main httpd process. This is a measure against starvation/DOS attacks to the webserver. webroot has to point to a directory containing the login and error pages the build-in webserver of jkaptive should deliver to users. loginpage, tokenfailpage, tokenoccupiedpage, tokenoccupiedpage, tokenexpiredpage, tokenexpiredpage, error404page and error500page are the names of the special HTML files within webroot. backend is the backend to use for adding ip adresses of ticketed clients to the netfilter. It's either "iptables" or "ipset". iptables has to point to the iptables binary jkaptive should use to place its private rules into the netfilter. If the backend is set to "ipset", this isn't used by jkaptive. ipset has to point to the ipset binary jkaptive should use to place its private rules into the netfilter. If the backend is set to "iptables", this isn't used by jkaptive. chain is the private chain/set inside netfilter jkaptive should place its rules. mark is a packet tracking mark like explained in iptables documentation. In general, this is a 32-bit integer (written in hex for easier understanding) with one single bit set. It is not important which bit is set, only that no other part of the netfilter uses the same bit; if it does, that will result to a big mess. NETFILTER CONFIGURATION ======================= In addition to editing the configuration file /etc/jkaptive.conf, which is explained above, you have to supply some site-specific netfilter rules to make your captive portal actually work. There is a generic setup which works if no other firewall is interfering and a special one for use with SuSEfirewall2. For other distributions you can use the script for SuSEfirewall2 at /usr/share/jkaptive/SuSEfirewall2 as a template for your own scripts. Starting with jkaptive-1.9, you can use the "ipset" netfilter framework in addition to "iptables". The use of ipset is straightforward. Instead of a private chain "jkaptive" (or whatever you named it), jkaptive will use a private set "jkaptive" to store the ip addresses of ticketed users. The netfilter rules are a little different, please see below. The SuSEfirewall2 custom script can use ipset, too. Generic setup ------------- Without ipset, create an additional chain named "jkaptive". iptables -t mangle -N jkaptive With ipset, create a hash:ip set named "jkaptive" instead. ipset create jkaptive hash:ip Activate jkaptive connection tracking: Without ipset, use iptables -t mangle -A PREROUTING -j jkaptive With ipset, use iptables -t mangle -A PREROUTING -m set --match-set jkaptive src -j MARK --set-mark 0x00200000 instead. Redirect all unticketed HTTP traffic from inside (e.g. eth0) to outside to jkaptive server. iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -m mark ! --mark 0x00200000/0x00200000 -j REDIRECT --to-ports 8088 The "jkaptive" chain name, mark bit and port number have to match the ones configured in /etc/jkaptive.conf. It's recommended to rate-limit connections to the jkaptive server. Browsers try to access a lot of websites on startup simultaneously, and this will cause hundreds of simultaneous connections to the jkaptive server, making it fork and eat up hundreds of file descriptors in a short time. Ten connections per second should be enough to let at least one subwindow display the login page. iptables -t filter -A INPUT -p tcp --dport 8088 -m state --state NEW -m recent --set iptables -t filter -A INPUT -p tcp --dport 8088 -m state --state NEW -m recent --update --seconds 5 --hitcount 50 -j DROP Only if you have an upstream gateway e.g at 192.168.1.1 which should provide DHCP/DNS (instead of the host jkaptive is running on), you need additional rules to let that traffic pass. iptables -t filter -A FORWARD -d 192.168.1.1 -j ACCEPT iptables -t filter -A FORWARD -s 192.168.1.1 -j ACCEPT If your upstream gateway is delivering DHCP, but DNS is directly given by an internet server, specify iptables -t filter -A FORWARD -p udp --dport 53 -j ACCEPT iptables -t filter -A FORWARD -p udp --sport 53 -j ACCEPT Now you can reject all other unticketed traffic from inside (e.g. eth0) to outside. iptables -t filter -A FORWARD -i eth0 -m mark ! --mark 0x00200000/0x00200000 --j REJECT --reject-with=icmp-admin-prohibited The mark has to be the same as above, of course. If you want to filter traffic originating on the host running jkaptive, e.g. for an internet terminal, you need additional rules for the output chains: without ipset: iptables -t mangle -A OUTPUT -j jkaptive with ipset: iptables -t mangle -A OUTPUT -m set --match-set jkaptive src -j MARK --set-mark 0x00200000 both: iptables -t nat -A OUTPUT -p tcp --dport 80 -m mark ! --mark 0x00200000/0x00200000 -j REDIRECT --to-ports 8088 iptables -t filter -A OUTPUT -d 127.0.0.1 -j ACCEPT iptables -t filter -A OUTPUT -m mark ! --mark 0x00200000/0x00200000 --j REJECT --reject-with=icmp-admin-prohibited Chain, mark and port have to match again, of course. Now the host itself is filtered. This creates the problem it can't issue DNS requests to the upstream gateway. If you have such an upstream gateway e.g. at 192.168.1.1, add iptables -t filter -A OUTPUT -d 192.168.1.1 -j ACCEPT *before* the REJECT line and it is fixed. If your upstream gateway is delivering DHCP, but DNS is directly given by an internet server, add iptables -t filter -A OUTPUT -p udp --dport 53 -j ACCEPT *before* the REJECT line and it is fixed. If your host is directly connected to a modem (using e.g. pppoe), both problems don't apply. jkaptive will place its individual rules like iptables -t mangle -A jkaptive -s $address -j MARK --set-mark 0x00200000 into the jkaptive chain when a token is accepted and automatically delete them once the token is expired. Please note the kernel has to support the icmp-admin-prohibited reject method, otherwise you get a plain DROP instead. SuSEfirewall2 setup ------------------- For SuSEfirewall2, the generic setup above is put in a file /usr/share/jkaptive/SuSEfirewall2. You have to edit /etc/sysconfig/SuSEfirewall2 to activate it. Set FW_CUSTOMRULES="/usr/share/jkaptive/SuSEfirewall2" to activate the custom script. Add lines FW_JKAPTIVE="yes" FW_JKAPTIVE_BACKEND="iptables" or "ipset" instead of "iptables" if you want to use that. FW_JKAPTIVE_INPUT_INTERFACES="eth0" or which input interfaces you consider to filter for forwarding. FW_JKAPTIVE_CHAIN="jkaptive" FW_JKAPTIVE_MARK="0x00200000" FW_JKAPTIVE_PORT="8088" or whatever backend, chain name, mark bit and port number you have configured in /etc/jkaptive.conf. Only if you want to directly use an upstream gateway for user's DHCP/DNS instead of the host jkaptive runs on, you need additionally: FW_JKAPTIVE_FILTER_EXCEPTIONS="192.168.1.1" or whatever your upstream gateway is. You may supply more than one address. If your upstream gateway is delivering DHCP, but DNS is directly given by an internet server, specify FW_JKAPTIVE_FILTER_DNS="no" This allows forwarded DNS traffic to and from anywhere. If you want to filter traffic originating on the host running jkaptive, e.g. for a single host setup, you additionally need to set FW_LO_NOTRACK="no" and add a line FW_JKAPTIVE_FILTER_OUTPUT="yes" Now the host itself is filtered. This creates the problem it can't issue DNS requests to the upstream gateway. If you have such an upstream gateway add a line FW_JKAPTIVE_FILTER_OUTPUT_EXCEPTIONS="192.168.1.1" or whatever your upstream gateway is. You may supply more than one address. Don't forget to restart SuSEfirewall2 after configuring it. # rcSuSEfirewall2 restart INVOCATION ========== Jkaptive is meant to be run as a daemon started by root. You can test it on root's command line with # /usr/sbin/jkaptive --loglevel 6 Log messages are always sent to syslog, with the tag "jkaptive". Messages from the built-in webserver have the additional tag "httpd". If you want to start jkaptive automatically on system start, this should be done after the netfilter/firewall setup is done. For openSUSE 12.1 and other distributions featuring systemd, a systemd script is provided. Test it with # systemctl --system daemon-reload # systemctl start jkaptive.service # systemctl status jkaptive.service Installation as a system service can be done through # systemctl enable jkaptive.service jkaptive.service is linked to the multiuser.target (similar to runlevel 3). CREATING TOKENS =============== A simple utility for generating tokens is provided. Call it with something like # /usr/bin/jkaptive-token mysalt "6 hours" to create a six-hour token valid from now or # /usr/bin/jkaptive-token mysalt 19:00 to create a token valid until 19:00 today or # /usr/bin/jkaptive-token mysalt "2012-12-31 20:00" to create a token valid until your new year's party starts. First parameter has to be the same salt as set in /etc/jkaptive.conf. The second parameter has to be either a date/time specification or distance accepted by Tcl functions [clock format] and [clock add]. See clock(n) manpage for details. From jkaptive-1.12, the token has an additional permutation step which makes it possible to have two or more users share the same expiry date. This is a number in the range 0..255. Usually this a random number but you may supply a fixed number with the --perm switch. In addition, this number is used to do an additional xor permutation on the token string, making tokens with the same expiry date appear less similar. CUSTOMIZING THE WEB PAGES ========================= The web pages provided by jkaptive at /usr/share/jkaptive/webroot can be seen as templates for your own creations. You can easily modify them or even add a whole bunch of new pages, images, scripts and styles. Modifying templates ------------------- There are six files which are served directly by jkaptive's webserver: loginpage This page is delivered when any url is given to jkaptive, e.g. when the user actually wanted to access http://sinfest.net/ tokenfailpage This page is delivered if the user supplied an invalid token, e.g. because of typing it wrong. tokenoccupiedpage This page is delivered if the user supplied a valid token which is used by another IP address, e.g. because he wanted to re-use a thrown-away token with another computer before it expired. Another reason this page shows up is because the DHCP lease expired (See CAVEATS) and the user got a new IP address and tried to re-register using the old token. tokenexpiredpage This page is delivered if the user supplied a token which has already expired, e.g. because of using some old token. error404page This page is delivered if the user supplies an URL to jkaptive that points somewhere it shouldn't. Usually that means some link on the other pages is wrong. The user should never be able to provoke this behaviour. error500page This page is delivered if something really bad happened with jkaptive. If it ever appears, prepare to file a bug report. Please note all pages but the loginpage are only delivered after the user filled out the form on the loginpage - they never appear out of sudden when a token expires or any other error occurs. All but the error pages should feature a HTML form which lets the user enter the token. This form should look like <form action="%ROOT/login" method="get"> <input type="text" name="token" size="12" maxlength="12" /> <input type="submit" value="OK" /> <input type="hidden" name="ourl" value="%OURL" /> </form> %ROOT and %OURL are special strings which are replaced by a modified request root and the original URL the user provided before his request was redirected to jkaptive. If you don't want to replace the templates, put your files into a directory and point the "webroot" configuration variable in /etc/jkaptive.conf to that directory. jkaptive's webroot directory must be readable by the user provided in its config file. Usually this means it has to be world-read/browseable. Same applies for all the files which are about to be served. Adding files ------------ If you want to have custom images, styles or scripts, or a printable PDF documentation how to obtain a token (or other fancy things) to be included into your very own captive portal, you can simply add these files into the webroot directory and use <img src="%ROOT/warning.png"> in the HTML file to point the browser to the URL where it can load the image from. The same applies for all other links. Jkaptive finds out the MIME type of the file by looking at its filename extension. The following extensions are known to it: css, gif, html, jpg, jpeg, js, pdf, png All files with unknown extension are served as text/plain. Please note it isn't possible to have a file named "login" in the webroot, as this is a special string to jkaptive triggering the login mechanism. CAVEATS ======= DNS/DHCP server --------------- Before a web browser on the user's computer tries to load any HTML page through HTTP, it issues a DNS request for the host part of the address to find out which IP address to point the HTTP request to. Jkaptive's default behaviour is to reject all forwarded traffic, even DNS and DHCP. This isn't a problem if you run a DNS/DHCP server/proxy somewhere inside the local network or the same host jkaptive runs on. But if you have an upstream gateway providing this, e.g. a box given by your telco, it may also serve as the DNS/DHCP server in your network. In that case, you have to place additional netfilter rules (see NETFILTER CONFIGURATION) to allow forwarded traffic to that box. To make it even more complicated, there are telco boxes out there which work as a router and DHCP server, but not as a local DNS repeater. In that case, the box will tell the user's computers a DNS server in the internet via DHCP. As it may change at the will of the telco, there is no other chance but to allow DNS traffic to and from all adresses. See NETFILTER CONFIGURATION again. DHCP lease time --------------- As all the filtering is done with ip addresses, is has to be made sure DHCP always gives the same IP address to the same computer as long the token has not expired. If you don't honor this, users may not use their full time of free internet access. With ISC dhcpd, this is done through /etc/dhcpd.conf default-lease-time max-lease-time min-lease-time You have to specify a default lease time higher than the expiry time of the longest jkaptive token you have. Usually a setting of 86400 (one day) is ok. Specifing max and min lease times may be needed, too. With the built-in dhcpd of dnsmasq, this is done on the command line through the -F/--dhcp-range option. See the manpage of dnsmasq and your distribution's documentation on how dnsmasq is embedded in it. Packet marking -------------- Make sure the packet tracking mark used by jkaptive (configured in /etc/jkaptive.conf) is used by jkaptive exclusively. The netfilter/firewall code already present on your machine may accidentally use the same bit, causing a lot of random glitches in conjunction with jkaptive. So before activating jkaptive, list the mangle rules of your machine for lines having a "MARK" target iptables -t mangle -nvL | grep MARK and make sure that the bit used by jkaptive is not used by any of them. DHCP client (openSUSE specific) ------------------------------- There is a known glitch when the host jkaptive runs on get its own ip address via DHCP: as soon the lease expires, the network is reconfigured and SuSEfirewall2 is restarted, flushing all rules in the jkaptive chain. Users have to relogin with their old tokens after that. If you want to avoid this glitch, configure the host jkaptive runs on with a fixed IP address. BUGS ==== All known ones hunted down so far. Please report any new bugs or difficulties to the author.