Firewall: Diagnostics: Sessions - refactor pftop output, move search to controller layer and implement cache.

This commit should improve responsiveness of the sessions screen, since we needed to parse the full data in the previous version as well before returning it, this shouldn't be much slower on initial load.
Only risk is the size of the generated json output, by moving the label parsing we replicate less data and reduce total size.
This commit is contained in:
Ad Schellevis 2024-06-08 19:14:38 +02:00
parent d496eea29c
commit e66dbbd6eb
4 changed files with 64 additions and 116 deletions

View File

@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2017-2021 Deciso B.V.
* Copyright (C) 2017-2024 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -32,6 +32,7 @@ use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Core\SanitizeFilter;
use OPNsense\Firewall\Util;
/**
* Class FirewallController
@ -226,48 +227,59 @@ class FirewallController extends ApiControllerBase
{
if ($this->request->isPost()) {
$this->sessionClose();
$filter = new SanitizeFilter();
$searchPhrase = '';
$ruleId = '';
$sortBy = '';
$itemsPerPage = $this->request->getPost('rowCount', 'int', 9999);
$currentPage = $this->request->getPost('current', 'int', 1);
$pftop = json_decode((new Backend())->configdpRun('filter diag top') ?? '', true) ?? [];
if ($this->request->getPost('ruleid', 'string', '') != '') {
$ruleId = $filter->sanitize($this->request->getPost('ruleid'), 'query');
$clauses = [];
$networks = [];
foreach (preg_split('/\s+/', (string)$this->request->getPost('searchPhrase', null, '')) as $item) {
if (empty($item)) {
continue;
} elseif (Util::isSubnet($item)) {
$networks[] = $item;
} elseif (Util::isIpAddress($item)) {
$networks[] = $item . "/". (strpos($item, ':') ? '128' : '32');
} else {
$clauses[] = $item;
}
}
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")) &&
!empty($this->request->getPost("sort"))
) {
$tmp = array_keys($this->request->getPost("sort"));
$sortBy = $tmp[0] . " " . $this->request->getPost("sort")[$tmp[0]];
}
$ruleid = $this->request->getPost('ruleid', 'string', '');
$labels = $pftop['metadata']['labels'];
$filter_funct = function (&$row) use ($networks, $labels, $ruleid) {
/* update record */
if (isset($labels[$row['rule']])) {
$row['label'] = $labels[$row['rule']]['rid'];
$row['descr'] = $labels[$row['rule']]['descr'];
}
$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
];
}
if (!empty($ruleid) && trim($row['label']) != $ruleid) {
return false;
}
/* filter using network clauses*/
if (empty($networks)) {
return true;
}
foreach (['dst_addr', 'src_addr', 'gw_addr'] as $addr) {
foreach ($networks as $net) {
if (Util::isIPInCIDR($row[$addr] ?? '', $net)) {
return true;
}
}
}
return false;
};
return $this->searchRecordsetBase(
$pftop['details'],
null,
null,
$filter_funct,
SORT_NATURAL | SORT_FLAG_CASE,
$clauses
);
}
return [
'rows' => [],
'rowCount' => 0,
'total' => 0,
'current' => 0
];
}
/**
* delete / drop a specific state by state+creator id
*/

View File

@ -1,5 +1,5 @@
"""
Copyright (c) 2015-2021 Ad Schellevis <ad@opnsense.org>
Copyright (c) 2015-2024 Ad Schellevis <ad@opnsense.org>
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -247,18 +247,21 @@ def query_states(rule_label, filter_str):
def query_top(rule_label, filter_str):
def query_top():
addr_parser = AddressParser()
result = list()
rule_labels = fetch_rule_labels()
result = {
'details': [],
'metadata': {
'labels': fetch_rule_labels()
}
}
sp = subprocess.run(
['/usr/local/sbin/pftop', '-w', '1000', '-b','-v', 'long','9999999999999'],
['/usr/local/sbin/pftop', '-w', '1000', '-b','-v', 'long','200000'],
capture_output=True,
text=True
)
filter_net_clauses, filter_clauses = split_filter_clauses(filter_str)
for rownum, line in enumerate(sp.stdout.strip().split('\n')):
parts = line.strip().split()
if rownum >= 2 and len(parts) > 5:
@ -270,7 +273,7 @@ def query_top(rule_label, filter_str):
'dst_addr': addr_parser.split_ip_port(parts[3])['addr'],
'dst_port': addr_parser.split_ip_port(parts[3])['port'],
'gw_addr': None,
'gw_port': None,
'gw_port': None
}
if parts[4].count(':') > 2 or parts[4].count('.') > 2:
record['gw_addr'] = addr_parser.split_ip_port(parts[4])['addr']
@ -301,12 +304,6 @@ def query_top(rule_label, filter_str):
record['bytes'] = 0
record['avg'] = int(parts[idx+5]) if parts[idx+5].isdigit() else 0
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']:
if ':' in record[timefield]:
tmp = record[timefield].split(':')
@ -318,35 +315,7 @@ def query_top(rule_label, filter_str):
else:
record[timefield] = 0
if rule_label != "" and record['label'].lower().find(rule_label) == -1:
# label
continue
elif filter_clauses or filter_net_clauses:
match = False
for filter_net in filter_net_clauses:
try:
match = False
for field in ['src_addr', 'dst_addr', 'gw_addr']:
port_field = "%s_port" % field[0:3]
if record[field] is not None and addr_parser.overlaps(filter_net[0], record[field]):
if filter_net[1] is None or filter_net[1] == record[port_field]:
match = True
if not match:
break
except:
continue
if not match:
continue
if filter_clauses:
search_line = " ".join(str(item) for item in filter(None, record.values()))
for filter_clause in filter_clauses:
if search_line.find(filter_clause) == -1:
match = False
break
if not match:
continue
result.append(record)
result['details'].append(record)
return result

View File

@ -1,7 +1,7 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2021 Ad Schellevis <ad@opnsense.org>
Copyright (c) 2021-2024 Ad Schellevis <ad@opnsense.org>
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -26,41 +26,8 @@
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))
print(ujson.dumps(query_top()))

View File

@ -97,8 +97,8 @@ message:request pf rules
[diag.top]
command:/usr/local/opnsense/scripts/filter/pftop.py
parameters: --filter=%s --limit=%s --offset=%s --label=%s --sort_by=%s
type:script_output
cache_ttl:30
message:request pftop statistics
[diag.info]