Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
eda8976
Firewall: NAT: Port Forward - refactor to MVC (work in progress) for …
AdSchellevis Nov 25, 2025
46de488
Firewall: NAT: Port Forward - refactor to MVC (work in progress) for …
Monviech Nov 26, 2025
4eacf64
make plist-fix
Monviech Nov 26, 2025
71d0c01
Firewall: NAT: Port Forward - refactor to MVC (work in progress) for …
AdSchellevis Nov 27, 2025
e85bc44
plist
AdSchellevis Nov 27, 2025
bc763c8
Enabled tooltips were the wrong way around
Monviech Nov 27, 2025
7fc2494
Enabled tooltips were the wrong way around
Monviech Nov 27, 2025
4ddc4d7
Add showDialogAlert() since we use that but it was missing
Monviech Nov 28, 2025
ab62174
Since ipprotocol is optional, add any formatter
Monviech Nov 28, 2025
a0a7a57
Add the port and network alias selectpickers to the dialog
Monviech Nov 28, 2025
4d25ba7
Correct toggle_rule_log endpoint in command
Monviech Nov 28, 2025
210f317
Add the category fa-tag to selectpicker
Monviech Nov 28, 2025
9c0b553
Add labels, hints, BlankDesc
Monviech Nov 28, 2025
32d6915
Return description in the any formatter when possible
Monviech Nov 28, 2025
ed782ed
target and local-port use the alias formatter too, add them to result
Monviech Nov 28, 2025
091625d
Firewall: NAT: Port Forward - refactor to MVC (work in progress) for …
AdSchellevis Nov 30, 2025
8f26a98
Firewall: NAT: Port Forward - refactor to MVC (work in progress) for …
AdSchellevis Nov 30, 2025
f0e5904
Firewall: NAT: Port Forward - refactor to MVC (work in progress) for …
AdSchellevis Nov 30, 2025
490efb1
Update src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialo…
AdSchellevis Nov 30, 2025
844d3a8
Update src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml
AdSchellevis Dec 2, 2025
6a4b6d4
Update src/opnsense/mvc/app/models/OPNsense/Firewall/Menu/Menu.xml
AdSchellevis Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions plist
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/AliasController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/AliasUtilController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/CategoryController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/DNatController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterBaseController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterUtilController.php
Expand All @@ -310,12 +311,14 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/OneToOneController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/SourceNatController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/CategoryController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/DNatController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/FilterController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/GroupController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/NptController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/OneToOneController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/SourceNatController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/categoryEdit.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogDNatRule.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogNptRule.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogOneToOneRule.xml
Expand Down Expand Up @@ -729,18 +732,26 @@
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Alias.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Category.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Category.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DNat.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DNat.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/CaptivePortalAliases.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/InterfaceNetworkAliases.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/README.md
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/StaticAliases.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php
/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/CategoryField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/CategoryMapField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/DNatAssociatedRuleField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/DNatSequenceField.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
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/NetworkMappedField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/PortMappedField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/ScheduleField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/SourceNatRuleField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/TosField.php
Expand Down Expand Up @@ -946,6 +957,7 @@
/usr/local/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt
/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/dnat_rule.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
Expand Down Expand Up @@ -2476,8 +2488,6 @@
/usr/local/www/diag_authentication.php
/usr/local/www/diag_backup.php
/usr/local/www/fbegin.inc
/usr/local/www/firewall_nat.php
/usr/local/www/firewall_nat_edit.php
/usr/local/www/firewall_nat_out.php
/usr/local/www/firewall_nat_out_edit.php
/usr/local/www/firewall_rule_lookup.php
Expand Down
9 changes: 2 additions & 7 deletions src/etc/inc/filter.inc
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,8 @@ function filter_configure_sync($verbose = false, $load_aliases = true)
}
}

