diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/FirewallController.php b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/FirewallController.php
index 269488a7f..527edc5d3 100644
--- a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/FirewallController.php
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/FirewallController.php
@@ -202,6 +202,50 @@ class FirewallController extends ApiControllerBase
return [];
}
+ /**
+ * query pftop
+ */
+ public function queryPfTopAction()
+ {
+ if ($this->request->isPost()) {
+ $this->sessionClose();
+ $filter = new Filter([
+ 'query' => function ($value) {
+ return preg_replace("/[^0-9,a-z,A-Z, ,\/,*,\-,_,.,\#]/", "", $value);
+ }
+ ]);
+ $searchPhrase = '';
+ $ruleId = '';
+ $sortBy = '';
+ $itemsPerPage = $this->request->getPost('rowCount', 'int', 9999);
+ $currentPage = $this->request->getPost('current', 'int', 1);
+
+ if ($this->request->getPost('ruleid', 'string', '') != '') {
+ $ruleId = $filter->sanitize($this->request->getPost('ruleid'), 'query');
+ }
+
+ if ($this->request->getPost('searchPhrase', 'string', '') != '') {
+ $searchPhrase = $filter->sanitize($this->request->getPost('searchPhrase'), 'query');
+ }
+ if ($this->request->has('sort') && is_array($this->request->getPost("sort"))) {
+ $tmp = array_keys($this->request->getPost("sort"));
+ $sortBy = $tmp[0] . " " . $this->request->getPost("sort")[$tmp[0]];
+ }
+
+ $response = (new Backend())->configdpRun('filter diag top', [$searchPhrase, $itemsPerPage,
+ ($currentPage - 1) * $itemsPerPage, $ruleId, $sortBy]);
+ $response = json_decode($response, true);
+ if ($response != null) {
+ return [
+ 'rows' => $response['details'],
+ 'rowCount' => count($response['details']),
+ 'total' => $response['total_entries'],
+ 'current' => (int)$currentPage
+ ];
+ }
+ }
+ return [];
+ }
/**
* delete / drop a specific state by state+creator id
*/
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/FirewallController.php b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/FirewallController.php
index 44c0551a2..bf42e7b6c 100644
--- a/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/FirewallController.php
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/FirewallController.php
@@ -59,4 +59,11 @@ class FirewallController extends IndexController
{
$this->view->pick('OPNsense/Diagnostics/fw_states');
}
+ /**
+ * firewall pftop
+ */
+ public function pfTopAction()
+ {
+ $this->view->pick('OPNsense/Diagnostics/fw_pftop');
+ }
}
diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml
index 7143eaf02..7504d30ca 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml
+++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml
@@ -147,9 +147,10 @@
- Diagnostics: pfTop
+ Diagnostics: Firewall sessions
- diag_system_pftop.php*
+ ui/diagnostics/firewall/pf_top*
+ api/diagnostics/firewall/pf_top*
diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml
index a5cf2519c..f19f1cff4 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml
+++ b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml
@@ -188,8 +188,8 @@
+
-
diff --git a/src/opnsense/mvc/app/views/OPNsense/Diagnostics/fw_pftop.volt b/src/opnsense/mvc/app/views/OPNsense/Diagnostics/fw_pftop.volt
new file mode 100644
index 000000000..ce1108b66
--- /dev/null
+++ b/src/opnsense/mvc/app/views/OPNsense/Diagnostics/fw_pftop.volt
@@ -0,0 +1,140 @@
+{#
+ # Copyright (c) 2021 Deciso B.V.
+ # 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.
+ #}
+
+
+
+
+
+
+
+
+
+
{{ lang._('state id') }}
+
{{ lang._('Dir') }}
+
{{ lang._('Proto') }}
+
{{ lang._('Source') }}
+
{{ lang._('Gateway') }}
+
{{ lang._('Destination') }}
+
{{ lang._('State') }}
+
{{ lang._('Age (sec)') }}
+
{{ lang._('Expires (sec)') }}
+
{{ lang._('Pkts') }}
+
{{ lang._('Bytes') }}
+
{{ lang._('Avg') }}
+
{{ lang._('Rule') }}
+
+
+
+
+
+
diff --git a/src/opnsense/scripts/filter/lib/states.py b/src/opnsense/scripts/filter/lib/states.py
index bb5cda4b8..6ef2dd9e6 100755
--- a/src/opnsense/scripts/filter/lib/states.py
+++ b/src/opnsense/scripts/filter/lib/states.py
@@ -157,3 +157,75 @@ def query_states(rule_label, filter_str):
record['state'] = parts[-1]
return result
+
+
+def query_top(rule_label, filter_str):
+ result = list()
+ rule_labels = fetch_rule_labels()
+ sp = subprocess.run(['/usr/local/sbin/pftop', '-w', '1000', '-b','-v', 'long','9999999999999'], capture_output=True, text=True)
+ header = None
+ try:
+ filter_network = ipaddress.ip_network(filter_str.strip())
+ except ValueError:
+ filter_network = None
+
+ for rownum, line in enumerate(sp.stdout.strip().split('\n')):
+ parts = line.strip().split()
+ if rownum >= 2 and len(parts) > 5:
+ record = {
+ 'proto': parts[0],
+ 'dir': parts[1].lower(),
+ 'src_addr': parse_address(parts[2])['addr'],
+ 'src_port': parse_address(parts[2])['port'],
+ 'dst_addr': parse_address(parts[3])['addr'],
+ 'dst_port': parse_address(parts[3])['port'],
+ 'gw_addr': None,
+ 'gw_port': None,
+ }
+ if parts[4].count(':') > 2 or parts[4].count('.') > 2:
+ record['gw_addr'] = parse_address(parts[4])['addr']
+ record['gw_port'] = parse_address(parts[4])['port']
+ idx = 5
+ else:
+ idx = 4
+
+ record['state'] = parts[idx]
+ record['age'] = parts[idx+1]
+ record['expire'] = parts[idx+2]
+ record['pkts'] = int(parts[idx+3])
+ record['bytes'] = int(parts[idx+4])
+ record['avg'] = int(parts[idx+5])
+ record['rule'] = parts[idx+6]
+ if record['rule'] in rule_labels:
+ record['label'] = rule_labels[record['rule']]['rid']
+ record['descr'] = rule_labels[record['rule']]['descr']
+ else:
+ record['label'] = None
+ record['descr'] = None
+ for timefield in ['age', 'expire']:
+ tmp = record[timefield].split(':')
+ record[timefield] = int(tmp[0]) * 3600 + int(tmp[1]) * 60 + int(tmp[2])
+
+ search_line = " ".join(str(item) for item in filter(None, record.values()))
+ if rule_label != "" and record['label'].lower().find(rule_label) == -1:
+ # label
+ continue
+ elif filter_network is not None:
+ try:
+ match = False
+ for field in ['src_addr', 'dst_addr', 'gateway']:
+ addr = ipaddress.ip_network(record[field])
+ if field is not None and ipaddress.ip_network(filter_network).overlaps(addr):
+ match = True
+ break
+ if not match:
+ continue
+ except:
+ continue
+ elif filter_str != "" and search_line.lower().find(filter_str.lower()) == -1:
+ # apply filter when provided
+ continue
+
+ result.append(record)
+
+ return result
diff --git a/src/opnsense/scripts/filter/pftop.py b/src/opnsense/scripts/filter/pftop.py
new file mode 100755
index 000000000..cbc0b9a4e
--- /dev/null
+++ b/src/opnsense/scripts/filter/pftop.py
@@ -0,0 +1,66 @@
+#!/usr/local/bin/python3
+
+"""
+ Copyright (c) 2021 Ad Schellevis
+ 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.
+"""
+import ujson
+import argparse
+from lib.states import query_top
+
+
+if __name__ == '__main__':
+ # parse input arguments
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--filter', help='filter results', default='')
+ parser.add_argument('--limit', help='limit number of results', default='')
+ parser.add_argument('--offset', help='offset results', default='')
+ parser.add_argument('--label', help='label / rule id', default='')
+ parser.add_argument('--sort_by', help='sort by (field asc|desc)', default='')
+ inputargs = parser.parse_args()
+
+ result = {
+ 'details': query_top(filter_str=inputargs.filter, rule_label=inputargs.label)
+ }
+ # sort results
+ if inputargs.sort_by.strip() != '' and len(result['details']) > 0:
+ sort_key = inputargs.sort_by.split()[0]
+ sort_desc = inputargs.sort_by.split()[-1] == 'desc'
+ if sort_key in result['details'][0]:
+ if type(result['details'][0][sort_key]) is int:
+ sorter = lambda k: k[sort_key] if sort_key in k else 0
+ else:
+ sorter = lambda k: str(k[sort_key]).lower() if sort_key in k else ''
+ result['details'] = sorted(result['details'], key=sorter, reverse=sort_desc)
+
+ result['total_entries'] = len(result['details'])
+ # apply offset and limit
+ if inputargs.offset.isdigit():
+ result['details'] = result['details'][int(inputargs.offset):]
+ if inputargs.limit.isdigit() and len(result['details']) >= int(inputargs.limit):
+ result['details'] = result['details'][:int(inputargs.limit)]
+
+ result['total'] = len(result['details'])
+
+ print(ujson.dumps(result))
diff --git a/src/opnsense/service/conf/actions.d/actions_filter.conf b/src/opnsense/service/conf/actions.d/actions_filter.conf
index e0cb6792a..bbd8fefd6 100644
--- a/src/opnsense/service/conf/actions.d/actions_filter.conf
+++ b/src/opnsense/service/conf/actions.d/actions_filter.conf
@@ -96,8 +96,8 @@ type:script_output
message:request pf rules
[diag.top]
-command:/usr/local/sbin/pftop
-parameters: -w 200 -b -o %s -v %s %s
+command:/usr/local/opnsense/scripts/filter/pftop.py
+parameters: --filter=%s --limit=%s --offset=%s --label=%s --sort_by=%s
type:script_output
message:request pftop statistics
diff --git a/src/www/diag_system_pftop.php b/src/www/diag_system_pftop.php
deleted file mode 100644
index ad76f701c..000000000
--- a/src/www/diag_system_pftop.php
+++ /dev/null
@@ -1,170 +0,0 @@
-
- * 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.
- */
-
-require_once("guiconfig.inc");
-
-$sorttypes = array('age', 'bytes', 'dest', 'dport', 'exp', 'none', 'peak', 'pkt', 'rate', 'size', 'sport', 'src');
-$viewtypes = array('default', 'label', 'long', 'rules', 'size', 'speed', 'state', 'time');
-$numstates = array('50', '100', '200', '500', '1000', '99999999999');
-
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- // fetch only valid input data (items from above lists)
- $viewtype = 'default';
- $numstate = '200';
- $sorttype ='bytes';
- if (isset($_POST['viewtype']) && in_array($_POST['viewtype'], $viewtypes)) {
- $viewtype = $_POST['viewtype'];
- }
- if (isset($_POST['states']) && in_array($_POST['states'], $numstates)) {
- $numstate = $_POST['states'];
- }
- if (isset($_POST['sorttype']) && in_array($_POST['sorttype'], $sorttypes)) {
- $sorttype = $_POST['sorttype'];
- }
-
- // fetch pftop data
- echo configdp_run('filter diag top', array($sorttype, $viewtype, $numstate));
- exit;
-}
-
-include("head.inc");
-
-?>
-
-
-
-
-
-