mirror of
https://github.com/lucaspalomodevelop/core.git
synced 2026-03-13 00:07:26 +00:00
Firewall: Automation filter ui revamp (#8377)
This commit adds backwards compatible changes to the automation api and associated user interface. Although this is likely not the final state, it adds quite some improvements in making this a valid replacement for the current firewall user interface.
This commit is contained in:
parent
0759133373
commit
af5e9fcbf8
3
plist
3
plist
@ -752,6 +752,7 @@
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasNameField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/FilterRuleField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/FilterSequenceField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/GroupField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/GroupNameField.php
|
||||
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/InterfaceField.php
|
||||
@ -946,6 +947,7 @@
|
||||
/usr/local/opnsense/mvc/app/views/OPNsense/Firewall/alias_util.volt
|
||||
/usr/local/opnsense/mvc/app/views/OPNsense/Firewall/category.volt
|
||||
/usr/local/opnsense/mvc/app/views/OPNsense/Firewall/filter.volt
|
||||
/usr/local/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt
|
||||
/usr/local/opnsense/mvc/app/views/OPNsense/Firewall/group.volt
|
||||
/usr/local/opnsense/mvc/app/views/OPNsense/IDS/index.volt
|
||||
/usr/local/opnsense/mvc/app/views/OPNsense/IDS/policy.volt
|
||||
@ -1118,6 +1120,7 @@
|
||||
/usr/local/opnsense/scripts/filter/lib/alias/pf.py
|
||||
/usr/local/opnsense/scripts/filter/lib/alias/uri.py
|
||||
/usr/local/opnsense/scripts/filter/lib/states.py
|
||||
/usr/local/opnsense/scripts/filter/list_non_mvc_rules.php
|
||||
/usr/local/opnsense/scripts/filter/list_osfp.py
|
||||
/usr/local/opnsense/scripts/filter/list_pfsync.py
|
||||
/usr/local/opnsense/scripts/filter/list_rule_ids.py
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 Deciso B.V.
|
||||
* Copyright (C) 2020-2025 Deciso B.V.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
@ -27,17 +27,174 @@
|
||||
*/
|
||||
namespace OPNsense\Firewall\Api;
|
||||
|
||||
use OPNsense\Base\UserException;
|
||||
use OPNsense\Core\Config;
|
||||
use OPNsense\Core\Backend;
|
||||
use OPNsense\Firewall\Category;
|
||||
use OPNsense\Firewall\Group;
|
||||
use OPNsense\Firewall\Util;
|
||||
|
||||
|
||||
class FilterController extends FilterBaseController
|
||||
{
|
||||
protected static $categorysource = "rules.rule";
|
||||
|
||||
private function getFieldMap()
|
||||
{
|
||||
$result = ['category' => [], 'interface' => []];
|
||||
foreach ((new Category())->categories->category->iterateItems() as $key => $category) {
|
||||
$result['category'][$key] = (string)$category->name;
|
||||
}
|
||||
foreach ((Config::getInstance()->object())->interfaces->children() as $key => $ifdetail) {
|
||||
$descr = !empty($ifdetail->descr) ? $ifdetail->descr : strtoupper($key);
|
||||
$result['interface'][$key] = $descr;
|
||||
}
|
||||
$result['action'] = [
|
||||
'pass' => 'Pass',
|
||||
'block' => 'Block',
|
||||
'reject' => 'Reject'
|
||||
];
|
||||
|
||||
$result['ipprotocol'] = [
|
||||
'inet' => gettext('IPv4'),
|
||||
'inet6' => gettext('IPv6'),
|
||||
'inet46' => gettext('IPv4+IPv6')
|
||||
];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and merges firewall rules from model and internal sources, then paginates them.
|
||||
*
|
||||
* @return array The final paginated, merged result.
|
||||
*/
|
||||
public function searchRuleAction()
|
||||
{
|
||||
$category = $this->request->get('category');
|
||||
$filter_funct = function ($record) use ($category) {
|
||||
return empty($category) || array_intersect(explode(',', $record->categories), $category);
|
||||
$categories = $this->request->get('category');
|
||||
if (!empty($this->request->get('interface'))) {
|
||||
$interface = $this->request->get('interface');
|
||||
$interfaces = [$interface];
|
||||
/* add groups which contain the selected interface */
|
||||
foreach ((new Group())->ifgroupentry->iterateItems() as $groupItem) {
|
||||
if (in_array($interface, explode(',', (string)$groupItem->members))) {
|
||||
$interfaces[] = (string)$groupItem->ifname;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$interfaces = null;
|
||||
}
|
||||
$show_all = !empty($this->request->get('show_all'));
|
||||
|
||||
/* filter logic for mvc rules */
|
||||
$filter_funct_mvc = function ($record) use ($categories, $interfaces, $show_all) {
|
||||
$is_cat = empty($categories) || array_intersect(explode(',', $record->categories), $categories);
|
||||
if (empty($interfaces)) {
|
||||
$is_if = count(explode(',', $record->interface)) > 1 || $record->interface->isEmpty();
|
||||
} else {
|
||||
$is_if = array_intersect(explode(',', $record->interface), $interfaces) || $record->interface->isEmpty();
|
||||
}
|
||||
return $is_cat && $is_if;
|
||||
};
|
||||
return $this->searchBase("rules.rule", ['enabled', 'sequence', 'action', 'description'], "sequence", $filter_funct);
|
||||
|
||||
/* filter logic for legacy and internal rules */
|
||||
$fieldmap = $this->getFieldMap();
|
||||
if ($show_all) {
|
||||
/* only query stats when fill info is requested */
|
||||
$rule_stats = json_decode((new Backend())->configdRun("filter rule stats") ?? '', true) ?? [];
|
||||
} else {
|
||||
$rule_stats = [];
|
||||
}
|
||||
|
||||
$catcolors = [];
|
||||
foreach ((new Category())->categories->category->iterateItems() as $category) {
|
||||
$color = trim((string)$category->color);
|
||||
// Assign default color if empty
|
||||
$catcolors[trim((string)$category->name)] = empty($color) ? "#C03E14" : "#{$color}";
|
||||
}
|
||||
|
||||
$filter_funct_rs = function (&$record) use (
|
||||
$categories,
|
||||
$interfaces,
|
||||
$show_all,
|
||||
$fieldmap,
|
||||
$rule_stats,
|
||||
$catcolors
|
||||
) {
|
||||
/* always merge stats when found */
|
||||
if (!empty($record['uuid']) && !empty($rule_stats[$record['uuid']])) {
|
||||
foreach ($rule_stats[$record['uuid']] as $key => $value) {
|
||||
$record[$key] = $value;
|
||||
}
|
||||
}
|
||||
/* frontend can format aliases with an alias icon */
|
||||
foreach (['source_net', 'source_port', 'destination_net', 'destination_port'] as $field) {
|
||||
if (!empty($record[$field])) {
|
||||
$record["is_alias_{$field}"] = array_map(function ($value) {
|
||||
return Util::isAlias($value);
|
||||
}, array_map('trim', explode(',', $record[$field])));
|
||||
}
|
||||
}
|
||||
|
||||
/* frontend can format categories with colors */
|
||||
if (!empty($record['categories'])) {
|
||||
$catnames = array_map('trim', explode(',', $record['categories']));
|
||||
$record['category_colors'] = array_map(fn($name) => $catcolors[$name], $catnames);;
|
||||
} else {
|
||||
$record['category_colors'] = [];
|
||||
}
|
||||
|
||||
|
||||
if (empty($record['legacy'])) {
|
||||
/* mvc already filtered */
|
||||
return true;
|
||||
}
|
||||
$is_cat = empty($categories) || array_intersect(explode(',', $record['category'] ?? ''), $categories);
|
||||
|
||||
if (empty($interfaces)) {
|
||||
$is_if = count(explode(',', $record['interface'])) > 1 || empty($record['interface']);
|
||||
} else {
|
||||
$is_if = array_intersect(explode(',', $record['interface'] ?? ''), $interfaces) ;
|
||||
$is_if = $is_if || empty($record['interface']);
|
||||
}
|
||||
if ($is_cat && $is_if) {
|
||||
/* translate/convert legacy fields before returning, similar to mvc handling */
|
||||
foreach ($fieldmap as $topic => $data) {
|
||||
if (!empty($record[$topic])) {
|
||||
$tmp = [];
|
||||
foreach (explode(',', $record[$topic]) as $item) {
|
||||
$tmp[] = $data[$item] ?? $item;
|
||||
}
|
||||
$record[$topic] = implode(',', $tmp);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* XXX: fetch mvc results first, we need to collect all to ensure proper pagination
|
||||
* as pagination is passed using the request, we need to reset it temporary here as we don't know
|
||||
* which page we need (yet) and don't want to duplicate large portions of code.
|
||||
**/
|
||||
$ORG_REQ = $_REQUEST;
|
||||
unset($_REQUEST['rowCount']);
|
||||
unset($_REQUEST['current']);
|
||||
$filterset = $this->searchBase("rules.rule", null, "sort_order", $filter_funct_mvc)['rows'];
|
||||
|
||||
/* only fetch internal and legacy rules when 'show_all' is set */
|
||||
if ($show_all) {
|
||||
$otherrules = json_decode((new Backend())->configdRun("filter list non_mvc_rules") ?? '', true) ?? [];
|
||||
} else {
|
||||
$otherrules = [];
|
||||
}
|
||||
|
||||
$_REQUEST = $ORG_REQ; /* XXX: fix me ?*/
|
||||
$result = $this->searchRecordsetBase(array_merge($otherrules, $filterset), null, "sort_order", $filter_funct_rs);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function setRuleAction($uuid)
|
||||
@ -52,7 +209,16 @@ class FilterController extends FilterBaseController
|
||||
|
||||
public function getRuleAction($uuid = null)
|
||||
{
|
||||
return $this->getBase("rule", "rules.rule", $uuid);
|
||||
$result = $this->getBase("rule", "rules.rule", $uuid);
|
||||
if ($this->request->get('fetchmode') === 'copy' && !empty($result['rule'])) {
|
||||
/* copy mode, generate new sequence at the end */
|
||||
$max = 0;
|
||||
foreach ($this->getModel()->rules->rule->iterateItems() as $rule) {
|
||||
$max = (int)((string)$rule->sequence) > $max ? (int)((string)$rule->sequence) : $max;
|
||||
}
|
||||
$result['rule']['sequence'] = $max + 100;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function delRuleAction($uuid)
|
||||
@ -64,4 +230,152 @@ class FilterController extends FilterBaseController
|
||||
{
|
||||
return $this->toggleBase("rules.rule", $uuid, $enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the selected rule so that it appears immediately before the target rule.
|
||||
*
|
||||
* Uses integer gap numbering to update the sequence for only the moved rule.
|
||||
* Rules will be renumbered within the selected range to prevent movements causing overlaps,
|
||||
* but try to keep the changes as minimal as possible.
|
||||
*
|
||||
* Floating, Group, and Interface rules cannot be moved before another.
|
||||
*
|
||||
* @param string $selected_uuid The UUID of the rule to be moved.
|
||||
* @param string $target_uuid The UUID of the target rule (the rule before which the selected rule is to be placed).
|
||||
* @return array Returns ["status" => "ok"] on success, throws a userexception otherwise.
|
||||
*/
|
||||
public function moveRuleBeforeAction($selected_uuid, $target_uuid)
|
||||
{
|
||||
if (!$this->request->isPost()) {
|
||||
return ["status" => "error", "message" => gettext("Invalid request method")];
|
||||
}
|
||||
Config::getInstance()->lock();
|
||||
$mdl = $this->getModel();
|
||||
$prev_record = null;
|
||||
$selected_id = null;
|
||||
$selected_node = null;
|
||||
$target_node = null;
|
||||
foreach ($mdl->rules->rule->sortedBy(['prio_group', 'sequence']) as $record) {
|
||||
$uuid = $record->getAttribute('uuid');
|
||||
if ($prev_record != null && (string)$prev_record->prio_group == (string)$record->prio_group) {
|
||||
$prev_sequence = $prev_record->sequence->asFloat();
|
||||
/* distance will be averaged, which is why the minimum should be at least 2 (half is 1) */
|
||||
$distance = max($record->sequence->asFloat() - $prev_record->sequence->asFloat(), 2);
|
||||
} elseif ($selected_node !== null && $target_node !== null){
|
||||
/* group processed */
|
||||
break;
|
||||
} else {
|
||||
/* first record, no previous one */
|
||||
$prev_sequence = 1;
|
||||
$distance = 2;
|
||||
}
|
||||
|
||||
if ($uuid == $target_uuid) {
|
||||
/**
|
||||
* found our target, which will be the sources new place,
|
||||
* reserve the full distance to facilitate for a swap.
|
||||
**/
|
||||
$selected_id = (int)$record->sequence->asFloat();
|
||||
$record->sequence = (string)($prev_sequence + $distance);
|
||||
$target_node = $record;
|
||||
} elseif ($uuid == $selected_uuid) {
|
||||
$selected_node = $record;
|
||||
} elseif ($selected_id !== null && $prev_sequence >= $record->sequence->asFloat()) {
|
||||
$record->sequence = (string)($prev_sequence + $distance/2);
|
||||
} elseif ($target_node !== null && $selected_node !== null) {
|
||||
/* both nodes found and the next one is in sequence, stop moving data */
|
||||
break;
|
||||
}
|
||||
/* validate overflow */
|
||||
if ($record->sequence->asFloat() > 999999) {
|
||||
throw new UserException(
|
||||
gettext("Cannot renumber rules without exceeding the maximum sequence limit"),
|
||||
gettext("Filter")
|
||||
);
|
||||
}
|
||||
$prev_record = $record;
|
||||
}
|
||||
|
||||
if ($selected_node !== null) {
|
||||
$selected_node->sequence = (string)$selected_id;
|
||||
}
|
||||
|
||||
/* validate what we plan to commit */
|
||||
if ($selected_node === null || $target_node === null) {
|
||||
/* out of scope */
|
||||
throw new UserException(
|
||||
gettext("Either source or destination is not a rule managed with this component"),
|
||||
gettext("Filter")
|
||||
);
|
||||
} elseif ((string)$selected_node->prio_group != (string)$target_node->prio_group) {
|
||||
/* types don't match */
|
||||
$typeNames = [
|
||||
'2' => gettext("Floating"),
|
||||
'3' => gettext("Group"),
|
||||
'4' => gettext("Interface")
|
||||
];
|
||||
$selectedType = $typeNames[substr($selected_node->prio_group, 0, 1)] ?? gettext("Unknown");
|
||||
$targetType = $typeNames[substr($target_node->prio_group, 0, 1)] ?? gettext("Unknown");
|
||||
throw new UserException(
|
||||
sprintf(
|
||||
gettext("Cannot move '%s Rule' before '%s Rule'."),
|
||||
$selectedType,
|
||||
$targetType
|
||||
),
|
||||
gettext("Filter")
|
||||
);
|
||||
}
|
||||
$mdl->serializeToConfig(false, true); /* we're only changing sequences, forcefully save */
|
||||
Config::getInstance()->save();
|
||||
|
||||
return ["status" => "ok"];
|
||||
}
|
||||
|
||||
/**
|
||||
* return interface options
|
||||
*/
|
||||
public function getInterfaceListAction()
|
||||
{
|
||||
$result = [
|
||||
'floating' => [
|
||||
'label' => gettext('Floating'),
|
||||
'icon' => 'fa fa-layer-group text-primary',
|
||||
'items' => [
|
||||
[
|
||||
'value' => '',
|
||||
'label' => gettext('Any')
|
||||
]
|
||||
]
|
||||
],
|
||||
'groups' => [
|
||||
'label' => gettext('Groups'),
|
||||
'icon' => 'fa fa-sitemap text-warning',
|
||||
'items' => []
|
||||
],
|
||||
'interfaces' => [
|
||||
'label' => gettext('Interfaces'),
|
||||
'icon' => 'fa fa-ethernet text-info',
|
||||
'items' => []
|
||||
]
|
||||
];
|
||||
|
||||
foreach ((new Group())->ifgroupentry->iterateItems() as $groupItem) {
|
||||
$groupName = (string)$groupItem->ifname;
|
||||
$result['groups']['items'][$groupName] = ['value' => $groupName, 'label' => $groupName];
|
||||
}
|
||||
foreach (Config::getInstance()->object()->interfaces->children() as $key => $intf) {
|
||||
if (!isset($result['groups']['items'][$key])) {
|
||||
$result['interfaces']['items'][$key] = [
|
||||
'value' => $key,
|
||||
'label' => empty($intf->descr) ? strtoupper($key) : (string)$intf->descr
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($result) as $key) {
|
||||
usort($result[$key]['items'], fn($a, $b) => strcasecmp($a['label'], $b['label']));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 Deciso B.V.
|
||||
* Copyright (C) 2020-2025 Deciso B.V.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
@ -31,20 +31,32 @@ class FilterController extends \OPNsense\Base\IndexController
|
||||
{
|
||||
public function indexAction()
|
||||
{
|
||||
$this->view->pick('OPNsense/Firewall/filter');
|
||||
$this->view->SavePointBtns = true;
|
||||
$this->view->ruleController = "filter";
|
||||
$this->view->gridFields = [
|
||||
[
|
||||
'id' => 'enabled', 'formatter' => 'rowtoggle' ,'width' => '6em', 'heading' => gettext('Enabled')
|
||||
],
|
||||
[
|
||||
'id' => 'sequence','width' => '9em', 'heading' => gettext('Sequence')
|
||||
],
|
||||
[
|
||||
'id' => 'description', 'heading' => gettext('Description')
|
||||
]
|
||||
];
|
||||
$this->view->pick('OPNsense/Firewall/filter_rule');
|
||||
$this->view->formDialogFilterRule = $this->getForm("dialogFilterRule");
|
||||
$this->view->formGridFilterRule = $this->getFormGrid('dialogFilterRule');
|
||||
$this->view->advancedFieldIds = $this->getAdvancedIds($this->view->formDialogFilterRule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of field IDs that have the advanced flag set to "true".
|
||||
*
|
||||
* @param array $form An array of field definitions
|
||||
* @return string list of fieldnames, comma separated for easy template usage
|
||||
*/
|
||||
protected function getAdvancedIds($form)
|
||||
{
|
||||
$advancedFieldIds = [];
|
||||
|
||||
foreach ($form as $field) {
|
||||
if (!empty($field['advanced']) && $field['advanced'] == "true") {
|
||||
if (!empty($field['id'])) {
|
||||
$tmp = explode('.', $field['id']);
|
||||
$advancedFieldIds[] = $tmp[count($tmp)-1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode(',', $advancedFieldIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,13 +1,46 @@
|
||||
|
||||
<form>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Organisation</label>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.enabled</id>
|
||||
<label>Enabled</label>
|
||||
<type>checkbox</type>
|
||||
<help>Enable this rule</help>
|
||||
<grid_view>
|
||||
<width>2em</width>
|
||||
<type>boolean</type>
|
||||
<formatter>rowtoggle</formatter>
|
||||
<sequence>10</sequence>
|
||||
<align>center</align>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.sort_order</id>
|
||||
<label>Sort order</label>
|
||||
<type>info</type>
|
||||
<help>The order in which rules are being processed.</help>
|
||||
<grid_view>
|
||||
<sequence>20</sequence>
|
||||
<visible>false</visible>
|
||||
<!-- The sequence order of firewall rules is absolute, no other field shall be sorted -->
|
||||
<order>asc</order>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.sequence</id>
|
||||
<label>Sequence</label>
|
||||
<type>text</type>
|
||||
<help>The order in which rules are being considered.</help>
|
||||
<help>The order in which rules are being processed. Please note that this is not a unique identifier, the system will automatically recalculate the ruleset when rule positions are changed with the available "Move rule before this rule" button.</help>
|
||||
<grid_view>
|
||||
<sequence>20</sequence>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.categories</id>
|
||||
@ -15,36 +48,62 @@
|
||||
<type>select_multiple</type>
|
||||
<style>tokenize</style>
|
||||
<help>For grouping purposes you may select multiple groups here to organize items.</help>
|
||||
<grid_view>
|
||||
<width>3em</width>
|
||||
<sortable>false</sortable>
|
||||
<formatter>category</formatter>
|
||||
<sequence>112</sequence>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.nosync</id>
|
||||
<label>No XMLRPC Sync</label>
|
||||
<type>checkbox</type>
|
||||
<help>Hint: This prevents the rule on Master from automatically syncing to other CARP members. This does NOT prevent the rule from being overwritten on Slave.</help>
|
||||
<help>Exclude this item from the HA synchronization process. An already existing item with the same UUID on the synchronization target will not be altered or deleted as long as this is active.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help>You may enter a description here for your reference (not parsed).</help>
|
||||
<grid_view>
|
||||
<sequence>110</sequence>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>General Settings</label>
|
||||
<label>Interface</label>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.enabled</id>
|
||||
<label>enabled</label>
|
||||
<id>rule.interfacenot</id>
|
||||
<label>Invert Interface</label>
|
||||
<type>checkbox</type>
|
||||
<help>Enable this rule</help>
|
||||
<help>Use all but selected interfaces</help>
|
||||
<grid_view>
|
||||
<ignore>true</ignore>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.action</id>
|
||||
<label>Action</label>
|
||||
<type>dropdown</type>
|
||||
<help>Choose what to do with packets that match the criteria specified below.
|
||||
Hint: the difference between block and reject is that with reject, a packet (TCP RST or ICMP port unreachable for UDP) is returned to the sender, whereas with block the packet is dropped silently. In either case, the original packet is discarded.
|
||||
</help>
|
||||
<id>rule.interface</id>
|
||||
<label>Interface</label>
|
||||
<type>select_multiple</type>
|
||||
<hint>any</hint>
|
||||
<grid_view>
|
||||
<formatter>interfaces</formatter>
|
||||
<sequence>25</sequence>
|
||||
<sortable>false</sortable>
|
||||
<width>4em</width>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Filter</label>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.quick</id>
|
||||
@ -54,6 +113,20 @@
|
||||
If a packet matches a rule specifying quick, then that rule is considered the last matching rule and the specified action is taken.
|
||||
When a rule does not have quick enabled, the last matching rule wins.
|
||||
</help>
|
||||
<grid_view>
|
||||
<ignore>true</ignore>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.action</id>
|
||||
<label>Action</label>
|
||||
<type>dropdown</type>
|
||||
<help>Choose what to do with packets that match the criteria specified below.
|
||||
Hint: the difference between block and reject is that with reject, a packet (TCP RST or ICMP port unreachable for UDP) is returned to the sender, whereas with block the packet is dropped silently. In either case, the original packet is discarded.
|
||||
</help>
|
||||
<grid_view>
|
||||
<ignore>true</ignore>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.allowopts</id>
|
||||
@ -61,17 +134,10 @@
|
||||
<type>checkbox</type>
|
||||
<help>This allows packets with IP options to pass. Otherwise they are blocked by default.</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.interfacenot</id>
|
||||
<label>Interface / Invert</label>
|
||||
<type>checkbox</type>
|
||||
<help>Use all but selected interfaces</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.interface</id>
|
||||
<label>Interface</label>
|
||||
<type>select_multiple</type>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.direction</id>
|
||||
@ -80,59 +146,101 @@
|
||||
<help>
|
||||
Direction of the traffic. The default policy is to filter inbound traffic, which sets the policy to the interface originally receiving the traffic.
|
||||
</help>
|
||||
<grid_view>
|
||||
<ignore>true</ignore>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.ipprotocol</id>
|
||||
<label>TCP/IP Version</label>
|
||||
<label>Version</label>
|
||||
<type>dropdown</type>
|
||||
<grid_view>
|
||||
<ignore>true</ignore>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.protocol</id>
|
||||
<label>Protocol</label>
|
||||
<type>dropdown</type>
|
||||
<grid_view>
|
||||
<formatter>protocol</formatter>
|
||||
<width>6em</width>
|
||||
<sequence>40</sequence>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.source_not</id>
|
||||
<label>Invert Source</label>
|
||||
<type>checkbox</type>
|
||||
<help>Use this option to invert the sense of the match.</help>
|
||||
<grid_view>
|
||||
<ignore>true</ignore>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.source_net</id>
|
||||
<label>Source</label>
|
||||
<type>text</type>
|
||||
<style>net_selector net_selector_multi</style>
|
||||
<grid_view>
|
||||
<formatter>alias</formatter>
|
||||
<sequence>50</sequence>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.source_port</id>
|
||||
<label>Source port</label>
|
||||
<label>Source Port</label>
|
||||
<type>text</type>
|
||||
<advanced>true</advanced>
|
||||
<help>Source port number or well known name (imap, imaps, http, https, ...), for ranges use a dash</help>
|
||||
<hint>any</hint>
|
||||
<grid_view>
|
||||
<sequence>60</sequence>
|
||||
<sortable>false</sortable>
|
||||
<formatter>alias</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.source_not</id>
|
||||
<label>Source / Invert</label>
|
||||
<id>rule.destination_not</id>
|
||||
<label>Invert Destination</label>
|
||||
<type>checkbox</type>
|
||||
<help>Use this option to invert the sense of the match.</help>
|
||||
<grid_view>
|
||||
<ignore>true</ignore>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.destination_net</id>
|
||||
<label>Destination</label>
|
||||
<type>text</type>
|
||||
<style>net_selector net_selector_multi</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.destination_not</id>
|
||||
<label>Destination / Invert</label>
|
||||
<type>checkbox</type>
|
||||
<help>Use this option to invert the sense of the match.</help>
|
||||
<grid_view>
|
||||
<formatter>alias</formatter>
|
||||
<sequence>70</sequence>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.destination_port</id>
|
||||
<label>Destination port</label>
|
||||
<label>Destination Port</label>
|
||||
<type>text</type>
|
||||
<help>Destination port number or well known name (imap, imaps, http, https, ...), for ranges use a dash</help>
|
||||
<hint>any</hint>
|
||||
<grid_view>
|
||||
<sequence>80</sequence>
|
||||
<sortable>false</sortable>
|
||||
<formatter>alias</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.log</id>
|
||||
<label>Log</label>
|
||||
<type>checkbox</type>
|
||||
<help>Log packets that are handled by this rule</help>
|
||||
<grid_view>
|
||||
<ignore>true</ignore>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.tcpflags1</id>
|
||||
@ -140,6 +248,10 @@
|
||||
<type>select_multiple</type>
|
||||
<help>Use this to choose TCP flags that must be set this rule to match.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.tcpflags2</id>
|
||||
@ -147,21 +259,36 @@
|
||||
<type>select_multiple</type>
|
||||
<help>Use this to choose TCP flags that must be cleared for this rule to match.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.sched</id>
|
||||
<label>Schedule</label>
|
||||
<type>dropdown</type>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Stateful firewall</label>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.statetype</id>
|
||||
<label>State type</label>
|
||||
<type>dropdown</type>
|
||||
<help>State tracking mechanism to use, default is full stateful tracking, sloppy ignores sequence numbers, use none for stateless rules.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.state-policy</id>
|
||||
@ -172,6 +299,11 @@
|
||||
floating in which case states are valid on all interfaces or interface bound.
|
||||
Interface bound states are more secure, floating more flexible
|
||||
</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.statetimeout</id>
|
||||
@ -179,6 +311,10 @@
|
||||
<type>text</type>
|
||||
<help>State Timeout in seconds (TCP only)</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.adaptivestart</id>
|
||||
@ -186,6 +322,10 @@
|
||||
<type>text</type>
|
||||
<help>When the number of state entries exceeds this value, adaptive scaling begins. All timeout values are scaled linearly with factor (adaptive.end - number of states) / (adaptive.end - adaptive.start).</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.adaptiveend</id>
|
||||
@ -193,6 +333,10 @@
|
||||
<type>text</type>
|
||||
<help>When reaching this number of state entries, all timeout values become zero, effectively purging all state entries immediately. This value is used to define the scale factor, it should not actually be reached (set a lower state limit).</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.max</id>
|
||||
@ -203,6 +347,10 @@
|
||||
When this limit is reached, further packets that would create state are dropped until existing states time out.
|
||||
</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.max-src-nodes</id>
|
||||
@ -210,6 +358,10 @@
|
||||
<type>text</type>
|
||||
<help>Limits the maximum number of source addresses which can simultaneously have state table entries.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.max-src-states</id>
|
||||
@ -217,6 +369,10 @@
|
||||
<type>text</type>
|
||||
<help>Limits the maximum number of simultaneous state entries that a single source address can create with this rule.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.max-src-conn</id>
|
||||
@ -224,6 +380,10 @@
|
||||
<type>text</type>
|
||||
<help>Limit the maximum number of simultaneous TCP connections which have completed the 3-way handshake that a single host can make.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.max-src-conn-rate</id>
|
||||
@ -231,6 +391,10 @@
|
||||
<type>text</type>
|
||||
<help>Maximum new connections per host, measured over time.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.max-src-conn-rates</id>
|
||||
@ -238,43 +402,69 @@
|
||||
<type>text</type>
|
||||
<help>Time interval (seconds) to measure the number of connections</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.overload</id>
|
||||
<label>Overload table</label>
|
||||
<type>dropdown</type>
|
||||
<advanced>true</advanced>
|
||||
<help>
|
||||
Overload table used when max new connections per time interval has been reached.
|
||||
The default virusprot table comes with a default block rule in floating rules,
|
||||
alternatively specify your own table here
|
||||
</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.nopfsync</id>
|
||||
<label>NO pfsync</label>
|
||||
<type>checkbox</type>
|
||||
<help>Hint: This prevents states created by this rule to be sync'ed over pfsync.</help>
|
||||
<help>This prevents states created by this rule to be synced with pfsync.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<type>boolean</type>
|
||||
<formatter>boolean</formatter>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Traffic shaping [experimental]</label>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.shaper1</id>
|
||||
<label>Traffic shaper</label>
|
||||
<type>dropdown</type>
|
||||
<help>Shape packets using the selected pipe or queue in the rule direction.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.shaper2</id>
|
||||
<label>Traffic shaper [reverse]</label>
|
||||
<type>dropdown</type>
|
||||
<help>Shape packets using the selected pipe or queue in the reverse rule direction.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Source routing</label>
|
||||
<label>Source Routing</label>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.gateway</id>
|
||||
@ -283,6 +473,11 @@
|
||||
<help>
|
||||
Leave as 'default' to use the system routing table. Or choose a gateway to utilize policy based routing.
|
||||
</help>
|
||||
<grid_view>
|
||||
<formatter>any</formatter>
|
||||
<sequence>100</sequence>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.disablereplyto</id>
|
||||
@ -290,6 +485,13 @@
|
||||
<type>checkbox</type>
|
||||
<style>disable_replyto</style>
|
||||
<help>Explicit disable reply-to for this rule</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<type>boolean</type>
|
||||
<formatter>boolean</formatter>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.replyto</id>
|
||||
@ -299,17 +501,28 @@
|
||||
<help>
|
||||
Determines how packets route back in the opposite direction (replies), when set to default, packets on WAN type interfaces reply to their connected gateway on the interface (unless globally disabled). A specific gateway may be chosen as well here. This setting is only relevant in the context of a state, for stateless rules there is no defined opposite direction.
|
||||
</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
<formatter>default</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Priority</label>
|
||||
<collapse>true</collapse>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.prio</id>
|
||||
<label>Match priority</label>
|
||||
<type>dropdown</type>
|
||||
<help>Only match packets which have the given queueing priority assigned.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.set-prio</id>
|
||||
@ -321,6 +534,11 @@
|
||||
will be written as the priority code point in the 802.1Q VLAN
|
||||
header
|
||||
</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.set-prio-low</id>
|
||||
@ -330,16 +548,26 @@
|
||||
Used in combination with set priority, packets which have a TOS of lowdelay and TCP ACKs with no
|
||||
data payload will be assigned this priority when offered.
|
||||
</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.tos</id>
|
||||
<label>Match TOS / DSCP</label>
|
||||
<type>dropdown</type>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Internal tagging</label>
|
||||
<collapse>true</collapse>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.tag</id>
|
||||
@ -353,11 +581,81 @@
|
||||
if the rule is not the last matching rule. Further matching rules can replace the tag with a
|
||||
new one but will not remove a previously applied tag. A packet is only ever assigned one tag at a time.
|
||||
</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.tagged</id>
|
||||
<label>Match local tag</label>
|
||||
<type>text</type>
|
||||
<help>Used to specify that packets must already be tagged with the given tag in order to match the rule.</help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
</grid_view>
|
||||
</field>
|
||||
<!-- Not exposed in dialog, do not exist in model, only for grid -->
|
||||
<field>
|
||||
<id>rule.evaluations</id>
|
||||
<label>Evaluations</label>
|
||||
<type>ignore</type>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
<label>Stats - Evaluations</label>
|
||||
<width>4em</width>
|
||||
<sequence>115</sequence>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.states</id>
|
||||
<label>States</label>
|
||||
<type>ignore</type>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
<label>Stats - States</label>
|
||||
<width>4em</width>
|
||||
<sequence>116</sequence>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.packets</id>
|
||||
<label>Packets</label>
|
||||
<type>ignore</type>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
<label>Stats - Packets</label>
|
||||
<width>4em</width>
|
||||
<sequence>117</sequence>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.bytes</id>
|
||||
<label>Bytes</label>
|
||||
<type>ignore</type>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<sortable>false</sortable>
|
||||
<label>Stats - Bytes</label>
|
||||
<width>4em</width>
|
||||
<sequence>118</sequence>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>rule.icons</id>
|
||||
<label>Icons</label>
|
||||
<type>ignore</type>
|
||||
<grid_view>
|
||||
<sortable>false</sortable>
|
||||
<formatter>ruleIcons</formatter>
|
||||
<width>10em</width>
|
||||
<sequence>15</sequence>
|
||||
</grid_view>
|
||||
</field>
|
||||
</form>
|
||||
|
||||
@ -37,21 +37,21 @@ use OPNsense\Core\Config;
|
||||
class Plugin
|
||||
{
|
||||
private $gateways = null;
|
||||
private $anchors = array();
|
||||
private $filterRules = array();
|
||||
private $natRules = array();
|
||||
private $interfaceMapping = array();
|
||||
private $gatewayMapping = array();
|
||||
private $systemDefaults = array();
|
||||
private $tables = array();
|
||||
private $ifconfigDetails = array();
|
||||
private $anchors = [];
|
||||
private $filterRules = [];
|
||||
private $natRules = [];
|
||||
private $interfaceMapping = [];
|
||||
private $gatewayMapping = [];
|
||||
private $systemDefaults = [];
|
||||
private $tables = [];
|
||||
private $ifconfigDetails = [];
|
||||
|
||||
/**
|
||||
* init firewall plugin component
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->systemDefaults = array("filter" => array(), "forward" => array(), "nat" => array());
|
||||
$this->systemDefaults = array("filter" => [], "forward" => [], "nat" => []);
|
||||
if (!empty(Config::getInstance()->object()->system->disablereplyto)) {
|
||||
$this->systemDefaults['filter']['disablereplyto'] = true;
|
||||
}
|
||||
@ -82,7 +82,7 @@ class Plugin
|
||||
if (!empty($intf['ipaddrv6']) && ($intf['ipaddrv6'] == '6rd' || $intf['ipaddrv6'] == '6to4')) {
|
||||
$realif = "{$key}_stf";
|
||||
// create new interface
|
||||
$this->interfaceMapping[$realif] = array();
|
||||
$this->interfaceMapping[$realif] = [];
|
||||
$this->interfaceMapping[$realif]['ifconfig']['ipv6'] = $intf['ifconfig']['ipv6'];
|
||||
$this->interfaceMapping[$realif]['gatewayv6'] = $intf['gatewayv6'];
|
||||
$this->interfaceMapping[$realif]['is_IPv6_override'] = true;
|
||||
@ -134,7 +134,7 @@ class Plugin
|
||||
{
|
||||
if (is_array($groups)) {
|
||||
foreach ($groups as $key => $gwgr) {
|
||||
$routeto = array();
|
||||
$routeto = [];
|
||||
$proto = 'inet';
|
||||
foreach ($gwgr as $gw) {
|
||||
if (Util::isIpAddress($gw['gwip']) && !empty($gw['int'])) {
|
||||
@ -277,7 +277,7 @@ class Plugin
|
||||
$conf['#priority'] = $prio;
|
||||
$rule = new FilterRule($this->interfaceMapping, $this->gatewayMapping, $conf);
|
||||
if (empty($this->filterRules[$prio])) {
|
||||
$this->filterRules[$prio] = array();
|
||||
$this->filterRules[$prio] = [];
|
||||
}
|
||||
$this->filterRules[$prio][] = $rule;
|
||||
}
|
||||
@ -294,7 +294,7 @@ class Plugin
|
||||
}
|
||||
$rule = new ForwardRule($this->interfaceMapping, $conf);
|
||||
if (empty($this->natRules[$prio])) {
|
||||
$this->natRules[$prio] = array();
|
||||
$this->natRules[$prio] = [];
|
||||
}
|
||||
$this->natRules[$prio][] = $rule;
|
||||
}
|
||||
@ -311,7 +311,7 @@ class Plugin
|
||||
}
|
||||
$rule = new DNatRule($this->interfaceMapping, $conf);
|
||||
if (empty($this->natRules[$prio])) {
|
||||
$this->natRules[$prio] = array();
|
||||
$this->natRules[$prio] = [];
|
||||
}
|
||||
$this->natRules[$prio][] = $rule;
|
||||
}
|
||||
@ -325,7 +325,7 @@ class Plugin
|
||||
{
|
||||
$rule = new SNatRule($this->interfaceMapping, $conf);
|
||||
if (empty($this->natRules[$prio])) {
|
||||
$this->natRules[$prio] = array();
|
||||
$this->natRules[$prio] = [];
|
||||
}
|
||||
$this->natRules[$prio][] = $rule;
|
||||
}
|
||||
@ -339,7 +339,7 @@ class Plugin
|
||||
{
|
||||
$rule = new NptRule($this->interfaceMapping, $conf);
|
||||
if (empty($this->natRules[$prio])) {
|
||||
$this->natRules[$prio] = array();
|
||||
$this->natRules[$prio] = [];
|
||||
}
|
||||
$this->natRules[$prio][] = $rule;
|
||||
}
|
||||
@ -370,7 +370,7 @@ class Plugin
|
||||
ksort($this->filterRules); /* sort rules by priority */
|
||||
foreach ($this->filterRules as $prio => $ruleset) {
|
||||
foreach ($ruleset as $rule) {
|
||||
yield $rule;
|
||||
yield $prio => $rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,4 +153,17 @@ class FilterRuleField extends ArrayField
|
||||
$container_node->setParentModel($parentmodel);
|
||||
return $container_node;
|
||||
}
|
||||
|
||||
protected function actionPostLoadingEvent()
|
||||
{
|
||||
foreach ($this->internalChildnodes as $node) {
|
||||
/**
|
||||
* Evaluation order consists of a priority group and a sequence within the set,
|
||||
* prefixed with 0 as these precede legacy rules
|
||||
**/
|
||||
$node->sort_order = sprintf("%d.0%06d", $node->getPriority(), (string)$node->sequence);
|
||||
$node->prio_group = (string)$node->getPriority();
|
||||
}
|
||||
return parent::actionPostLoadingEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 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.
|
||||
*/
|
||||
|
||||
namespace OPNsense\Firewall\FieldTypes;
|
||||
|
||||
use OPNsense\Base\FieldTypes\AutoNumberField;
|
||||
|
||||
/**
|
||||
* Class FilterSequenceField
|
||||
* Extends the built-in AutoNumberField
|
||||
* The next number will not be in an available gap, but always at the end of the sequence and +100.
|
||||
*/
|
||||
class FilterSequenceField extends AutoNumberField
|
||||
{
|
||||
public function applyDefault()
|
||||
{
|
||||
// Start from the minimum value if no entries exist
|
||||
$maxNumber = (int)$this->minimum_value;
|
||||
if (isset($this->internalParentNode->internalParentNode)) {
|
||||
foreach ($this->internalParentNode->internalParentNode->iterateItems() as $node) {
|
||||
$currentNumber = (int)((string)$node->{$this->internalXMLTagName});
|
||||
// Update maxNumber if this value is greater
|
||||
if ($currentNumber >= $maxNumber) {
|
||||
$maxNumber = $currentNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->internalValue = (string)($maxNumber + 100);
|
||||
}
|
||||
}
|
||||
@ -52,6 +52,11 @@ class GroupField extends ArrayField
|
||||
'sequence' => 10,
|
||||
'ifname' => 'enc0',
|
||||
'descr' => gettext('IPsec')
|
||||
],
|
||||
'wireguard' => [
|
||||
'sequence' => 10,
|
||||
'ifname' => 'wireguard',
|
||||
'descr' => gettext('Wireguard')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@ -28,13 +28,15 @@
|
||||
<floating>Floating states</floating>
|
||||
</OptionValues>
|
||||
</state-policy>
|
||||
<sequence type="IntegerField">
|
||||
<sequence type=".\FilterSequenceField">
|
||||
<MinimumValue>1</MinimumValue>
|
||||
<MaximumValue>99999</MaximumValue>
|
||||
<ValidationMessage>provide a valid sequence for sorting</ValidationMessage>
|
||||
<MaximumValue>999999</MaximumValue>
|
||||
<ValidationMessage>Sequence shall be between 1 and 999999.</ValidationMessage>
|
||||
<Required>Y</Required>
|
||||
<Default>1</Default>
|
||||
</sequence>
|
||||
<sort_order type="TextField" volatile="true"/>
|
||||
<prio_group type="TextField" volatile="true"/>
|
||||
<action type="OptionField">
|
||||
<Required>Y</Required>
|
||||
<Default>pass</Default>
|
||||
|
||||
746
src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt
Normal file
746
src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt
Normal file
@ -0,0 +1,746 @@
|
||||
{#
|
||||
# Copyright (c) 2020-2025 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.
|
||||
#}
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Show errors in modal
|
||||
function showDialogAlert(type, title, message) {
|
||||
BootstrapDialog.show({
|
||||
type: type,
|
||||
title: title,
|
||||
message: message,
|
||||
buttons: [{
|
||||
label: '{{ lang._('Close') }}',
|
||||
action: function(dialogRef) {
|
||||
dialogRef.close();
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Get all advanced fields, used for advanced mode tooltips
|
||||
const advancedFieldIds = "{{ advancedFieldIds }}".split(',');
|
||||
|
||||
// Get all column labels, used for advanced mode tooltips
|
||||
const columnLabels = {};
|
||||
$('#{{formGridFilterRule["table_id"]}} thead th').each(function () {
|
||||
const columnId = $(this).attr('data-column-id');
|
||||
if (columnId) {
|
||||
columnLabels[columnId] = $(this).text().trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Test if the UUID is valid, used to determine if automation model or internal rule
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
// XXX: Clear any stored column visibility settings for the firewall filter page
|
||||
// Synergizes with the Inspect Button since it unhides certain columns that should be hidden on page load
|
||||
// Prevents messed up views, we always control the intentional default of the page
|
||||
Object.keys(localStorage)
|
||||
.filter(key => key.startsWith("visibleColumns[/ui/firewall/filter/"))
|
||||
.forEach(key => localStorage.removeItem(key));
|
||||
|
||||
// Initialize grid
|
||||
const grid = $("#{{formGridFilterRule['table_id']}}").UIBootgrid({
|
||||
search:'/api/firewall/filter/search_rule/',
|
||||
get:'/api/firewall/filter/get_rule/',
|
||||
set:'/api/firewall/filter/set_rule/',
|
||||
add:'/api/firewall/filter/add_rule/',
|
||||
del:'/api/firewall/filter/del_rule/',
|
||||
toggle:'/api/firewall/filter/toggle_rule/',
|
||||
options: {
|
||||
triggerEditFor: getUrlHash('edit'),
|
||||
initialSearchPhrase: getUrlHash('search'),
|
||||
rowCount: [20,50,100,200,500,1000],
|
||||
requestHandler: function(request){
|
||||
// Add category selectpicker
|
||||
if ( $('#category_filter').val().length > 0) {
|
||||
request['category'] = $('#category_filter').val();
|
||||
}
|
||||
// Add interface selectpicker
|
||||
let selectedInterface = $('#interface_select').val();
|
||||
if (selectedInterface && selectedInterface.length > 0) {
|
||||
request['interface'] = selectedInterface;
|
||||
}
|
||||
if ($('#all_rules_checkbox').is(':checked')) {
|
||||
// Send as a comma separated string
|
||||
request['show_all'] = true;
|
||||
}
|
||||
return request;
|
||||
},
|
||||
formatters:{
|
||||
// Only show command buttons for rules that have a uuid, internal rules will not have one
|
||||
commands: function (column, row) {
|
||||
let rowId = row.uuid;
|
||||
|
||||
// If UUID is invalid, its an internal rule, use the #ref field to show a lookup button.
|
||||
if (!rowId || !uuidRegex.test(rowId)) {
|
||||
let ref = row["ref"] || "";
|
||||
if (ref.trim().length > 0) {
|
||||
let url = `/${ref}`;
|
||||
return `
|
||||
<a href="${url}"
|
||||
class="btn btn-xs btn-default bootgrid-tooltip"
|
||||
title="{{ lang._('Lookup Rule') }}">
|
||||
<span class="fa fa-fw fa-search"></span>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
// If ref is empty
|
||||
return "";
|
||||
}
|
||||
|
||||
return `
|
||||
<button type="button" class="btn btn-xs btn-default command-move_before
|
||||
bootgrid-tooltip" data-row-id="${rowId}"
|
||||
title="{{ lang._('Move selected rule before this rule') }}">
|
||||
<span class="fa fa-fw fa-arrow-left"></span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-xs btn-default command-edit
|
||||
bootgrid-tooltip" data-row-id="${rowId}"
|
||||
title="{{ lang._('Edit') }}">
|
||||
<span class="fa fa-fw fa-pencil"></span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-xs btn-default command-copy
|
||||
bootgrid-tooltip" data-row-id="${rowId}"
|
||||
title="{{ lang._('Clone') }}">
|
||||
<span class="fa fa-fw fa-clone"></span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-xs btn-default command-delete
|
||||
bootgrid-tooltip" data-row-id="${rowId}"
|
||||
title="{{ lang._('Delete') }}">
|
||||
<span class="fa fa-fw fa-trash-o"></span>
|
||||
</button>
|
||||
`;
|
||||
},
|
||||
// Show rowtoggle for all rules, but disable interaction for internal rules with no valid UUID
|
||||
rowtoggle: function (column, row) {
|
||||
let rowId = row.uuid;
|
||||
let isEnabled = parseInt(row[column.id], 2) === 1;
|
||||
|
||||
let iconClass = isEnabled
|
||||
? "fa-check-square-o"
|
||||
: "fa-square-o text-muted";
|
||||
|
||||
let tooltipText = isEnabled
|
||||
? "{{ lang._('Enabled') }}"
|
||||
: "{{ lang._('Disabled') }}";
|
||||
|
||||
// For valid UUIDs, make it interactive
|
||||
if (rowId && uuidRegex.test(rowId)) {
|
||||
return `
|
||||
<span style="cursor: pointer;" class="fa fa-fw ${iconClass}
|
||||
command-toggle bootgrid-tooltip" data-value="${isEnabled ? 1 : 0}"
|
||||
data-row-id="${rowId}" title="${tooltipText}">
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// For internal rules, show a non-interactive toggle
|
||||
return `
|
||||
<span style="opacity: 0.5"
|
||||
class="fa fa-fw ${iconClass} bootgrid-tooltip"
|
||||
data-value="${isEnabled ? 1 : 0}"
|
||||
data-row-id="${rowId}" title="${tooltipText}">
|
||||
</span>
|
||||
`;
|
||||
},
|
||||
any: function(column, row) {
|
||||
if (
|
||||
row[column.id] !== '' &&
|
||||
row[column.id] !== 'any' &&
|
||||
row[column.id] !== 'None'
|
||||
) {
|
||||
return row[column.id];
|
||||
} else {
|
||||
return '*';
|
||||
}
|
||||
},
|
||||
protocol: function(column, row) {
|
||||
const ipProtocol = row.ipprotocol ? row.ipprotocol : '';
|
||||
let targetValue = row[column.id] ? row[column.id] : '';
|
||||
|
||||
if (!targetValue || targetValue === '' || targetValue === 'any' || targetValue === 'None') {
|
||||
targetValue = '*';
|
||||
}
|
||||
|
||||
return ipProtocol ? `${ipProtocol} ${targetValue}` : targetValue;
|
||||
},
|
||||
category: function (column, row) {
|
||||
if (!row.categories || !row.category_colors) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const categories = row.categories.split(',').map(cat => cat.trim());
|
||||
const colors = Array.isArray(row.category_colors) ? row.category_colors : row.category_colors.split(',');
|
||||
|
||||
return categories.map((cat, index) => {
|
||||
const color = colors[index]
|
||||
return `<span class="category-icon" data-toggle="tooltip" title="${cat}">
|
||||
<i class="fas fa-circle" style="color: ${color};"></i>
|
||||
</span>`;
|
||||
}).join(' ');
|
||||
},
|
||||
interfaces: function(column, row) {
|
||||
const interfaces = row[column.id] != null ? String(row[column.id]) : "";
|
||||
|
||||
// Apply negation
|
||||
const isNegated = row.interfacenot == 1 ? "! " : "";
|
||||
|
||||
if (!interfaces || interfaces.trim() === "") {
|
||||
return isNegated + '*';
|
||||
}
|
||||
|
||||
const interfaceList = interfaces.split(",").map(iface => iface.trim());
|
||||
|
||||
if (interfaceList.length === 1) {
|
||||
return isNegated + interfaceList[0];
|
||||
}
|
||||
|
||||
const tooltipText = interfaceList.join("<br>");
|
||||
|
||||
return `
|
||||
${isNegated}
|
||||
<span data-toggle="tooltip" data-html="true" title="${tooltipText}" style="white-space: nowrap;">
|
||||
<span class="interface-count">${interfaceList.length}</span>
|
||||
<i class="fa-solid fa-fw fa-network-wired"></i>
|
||||
</span>
|
||||
`;
|
||||
},
|
||||
// Icons
|
||||
ruleIcons: function(column, row) {
|
||||
let result = "";
|
||||
const iconStyle = (row.enabled == 0)
|
||||
? 'style="opacity: 0.4; pointer-events: none;"'
|
||||
: '';
|
||||
|
||||
// Rule Type Icons (Determined by first digit of sort_order)
|
||||
const ruleTypeIcons = {
|
||||
'0': { icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" },
|
||||
'1': { icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" },
|
||||
'2': { icon: "fa-layer-group", tooltip: "{{ lang._('Floating Rule') }}", color: "text-primary" },
|
||||
'3': { icon: "fa-sitemap", tooltip: "{{ lang._('Group Rule') }}", color: "text-warning" },
|
||||
'4': { icon: "fa-ethernet", tooltip: "{{ lang._('Interface Rule') }}", color: "text-info" },
|
||||
'5': { icon: "fa-magic", tooltip: "{{ lang._('Automatic Rule') }}", color: "text-secondary" },
|
||||
};
|
||||
|
||||
const sortOrder = row.sort_order ? row.sort_order.toString() : "";
|
||||
if (sortOrder.length > 0) {
|
||||
const typeDigit = sortOrder.charAt(0);
|
||||
if (ruleTypeIcons[typeDigit]) {
|
||||
result += `<i class="fa ${ruleTypeIcons[typeDigit].icon} fa-fw ${ruleTypeIcons[typeDigit].color}"
|
||||
data-toggle="tooltip" title="${ruleTypeIcons[typeDigit].tooltip}"></i> `;
|
||||
}
|
||||
}
|
||||
|
||||
// Action
|
||||
if (row.action.toLowerCase() === "block") {
|
||||
result += '<i class="fa fa-times fa-fw text-danger" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("Block") }}"></i> ';
|
||||
} else if (row.action.toLowerCase() === "reject") {
|
||||
result += '<i class="fa fa-times-circle fa-fw text-danger" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("Reject") }}"></i> ';
|
||||
} else {
|
||||
result += '<i class="fa fa-play fa-fw text-success" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("Pass") }}"></i> ';
|
||||
}
|
||||
|
||||
// Direction
|
||||
if (row.direction.toLowerCase() === "in") {
|
||||
result += '<i class="fa fa-long-arrow-right fa-fw text-info" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("In") }}"></i> ';
|
||||
} else if (row.direction.toLowerCase() === "out") {
|
||||
result += '<i class="fa fa-long-arrow-left fa-fw" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("Out") }}"></i> ';
|
||||
} else {
|
||||
result += '<i class="fa fa-exchange fa-fw" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("Any") }}"></i> ';
|
||||
}
|
||||
|
||||
// Quick match
|
||||
if (row.quick == 0) {
|
||||
result += '<i class="fa fa-flash fa-fw text-muted" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("Last match") }}"></i> ';
|
||||
} else {
|
||||
// Default to "First match"
|
||||
result += '<i class="fa fa-flash fa-fw text-warning" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("First match") }}"></i> ';
|
||||
}
|
||||
|
||||
// Logging
|
||||
if (row.log == 0) {
|
||||
result += '<i class="fa fa-exclamation-circle fa-fw text-muted" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("Logging disabled") }}"></i> ';
|
||||
} else {
|
||||
result += '<i class="fa fa-exclamation-circle fa-fw text-info" ' + iconStyle +
|
||||
' data-toggle="tooltip" title="{{ lang._("Logging enabled") }}"></i> ';
|
||||
}
|
||||
|
||||
// XXX: Advanced fields all have different default values, so it cannot be generalized completely
|
||||
const advancedDefaultPrefixes = ["0", "none", "any", "default", "keep"];
|
||||
|
||||
const usedAdvancedFields = [];
|
||||
|
||||
advancedFieldIds.forEach(function (fieldId) {
|
||||
const value = row[fieldId];
|
||||
|
||||
if (value !== undefined) {
|
||||
const lowerValue = value.toString().toLowerCase().trim();
|
||||
// Check: if the value is empty OR starts with any default prefix, consider it default
|
||||
const isDefault = (lowerValue === "") || advancedDefaultPrefixes.some(function(prefix) {
|
||||
return lowerValue.startsWith(prefix);
|
||||
});
|
||||
|
||||
if (!isDefault) {
|
||||
// Use label if available, otherwise fallback to field ID
|
||||
const label = columnLabels[fieldId] || fieldId;
|
||||
usedAdvancedFields.push(`${label}: ${value}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let iconClass;
|
||||
let tooltip;
|
||||
if (usedAdvancedFields.length > 0) {
|
||||
iconClass = "text-warning";
|
||||
tooltip = `{{ lang._("Advanced mode enabled") }}<br>${usedAdvancedFields.join("<br>")}`;
|
||||
} else {
|
||||
iconClass = "text-muted";
|
||||
tooltip = "{{ lang._('Advanced mode disabled') }}";
|
||||
}
|
||||
|
||||
result += `<i class="fa fa-cog fa-fw ${iconClass}" ${iconStyle}
|
||||
data-toggle="tooltip" data-html="true" title="${tooltip}"></i>`;
|
||||
|
||||
// Return all icons
|
||||
return result;
|
||||
},
|
||||
// Show Edit alias icon and integrate "not" functionality
|
||||
alias: function(column, row) {
|
||||
const value = row[column.id] != null ? String(row[column.id]) : "";
|
||||
|
||||
// Explicitly map fields that support negation
|
||||
const notFieldMap = {
|
||||
"source_net": "source_not",
|
||||
"destination_net": "destination_not"
|
||||
};
|
||||
|
||||
const notField = notFieldMap[column.id];
|
||||
|
||||
// Apply negation
|
||||
const isNegated = notField && row.hasOwnProperty(notField) && row[notField] == 1 ? "! " : "";
|
||||
|
||||
if (!value || value.trim() === "" || value === "any" || value === "None") {
|
||||
return isNegated + '*';
|
||||
}
|
||||
|
||||
// Ensure it's a string, or internal rules will not load anymore
|
||||
const stringValue = typeof value === "string" ? value : String(value);
|
||||
|
||||
const aliasFlagName = "is_alias_" + column.id;
|
||||
if (!row.hasOwnProperty(aliasFlagName)) {
|
||||
return isNegated + stringValue;
|
||||
}
|
||||
|
||||
const generateAliasMarkup = (val) => `
|
||||
<span data-toggle="tooltip" title="${val}">
|
||||
${val}
|
||||
</span>
|
||||
<a href="/ui/firewall/alias/index/${val}" data-toggle="tooltip" title="{{ lang._('Edit alias') }}">
|
||||
<i class="fa fa-fw fa-list"></i>
|
||||
</a>
|
||||
`;
|
||||
|
||||
// If the alias flag is an array, handle multiple comma-separated aliases
|
||||
if (Array.isArray(row[aliasFlagName])) {
|
||||
const values = stringValue.split(',').map(s => s.trim());
|
||||
const aliasFlags = row[aliasFlagName];
|
||||
|
||||
return isNegated + values.map((val, index) => aliasFlags[index] ? generateAliasMarkup(val) : val).join(', ');
|
||||
}
|
||||
|
||||
// If alias flag is not an array, assume it's a boolean and a single alias
|
||||
return isNegated + (row[aliasFlagName] ? generateAliasMarkup(stringValue) : stringValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
move_before: {
|
||||
method: function(event) {
|
||||
// Ensure exactly one rule is selected to be moved
|
||||
const selected = $("#{{ formGridFilterRule['table_id'] }}").bootgrid("getSelectedRows");
|
||||
if (selected.length !== 1) {
|
||||
showDialogAlert(
|
||||
BootstrapDialog.TYPE_WARNING,
|
||||
"{{ lang._('Selection Error') }}",
|
||||
"{{ lang._('Please select exactly one rule to move.') }}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// The rule the user selected
|
||||
const selectedUuid = selected[0];
|
||||
// The rule the button was pressed on
|
||||
const targetUuid = $(this).data("row-id");
|
||||
|
||||
// Prevent moving a rule before itself
|
||||
if (selectedUuid === targetUuid) {
|
||||
showDialogAlert(
|
||||
BootstrapDialog.TYPE_WARNING,
|
||||
"{{ lang._('Move Error') }}",
|
||||
"{{ lang._('Cannot move a rule before itself.') }}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ajaxCall(
|
||||
"/api/firewall/filter/move_rule_before/" + selectedUuid + "/" + targetUuid,
|
||||
{},
|
||||
function(data, status) {
|
||||
if (data.status === "ok") {
|
||||
std_bootgrid_reload("{{ formGridFilterRule['table_id'] }}");
|
||||
// Trigger change message, e.g., when using move_before
|
||||
$("#change_message_base_form").slideDown(1000, function() {
|
||||
setTimeout(function() {
|
||||
$("#change_message_base_form").slideUp(2000);
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
},
|
||||
function(xhr, textStatus, errorThrown) {
|
||||
showDialogAlert(
|
||||
BootstrapDialog.TYPE_DANGER,
|
||||
"{{ lang._('Request Failed') }}",
|
||||
errorThrown
|
||||
);
|
||||
},
|
||||
'POST'
|
||||
);
|
||||
},
|
||||
classname: 'fa fa-fw fa-arrow-left',
|
||||
title: "{{ lang._('Move selected rule before this rule') }}",
|
||||
sequence: 10
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
grid.on("loaded.rs.jquery.bootgrid", function () {
|
||||
// XXX: Replace these labels to save some space in the grid
|
||||
// This is a workaround, to change labels in the grid, but NOT in the grid selection dropdown
|
||||
$(this).find('th[data-column-id="enabled"] .text').text("");
|
||||
$(this).find('th[data-column-id="icons"] .text').text("");
|
||||
$(this).find('th[data-column-id="source_port"] .text').text("{{ lang._('Port') }}");
|
||||
$(this).find('th[data-column-id="destination_port"] .text').text("{{ lang._('Port') }}");
|
||||
$(this).find('th[data-column-id="interface"] .text').html(`
|
||||
<i class="fa-solid fa-fw fa-network-wired" data-toggle="tooltip" data-placement="right" title="{{ lang._('Network Interface') }}"></i>
|
||||
`);
|
||||
$(this).find('th[data-column-id="evaluations"] .text').html(`
|
||||
<i class="fa-solid fa-fw fa-bullseye" data-toggle="tooltip" data-placement="left" title="{{ lang._('Number of rule evaluations') }}"></i>
|
||||
`);
|
||||
$(this).find('th[data-column-id="states"] .text').html(`
|
||||
<i class="fa-solid fa-fw fa-chart-line" data-toggle="tooltip" data-placement="left" title="{{ lang._('Current active states for this rule') }}"></i>
|
||||
`);
|
||||
$(this).find('th[data-column-id="packets"] .text').html(`
|
||||
<i class="fa-solid fa-fw fa-box" data-toggle="tooltip" data-placement="left" title="{{ lang._('Total packets matched by this rule') }}"></i>
|
||||
`);
|
||||
$(this).find('th[data-column-id="bytes"] .text').html(`
|
||||
<i class="fa-solid fa-fw fa-database" data-toggle="tooltip" data-placement="left" title="{{ lang._('Total bytes matched by this rule') }}"></i>
|
||||
`);
|
||||
$(this).find('th[data-column-id="categories"] .text').html(`
|
||||
<i class="fa-solid fa-fw fa-tag" data-toggle="tooltip" data-placement="left" title="{{ lang._('Categories') }}"></i>
|
||||
`);
|
||||
|
||||
// Initialize tooltips
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
});
|
||||
|
||||
/* for performance reasons, only load catagories on page load */
|
||||
ajaxCall('/api/firewall/filter/list_categories', {}, function (data) {
|
||||
if (!data.rows) return;
|
||||
|
||||
const $categoryFilter = $("#category_filter");
|
||||
const currentSelection = $categoryFilter.val();
|
||||
|
||||
$categoryFilter.empty().append(
|
||||
data.rows.map(row => {
|
||||
const optVal = $('<div/>').text(row.name).html();
|
||||
const bgColor = row.color || '31708f';
|
||||
|
||||
return $("<option/>", {
|
||||
value: row.uuid,
|
||||
html: row.name,
|
||||
id: row.used > 0 ? row.uuid : undefined,
|
||||
"data-content": row.used > 0
|
||||
? `<span>${optVal}</span><span style='background:#${bgColor};' class='badge pull-right'>${row.used}</span>`
|
||||
: undefined
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
$categoryFilter.val(currentSelection).selectpicker('refresh');
|
||||
});
|
||||
|
||||
// Populate interface selectpicker
|
||||
$("#interface_select_container").show();
|
||||
ajaxCall('/api/firewall/filter/get_interface_list', {},
|
||||
function(data, status) {
|
||||
const $select = $('#interface_select');
|
||||
$select.empty();
|
||||
for (const [groupkey, group] of Object.entries(data)) {
|
||||
if (group.items.length > 0) {
|
||||
let $optgroup = $('<optgroup>', {
|
||||
"label": `${group.label}`,
|
||||
"data-icon": `${group.icon}`
|
||||
});
|
||||
group.items.forEach(function(iface) {
|
||||
let optprops = {
|
||||
value: iface.value,
|
||||
'data-subtext': group.label,
|
||||
text: iface.label
|
||||
};
|
||||
if (iface.value === '') {
|
||||
/* floating selected by default */
|
||||
optprops['selected'] = 'selected';
|
||||
}
|
||||
$optgroup.append($('<option>', optprops));
|
||||
});
|
||||
$select.append($optgroup);
|
||||
}
|
||||
}
|
||||
$select.selectpicker('refresh');
|
||||
},
|
||||
function(xhr, textStatus, errorThrown) {
|
||||
console.error("Failed to load interface list:", textStatus, errorThrown);
|
||||
}
|
||||
);
|
||||
|
||||
// move selectpickers into action bar
|
||||
$("#interface_select_container").detach().insertBefore('#{{formGridFilterRule["table_id"]}}-header > .row > .actionBar > .search');
|
||||
$('#interface_select').change(function(){
|
||||
grid.bootgrid('reload');
|
||||
});
|
||||
|
||||
$("#type_filter_container").detach().insertAfter("#interface_select_container");
|
||||
$("#category_filter").change(function(){
|
||||
grid.bootgrid('reload');
|
||||
});
|
||||
|
||||
$("#internal_rule_selector").detach().insertAfter("#type_filter_container");
|
||||
$('#all_rules_checkbox').change(function(){
|
||||
grid.bootgrid('reload');
|
||||
});
|
||||
|
||||
/* XXX: needs fix if we want to show the inspect columns on button press */
|
||||
// Once the grid has reloaded, update the specified checkboxes
|
||||
// grid.on("loaded.rs.jquery.bootgrid", function () {
|
||||
// const isChecked = $('#all_rules_checkbox').is(':checked');
|
||||
// const checkboxes = ['evaluations', 'states', 'packets', 'bytes'];
|
||||
// checkboxes.forEach(name => {
|
||||
// const $checkbox = $('input[name="' + name + '"].dropdown-item-checkbox');
|
||||
// if ($checkbox.length && $checkbox.prop('checked') !== isChecked) {
|
||||
// $checkbox.click();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
$('#all_rules_button').click(function(){
|
||||
let $checkbox = $('#all_rules_checkbox');
|
||||
|
||||
$checkbox.prop("checked", !$checkbox.prop("checked"));
|
||||
$(this).toggleClass('active btn-primary');
|
||||
|
||||
$checkbox.trigger("change");
|
||||
$(this).tooltip('hide');
|
||||
});
|
||||
|
||||
$('#all_rules_button').mouseleave(function(){
|
||||
$('#all_rules_button').tooltip('hide')
|
||||
});
|
||||
|
||||
// replace all "net" selectors with details retrieved from "list_network_select_options" endpoint
|
||||
ajaxGet('/api/firewall/filter/list_network_select_options', [], function(data, status){
|
||||
if (data.single) {
|
||||
$(".net_selector").each(function(){
|
||||
$(this).replaceInputWithSelector(data, $(this).hasClass('net_selector_multi'));
|
||||
/* enforce single selection when "single host or network" or "any" are selected */
|
||||
if ($(this).hasClass('net_selector_multi')) {
|
||||
$("select[for='" + $(this).attr('id') + "']").on('shown.bs.select', function(){
|
||||
$(this).data('previousValue', $(this).val());
|
||||
}).change(function(){
|
||||
const prev = Array.isArray($(this).data('previousValue')) ? $(this).data('previousValue') : [];
|
||||
const is_single = $(this).val().includes('') || $(this).val().includes('any');
|
||||
const was_single = prev.includes('') || prev.includes('any');
|
||||
let refresh = false;
|
||||
if (was_single && is_single && $(this).val().length > 1) {
|
||||
$(this).val($(this).val().filter(value => !prev.includes(value)));
|
||||
refresh = true;
|
||||
} else if (is_single && $(this).val().length > 1) {
|
||||
if ($(this).val().includes('any') && !prev.includes('any')) {
|
||||
$(this).val('any');
|
||||
} else{
|
||||
$(this).val('');
|
||||
}
|
||||
refresh = true;
|
||||
}
|
||||
if (refresh) {
|
||||
$(this).selectpicker('refresh');
|
||||
$(this).trigger('change');
|
||||
}
|
||||
$(this).data('previousValue', $(this).val());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Hook into add event
|
||||
$('#{{formGridFilterRule["edit_dialog_id"]}}').on('opnsense_bootgrid_mapped', function(e, actionType) {
|
||||
if (actionType === 'add') {
|
||||
// and choose same interface in new rule as selected in #interface_select
|
||||
const selectedInterface = $('#interface_select').val();
|
||||
if (selectedInterface) {
|
||||
$('#rule\\.interface').selectpicker('val', [selectedInterface]);
|
||||
$('#rule\\.interface').selectpicker('refresh');
|
||||
}
|
||||
// and do the same with category selection (supports multiple)
|
||||
const selectedCategories = $('#category_filter').val();
|
||||
if (selectedCategories && selectedCategories.length > 0) {
|
||||
let categorySelect = $('#rule\\.categories');
|
||||
|
||||
categorySelect.tokenize2().trigger('tokenize:clear');
|
||||
|
||||
selectedCategories.forEach(function(categoryUUID) {
|
||||
let categoryLabel = $('#rule\\.categories option[value="' + categoryUUID + '"]').text();
|
||||
categorySelect.tokenize2().trigger('tokenize:tokens:add', [categoryUUID, categoryLabel]);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Wrap buttons and grid into divs to target them with css for responsiveness
|
||||
$("#{{ formGridFilterRule['table_id'] }}").wrap('<div class="bootgrid-box"></div>');
|
||||
|
||||
// Dynamically add fa icons to selectpickers
|
||||
$('#category_filter').parent().find('.dropdown-toggle').prepend('<i class="fa fa-tag" style="margin-right: 6px;"></i>');
|
||||
|
||||
$("#reconfigureAct").SimpleActionButton();
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* The filter rules column dropdown has many items */
|
||||
.actions .dropdown-menu.pull-right {
|
||||
max-height: 200px;
|
||||
min-width: max-content;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
/* Advanced mode tooltip */
|
||||
.tooltip-inner {
|
||||
max-width: 600px;
|
||||
text-align: left;
|
||||
}
|
||||
/* Align selectpickers */
|
||||
#interface_select_container {
|
||||
float: left;
|
||||
}
|
||||
#type_filter_container {
|
||||
float: left;
|
||||
margin-left: 5px;
|
||||
}
|
||||
#internal_rule_selector {
|
||||
float: left;
|
||||
margin-left: 5px;
|
||||
}
|
||||
/* Prevent bootgrid to break out of content box*/
|
||||
.content-box {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.bootgrid-header,
|
||||
.bootgrid-box,
|
||||
.bootgrid-footer {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
max-width: 100%;
|
||||
/* Prevents the grid from collapsing all dynamic columns completely */
|
||||
min-width: 1200px;
|
||||
}
|
||||
/* Not all dropdowns support data-container="body", ensure minimal vertical space for them */
|
||||
.bootgrid-box {
|
||||
min-height: 150px;
|
||||
}
|
||||
#all_rules_button i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
/* Allow grid to wrap text to use more diagonal space */
|
||||
.bootgrid-table tbody td {
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="tab-content content-box">
|
||||
<!-- filters -->
|
||||
<div class="hidden">
|
||||
<div id="type_filter_container" class="btn-group">
|
||||
<select id="category_filter" data-title="{{ lang._('Categories') }}" class="selectpicker" data-live-search="true" data-size="5" multiple data-width="200px" data-container="body">
|
||||
</select>
|
||||
</div>
|
||||
<div id="interface_select_container" class="btn-group">
|
||||
<select id="interface_select" class="selectpicker" data-live-search="true" data-show-subtext="true" data-size="15" data-width="200px" data-container="body">
|
||||
</select>
|
||||
</div>
|
||||
<div id="internal_rule_selector" class="btn-group">
|
||||
<button id="all_rules_button"
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
data-delay='{"show": 1000}'
|
||||
title="{{ lang._('Show automatically generated rules and statistics') }}">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i>
|
||||
{{ lang._('Inspect') }}
|
||||
</button>
|
||||
<input id="all_rules_checkbox" type="checkbox" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
<!-- grid -->
|
||||
{{ partial('layout_partials/base_bootgrid_table', formGridFilterRule + {'command_width': '9em'}) }}
|
||||
</div>
|
||||
|
||||
{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/firewall/filter/apply'}) }}
|
||||
{{ partial("layout_partials/base_dialog",['fields':formDialogFilterRule,'id':formGridFilterRule['edit_dialog_id'],'label':lang._('Edit rule')])}}
|
||||
89
src/opnsense/scripts/filter/list_non_mvc_rules.php
Executable file
89
src/opnsense/scripts/filter/list_non_mvc_rules.php
Executable file
@ -0,0 +1,89 @@
|
||||
#!/usr/local/bin/php
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2025 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.
|
||||
*/
|
||||
require_once('script/load_phalcon.php');
|
||||
require_once('util.inc');
|
||||
require_once('config.inc');
|
||||
require_once('interfaces.inc');
|
||||
require_once('plugins.inc');
|
||||
require_once('filter.inc');
|
||||
|
||||
|
||||
$fw = filter_core_get_initialized_plugin_system();
|
||||
filter_core_bootstrap($fw);
|
||||
/* fetch all firewall plugins, except pf_firewall as this registers our mvc rules */
|
||||
foreach (plugins_scan() as $name => $path) {
|
||||
try {
|
||||
include_once $path;
|
||||
} catch (\Error $e) {
|
||||
error_log($e);
|
||||
}
|
||||
$func = sprintf('%s_firewall', $name);
|
||||
if ($func != 'pf_firewall' && function_exists($func)) {
|
||||
$func($fw);
|
||||
}
|
||||
}
|
||||
filter_core_rules_user($fw);
|
||||
|
||||
$mapping = [
|
||||
'type' => 'action',
|
||||
'reply-to' => 'replyto',
|
||||
'descr' => 'description',
|
||||
'from' => 'source_net',
|
||||
'from_port' => 'source_port',
|
||||
'to' => 'destination_net',
|
||||
'to_port' => 'destination_port',
|
||||
'#ref' => 'ref',
|
||||
'label' => 'uuid'
|
||||
];
|
||||
|
||||
$rules = [];
|
||||
$sequence = 1;
|
||||
foreach ($fw->iterateFilterRules() as $prio => $item) {
|
||||
$rule = $item->getRawRule();
|
||||
if (empty($rule['disabled'])) {
|
||||
$rule['enabled'] = '1';
|
||||
$rule['direction'] = $rule['direction'] ?? 'in';
|
||||
foreach ($mapping as $src => $dst) {
|
||||
$rule[$dst] = $rule[$src] ?? '';
|
||||
if (isset($rule[$src])) {
|
||||
unset($rule[$src]);
|
||||
}
|
||||
}
|
||||
$rule['action'] = $rule['action'] ?? 'pass';
|
||||
$rule['ipprotocol'] = $rule['ipprotocol'] ?? 'inet';
|
||||
|
||||
/**
|
||||
* Evaluation order consists of a priority group and a sequence within the set,
|
||||
* prefixed with 1 as these are located after mvc rules.
|
||||
**/
|
||||
$rule['sort_order'] = sprintf("%06d.1%06d", $prio, $sequence++);
|
||||
$rule['legacy'] = true;
|
||||
$rules[] = $rule;
|
||||
}
|
||||
}
|
||||
echo json_encode($rules, JSON_PRETTY_PRINT);
|
||||
@ -45,7 +45,7 @@ message:request pf states
|
||||
command:/usr/local/opnsense/scripts/filter/list_rule_ids.py
|
||||
parameters:
|
||||
type:script_output
|
||||
message:request active rule id's and descriptions
|
||||
message:request active rule ids and descriptions
|
||||
|
||||
[list.pfsync]
|
||||
command:/usr/local/opnsense/scripts/filter/list_pfsync.py
|
||||
@ -172,3 +172,10 @@ command:/usr/local/opnsense/scripts/filter/rollback_cancel.php
|
||||
parameters: %s
|
||||
type:script_output
|
||||
message:cancel filter rollback
|
||||
|
||||
[list.non_mvc_rules]
|
||||
command:/usr/local/opnsense/scripts/filter/list_non_mvc_rules.php
|
||||
parameters:
|
||||
type:script_output
|
||||
cache_ttl:60
|
||||
message:list internal/legacy rules
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user