summaryrefslogtreecommitdiffstats
path: root/etc/inc/captiveportal.inc
diff options
context:
space:
mode:
Diffstat (limited to 'etc/inc/captiveportal.inc')
-rw-r--r--etc/inc/captiveportal.inc642
1 files changed, 642 insertions, 0 deletions
diff --git a/etc/inc/captiveportal.inc b/etc/inc/captiveportal.inc
new file mode 100644
index 0000000..d5d78b1
--- /dev/null
+++ b/etc/inc/captiveportal.inc
@@ -0,0 +1,642 @@
+<?php
+/*
+ captiveportal.inc
+ part of m0n0wall (http://m0n0.ch/wall)
+
+ Copyright (C) 2003-2004 Manuel Kasper <mk@neon1.net>.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* include all configuration functions */
+require_once("functions.inc");
+require_once("radius_accounting.inc") ;
+
+function captiveportal_configure() {
+ global $config, $g;
+
+ if (isset($config['captiveportal']['enable']) &&
+ (($config['captiveportal']['interface'] == "lan") ||
+ isset($config['interfaces'][$config['captiveportal']['interface']]['enable']))) {
+
+ if ($g['booting'])
+ echo "Starting captive portal... ";
+
+ /* kill any running mini_httpd */
+ killbypid("{$g['varrun_path']}/mini_httpd.cp.pid");
+ killbypid("{$g['varrun_path']}/mini_httpd.cps.pid");
+
+ /* kill any running minicron */
+ killbypid("{$g['varrun_path']}/minicron.pid");
+
+ /* generate ipfw rules */
+ $cprules = captiveportal_rules_generate();
+
+ /* make sure ipfw is loaded */
+ mwexec("/sbin/kldload ipfw");
+
+ /* stop accounting on all clients */
+ captiveportal_radius_stop_all() ;
+
+ /* remove old information */
+ unlink_if_exists("{$g['vardb_path']}/captiveportal.nextrule");
+ unlink_if_exists("{$g['vardb_path']}/captiveportal.db");
+ unlink_if_exists("{$g['vardb_path']}/captiveportal_mac.db");
+ unlink_if_exists("{$g['vardb_path']}/captiveportal_ip.db");
+ unlink_if_exists("{$g['vardb_path']}/captiveportal_radius.db");
+
+ /* write portal page */
+ if ($config['captiveportal']['page']['htmltext'])
+ $htmltext = base64_decode($config['captiveportal']['page']['htmltext']);
+ else {
+ /* example/template page */
+ $htmltext = <<<EOD
+<html>
+<head>
+<title>m0n0wall captive portal</title>
+</head>
+<body>
+<h2>m0n0wall captive portal</h2>
+<p>This is the default captive portal page. Please upload your own custom HTML file on the <em>Services: Captive portal</em> screen in the m0n0wall webGUI.</p>
+<form method="post" action="">
+ <input name="accept" type="submit" value="Continue">
+</form>
+</body>
+</html>
+
+EOD;
+ }
+
+ $fd = @fopen("{$g['varetc_path']}/captiveportal.html", "w");
+ if ($fd) {
+ fwrite($fd, $htmltext);
+ fclose($fd);
+ }
+
+ /* write error page */
+ if ($config['captiveportal']['page']['errtext'])
+ $errtext = base64_decode($config['captiveportal']['page']['errtext']);
+ else {
+ /* example page */
+ $errtext = <<<EOD
+<html>
+<head>
+<title>Authentication error</title>
+</head>
+<body>
+<font color="#cc0000"><h2>Authentication error</h2></font>
+<b>
+Username and/or password invalid.
+<br><br>
+<a href="javascript:history.back()">Go back</a>
+</b>
+</body>
+</html>
+
+EOD;
+ }
+
+ $fd = @fopen("{$g['varetc_path']}/captiveportal-error.html", "w");
+ if ($fd) {
+ fwrite($fd, $errtext);
+ fclose($fd);
+ }
+
+ /* load rules */
+ mwexec("/sbin/ipfw -f delete set 1");
+ mwexec("/sbin/ipfw -f delete set 2");
+ mwexec("/sbin/ipfw -f delete set 3");
+
+ /* XXX - seems like ipfw cannot accept rules directly on stdin,
+ so we have to write them to a temporary file first */
+ $fd = @fopen("{$g['tmp_path']}/ipfw.cp.rules", "w");
+ if (!$fd) {
+ printf("Cannot open ipfw.cp.rules in captiveportal_configure()\n");
+ return 1;
+ }
+
+ fwrite($fd, $cprules);
+ fclose($fd);
+
+ mwexec("/sbin/ipfw {$g['tmp_path']}/ipfw.cp.rules");
+
+ unlink("{$g['tmp_path']}/ipfw.cp.rules");
+
+ /* filter on layer2 as well so we can check MAC addresses */
+ mwexec("/sbin/sysctl net.link.ether.ipfw=1");
+
+ chdir($g['captiveportal_path']);
+
+ /* start web server */
+ mwexec("/usr/local/sbin/mini_httpd -a -M 0 -u root -maxproc 16" .
+ " -p 8000 -i {$g['varrun_path']}/mini_httpd.cp.pid");
+
+ /* fire up another one for HTTPS if requested */
+ if (isset($config['captiveportal']['httpslogin']) &&
+ $config['captiveportal']['certificate'] && $config['captiveportal']['private-key']) {
+
+ $cert = base64_decode($config['captiveportal']['certificate']);
+ $key = base64_decode($config['captiveportal']['private-key']);
+
+ $fd = fopen("{$g['varetc_path']}/cert-portal.pem", "w");
+ if (!$fd) {
+ printf("Error: cannot open cert-portal.pem in system_webgui_start().\n");
+ return 1;
+ }
+ chmod("{$g['varetc_path']}/cert-portal.pem", 0600);
+ fwrite($fd, $cert);
+ fwrite($fd, "\n");
+ fwrite($fd, $key);
+ fclose($fd);
+
+ mwexec("/usr/local/sbin/mini_httpd -S -a -M 0 -E {$g['varetc_path']}/cert-portal.pem" .
+ " -u root -maxproc 16 -p 8001" .
+ " -i {$g['varrun_path']}/mini_httpd.cps.pid");
+ }
+
+ /* start pruning process (interval = 60 seconds) */
+ mwexec("/usr/local/bin/minicron 60 {$g['varrun_path']}/minicron.pid " .
+ "/etc/rc.prunecaptiveportal");
+
+ /* generate passthru mac database */
+ captiveportal_passthrumac_configure() ;
+ /* create allowed ip database and insert ipfw rules to make it so */
+ captiveportal_allowedip_configure() ;
+
+ /* generate radius server database */
+ if($config['captiveportal']['radiusip']) {
+ $radiusip = $config['captiveportal']['radiusip'] ;
+
+ if($config['captiveportal']['radiusport'])
+ $radiusport = $config['captiveportal']['radiusport'] ;
+ else
+ $radiusport = 1812;
+
+ if($config['captiveportal']['radiusacctport'])
+ $radiusacctport = $config['captiveportal']['radiusacctport'] ;
+ else
+ $radiusacctport = 1813;
+
+ $radiuskey = $config['captiveportal']['radiuskey'];
+
+ $fd = @fopen("{$g['vardb_path']}/captiveportal_radius.db", "w");
+ if (!$fd) {
+ printf("Error: cannot open radius DB file in captiveportal_configure().\n");
+ return 1;
+ } else {
+ fwrite($fd,$radiusip . "," . $radiusport . "," . $radiusacctport . "," . $radiuskey) ;
+ }
+ fclose($fd) ;
+ }
+
+
+ if ($g['booting'])
+ echo "done\n";
+
+ } else {
+ killbypid("{$g['varrun_path']}/mini_httpd.cp.pid");
+ killbypid("{$g['varrun_path']}/minicron.pid");
+ captiveportal_radius_stop_all() ;
+ mwexec("/sbin/sysctl net.link.ether.ipfw=0");
+ if (!isset($config['shaper']['enable'])) {
+ /* unload ipfw */
+ mwexec("/sbin/kldunload ipfw");
+ } else {
+ /* shaper is on - just remove our rules */
+ mwexec("/sbin/ipfw -f delete set 1");
+ mwexec("/sbin/ipfw -f delete set 2");
+ mwexec("/sbin/ipfw -f delete set 3");
+ }
+ }
+
+ return 0;
+}
+
+function captiveportal_rules_generate() {
+ global $config, $g;
+
+ $cpifn = $config['captiveportal']['interface'];
+ $cpif = $config['interfaces'][$cpifn]['if'];
+ $cpip = $config['interfaces'][$cpifn]['ipaddr'];
+
+ /* note: the captive portal daemon inserts all pass rules for authenticated
+ clients as skipto 50000 rules to make traffic shaping work */
+
+ $cprules = "";
+
+ /* captive portal on LAN interface? */
+ if ($cpifn == "lan") {
+ /* add anti-lockout rules */
+ $cprules .= <<<EOD
+add 500 set 1 pass all from $cpip to any out via $cpif
+add 501 set 1 pass all from any to $cpip in via $cpif
+
+EOD;
+ }
+
+ $cprules .= <<<EOD
+# skip to traffic shaper if not on captive portal interface
+add 1000 set 1 skipto 50000 all from any to any not layer2 not via $cpif
+# pass all layer2 traffic on other interfaces
+add 1001 set 1 pass layer2 not via $cpif
+
+# layer 2: pass ARP
+add 1100 set 1 pass layer2 mac-type arp
+# layer 2: block anything else non-IP
+add 1101 set 1 deny layer2 not mac-type ip
+# layer 2: check if MAC addresses of authenticated clients are correct
+add 1102 set 1 skipto 20000 layer2
+
+# allow access to our DHCP server (which needs to be able to ping clients as well)
+add 1200 set 1 pass udp from any 68 to 255.255.255.255 67 in
+add 1201 set 1 pass udp from any 68 to $cpip 67 in
+add 1202 set 1 pass udp from $cpip 67 to any 68 out
+add 1203 set 1 pass icmp from $cpip to any out icmptype 8
+add 1204 set 1 pass icmp from any to $cpip in icmptype 0
+
+# allow access to our DNS forwarder
+add 1300 set 1 pass udp from any to $cpip 53 in
+add 1301 set 1 pass udp from $cpip 53 to any out
+
+# allow access to our web server
+add 1302 set 1 pass tcp from any to $cpip 8000 in
+add 1303 set 1 pass tcp from $cpip 8000 to any out
+
+EOD;
+
+ if (isset($config['captiveportal']['httpslogin'])) {
+ $cprules .= <<<EOD
+add 1304 set 1 pass tcp from any to $cpip 8001 in
+add 1305 set 1 pass tcp from $cpip 8001 to any out
+
+EOD;
+ }
+
+ $cprules .= <<<EOD
+
+# ... 10000-19899: rules per authenticated client go here...
+
+# redirect non-authenticated clients to captive portal
+add 19900 set 1 fwd 127.0.0.1,8000 tcp from any to any 80 in
+# let the responses from the captive portal web server back out
+add 19901 set 1 pass tcp from any 80 to any out
+# block everything else
+add 19902 set 1 deny all from any to any
+
+# ... 20000-29899: layer2 block rules per authenticated client go here...
+
+# pass everything else on layer2
+add 29900 set 1 pass all from any to any layer2
+
+EOD;
+
+ return $cprules;
+}
+
+/* remove clients that have been around for longer than the specified amount of time */
+/* db file structure: timestamp,ipfw_rule_no,clientip,clientmac,username,sessionid */
+function captiveportal_prune_old() {
+
+ global $g, $config;
+
+ /* check for expired entries */
+ if ($config['captiveportal']['timeout'])
+ $timeout = $config['captiveportal']['timeout'] * 60;
+ else
+ $timeout = 0;
+
+ if ($config['captiveportal']['idletimeout'])
+ $idletimeout = $config['captiveportal']['idletimeout'] * 60;
+ else
+ $idletimeout = 0;
+
+ if (!$timeout && !$idletimeout)
+ return;
+
+ captiveportal_lock();
+
+ /* read database */
+ $cpdb = captiveportal_read_db();
+
+ $radiusservers = captiveportal_get_radius_servers();
+
+ for ($i = 0; $i < count($cpdb); $i++) {
+
+ $timedout = false;
+
+ /* hard timeout? */
+ if ($timeout) {
+ if ((time() - $cpdb[$i][0]) >= $timeout)
+ $timedout = true;
+ }
+
+ /* if an idle timeout is specified, get last activity timestamp from ipfw */
+ if (!$timedout && $idletimeout) {
+ $lastact = captiveportal_get_last_activity($cpdb[$i][1]);
+ if ($lastact && ((time() - $lastact) >= $idletimeout))
+ $timedout = true;
+ }
+
+ if ($timedout) {
+ /* this client needs to be deleted - remove ipfw rules */
+ if (isset($config['captiveportal']['radacct_enable']) && isset($radiusservers[0])) {
+ RADIUS_ACCOUNTING_STOP($cpdb[$i][1], // ruleno
+ $cpdb[$i][4], // username
+ $cpdb[$i][5], // sessionid
+ $cpdb[$i][0], // start time
+ $radiusservers[0]['ipaddr'],
+ $radiusservers[0]['acctport'],
+ $radiusservers[0]['key']);
+ }
+ mwexec("/sbin/ipfw delete " . $cpdb[$i][1] . " " . ($cpdb[$i][1]+10000));
+ unset($cpdb[$i]);
+ }
+ }
+
+ /* write database */
+ captiveportal_write_db($cpdb);
+
+ captiveportal_unlock();
+}
+
+/* remove a single client by ipfw rule number */
+function captiveportal_disconnect_client($id) {
+
+ global $g, $config;
+
+ captiveportal_lock();
+
+ /* read database */
+ $cpdb = captiveportal_read_db();
+ $radiusservers = captiveportal_get_radius_servers();
+
+ /* find entry */
+ for ($i = 0; $i < count($cpdb); $i++) {
+ if ($cpdb[$i][1] == $id) {
+ /* this client needs to be deleted - remove ipfw rules */
+ if (isset($config['captiveportal']['radacct_enable']) && isset($radiusservers[0])) {
+ RADIUS_ACCOUNTING_STOP($cpdb[$i][1], // ruleno
+ $cpdb[$i][4], // username
+ $cpdb[$i][5], // sessionid
+ $cpdb[$i][0], // start time
+ $radiusservers[0]['ipaddr'],
+ $radiusservers[0]['acctport'],
+ $radiusservers[0]['key']);
+ }
+ mwexec("/sbin/ipfw delete " . $cpdb[$i][1] . " " . ($cpdb[$i][1]+10000));
+ unset($cpdb[$i]);
+ break;
+ }
+ }
+
+ /* write database */
+ captiveportal_write_db($cpdb);
+
+ captiveportal_unlock();
+}
+
+/* send RADIUS acct stop for all current clients */
+function captiveportal_radius_stop_all() {
+ global $g, $config;
+
+ captiveportal_lock() ;
+ $cpdb = captiveportal_read_db() ;
+
+ $radiusservers = captiveportal_get_radius_servers();
+
+ if (isset($radiusservers[0])) {
+ for ($i = 0; $i < count($cpdb); $i++) {
+ RADIUS_ACCOUNTING_STOP($cpdb[$i][1], // ruleno
+ $cpdb[$i][4], // username
+ $cpdb[$i][5], // sessionid
+ $cpdb[$i][0], // start time
+ $radiusservers[0]['ipaddr'],
+ $radiusservers[0]['acctport'],
+ $radiusservers[0]['key']);
+ }
+ }
+ captiveportal_unlock() ;
+}
+
+function captiveportal_passthrumac_configure() {
+ global $config, $g;
+
+ /* clear out passthru macs, if necessary */
+ if (file_exists("{$g['vardb_path']}/captiveportal_mac.db")) {
+ unlink("{$g['vardb_path']}/captiveportal_mac.db");
+ }
+
+ if (is_array($config['captiveportal']['passthrumac'])) {
+
+ $fd = @fopen("{$g['vardb_path']}/captiveportal_mac.db", "w");
+ if (!$fd) {
+ printf("Error: cannot open passthru mac DB file in captiveportal_passthrumac_configure().\n");
+ return 1;
+ }
+
+ foreach ($config['captiveportal']['passthrumac'] as $macent) {
+ /* record passthru mac so it can be recognized and let thru */
+ fwrite($fd, $macent['mac'] . "\n");
+ }
+
+ fclose($fd);
+ }
+
+ return 0;
+}
+
+function captiveportal_allowedip_configure() {
+ global $config, $g;
+
+ captiveportal_lock() ;
+
+ /* clear out existing allowed ips, if necessary */
+ if (file_exists("{$g['vardb_path']}/captiveportal_ip.db")) {
+ $fd = @fopen("{$g['vardb_path']}/captiveportal_ip.db", "r");
+ if ($fd) {
+ while (!feof($fd)) {
+ $line = trim(fgets($fd));
+ if($line) {
+ list($ip,$rule) = explode(",",$line);
+ mwexec("/sbin/ipfw delete $rule") ;
+ }
+ }
+ }
+ fclose($fd) ;
+ unlink("{$g['vardb_path']}/captiveportal_ip.db");
+ }
+
+ /* get next ipfw rule number */
+ if (file_exists("{$g['vardb_path']}/captiveportal.nextrule"))
+ $ruleno = trim(file_get_contents("{$g['vardb_path']}/captiveportal.nextrule"));
+ if (!$ruleno)
+ $ruleno = 10000; /* first rule number */
+
+ if (is_array($config['captiveportal']['allowedip'])) {
+
+ $fd = @fopen("{$g['vardb_path']}/captiveportal_ip.db", "w");
+ if (!$fd) {
+ printf("Error: cannot open allowed ip DB file in captiveportal_allowedip_configure().\n");
+ captiveportal_unlock() ;
+ return 1;
+ }
+
+ foreach ($config['captiveportal']['allowedip'] as $ipent) {
+ /* record allowed ip so it can be recognized and removed later */
+ fwrite($fd, $ipent['ip'] . "," . $ruleno ."\n");
+ /* insert ipfw rule to allow ip thru */
+ if($ipent['dir'] == "from") {
+ mwexec("/sbin/ipfw add $ruleno set 2 skipto 50000 ip from ".$ipent['ip']." to any in") ;
+ mwexec("/sbin/ipfw add $ruleno set 2 skipto 50000 ip from any to ".$ipent['ip']." out") ;
+ } else {
+ mwexec("/sbin/ipfw add $ruleno set 2 skipto 50000 ip from any to ".$ipent['ip']." in") ;
+ mwexec("/sbin/ipfw add $ruleno set 2 skipto 50000 ip from ".$ipent['ip']." to any out") ;
+ }
+ $ruleno++ ;
+ if ($ruleno > 19899)
+ $ruleno = 10000;
+ }
+
+ fclose($fd);
+
+ /* write next rule number */
+ $fd = @fopen("{$g['vardb_path']}/captiveportal.nextrule", "w");
+ if ($fd) {
+ fwrite($fd, $ruleno);
+ fclose($fd);
+ }
+ }
+
+ captiveportal_unlock() ;
+ return 0;
+}
+
+/* get last activity timestamp given ipfw rule number */
+function captiveportal_get_last_activity($ruleno) {
+
+ exec("/sbin/ipfw -T list {$ruleno} 2>/dev/null", $ipfwoutput);
+
+ /* in */
+ if ($ipfwoutput[0]) {
+ $ri = explode(" ", $ipfwoutput[0]);
+ if ($ri[1])
+ return $ri[1];
+ }
+
+ return 0;
+}
+
+/* read captive portal DB into array */
+function captiveportal_read_db() {
+
+ global $g;
+
+ $cpdb = array();
+ $fd = @fopen("{$g['vardb_path']}/captiveportal.db", "r");
+ if ($fd) {
+ while (!feof($fd)) {
+ $line = trim(fgets($fd));
+ if ($line) {
+ $cpdb[] = explode(",", $line);
+ }
+ }
+ fclose($fd);
+ }
+ return $cpdb;
+}
+
+/* write captive portal DB */
+function captiveportal_write_db($cpdb) {
+
+ global $g;
+
+ $fd = @fopen("{$g['vardb_path']}/captiveportal.db", "w");
+ if ($fd) {
+ foreach ($cpdb as $cpent) {
+ fwrite($fd, join(",", $cpent) . "\n");
+ }
+ fclose($fd);
+ }
+}
+
+/* read RADIUS servers into array */
+function captiveportal_get_radius_servers() {
+
+ global $g;
+
+ if (file_exists("{$g['vardb_path']}/captiveportal_radius.db")) {
+ $fd = @fopen("{$g['vardb_path']}/captiveportal_radius.db","r");
+ if ($fd) {
+ $radiusservers = array();
+ while (!feof($fd)) {
+ $line = trim(fgets($fd));
+ if ($line) {
+ $radsrv = array();
+ list($radsrv['ipaddr'],$radsrv['port'],$radsrv['acctport'],$radsrv['key']) = explode(",",$line);
+ $radiusservers[] = $radsrv;
+ }
+ }
+ fclose($fd);
+
+ return $radiusservers;
+ }
+ }
+
+ return false;
+}
+
+/* lock captive portal information, decide that the lock file is stale after
+ 10 seconds */
+function captiveportal_lock() {
+
+ global $g;
+
+ $lockfile = "{$g['varrun_path']}/captiveportal.lock";
+
+ $n = 0;
+ while ($n < 10) {
+ /* open the lock file in append mode to avoid race condition */
+ if ($fd = @fopen($lockfile, "x")) {
+ /* succeeded */
+ fclose($fd);
+ return;
+ } else {
+ /* file locked, wait and try again */
+ sleep(1);
+ $n++;
+ }
+ }
+}
+
+/* unlock configuration file */
+function captiveportal_unlock() {
+
+ global $g;
+
+ $lockfile = "{$g['varrun_path']}/captiveportal.lock";
+
+ if (file_exists($lockfile))
+ unlink($lockfile);
+}
+
+?>
OpenPOWER on IntegriCloud