if (!empty($config['nat']['rule'])) {
// register user forward rules
foreach ($config['nat']['rule'] as $rule) {
if (is_array($rule)) {
$fw->registerForwardRule(600, $rule);
}
}
foreach ((new OPNsense\Firewall\DNat())->rule->sortedBy(['sequence']) as $key => $rule) {
$fw->registerForwardRule(600, $rule->getNodeContent());
}

openlog("firewall", LOG_DAEMON, LOG_LOCAL4);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<?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\Api;
use OPNsense\Base\UserException;
use OPNsense\Core\Config;
use OPNsense\Firewall\Category;

class DNatController extends FilterBaseController
{
protected static $internalModelName = 'DNat';
protected static $internalModelClass = 'OPNsense\\Firewall\\DNat';
protected static $categorysource = 'rule';

/**
* @inheritdoc
*/
protected function setBaseHook($node)
{
$node->updated->time = sprintf('%0.2f', microtime(true));
$node->updated->username = $this->getUserName();
$node->updated->description = sprintf('%s made changes', $_SERVER['SCRIPT_NAME']);
if ($node->created->time->isEmpty()) {
$node->created->time = $node->updated->time;
$node->created->username = $node->updated->username;
$node->created->description = $node->updated->description;
}
}

public function searchRuleAction()
{
$category = (array)$this->request->get('category');
$filter_funct = function ($record) use ($category) {
/* categories are indexed by name in the record, but offered as uuid in the selector */
$catids = !empty((string)$record->categories) ? explode(',', (string)$record->categories) : [];
return empty($category) || array_intersect($catids, $category);
};

$results = $this->searchBase("rule", null, "sequence", $filter_funct);

/* carry results */
foreach ($results['rows'] as &$record) {
/* offer list of colors to be used by the frontend */
$record['category_colors'] = $this->getCategoryColors(explode(',', $record['categories']));
/* format "networks" and ports */
foreach (['source.network','source.port','destination.network','destination.port', 'target', 'local-port'] as $field) {
if (!empty($record[$field])) {
$record["alias_meta_{$field}"] = $this->getNetworks($record[$field]);
}
}
}

return $results;
}

public function setRuleAction($uuid)
{
/* prevent created metadata being overwritten or offered */
if (is_array($_POST['rule']) && isset($_POST['rule']['created'])) {
unset($_POST['rule']['created']);
}
return $this->setBase("rule", "rule", $uuid);
}

public function addRuleAction()
{
/* prevent created metadata being overwritten or offered */
if (is_array($_POST['rule']) && isset($_POST['rule']['created'])) {
unset($_POST['rule']['created']);
}
return $this->addBase("rule", "rule");
}

public function getRuleAction($uuid = null)
{
return $this->getBase("rule", "rule", $uuid);
}

public function delRuleAction($uuid)
{
return $this->delBase("rule", $uuid);
}

/**
* opposite toggle (disable instead of enable)
*/
public function toggleRuleAction($uuid, $disabled = null)
{
$result = ['result' => 'failed'];
if ($this->request->isPost() && $uuid != null) {
Config::getInstance()->lock();
$node = $this->getModel()->getNodeByReference('rule.' . $uuid);
if ($node != null) {
if (in_array($disabled, ['0', '1'])) {
$node->disabled = (string)$disabled;
} else {
$node->disabled = (string)$node->disabled == '1' ? '0' : '1';
}
$result['result'] = $node->disabled->isEmpty() ? 'Enabled' : 'Disabled';
$this->save(false, true);
}
}
return $result;
}

/**
* 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.
*
* @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")];
}
$target_node = $this->getModel()->getNodeByReference('rule.' . $target_uuid);
$selected_node = $this->getModel()->getNodeByReference('rule.' . $selected_uuid);
if ($target_node === null || $selected_node === null) {
throw new UserException(
gettext("Either source or destination is not a rule managed with this component"),
gettext("DNat")
);
}
$step_size = 50;
$new_key = null;
$prev_record = null;
foreach ($this->getModel()->rule->sortedBy(['sequence']) as $record) {
$uuid = $record->getAttribute('uuid');
if ($target_uuid === $uuid) {
$prev_sequence = (($prev_record?->sequence->asFloat()) ?? 1);
$distance = $record->sequence->asFloat() - $prev_sequence;
if ($distance > 2) {
$new_key = intdiv($distance, 2) + $prev_sequence;
break;
} else {
$new_key = $prev_record === null ? 1 : ($prev_sequence + $step_size);
$record->sequence = (string)($new_key + $step_size);
}
} elseif ($new_key !== null) {
if ($record->sequence->asFloat() < $prev_record?->sequence->asFloat()) {
$record->sequence = (string)($prev_record?->sequence->asFloat() + $step_size);
}
}
$prev_record = $record;
}
if ($new_key !== null) {
$selected_node->sequence = (string)$new_key;
/* we're only changing sequences, forcefully save */
$this->getModel()->serializeToConfig(false, true);
Config::getInstance()->save();
}

return ["status" => "ok"];
}

public function toggleRuleLogAction($uuid, $log)
{
if (!$this->request->isPost()) {
return ['status' => 'error', 'message' => gettext('Invalid request method')];
}

$mdl = $this->getModel();
$node = null;
foreach ($mdl->rule->iterateItems() as $item) {
if ((string)$item->getAttribute('uuid') === $uuid) {
$node = $item;
break;
}
}
if ($node === null) {
throw new UserException(gettext("Rule not found"), gettext("DNat"));
}

$node->log = $log;
$mdl->serializeToConfig();
Config::getInstance()->save();

return ['status' => 'ok'];
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?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;

class DNatController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/Firewall/dnat_rule');
$this->view->formDialogDNatRule = $this->getForm("dialogDNatRule");
$this->view->formGridDNatRule = $this->getFormGrid('dialogDNatRule');
}
}
Loading