Firewall: Diagnostics: States Dump - refactor to MVC.

o add api endpoint and ui to kill states using filter (kill by host or network as well)
o show ruleids in service control spot to filter states for a specific (auto-generated) rule
o support passing a ruleid to the ui page to limit selection, e.g. /ui/diagnostics/firewall/states#d0953c4424f27d5249027086b4599999
This commit is contained in:
Ad Schellevis 2021-06-21 18:27:21 +02:00
parent 512b83463b
commit 63bdff8cf3
7 changed files with 232 additions and 4 deletions

View File

@ -166,16 +166,20 @@ class FirewallController extends ApiControllerBase
}
]);
$searchPhrase = '';
$ruleLabel = '';
$ruleId = '';
$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');
}
$response = (new Backend())->configdpRun('filter list states', [$searchPhrase, $itemsPerPage,
($currentPage - 1) * $itemsPerPage, $ruleLabel]);
($currentPage - 1) * $itemsPerPage, $ruleId]);
$response = json_decode($response, true);
if ($response != null) {
foreach ($response['details'] as &$row) {
@ -214,4 +218,51 @@ class FirewallController extends ApiControllerBase
}
return ['result' => ""];
}
/**
* drop pf states by filter and/or rule id
*/
public function killStatesAction()
{
if ($this->request->isPost()) {
$filter = new Filter([
'query' => function ($value) {
return preg_replace("/[^0-9,a-z,A-Z, ,\/,*,\-,_,.,\#]/", "", $value);
},
'hexval' => function ($value) {
return preg_replace("/[^0-9,a-f,A-F]/", "", $value);
}
]);
$ruleid = null;
$filterString = null;
if (!empty($this->request->getPost('filter'))) {
$filterString = $filter->sanitize($this->request->getPost('filter'), 'query');
}
if (!empty($this->request->getPost('ruleid'))) {
$ruleid = $filter->sanitize($this->request->getPost('ruleid'), 'hexval');
}
if ($filterString != null || $ruleid != null) {
$response = (new Backend())->configdpRun("filter kill states", [$filterString, $ruleid]);
$response = json_decode($response, true);
if ($response != null) {
return ["result" => "ok", "dropped_states" => $response['dropped_states']];
}
}
}
return ["result" => "failed"];
}
/**
* return rule'ids and descriptions from running config
*/
public function listRuleIdsAction()
{
if ($this->request->isGet()) {
$response = json_decode((new Backend())->configdpRun("filter list rule_ids"), true);
if ($response != null) {
return ["items" => $response];
}
}
return ["items" => []];
}
}

View File

@ -177,6 +177,7 @@
<pattern>ui/diagnostics/firewall/states*</pattern>
<pattern>api/diagnostics/firewall/query_states*</pattern>
<pattern>api/diagnostics/firewall/del_state*</pattern>
<pattern>api/diagnostics/firewall/kill_states*</pattern>
</patterns>
</page-diagnostics-showstates>
<page-diagnostics-statessummary>

View File

