A 'captive portal' system uses a web browser to authenticate a user. This avoids the need for custom software or setups on client systems that want to access a network.
The gateway server used is running OpenBSD because it has all the neccessary components.
When a client connects to the network, they have access to the local LAN and can auto configure their computer using DHCP. This is the default with most laptops. Their access outside the LAN is blocked at the gateway. Any attempt to access outside the LAN using port 80 (HTTP) is redirected to the web server on the gateway. The gateway 'captures' attempts to access the Internet from the client.
The webserver on the gateway responds to all HTTP requests by putting up a login screen. When the client authenticates to the webserver, their IP address is added to the list of addresses that are not to be redirected. The client then has access to the Internet.
In this example, the reserved network 192.168.3.0/24 was chosen for the internal network. This is bound to interface 'dc1'.
inet 192.168.3.1 255.255.255.0 192.168.3.255
The external network uses 10.0.0.136 and is bound to interface 'dc0'.
inet 10.0.0.136 255.0.0.0 10.255.255.255
In /etc/sysctl.conf uncomment the line:
net.inet.ip.forwarding=1 # 1=Permit forwarding (routing) of IPv4 packets
The default gateway for the gateway server is set to the ADSL router:
10.0.0.138
The ADSL router will need to be told to route your private network to the gateway server. You'll need to set up an entry in the routing table of the ADSL router.
The gateway server does not do NAT, so if you want NAT (you will if you're using reserved network addresses) you'll need to configure the ADSL router to do NAT. That's normally the default for ADSL routers.
The gateway server blocks unathenticated traffic and redirects unathenticated http requests to the authentication server.
With pf this simply requires /etc/pf.conf:
ext_if="dc0" int_if="dc1" table <registered-host> persist set skip on lo set loginterface $int_if scrub in all rdr pass on $int_if proto tcp from ! <registered-host> to port www -> 127.0.0.1 port www block in all antispoof quick for $int_if inet pass in proto icmp pass proto {udp, tcp} to port domain keep state pass in proto {udp, tcp} to port bootpc keep state pass in proto {udp, tcp} to port bootps keep state pass proto tcp from any to 192.168.3.1 port http keep state pass proto tcp from any to 192.168.3.1 port https keep state pass log (all) from <registered-host> keep state pass out from $ext_if keep state
This uses a table name 'registered-host' to keep a list of authenticated clients.
All traffic from registered hosts is logged for accounting.
Security note: This setup doesn't prevent setting up a IP over DNS tunnel.
Access to the pf table that is used to control network access requires
using pfctl
, which needs root privilege. The way this is
enabled for the CGI or whatever is used by the portal application is
to have sudo
allow the user that the webserver runs as
to execute pfctl
as root without a password.
www localhost=NOPASSWD: /sbin/pfctl *
The gateway server runs a local nameserver to service the local network. This is configured to forward queries to the nameservers provided by the ISP. It also has entries for the local network, most critically for itself.
acl clients { localnets; ::1; }; options { version ""; // remove this to allow version queries listen-on { any; }; listen-on-v6 { any; }; allow-recursion { clients; }; forward first; forwarders { 202.27.158.40; 202.27.156.72; }; };
Also add entries for your local network hosts.
zone "go.net" { type master; file "master/go.net"; allow-transfer { localhost; 10.0.0.129; }; }; zone "3.168.192.in-addr.arpa" { type master; file "master/192.168.3.rev"; };
$TTL 86400 go.net. IN SOA wag.go.net. philip.go.net. ( 12 7200 3600 604800 86400 ) IN NS wag.go.net. IN MX 10 wag.go.net. wag IN A 192.168.3.1
$ORIGIN 3.168.192.in-addr.arpa. $TTL 6h @ IN SOA wag.go.net. philip.go.net. ( 3 ; Serial 3600 ; Refresh 900 ; Retry 3600000 ; Expire 3600 ) ; Minimum IN NS wag.go.net. 1 IN PTR wag.go.net.
option domain-name "go.net"; option domain-name-servers 192.168.3.1; subnet 192.168.3.0 netmask 255.255.255.0 { option routers 192.168.3.1; range 192.168.3.129 192.168.3.254; }
The basic httpd.conf for the webserver is altered as follows:
ErrorDocument 404 /404.html ErrorDocument 401 /401.html ErrorDocument 500 /500.html
The error documents are links to the login screen, so requests for any document get served up the login screen.
RewriteEngine on RewriteCond %{HTTP_HOST} !^wag\.go\.net [NC] RewriteRule ^.*$ https://wag.go.net/ [L,R]
The rewrite is the magic bit. It takes any request that is not for the gateway and rewrites it to go to the gateway. It also uses https so that the logins are served up over an encrypted link so that logins cannot be easily sniffed off the network. This is particularly useful for wireless networks.
The rest of the setup is left as an exercise for the student. It could be a simple HTTP basic auth and a CGI to update the access list, or it could be a full portal application.