Building a Captive Portal – controlling access to the internet from your network
What is a captive portal?
Wikipedia says: “The captive portal technique forces an HTTP client on a network to see a special web page (usually for authentication purposes) before using the Internet normally. A captive portal turns a Web browser into an authentication device. This is done by intercepting all packets, regardless of address or port, until the user opens a browser and tries to access the Internet.”
Basically, when accessing a network (in most cases a WIFI network), a captive portal will block any traffic (to for instance the internet) as long as the client did not go through a predefined workflow. That workflow begins when the user opens a web browser, and via that same browser the client is required to for instance:
- authenticate itself
- accept terms
- pay fees
- etc.
In this post, I will show you how you can build this kind of solution for your own network using several open source tools, primarily CoovaChilli.
Components
In order to build a Captive Portal solution, you will need several open source components, that need to work together, the diagram below illustrates this:
- An access controller: CoovaChilli – is a feature rich software access controller that provides a captive portal / walled-garden environment
- A radius server for provisioning and accounting: Freeradius – handles authentication and accounting (
rlm_jradius
module is required) - A library to implement the business logic: JRadius – an open-source Java RADIUS client and server framework, which helps you to implement RADIUS authentication and accounting in your Java application
- A database: MySQL – backing the Radius server
- A web server: Apache HTTPD – proxies request to tomcat
- An application server: Tomcat – for hosting the Captive Portal application
Interactions
When a client connects to an open WIFI network, an IP address is assigned (CoovaChilli comes with its owns DHCP server). Then, when the client opens a browser and tries to open a URL (e.g. http://www.google.com
) CoovaChilli will intercept that and redirect the client to the Captive Portal application which is hosted on Tomcat (in our case it is a Spring MVC application). In order to restrict all access to the outside world, CoovaChilli will add several iptable
rules to block all ports (no trafic is possible to any service) except CoovaChilli UAM port (DNS port, …).
When the client logs in via the Captive Portal application, CoovaChilli JSON will request an encrypted password (to an UAM service via HTTPS hosted in Tomcat) by sending an access request to the Freeradius server:
ChilliSpot-Version = 1.2.9 User-Name = myuser User-Password = [Encrypted String] Service-Type = Login-User Acct-Session-Id = 50ed34fb00000003 Framed-IP-Address = 172.50.70.173 NAS-Port-Type = Wireless-802.11 NAS-Port = 3 NAS-Port-Id = 00000003 Calling-Station-Id = 00-16-6F-69-8B-B7 Called-Station-Id = 00-0C-29-19-D9-D0 NAS-IP-Address = 172.50.70.1 NAS-Identifier = nas01 WISPr-Location-ID = isocc=,cc=,ac=,network= WISPr-Location-Name = MY_WLAN WISPr-Logoff-URL = http://172.50.70.1:3990/logoff Message-Authenticator = [Binary Data (length=16)]
When Freeradius receives the message, it will look in its configuration to determinate which module should handle it. In our case, we have set up JRadius to handle these requests, so Freeradius will forward the request to JRadius. In our case, we have configured our own Java classes in JRadius that talk to an external system to perform the authentication. But obviously, this could be any kind of authentication using whatever mechanism (e.g. LDAP and database). If the authentication is successful, an access accept
response is sent back to CoovaChilli. If not, an access reject
response is sent back. The response contains a reply message that will be passed on via CoovaChilli to the Captive Portal application. Freeradius will ensure that the result of this transaction is saved to an authentication log.
Configuration
This section will describe and list some of the specific configuration you need to do in order to build this setup.
CoovaChilli
CoovaChilli needs two network interface, we choose eth0 and eth1.
- eth0: The WAN interface that connect to the internet
- eth1: The LAN interface to which client connect
When started, CoovaChilli will create a new virtual interface tun0, a tunnel interface to eth1. Furthermore, CoovaChilli needs some extra settings: the hotspot IP address, the UAM port (default is 3990), the DNS server(s) and the Freeradius server (IP address, port, secret).
Next to the settings above, the one of interest to us next is the location of the Captive Portal page, CoovaChilli expects the uamserver
setting to be set to the URL of that page. In our case it is the URL of the login application (e.g. http://hotspot.trifork.nl/logon
).
Note that we use HTTP and not HTTPS, the clear password is sent by CoovaChilli JSON to the UAM service via HTTPS (e.g. https://hotspot.trifork.nl/uam.js
), while the service returns an encryted password (using the challenge passed by CoovaChilli).
Freeradius
Freeradius has quite a few configuration settings, I will just mention the JRadius bit, as the rest can be easily understood. The rlm_jradius
configuration is needed this can be added directly in radiusd.conf
:
jradius { name = "trifork" primary = "localhost" timeout = 1 onfail = NOOP keepalive = yes connections = 8 allow_codechange = yes }
Basically this configuration says that JRadius runs locally and on the default port.
The connection between a request type and the module that will handle it can be added in the default server (/etc/raddb/sites-enabled/default
):
authorize { preprocess jradius { fail = return } suffix files daily } authenticate { jradius }
Each module you specify in a section will handle the request. The request is handled in the order in which the modules are listed. What is important is the presence of the jradius
module we need in order to get our business logic to be triggered. of course there are a lot of other settings, but those are out-of-scope for this blog entry.
JRadius
JRadius is a daemon written in Java, which listens to requests on a port (default 1814). The rlm_jradius
freeradius module sends these requests and takes care of the responses. Extract the JRadius archive somewhere and open the jradius-config.xml
config file. We need to add some configuration in order for our classes to be triggered when a given request type comes in. We first add a packet-handler under the packet-handlers node:
<packet-handler name="MyAuth"> <description>Custom authorization</description> <class>nl.trifork.jradius.handler.MyAuthorizationHandler</class> </packet-handler>
And we add this handler in the existing FreeRadiusListener
:
<packet-handler type="authorize" handler="MyAuth"/>
This means that authorization requests (packets) will be processed by our packet handler. The handler is a Java class extending net.jradius.handler.PacketHandlerBase
. You need to override the handle()
method and set the reply packet of the JRadiusRequest
to AccessAccept
or AccessReject
. Make sure to package this class in a JAR file and put it in the lib directory of JRadius.
Tomcat
Last, you need a web application that will display the login page and send the authentication to CoovaChilli. You can start with a simple HTML form that will actually not be posted to the backend because everything will happen in Javascript thanks to CoovaChilli JSON. You need to include the ChilliLibrary.js in the HEAD section of your form page. That Javascript file will give you access to a Javascript object: chilliController
It needs to be initialized:
chilliController.host = "172.50.70.1"; chilliController.port = "3990"; chilliController.uamService = "https://hotspot.trifork.nl/uam.js"; chilliController.onUpdate = updateUI;
When clicking the login button, we call a javascript function:
function connect() { var username = document.getElementById('username').value ; var password = document.getElementById('password').value ; if (username == null || username == '') { showError(); } showWaitPage(1000); chilliController.logon( username , password ) ; }
This function just calls the login method of the chilliController
, that will first use uamService
to get an encrypted password and then send the credentials to CoovaChilli.
You also need to define an updateUI
function that will be called after logon is done.
That function will check the value of chilliController.clientState
and display an error message or a success message.
function updateUI (cmd ) { if ( chilliController.clientState == 0 ) { showErrorPage(); } if ( chilliController.clientState == 1 ) { showSuccessPage(); } }
You can add a logout button that calls the function below in order to log off the user.
function disconnect() { if (confirm('Really?')) { chilliController.logoff(); } return false; }
Conclusion
It can become quite a challenge when integrating with other systems. I didn’t mention accounting but you can also integrate accounting via JRadius, for example if you need to update a user balance on an external system (time spend, data downloaded,…). You can also setup a secure connection between the client and the access point (WPA) allow to be logged in as guest and then still present the user with the captive portal login page. All in all hope this helps you build the Captive Portal you need.