@ -68,17 +68,86 @@ POSSIBILITY OF SUCH DAMAGE.
}
return "";
}
}
},
requestHandler:function(request){
if ($("#ruleid").val() != "") {
request['ruleid'] = $("#ruleid").val();
}
return request;
},
}
}
);
grid_states.on('loaded.rs.jquery.bootgrid', function() {
$('[data-toggle="tooltip"]').tooltip();
if ($(".search-field").val() !== "") {
$("#actKillStates").show();
} else {
$("#actKillStates").hide();
}
});
// collect rule id's
ajaxGet("/api/diagnostics/firewall/list_rule_ids", {}, function(data, status){
if (data.items) {
for (let i=0; i < data.items.length ; ++i) {
$("#ruleid").append($("<option/>").val(data.items[i]['id']).text(data.items[i]['descr']));
}
$("#service_status_container").append($("#ruleid"));
$("#ruleid").change(function(){
$("#grid-states").bootgrid("reload");
});
let init_state = window.location.hash.substr(1);
if (init_state) {
$("#ruleid").val(init_state);
$("#ruleid").change();
}
}
});
$("#actKillStates").click(function(){
BootstrapDialog.show({
type:BootstrapDialog.TYPE_DANGER,
title: "{{ lang._('Delete all filtered states') }}",
message: "{{ lang._('Are you sure you do want to kill all states matching the provided search criteria?') }}",
buttons: [{
label: "{{ lang._('No') }}",
action: function(dialogRef) {
dialogRef.close();
}}, {
label: "{{ lang._('Yes') }}",
action: function(dialogRef) {
let params = {'filter': $(".search-field").val()};
if ($("#ruleid").val() != "") {
params['ruleid'] = $("#ruleid").val();
}
ajaxCall('/api/diagnostics/firewall/kill_states/', params, function(data, status){
$("#grid-states").bootgrid("reload");
dialogRef.close();
});
}
}]
});
});
// move kill states button
$("div.search.form-group").before($("#actKillStates"));
});
</script>
<div style="display:none">
<div class="btn-group" id="actKillStates" style="margin-right:20px;">
<button class="btn btn-danger" style="cursor: pointer;" data-toggle="tooltip" title="{{ lang._('kill all matched states') }}">
<span class="fa fa-remove"></span>
</button>
</div>
</div>
<select id="ruleid">
<option value="">{{ lang._("select rule") }}</option>
</select>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#grid-states">{{ lang._('States') }}</a></li>
</ul>

View File

@ -0,0 +1,52 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2021 Ad Schellevis <ad@opnsense.org>
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 argparse
import ujson
import subprocess
from lib.states import query_states
if __name__ == '__main__':
result = dict()
# parse input arguments
parser = argparse.ArgumentParser()
parser.add_argument('--filter', help='filter results', default='')
parser.add_argument('--label', help='label / rule id', default='')
inputargs = parser.parse_args()
# collect all unique state id's
commands = dict()
for record in query_states(rule_label=inputargs.label, filter_str=inputargs.filter):
commands[record['id']] = "/sbin/pfctl -k id -k %s" % record['id']
# drop list of states in chunks
chunk_size = 500
commands = list(commands.values())
for chunk in [commands[i:i + chunk_size] for i in range(0, len(commands), chunk_size)]:
sp = subprocess.run([";\n".join(chunk)], capture_output=True, text=True, shell=True)
print(ujson.dumps({'dropped_states': len(commands)}))

View File

@ -0,0 +1,44 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2021 Ad Schellevis <ad@opnsense.org>
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
from lib.states import fetch_rule_labels
HEX_DIGITS = set("0123456789abcdef")
result = list()
unique_ids = set()
for record in fetch_rule_labels().values():
if len(record['rid']) == 32 and set(record['rid']).issubset(HEX_DIGITS):
if record['rid'] not in unique_ids:
if record['descr'] != "":
result.append({'id': record['rid'], 'descr': record['descr']})
else:
result.append({'id': record['rid'], 'descr': "-- %s" % record['rid']})
unique_ids.add(record['rid'])
result = sorted(result, key=lambda k: k['descr'])
print(ujson.dumps(result))

View File

@ -42,11 +42,11 @@ if __name__ == '__main__':
parser.add_argument('--offset', help='offset results', default='')
inputargs = parser.parse_args()
# apply offset and limit
result = {
'details': query_states(rule_label=inputargs.label, filter_str=inputargs.filter)
}
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):

View File

@ -41,6 +41,11 @@ parameters: --filter=%s --limit=%s --offset=%s --label=%s
type:script_output
message:request pf states
[list.rule_ids]
command:/usr/local/opnsense/scripts/filter/list_rule_ids.py
parameters:
type:script_output
message:request active rule id's and descriptions
[list.pfsync]
command:/usr/local/opnsense/scripts/filter/list_pfsync.py
@ -114,6 +119,12 @@ parameters: -k id -k %s/%s 2>&1 && exit 0
type:script_output
message:kill pf state ( %s %s )
[kill.states]
command:/usr/local/opnsense/scripts/filter/kill_states.py
parameters: --filter=%s --label=%s
type:script_output
message:kill pf states
[find_table_references]
command:/usr/local/opnsense/scripts/filter/find_table_references.py
parameters: %s