diff --git a/plist b/plist
index 771946c82..dfa69ac84 100644
--- a/plist
+++ b/plist
@@ -18,6 +18,7 @@
/usr/local/etc/inc/interfaces.lib.inc
/usr/local/etc/inc/legacy_bindings.inc
/usr/local/etc/inc/plugins.inc
+/usr/local/etc/inc/plugins.inc.d/captiveportal.inc
/usr/local/etc/inc/plugins.inc.d/core.inc
/usr/local/etc/inc/plugins.inc.d/dhcpd.inc
/usr/local/etc/inc/plugins.inc.d/dhcrelay.inc
@@ -738,6 +739,7 @@
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Category.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Category.xml
+/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/CaptivePortalAliases.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/InterfaceNetworkAliases.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/README.md
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/StaticAliases.php
@@ -1054,7 +1056,7 @@
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/fonts/glyphicons-halflings-regular.ttf
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/fonts/glyphicons-halflings-regular.woff
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/fonts/glyphicons-halflings-regular.woff2
-/usr/local/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/default-logo.png
+/usr/local/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/default-logo.svg
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/favicon.png
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/index.html
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/js/bootstrap.min.js
@@ -1063,7 +1065,7 @@
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/lib/arp.py
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/lib/daemonize.py
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/lib/db.py
-/usr/local/opnsense/scripts/OPNsense/CaptivePortal/lib/ipfw.py
+/usr/local/opnsense/scripts/OPNsense/CaptivePortal/lib/pf.py
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/listClients.py
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/overlay_template.py
/usr/local/opnsense/scripts/OPNsense/CaptivePortal/process_accounting_messages.php
diff --git a/src/etc/inc/filter.inc b/src/etc/inc/filter.inc
index c684c4690..882357eee 100644
--- a/src/etc/inc/filter.inc
+++ b/src/etc/inc/filter.inc
@@ -355,6 +355,7 @@ function filter_configure_sync($verbose = false, $load_aliases = true)
$rules .= "set skip on lo0\n";
$rules .= "set skip on pfsync0\n";
$rules .= "\n";
+ $rules .= $fw->anchorToText('ether', 'head');
$rules .= filter_generate_scrubbing($cnfint);
$rules .= "\n";
$rules .= $fw->anchorToText('nat,binat,rdr', 'head');
diff --git a/src/etc/inc/plugins.inc.d/captiveportal.inc b/src/etc/inc/plugins.inc.d/captiveportal.inc
new file mode 100644
index 000000000..92819a696
--- /dev/null
+++ b/src/etc/inc/plugins.inc.d/captiveportal.inc
@@ -0,0 +1,172 @@
+isEnabled()) {
+ $services[] = array(
+ 'pidfile' => '/var/run/lighttpd-api-dispatcher.pid',
+ 'description' => gettext('Captive Portal'),
+ 'configd' => array(
+ 'restart' => array('captiveportal restart'),
+ 'start' => array('captiveportal start'),
+ 'stop' => array('captiveportal stop'),
+ ),
+ 'name' => 'captiveportal',
+ );
+ }
+
+ return $services;
+}
+
+function captiveportal_cron()
+{
+ global $config;
+
+ $jobs = [];
+
+ if (!empty($config['system']['captiveportalbackup']) && $config['system']['captiveportalbackup'] > 0) {
+ $jobs[]['autocron'] = array(
+ '/usr/local/etc/rc.syshook.d/backup/20-captiveportal',
+ '0',
+ '*/' . $config['system']['captiveportalbackup']
+ );
+ }
+
+ return $jobs;
+}
+
+function captiveportal_syslog()
+{
+ $logfacilities = [];
+
+ $logfacilities['portalauth'] = ['facility' => ['captiveportal']];
+
+ return $logfacilities;
+}
+
+function captiveportal_firewall($fw)
+{
+ global $config;
+
+ $cp = new \OPNsense\CaptivePortal\CaptivePortal();
+ if ($cp->isEnabled()) {
+ foreach ($cp->zones->zone->iterateItems() as $zone) {
+ if ($zone->enabled->isEmpty()) {
+ continue;
+ }
+
+ $zoneid = (string)$zone->zoneid;
+ $uuid = $zone->getAttribute('uuid');
+ // register anchor, to be filled with ether rules for accounting
+ $fw->registerAnchor("captiveportal_zone_{$zoneid}", "ether", 0, "head", false, (string)$zone->interfaces);
+
+ foreach (explode(',', $zone->interfaces) as $intf) {
+ // allow DNS
+ $fw->registerFilterRule(
+ 1,
+ [
+ 'type' => 'pass',
+ 'interface' => $intf,
+ 'protocol' => 'tcp/udp',
+ 'direction' => 'in',
+ 'from' => 'any',
+ 'to' => '(self)',
+ 'to_port' => 53,
+ 'descr' => "Allow DNS for Captive Portal (zone {$zoneid})",
+ 'log' => !isset($config['syslog']['nologdefaultpass']),
+ '#ref' => "ui/captiveportal#edit={$uuid}",
+ ]
+ );
+
+ foreach (['80', '443'] as $to_port) {
+ $rdr_port = $to_port === '443' ? (8000 + (int)$zoneid) : (9000 + (int)$zoneid);
+
+ // forward to localhost if not authenticated
+ $fw->registerForwardRule(
+ 2,
+ [
+ 'interface' => $intf,
+ 'pass' => true,
+ 'nordr' => false,
+ 'ipprotocol' => 'inet',
+ 'protocol' => 'tcp',
+ 'from' => "<__captiveportal_zone_{$zoneid}>",
+ 'from_not' => true,
+ 'to' => 'any',
+ 'to_port' => $to_port,
+ 'target' => '127.0.0.1',
+ 'localport' => $rdr_port,
+ 'descr' => "Redirect to Captive Portal (zone {$zoneid})",
+ '#ref' => "ui/captiveportal#edit={$uuid}"
+ ]
+ );
+
+ // Allow access to the captive portal
+ $proto = $to_port === '443' ? 'https': 'http';
+ $fw->registerFilterRule(
+ 2,
+ [
+ 'type' => 'pass',
+ 'interface' => $intf,
+ 'protocol' => 'tcp',
+ 'direction' => 'in',
+ 'from' => 'any',
+ 'to' => '(self)',
+ 'to_port' => $rdr_port,
+ 'descr' => "Allow access to Captive Portal ({$proto}, zone {$zoneid})",
+ 'log' => !isset($config['syslog']['nologdefaultpass']),
+ '#ref' => "ui/captiveportal#edit={$uuid}",
+ ]
+ );
+ }
+
+ // block all non-authenticated users
+ $fw->registerFilterRule(
+ 3,
+ [
+ 'type' => 'block',
+ 'interface' => $intf,
+ 'direction' => 'in',
+ 'from' => "<__captiveportal_zone_{$zoneid}>",
+ 'from_not' => true,
+ 'to' => 'any',
+ 'descr' => "Default Captive Portal block rule (zone {$zoneid})",
+ 'log' => !isset($config['syslog']['nologdefaultblock']),
+ '#ref' => "ui/captiveportal#edit={$uuid}",
+ ]
+ );
+
+ // we do not create a pass rule for authenticated clients here, any user-defined pass rule will
+ // automatically apply to the authenticated clients of this zone.
+ }
+ }
+ }
+}
diff --git a/src/etc/inc/plugins.inc.d/core.inc b/src/etc/inc/plugins.inc.d/core.inc
index d2115054f..2a9907de9 100644
--- a/src/etc/inc/plugins.inc.d/core.inc
+++ b/src/etc/inc/plugins.inc.d/core.inc
@@ -32,33 +32,6 @@ function core_services()
$services = array();
- if (isset($config['OPNsense']['captiveportal']['zones']['zone'])) {
- $enabled = false;
- if (!empty($config['OPNsense']['captiveportal']['zones']['zone']['enabled'])) {
- // single zone and enabled
- $enabled = true;
- } else {
- // possible more zones, traverse items
- foreach ($config['OPNsense']['captiveportal']['zones']['zone'] as $zone) {
- if (!empty($zone['enabled'])) {
- $enabled = true;
- }
- }
- }
- if ($enabled) {
- $services[] = array(
- 'pidfile' => '/var/run/lighttpd-api-dispatcher.pid',
- 'description' => gettext('Captive Portal'),
- 'configd' => array(
- 'restart' => array('captiveportal restart'),
- 'start' => array('captiveportal start'),
- 'stop' => array('captiveportal stop'),
- ),
- 'name' => 'captiveportal',
- );
- }
- }
-
$services[] = array(
'description' => gettext('System Configuration Daemon'),
'pidfile' => '/var/run/configd.pid',
@@ -330,14 +303,6 @@ function core_cron()
);
}
- if (!empty($config['system']['captiveportalbackup']) && $config['system']['captiveportalbackup'] > 0) {
- $jobs[]['autocron'] = array(
- '/usr/local/etc/rc.syshook.d/backup/20-captiveportal',
- '0',
- '*/' . $config['system']['captiveportalbackup']
- );
- }
-
foreach ((new OPNsense\Backup\BackupFactory())->listProviders() as $classname => $provider) {
if ($provider['handle']->isEnabled()) {
$jobs[]['autocron'] = array('/usr/local/sbin/configctl -d system remote backup 3600', 0, 1);
@@ -361,7 +326,6 @@ function core_syslog()
$logfacilities['dhcpd'] = ['facility' => ['dhcpd']];
$logfacilities['lighttpd'] = ['facility' => ['lighttpd']];
$logfacilities['pkg'] = ['facility' => ['pkg', 'pkg-static']];
- $logfacilities['portalauth'] = ['facility' => ['captiveportal']];
$logfacilities['ppps'] = ['facility' => ['ppp']];
$logfacilities['resolver'] = ['facility' => ['unbound']];
$logfacilities['routing'] = ['facility' => ['routed', 'olsrd', 'zebra', 'ospfd', 'bgpd', 'miniupnpd']];
diff --git a/src/etc/rc.d/captiveportal b/src/etc/rc.d/captiveportal
index 26b5493cc..ad87c0d73 100755
--- a/src/etc/rc.d/captiveportal
+++ b/src/etc/rc.d/captiveportal
@@ -25,7 +25,7 @@
# POSSIBILITY OF SUCH DAMAGE.
# PROVIDE: captiveportal
-# REQUIRE: ipfw
+# REQUIRE: pf
# KEYWORD: shutdown
. /etc/rc.subr
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/AccessController.php b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/AccessController.php
index c8716fce1..db16d10d0 100644
--- a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/AccessController.php
+++ b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/AccessController.php
@@ -117,7 +117,7 @@ class AccessController extends ApiControllerBase
/**
* logon client to zone, must use post type of request
- * @param int|string $zoneid zone id number
+ * @param int|string $zoneid zone id number, provided for backwards compatibility
* @return array
* @throws \OPNsense\Base\ModelException
*/
@@ -131,6 +131,7 @@ class AccessController extends ApiControllerBase
// init variables for authserver object and name
$authServer = null;
$authServerName = "";
+ $zoneid = $this->request->getHeader("zoneid");
// get username from post
$userName = $this->request->getPost("user", "striptags", null);
@@ -223,9 +224,10 @@ class AccessController extends ApiControllerBase
/**
* logoff client
- * @param int|string $zoneid zone id number
+ * @param int|string $zoneid zone id number, provided for backwards compatibility
* @return array
* @throws \OPNsense\Base\ModelException
+ *
*/
public function logoffAction($zoneid = 0)
{
@@ -233,6 +235,7 @@ class AccessController extends ApiControllerBase
// return empty result on CORS preflight
return [];
} else {
+ $zoneid = $this->request->getHeader("zoneid");
$clientSession = $this->clientSession((string)$zoneid);
if (
$clientSession['clientState'] == 'AUTHORIZED' &&
@@ -256,9 +259,10 @@ class AccessController extends ApiControllerBase
/**
* retrieve session info
- * @param int|string $zoneid zone id number
+ * @param int|string $zoneid zone id number, provided for backwards compatibility
* @return array
* @throws \OPNsense\Base\ModelException
+ *
*/
public function statusAction($zoneid = 0)
{
@@ -266,8 +270,64 @@ class AccessController extends ApiControllerBase
// return empty result on CORS preflight
return [];
} elseif ($this->request->isPost() || $this->request->isGet()) {
- $clientSession = $this->clientSession((string)$zoneid);
+ $clientSession = $this->clientSession($this->request->getHeader("zoneid"));
return $clientSession;
}
}
+
+ /**
+ * RFC 8908: Captive Portal API status object
+ *
+ * The URI for this endpoint can be provisioned to the client
+ * as defined by RFC 7710.
+ *
+ * Request and response must set media type as "application/captive+json".
+ *
+ * Response contains the following fields:
+ * - captive: boolean: client is currently in a state of captivity.
+ * - user-portal-url: string: URL to login web portal (must be HTTPS).
+ * - seconds-remaining: number: seconds until session expires,
+ * only relevant if hardtimeout set.
+ *
+ * Fields not implemented here but possible in the future:
+ * - venue-info-url: string: Information page (must be HTTPS)
+ * - can-extend-session: boolean: hint that client system can access
+ * user-portal-url to extend session.
+ * - bytes-remaining: number: no. of bytes after which session expires.
+ *
+ * Response must set Cache-Control to 'private' or 'no-store'
+ */
+ public function apiAction()
+ {
+ if ($this->request->isGet() &&
+ $this->request->getHeader("accept") == "application/captive+json") {
+ $result = [];
+ $zoneId = $this->request->getHeader("zoneid");
+ $clientSession = $this->clientSession($zoneId);
+ $captive = $clientSession["clientState"] != "AUTHORIZED";
+ $host = $this->request->getHeader('X-Forwarded-Host');
+
+ $zone = (new \OPNsense\CaptivePortal\CaptivePortal())->getByZoneId($zoneId);
+
+ if ($zone != null && !empty((string)$zone->hardtimeout) && !empty($clientSession['startTime'])) {
+ if ((time() - (int)$clientSession['startTime']) < (string)$zone->hardtimeout * 60) {
+ $result['seconds-remaining'] = (string)$zone->hardtimeout * 60 - ((time() - (int)$clientSession['startTime']));
+ }
+ }
+
+ $this->response->setRawHeader("Cache-Control: private");
+ $this->response->setContentType("application/captive+json");
+
+ $result["captive"] = $captive;
+ $result["user-portal-url"] = "https://{$host}/index.html";
+
+ $this->response->setContent($result);
+
+ return;
+ }
+
+ $this->response->setStatusCode(400);
+ $this->response->setContentType('application/json', 'UTF-8');
+ $this->response->setContent(['status' => 400, 'message' => 'Bad request']);
+ }
}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/ServiceController.php b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/ServiceController.php
index 96620e416..e2ce2e2c8 100644
--- a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/ServiceController.php
+++ b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/ServiceController.php
@@ -48,9 +48,7 @@ class ServiceController extends ApiControllerBase
{
if ($this->request->isPost()) {
$backend = new Backend();
- // the ipfw rules need to know about all the zones, so we need to reload ipfw for the portal to work
- $backend->configdRun('template reload OPNsense/IPFW');
- $bckresult = trim($backend->configdRun("ipfw reload"));
+ $bckresult = trim($backend->configdRun("filter reload"));
if ($bckresult == "OK") {
// generate captive portal config
$bckresult = trim($backend->configdRun('template reload OPNsense/Captiveportal'));
diff --git a/src/opnsense/mvc/app/library/OPNsense/Firewall/Plugin.php b/src/opnsense/mvc/app/library/OPNsense/Firewall/Plugin.php
index f74ffb947..4d74a6edd 100644
--- a/src/opnsense/mvc/app/library/OPNsense/Firewall/Plugin.php
+++ b/src/opnsense/mvc/app/library/OPNsense/Firewall/Plugin.php
@@ -214,10 +214,10 @@ class Plugin
* @param bool $quick
* @return null
*/
- public function registerAnchor($name, $type = "fw", $priority = 0, $placement = "tail", $quick = false)
+ public function registerAnchor($name, $type = "fw", $priority = 0, $placement = "tail", $quick = false, $ifs = null)
{
$anchorKey = sprintf("%s.%s.%08d.%08d", $type, $placement, $priority, count($this->anchors));
- $this->anchors[$anchorKey] = array('name' => $name, 'quick' => $quick);
+ $this->anchors[$anchorKey] = ['name' => $name, 'quick' => $quick, 'ifs' => $ifs];
ksort($this->anchors);
}
@@ -233,11 +233,21 @@ class Plugin
foreach (explode(',', $types) as $type) {
foreach ($this->anchors as $anchorKey => $anchor) {
if (strpos($anchorKey, "{$type}.{$placement}") === 0) {
- $result .= $type == "fw" ? "" : "{$type}-";
- $result .= "anchor \"{$anchor['name']}\"";
+ $result .= ($type == "fw" || $type == "ether") ? "" : "{$type}-";
+ $prefix = $type == "ether" ? "ether " : "";
+ $result .= "{$prefix}anchor \"{$anchor['name']}\"";
if ($anchor['quick']) {
$result .= " quick";
}
+ if (!empty($anchor['ifs'])) {
+ $ifs = array_filter(array_map(function($if) {
+ return $this->interfaceMapping[$if]['if'] ?? null;
+ }, explode(',', $anchor['ifs'])));
+
+ if (!empty($ifs)) {
+ $result .= " on {" . implode(', ', $ifs) . "}";
+ }
+ }
$result .= "\n";
}
}
diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/CaptivePortalAliases.php b/src/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/CaptivePortalAliases.php
new file mode 100644
index 000000000..41eb8f01b
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/CaptivePortalAliases.php
@@ -0,0 +1,60 @@
+isEnabled()) {
+ foreach ($cp->zones->zone->iterateItems() as $zone) {
+ if (empty((string)$zone->enabled)) {
+ continue;
+ }
+
+ $zoneid = (string)$zone->zoneid;
+ $result["__captiveportal_zone_{$zoneid}"] = [
+ "enabled" => "1",
+ "name" => "__captiveportal_zone_{$zoneid}",
+ "type" => "internal",
+ "description" => sprintf("%s %s", (string)$zone->descr, gettext("captiveportal")),
+ "content" => "",
+ ];
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/src/opnsense/mvc/app/views/OPNsense/CaptivePortal/index.volt b/src/opnsense/mvc/app/views/OPNsense/CaptivePortal/index.volt
index 0f4e6fcae..eda83ab17 100644
--- a/src/opnsense/mvc/app/views/OPNsense/CaptivePortal/index.volt
+++ b/src/opnsense/mvc/app/views/OPNsense/CaptivePortal/index.volt
@@ -37,7 +37,10 @@
set:'/api/captiveportal/settings/setZone/',
add:'/api/captiveportal/settings/addZone/',
del:'/api/captiveportal/settings/delZone/',
- toggle:'/api/captiveportal/settings/toggleZone/'
+ toggle:'/api/captiveportal/settings/toggleZone/',
+ options: {
+ triggerEditFor: getUrlHash('edit')
+ }
}
);
diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/allow.py b/src/opnsense/scripts/OPNsense/CaptivePortal/allow.py
index 937bb6047..6b1fcfdd4 100755
--- a/src/opnsense/scripts/OPNsense/CaptivePortal/allow.py
+++ b/src/opnsense/scripts/OPNsense/CaptivePortal/allow.py
@@ -33,7 +33,7 @@ import sys
import ujson
from lib.db import DB
from lib.arp import ARP
-from lib.ipfw import IPFW
+from lib.pf import PF
parser = argparse.ArgumentParser()
parser.add_argument('-username', help='username', type=str, required=True)
@@ -50,6 +50,6 @@ response = DB().add_client(
ip_address=args.ip_address,
mac_address=arp_entry['mac'] if arp_entry is not None else None
)
-IPFW().add_to_table(table_number=args.zoneid, address=args.ip_address)
+PF.add_to_table(zoneid=args.zoneid, address=args.ip_address)
response['clientState'] = 'AUTHORIZED'
print(ujson.dumps(response))
diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/cp-background-process.py b/src/opnsense/scripts/OPNsense/CaptivePortal/cp-background-process.py
index 4d536ca1a..a5809e30f 100755
--- a/src/opnsense/scripts/OPNsense/CaptivePortal/cp-background-process.py
+++ b/src/opnsense/scripts/OPNsense/CaptivePortal/cp-background-process.py
@@ -38,7 +38,7 @@ sys.path.insert(0, "/usr/local/opnsense/site-python")
from lib import Config
from lib.db import DB
from lib.arp import ARP
-from lib.ipfw import IPFW
+from lib.pf import PF
from lib.daemonize import Daemonize
from sqlite3_helper import check_and_repair
@@ -50,18 +50,45 @@ class CPBackgroundProcess(object):
# open syslog and notice startup
syslog.openlog('captiveportal', facility=syslog.LOG_LOCAL4)
syslog.syslog(syslog.LOG_NOTICE, 'starting captiveportal background process')
- # handles to ipfw, arp the config and the internal administration
- self.ipfw = IPFW()
+ # handles to pf, arp, the config and the internal administration
self.arp = ARP()
self.cnf = Config()
self.db = DB()
self._conf_zone_info = self.cnf.get_zones()
+ self._accounting_info = {k: {'cur': {}, 'prev': {}, 'reset': False} for k in self.list_zone_ids()}
+
def list_zone_ids(self):
""" return zone numbers
"""
return self._conf_zone_info.keys()
+ def get_accounting(self):
+ """ returns only how much the accounting should add in total
+ """
+ result = {}
+ for zoneid in self.list_zone_ids():
+ self._accounting_info[zoneid]['prev'] = self._accounting_info[zoneid]['cur']
+ self._accounting_info[zoneid]['cur'] = PF.list_accounting_info(zoneid)
+
+ if self._accounting_info[zoneid]['reset']:
+ # counters were reset to 0, simply add
+ result[zoneid] = self._accounting_info[zoneid]['cur']
+ else:
+ # counters still valid, calculate difference
+ result[zoneid] = {}
+ for ip in self._accounting_info[zoneid]['cur']:
+ result[zoneid][ip] = {'last_accessed': self._accounting_info[zoneid]['cur'][ip]['last_accessed']}
+ for key in ['in_pkts', 'in_bytes', 'out_pkts', 'out_bytes']:
+ if ip not in self._accounting_info[zoneid]['prev']:
+ result[zoneid][ip][key] = self._accounting_info[zoneid]['cur'][ip][key]
+ else:
+ result[zoneid][ip][key] = self._accounting_info[zoneid]['cur'][ip][key] \
+ - self._accounting_info[zoneid]['prev'][ip][key]
+
+ # map to flat dict of IPs
+ return {k: v for subdict in result.values() for k, v in subdict.items()}
+
def initialize_fixed(self):
""" initialize fixed ip / hosts per zone
"""
@@ -94,12 +121,12 @@ class CPBackgroundProcess(object):
for dbclient in self.db.list_clients(zoneid):
if dbclient['authenticated_via'] == '---ip---' \
and dbclient['ipAddress'] not in cpzones[zoneid]['allowedaddresses']:
- self.ipfw.delete(zoneid, dbclient['ipAddress'])
+ PF.remove_from_table(zoneid, dbclient['ipAddress'])
self.db.del_client(zoneid, dbclient['sessionId'])
elif dbclient['authenticated_via'] == '---mac---' \
and dbclient['macAddress'] not in cpzones[zoneid]['allowedmacaddresses']:
if dbclient['ipAddress'] != '':
- self.ipfw.delete(zoneid, dbclient['ipAddress'])
+ PF.remove_from_table(zoneid, dbclient['ipAddress'])
self.db.del_client(zoneid, dbclient['sessionId'])
def sync_zone(self, zoneid):
@@ -109,8 +136,7 @@ class CPBackgroundProcess(object):
if zoneid in self._conf_zone_info:
# fetch data for this zone
cpzone_info = self._conf_zone_info[zoneid]
- registered_addresses = self.ipfw.list_table(zoneid)
- registered_addr_accounting = self.ipfw.list_accounting_info()
+ registered_addresses = PF.list_table(zoneid)
expected_clients = self.db.list_clients(zoneid)
concurrent_users = self.db.find_concurrent_user_sessions(zoneid)
@@ -160,23 +186,22 @@ class CPBackgroundProcess(object):
if current_ip is not None and db_client['ipAddress'] != current_ip:
if db_client['ipAddress'] != '':
# remove old ip
- self.ipfw.delete(zoneid, db_client['ipAddress'])
+ PF.remove_from_table(zoneid, db_client['ipAddress'])
self.db.update_client_ip(zoneid, db_client['sessionId'], current_ip)
- self.ipfw.add_to_table(zoneid, current_ip)
+ PF.add_to_table(zoneid, current_ip)
# check session, if it should be active, validate its properties
if drop_session_reason is None:
- # registered client, but not active or missing accounting according to ipfw (after reboot)
- if cpnet not in registered_addresses or cpnet not in registered_addr_accounting:
- self.ipfw.add_to_table(zoneid, cpnet)
+ # registered client, but not active or missing accounting according to pf (after reboot)
+ if cpnet not in registered_addresses:
+ PF.add_to_table(zoneid, cpnet)
else:
# remove session
- syslog.syslog(syslog.LOG_NOTICE, drop_session_reason)
- self.ipfw.delete(zoneid, cpnet)
+ PF.remove_from_table(zoneid, cpnet)
self.db.del_client(zoneid, db_client['sessionId'])
- # if there are addresses/networks in the underlying ipfw table which are not in our administration,
- # remove them from ipfw.
+ # if there are addresses/networks in the underlying pf table which are not in our administration,
+ # remove them from pf.
for registered_address in registered_addresses:
address_active = False
for db_client in expected_clients:
@@ -184,12 +209,24 @@ class CPBackgroundProcess(object):
address_active = True
break
if not address_active:
- self.ipfw.delete(zoneid, registered_address)
+ PF.remove_from_table(zoneid, registered_address)
+ def sync_accounting(self):
+ for zoneid in self.list_zone_ids():
+ pf_stats = self._accounting_info[zoneid]['cur']
+ current_clients = self.db.list_clients(zoneid)
+
+ pf_ips = set(pf_stats.keys())
+ db_ips = {entry['ipAddress'] for entry in current_clients}
+
+ self._accounting_info[zoneid]['reset'] = False
+ if pf_ips != db_ips:
+ self._accounting_info[zoneid]['reset'] = True
+ PF.sync_accounting(zoneid)
def main():
""" Background process loop, runs as backend daemon for all zones. only one should be active at all times.
- The main job of this procedure is to sync the administration with the actual situation in the ipfw firewall.
+ The main job of this procedure is to sync the administration with the actual situation in pf.
"""
# perform integrity check and repair database if needed
check_and_repair('/var/captiveportal/captiveportal.sqlite')
@@ -211,13 +248,16 @@ def main():
# reload cached arp table contents
bgprocess.arp.reload()
- # update accounting info, for all zones
- bgprocess.db.update_accounting_info(bgprocess.ipfw.list_accounting_info())
-
# process sessions per zone
for zoneid in bgprocess.list_zone_ids():
bgprocess.sync_zone(zoneid)
+ # update accounting info, for all zones
+ bgprocess.db.update_accounting_info(bgprocess.get_accounting())
+
+ # sync accounting after db update, resets zone counters if sync is needed
+ bgprocess.sync_accounting()
+
# close the database handle while waiting for the next poll
bgprocess.db.close()
diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/disconnect.py b/src/opnsense/scripts/OPNsense/CaptivePortal/disconnect.py
index 6721d5cf8..08a7cfd49 100755
--- a/src/opnsense/scripts/OPNsense/CaptivePortal/disconnect.py
+++ b/src/opnsense/scripts/OPNsense/CaptivePortal/disconnect.py
@@ -31,8 +31,7 @@
import argparse
import ujson
from lib.db import DB
-from lib.ipfw import IPFW
-
+from lib.pf import PF
parser = argparse.ArgumentParser()
parser.add_argument('session', help='session id to delete', type=str)
@@ -43,7 +42,7 @@ response = {'terminateCause': 'UNKNOWN'}
client_session_info = DB().del_client(int(args.z) if str(args.z).isdigit() else None, args.session)
if client_session_info is not None:
if client_session_info['ip_address']:
- IPFW().delete(client_session_info['zoneid'], client_session_info['ip_address'])
+ PF.remove_from_table(client_session_info['zoneid'], client_session_info['ip_address'])
client_session_info['terminateCause'] = 'User-Request'
response = client_session_info
diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/default-logo.png b/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/default-logo.png
deleted file mode 100644
index cd389fc7d..000000000
Binary files a/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/default-logo.png and /dev/null differ
diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/default-logo.svg b/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/default-logo.svg
new file mode 100644
index 000000000..688ba464e
--- /dev/null
+++ b/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/default-logo.svg
@@ -0,0 +1,41 @@
+
+
+
diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/favicon.png b/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/favicon.png
index 3e6dde972..9a8472bc4 100644
Binary files a/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/favicon.png and b/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/images/favicon.png differ
diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/index.html b/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/index.html
index 663a05c1a..6ed74621f 100644
--- a/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/index.html
+++ b/src/opnsense/scripts/OPNsense/CaptivePortal/htdocs_default/index.html
@@ -18,9 +18,6 @@
-
-
-