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 @@ - - -