Setting up a Captive Portal Site with Web Filtering


To set up a cheap captive portal site with web filtering using Squid and PF


While there are any number of captive portal solutions out there, any one of which would probably be easier than this solution, I also wanted to be able to filter the web traffic, which none of the pre-made solutions seemed able to do. There are commercial solutions that would allow this setup easily using a cisco router and web-filtering server, but these were too pricy.


  1. Any wireless router/access point with a DHCP server (most, if not all have this). I used a linksys WRT310N running DD-WRT during testing.
  2. A computer to serve as a gateway with Squid, Squidguard, PF, and a webserver installed. These instructions were worked up using an OpenBSD machine running the Apache web server, but should be easily extensible to any platform. No special hardware is required. You will also, obviously, need an internet connection. This must be provided with a separate router - you can't just use the wireless function on your main router and expect this to work (I don't think)


The wireless access point uses the computer as the gateway. PF on the computer forwards all web requests to Squid, and blocks all other traffic. Squid "blocks" all web traffic using an "external" custom designed error page/landing page. When the end users accepts the terms of usage, the webpage adds their ip to an allowed list, which squid and pf see and start allowing traffic as normal. Squid filters all web traffic using squidguard.


I do not claim that this is the only, or even the best, way of accomplishing this goal. It is simply what worked for me. Your results may vary. If you have any feedback, please leave me a comment.


1) Set up the wireless router as a standard access point, using the address of the computer as the gateway address.

2) Set up the "gateway" computer.

This machine needs to be connected such that it can access the internet normally, and can be accessed by the wireless access point. Permissions on /dev/pf need to be set so that the webserver can write to it (so your landing page can update the allowed IP list). The cheater method is to simply allow anyone to write to this, but there are probably better ways. Additionally on OpenBSD the value of net.inet.ip.forwarding needs to be set to 1 in the /etc/sysctl.conf file

3) Set up a SQLite database to hold the allowed ip list for Squid. This can be accomplished with a command like the following:

sqlite3 /etc/squid/authIP.db "CREATE TABLE users (ip,time)"

The database can be located wherever you want it to be, and of course, you can get more fancy with your create table definition, such as declaring time as a timestamp, or ip to be unique etc. However this basic definition worked for me. The important thing is that you have at least two columns, one for the authorized IP’s and one for a timestamp. NOTE: your webserver will need to be able to write to this file, so make sure you set permissions accordingly.

4) Create a "" script.

This script needs to read the std_in to get the ip address, compare it against the database, and return (on the std_out) either "OK" or "ERR" depending on if the IP is in the allowed list or not. It will be used by squid to check users against the allowed IP list. You can see my version here.

5) Set up squidguard. See

6) Set up Squid (/etc/squid/squid.conf) with these options.

Using these settings, squid will use an “external ACL” to authorize IP’s for general browsing. Any unauthorized IP’s will be redirected to your custom landing page (see step 8). Authorized IP’s will have their traffic filtered.

7) Configure PF on the computer with these rules.

PF is set to block all traffic by default, except for traffic destined for the local machine (i.e. requests for your landing page). Access is restricted by a table, which has authorized IP’s added to it by your landing page (see next step)

8) Set up your landing page. You can do whatever you want here, but the important part is that when the user fulfills your requirements for access, you need to do three things:

  1. add an entry in the above database with their IP and the current timestamp,
  2. use the pfctl command line utility to add their IP to the PF table, as such:
    pfctl -t authIP -T add <client_IP>
  3. send them somewhere else. This can be your corporate home page, some random site (like google) or, ultimately, whatever page they first attempted to access. I don't know how to do that last yet, but I'm sure Squid can pass the requested page to the error page somehow :-)

I used a python script to accomplish this, using the sqlite3 module to update the SQLite database (see Google) and the call command from the subprocess module to run the pfctl command. You can use whatever web scripting language you feel most comfortable in.

At this point everything should be working. New users connecting to your access point should get the landing page- and only the landing page. Once they have accepted the usage agreement, they should then be allowed unrestricted access to the internet, aside from the filtering being provided by squidguard. Usually, however, you want this access to be for a limited time only, so you need one more step:

9) Set up a script to be run through cron to remove expired IP's

The basic procedure here is to run the script every few minutes or so (depending on how granular you want the access time restrictions to be) that generates a list of all IP addresses that have been allowed for longer than your limit and removes them from both the database and the PF table.

And that should do it. At this point you should have a fully-functional public portal, with web filtering being provided by Squid. You'll probably also want to update your squidguard blacklist files on occasion, but that is left as an exercise for the reader. Let me know if I missed anything!

Israel Brewster 2011-2016