diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php index 9754748281..dbac5fba83 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php @@ -81,18 +81,21 @@ class FilterController extends FilterBaseController { $categories = $this->request->get('category'); $show_all = !empty($this->request->get('show_all')); - if (!empty($this->request->get('interface'))) { - $interfaces = explode(',', $this->request->get('interface')); - if ($show_all) { - /* add groups which contain the selected interface when looking at full impact*/ + if (!$this->request->has('interface')) { + // ALL rules + $interfaces = null; + } else { + // interface param may be empty + $interfaces = array_filter(explode(',', (string)$this->request->get('interface')), 'strlen'); + + if ($show_all && !empty($interfaces)) { + /* add groups which contain the selected interface when looking at full impact */ foreach ((new Group())->ifgroupentry->iterateItems() as $groupItem) { if (array_intersect($interfaces, $groupItem->members->getValues())) { $interfaces[] = $groupItem->ifname->getValue(); } } } - } else { - $interfaces = []; } /* filter logic for mvc rules */ @@ -100,6 +103,11 @@ class FilterController extends FilterBaseController $is_cat = empty($categories) || array_intersect(explode(',', $record->categories), $categories); $rule_interfaces = $record->interface->getValues(); + // ALL rules, skip interface logic entirely + if ($interfaces === null) { + return $is_cat; + } + if (!$record->interfacenot->isEmpty()) { $if_intersects = !array_intersect($interfaces, $rule_interfaces); /* All but interface */ } else { @@ -150,12 +158,17 @@ class FilterController extends FilterBaseController } $is_cat = empty($categories) || array_intersect($r_categories, $categories); - if (!empty($record['interfacenot'])) { - $is_if = !array_intersect(explode(',', $record['interface'] ?? ''), $interfaces); + if ($interfaces === null) { + // ALL interfaces, interface always matches + $is_if = true; } else { - $is_if = array_intersect(explode(',', $record['interface'] ?? ''), $interfaces); + if (!empty($record['interfacenot'])) { + $is_if = !array_intersect(explode(',', $record['interface'] ?? ''), $interfaces); + } else { + $is_if = array_intersect(explode(',', $record['interface'] ?? ''), $interfaces); + } + $is_if = $is_if || empty($record['interface']); } - $is_if = $is_if || empty($record['interface']); if ($is_cat && $is_if) { /* translate/convert legacy fields before returning, similar to mvc handling */ @@ -383,6 +396,11 @@ class FilterController extends FilterBaseController 'label' => gettext('Interfaces'), 'icon' => 'fa fa-ethernet text-info', 'items' => [] + ], + 'any' => [ + 'label' => gettext('Any'), + 'icon' => 'fa fa-globe-europe text-muted', + 'items' => [] ] ]; @@ -399,6 +417,8 @@ class FilterController extends FilterBaseController $ruleCounts[$interfaces[0]] = ($ruleCounts[$interfaces[0]] ?? 0) + 1; } } + // ALL rules + $totalRules = array_sum($ruleCounts); // Helper to build item with label and count $makeItem = fn($value, $label, $count, $type) => [ @@ -426,6 +446,9 @@ class FilterController extends FilterBaseController } } + // ALL rules + $result['any']['items'][] = $makeItem('*', gettext('All rules'), $totalRules, 'any'); + // Sort items by count and alphabetically foreach ($result as &$section) { usort($section['items'], fn($a, $b) => diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt index 0862c0c3db..bbc1edad80 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter_rule.volt @@ -163,10 +163,16 @@ } // Add interface selectpicker, or fall back to hash for the first load let selectedInterface = $('#interface_select').val(); - if ((!selectedInterface || selectedInterface.length === 0) && pendingUrlInterface) { + if ( + (selectedInterface === null || selectedInterface === undefined || selectedInterface === '') && + pendingUrlInterface + ) { request['interface'] = pendingUrlInterface; pendingUrlInterface = null; // consume the hash so it is not used again - } else if (selectedInterface && selectedInterface.length > 0) { + // ALL interfaces + } else if (selectedInterface === '*') { + // do not send interface parameter! + } else if (selectedInterface !== null && selectedInterface !== undefined) { request['interface'] = selectedInterface; } if (inspectEnabled) { @@ -728,7 +734,8 @@ const bgClassMap = { floating: 'bg-primary', group: 'bg-warning', - interface: 'bg-info' + interface: 'bg-info', + any: 'text-muted' }; const badgeClass = bgClassMap[item.type] || 'bg-info'; @@ -758,6 +765,10 @@ $('#interface_select').val(ifaceFromHash).selectpicker('refresh'); } } + // Default to ALL interfaces when nothing is selected and no hash applied + if (!match && ($('#interface_select').val() === null || $('#interface_select').val() === '')) { + $('#interface_select').val('*').selectpicker('refresh'); + } interfaceInitialized = true; },