diff --git a/src/etc/inc/plugins.inc.d/pfplugin.inc b/src/etc/inc/plugins.inc.d/pfplugin.inc
new file mode 100644
index 000000000..3c9aa2fce
--- /dev/null
+++ b/src/etc/inc/plugins.inc.d/pfplugin.inc
@@ -0,0 +1,48 @@
+rules->rule->sortedBy(["sequence"]) as $key => $rule) {
+ $content = $rule->serialize();
+ $content["#ref"] = "ui/firewall/filter#" . (string)$rule->getAttributes()['uuid'];
+ $fw->registerFilterRule($rule->getPriority(), $content);
+ }
+
+ foreach ($mdlFilter->snatrules->rule->sortedBy(["sequence"]) as $key => $rule) {
+ $fw->registerSNatRule(50, $rule->serialize());
+ }
+ foreach ($mdlFilter->npt->rule->sortedBy(["sequence"]) as $key => $rule) {
+ $fw->registerNptRule(50, $rule->serialize());
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterBaseController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterBaseController.php
new file mode 100644
index 000000000..56cfeff77
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterBaseController.php
@@ -0,0 +1,178 @@
+ []];
+ $catcount = [];
+ if (!empty(static::$categorysource)) {
+ $node = $this->getModel();
+ foreach (explode('.', static::$categorysource) as $ref) {
+ $node = $node->$ref;
+ }
+ foreach ($node->iterateItems() as $item) {
+ if (!empty((string)$item->categories)) {
+ foreach (explode(',', (string)$item->categories) as $cat) {
+ if (!isset($catcount[$cat])) {
+ $catcount[$cat] = 0;
+ }
+ $catcount[$cat] += 1;
+ }
+ }
+ }
+ }
+ foreach ((new Category())->categories->category->iterateItems() as $key => $category) {
+ $response['rows'][] = [
+ "uuid" => $key,
+ "name" => (string)$category->name,
+ "color" => (string)$category->color,
+ "used" => isset($catcount[$key]) ? $catcount[$key] : 0
+ ];
+ }
+ array_multisort(array_column($response['rows'], "name"), SORT_ASC, SORT_NATURAL, $response['rows']);
+
+ return $response;
+ }
+
+ /**
+ * list of available network options
+ * @return array
+ */
+ public function listNetworkSelectOptionsAction()
+ {
+ $result = [
+ 'single' => [
+ 'label' => gettext("Single host or Network")
+ ],
+ 'aliases' => [
+ 'label' => gettext("Aliases"),
+ 'items' => []
+ ],
+ 'networks' => [
+ 'label' => gettext("Networks"),
+ 'items' => [
+ 'any' => gettext('any'),
+ '(self)' => gettext("This Firewall")
+ ]
+ ]
+ ];
+ foreach ((Config::getInstance()->object())->interfaces->children() as $ifname => $ifdetail) {
+ $descr = htmlspecialchars(!empty($ifdetail->descr) ? $ifdetail->descr : strtoupper($ifname));
+ $result['networks']['items'][$ifname] = $descr . " " . gettext("net");
+ if (!isset($ifdetail->virtual)) {
+ $result['networks']['items'][$ifname . "ip"] = $descr . " " . gettext("address");
+ }
+ }
+ foreach ((new Alias())->aliases->alias->iterateItems() as $alias) {
+ if (strpos((string)$alias->type, "port") === false) {
+ $result['aliases']['items'][(string)$alias->name] = (string)$alias->name;
+ }
+ }
+
+ return $result;
+ }
+
+
+ public function applyAction($rollback_revision = null)
+ {
+ if ($this->request->isPost()) {
+ if ($rollback_revision != null) {
+ // background rollback timer
+ (new Backend())->configdpRun('pfplugin rollback_timer', [$rollback_revision], true);
+ }
+ return array("status" => (new Backend())->configdRun('filter reload'));
+ } else {
+ return array("status" => "error");
+ }
+ }
+
+ public function cancelRollbackAction($rollback_revision)
+ {
+ if ($this->request->isPost()) {
+ return array(
+ "status" => (new Backend())->configdpRun('pfplugin cancel_rollback', [$rollback_revision])
+ );
+ } else {
+ return array("status" => "error");
+ }
+ }
+
+ public function savepointAction()
+ {
+ if ($this->request->isPost()) {
+ // trigger a save, so we know revision->time matches our running config
+ Config::getInstance()->save();
+ return array(
+ "status" => "ok",
+ "retention" => (string)Config::getInstance()->backupCount(),
+ "revision" => (string)Config::getInstance()->object()->revision->time
+ );
+ } else {
+ return array("status" => "error");
+ }
+ }
+
+ public function revertAction($revision)
+ {
+ if ($this->request->isPost()) {
+ Config::getInstance()->lock();
+ $filename = Config::getInstance()->getBackupFilename($revision);
+ if (!$filename) {
+ Config::getInstance()->unlock();
+ return ["status" => gettext("unknown (or removed) savepoint")];
+ }
+ $this->getModel()->rollback($revision);
+ Config::getInstance()->unlock();
+ (new Backend())->configdRun('filter reload');
+ return ["status" => "ok"];
+ } else {
+ return array("status" => "error");
+ }
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php
new file mode 100644
index 000000000..7af4676eb
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php
@@ -0,0 +1,67 @@
+request->get('category');
+ $filter_funct = function ($record) use ($category) {
+ return empty($category) || array_intersect(explode(',', $record->categories), $category);
+ };
+ return $this->searchBase("rules.rule", ['enabled', 'sequence', 'description'], "sequence", $filter_funct);
+ }
+
+ public function setRuleAction($uuid)
+ {
+ return $this->setBase("rule", "rules.rule", $uuid);
+ }
+
+ public function addRuleAction()
+ {
+ return $this->addBase("rule", "rules.rule");
+ }
+
+ public function getRuleAction($uuid = null)
+ {
+ return $this->getBase("rule", "rules.rule", $uuid);
+ }
+
+ public function delRuleAction($uuid)
+ {
+ return $this->delBase("rules.rule", $uuid);
+ }
+
+ public function toggleRuleAction($uuid, $enabled = null)
+ {
+ return $this->toggleBase("rules.rule", $uuid, $enabled);
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/NptController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/NptController.php
new file mode 100644
index 000000000..9b04139d2
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/NptController.php
@@ -0,0 +1,72 @@
+request->get('category');
+ $filter_funct = function ($record) use ($category) {
+ return empty($category) || array_intersect(explode(',', $record->categories), $category);
+ };
+ return $this->searchBase(
+ "npt.rule",
+ ['enabled', 'sequence', 'source_net', 'destination_net', 'trackif', 'description'],
+ "sequence",
+ $filter_funct
+ );
+ }
+
+ public function setRuleAction($uuid)
+ {
+ return $this->setBase("rule", "npt.rule", $uuid);
+ }
+
+ public function addRuleAction()
+ {
+ return $this->addBase("rule", "npt.rule");
+ }
+
+ public function getRuleAction($uuid = null)
+ {
+ return $this->getBase("rule", "npt.rule", $uuid);
+ }
+
+ public function delRuleAction($uuid)
+ {
+ return $this->delBase("npt.rule", $uuid);
+ }
+
+ public function toggleRuleAction($uuid, $enabled = null)
+ {
+ return $this->toggleBase("npt.rule", $uuid, $enabled);
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/SourceNatController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/SourceNatController.php
new file mode 100644
index 000000000..5833225d9
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/SourceNatController.php
@@ -0,0 +1,67 @@
+request->get('category');
+ $filter_funct = function ($record) use ($category) {
+ return empty($category) || array_intersect(explode(',', $record->categories), $category);
+ };
+ return $this->searchBase("snatrules.rule", ['enabled', 'sequence', 'description'], "sequence", $filter_funct);
+ }
+
+ public function setRuleAction($uuid)
+ {
+ return $this->setBase("rule", "snatrules.rule", $uuid);
+ }
+
+ public function addRuleAction()
+ {
+ return $this->addBase("rule", "snatrules.rule");
+ }
+
+ public function getRuleAction($uuid = null)
+ {
+ return $this->getBase("rule", "snatrules.rule", $uuid);
+ }
+
+ public function delRuleAction($uuid)
+ {
+ return $this->delBase("snatrules.rule", $uuid);
+ }
+
+ public function toggleRuleAction($uuid, $enabled = null)
+ {
+ return $this->toggleBase("snatrules.rule", $uuid, $enabled);
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/FilterController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/FilterController.php
new file mode 100644
index 000000000..461f990a5
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/FilterController.php
@@ -0,0 +1,49 @@
+view->pick('OPNsense/Firewall/filter');
+ $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->formDialogFilterRule = $this->getForm("dialogFilterRule");
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/NptController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/NptController.php
new file mode 100644
index 000000000..0e69ca4fe
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/NptController.php
@@ -0,0 +1,60 @@
+view->pick('OPNsense/Firewall/filter');
+ $this->view->ruleController = "npt";
+ $this->view->hideSavePointBtns = true;
+ $this->view->gridFields = [
+ [
+ 'id' => 'enabled', 'formatter' => 'rowtoggle' ,'width' => '6em', 'heading' => gettext('Enabled')
+ ],
+ [
+ 'id' => 'sequence','width' => '9em', 'heading' => gettext('Sequence')
+ ],
+ [
+ 'id' => 'source_net', 'heading' => gettext('Internal IPv6 Prefix')
+ ],
+ [
+ 'id' => 'destination_net', 'heading' => gettext('External IPv6 Prefix')
+ ],
+ [
+ 'id' => 'trackif', 'heading' => gettext('Track if')
+ ],
+ [
+ 'id' => 'description', 'heading' => gettext('Description')
+ ]
+ ];
+
+ $this->view->formDialogFilterRule = $this->getForm("dialogNptRule");
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/SourceNatController.php b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/SourceNatController.php
new file mode 100644
index 000000000..7bc2c632f
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/SourceNatController.php
@@ -0,0 +1,49 @@
+view->pick('OPNsense/Firewall/filter');
+ $this->view->ruleController = "source_nat";
+ $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->formDialogFilterRule = $this->getForm("dialogSNatRule");
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml
new file mode 100644
index 000000000..af4cda9bc
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogFilterRule.xml
@@ -0,0 +1,114 @@
+
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogNptRule.xml b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogNptRule.xml
new file mode 100644
index 000000000..4c68b0e40
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogNptRule.xml
@@ -0,0 +1,53 @@
+
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogSNatRule.xml b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogSNatRule.xml
new file mode 100644
index 000000000..90dc7a90b
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/dialogSNatRule.xml
@@ -0,0 +1,101 @@
+
diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Firewall/ACL/ACL.xml
new file mode 100644
index 000000000..070ce99e9
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/ACL/ACL.xml
@@ -0,0 +1,16 @@
+
+
+ Firewall: Rules: API
+
+ ui/firewall/filter/*
+ api/firewall/filter/*
+
+
+
+ Firewall: SourceNat: API
+
+ ui/firewall/source_nat/*
+ api/firewall/source_nat/*
+
+
+
diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/FilterRuleField.php b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/FilterRuleField.php
new file mode 100644
index 000000000..70996125e
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/FilterRuleField.php
@@ -0,0 +1,140 @@
+iterateItems() as $key => $node) {
+ if (!in_array($key, $map_manual)) {
+ if (is_a($node, "OPNsense\\Base\\FieldTypes\\BooleanField")) {
+ $result[$key] = !empty((string)$node);
+ } elseif (is_a($node, "OPNsense\\Base\\FieldTypes\\ProtocolField")) {
+ if ((string)$node != 'any') {
+ $result[$key] = (string)$node;
+ }
+ } else {
+ $result[$key] = (string)$node;
+ }
+ }
+ }
+ // source / destination mapping
+ $result['source'] = array();
+ if (!empty((string)$this->source_net)) {
+ $result['source']['network'] = (string)$this->source_net;
+ if (!empty((string)$this->source_not)) {
+ $result['source']['not'] = true;
+ }
+ if (!empty((string)$this->source_port)) {
+ $result['source']['port'] = (string)$this->source_port;
+ }
+ }
+ $result['destination'] = array();
+ if (!empty((string)$this->destination_net)) {
+ $result['destination']['network'] = (string)$this->destination_net;
+ if (!empty((string)$this->destination_not)) {
+ $result['destination']['not'] = true;
+ }
+ if (!empty((string)$this->destination_port)) {
+ $result['destination']['port'] = (string)$this->destination_port;
+ }
+ }
+ // field mappings and differences
+ $result['disabled'] = empty((string)$this->enabled);
+ $result['descr'] = (string)$this->description;
+ $result['type'] = (string)$this->action;
+ if (strpos((string)$this->interface, ",") !== false) {
+ $result['floating'] = true;
+ }
+ return $result;
+ }
+
+ /**
+ * rule priority is threaded equally to the legacy rules, first "floating" then groups and single interface
+ * rules are handled last
+ * @return int priority in the ruleset, sequence should determine sort order.
+ */
+ public function getPriority()
+ {
+ $configObj = Config::getInstance()->object();
+ $interface = (string)$this->interface;
+ if (strpos($interface, ",") !== false) {
+ // floating (multiple interfaces involved)
+ return 1000;
+ } elseif (
+ !empty($configObj->interfaces) &&
+ !empty($configObj->interfaces->$interface) &&
+ !empty($configObj->interfaces->$interface->type) &&
+ $configObj->interfaces->$interface->type == 'group'
+ ) {
+ // group type
+ return 2000;
+ } else {
+ // default
+ return 3000;
+ }
+ }
+}
+
+/**
+ * Class FilterRuleField
+ * @package OPNsense\Firewall\FieldTypes
+ */
+class FilterRuleField extends ArrayField
+{
+ /**
+ * @inheritDoc
+ */
+ public function newContainerField($ref, $tagname)
+ {
+ $container_node = new FilterRuleContainerField($ref, $tagname);
+ $parentmodel = $this->getParentModel();
+ $container_node->setParentModel($parentmodel);
+ return $container_node;
+ }
+}
diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/SourceNatRuleField.php b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/SourceNatRuleField.php
new file mode 100644
index 000000000..de1e1ad91
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/SourceNatRuleField.php
@@ -0,0 +1,113 @@
+ false,
+ 'source_net' => false,
+ 'source_not' => false,
+ 'source_port' => 'sourceport',
+ 'destination_net' => false,
+ 'destination_not' => false,
+ 'destination_port' => 'dstport',
+ 'target_port' => 'natport',
+ 'description' => 'descr'
+ ];
+ // 1-on-1 map (with type conversion if needed)
+ foreach ($this->iterateItems() as $key => $node) {
+ $target_fieldname = isset($source_mapper[$key]) ? $source_mapper[$key] : $key;
+ if ($target_fieldname) {
+ if (is_a($node, "OPNsense\\Base\\FieldTypes\\BooleanField")) {
+ $result[$target_fieldname] = !empty((string)$node);
+ } elseif (is_a($node, "OPNsense\\Base\\FieldTypes\\ProtocolField")) {
+ if ((string)$node != 'any') {
+ $result[$target_fieldname] = (string)$node;
+ }
+ } else {
+ $result[$target_fieldname] = (string)$node;
+ }
+ }
+ }
+
+ $result['disabled'] = empty((string)$this->enabled);
+ // source / destination mapping, doesn't use port construct like it would for rules.
+ $result['source'] = array();
+ if (!empty((string)$this->source_net)) {
+ $result['source']['network'] = (string)$this->source_net;
+ if (!empty((string)$this->source_not)) {
+ $result['source']['not'] = true;
+ }
+ }
+ $result['destination'] = array();
+ if (!empty((string)$this->destination_net)) {
+ $result['destination']['network'] = (string)$this->destination_net;
+ if (!empty((string)$this->destination_not)) {
+ $result['destination']['not'] = true;
+ }
+ }
+
+ return $result;
+ }
+}
+
+/**
+ * Class SourceNatRuleField
+ * @package OPNsense\Firewall\FieldTypes
+ */
+class SourceNatRuleField extends ArrayField
+{
+ /**
+ * @inheritDoc
+ */
+ public function newContainerField($ref, $tagname)
+ {
+ $container_node = new SourceNatRuleContainerField($ref, $tagname);
+ $parentmodel = $this->getParentModel();
+ $container_node->setParentModel($parentmodel);
+ return $container_node;
+ }
+}
diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.php b/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.php
new file mode 100644
index 000000000..6ca0fd4e6
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.php
@@ -0,0 +1,145 @@
+rules->rule, $this->snatrules->rule] as $rules) {
+ foreach ($rules->iterateItems() as $rule) {
+ if ($validateFullModel || $rule->isFieldChanged()) {
+ // port / protocol validation
+ if (!empty((string)$rule->source_port) && !in_array($rule->protocol, ['TCP', 'UDP'])) {
+ $messages->appendMessage(new Message(
+ gettext("Source ports are only valid for tcp or udp type rules."),
+ $rule->source_port->__reference
+ ));
+ }
+ if (!empty((string)$rule->destination_port) && !in_array($rule->protocol, ['TCP', 'UDP'])) {
+ $messages->appendMessage(new Message(
+ gettext("Destination ports are only valid for tcp or udp type rules."),
+ $rule->destination_port->__reference
+ ));
+ }
+ // validate protocol family
+ $dest_is_addr = Util::isSubnet($rule->destination_net) || Util::isIpAddress($rule->destination_net);
+ $dest_proto = strpos($rule->destination_net, ':') === false ? "inet" : "inet6";
+ if ($dest_is_addr && $dest_proto != $rule->ipprotocol) {
+ $messages->appendMessage(new Message(
+ gettext("Destination address type should match selected TCP/IP protocol version."),
+ $rule->destination_net->__reference
+ ));
+ }
+ $src_is_addr = Util::isSubnet($rule->source_net) || Util::isIpAddress($rule->source_net);
+ $src_proto = strpos($rule->source_net, ':') === false ? "inet" : "inet6";
+ if ($src_is_addr && $src_proto != $rule->ipprotocol) {
+ $messages->appendMessage(new Message(
+ gettext("Source address type should match selected TCP/IP protocol version."),
+ $rule->source_net->__reference
+ ));
+ }
+ // Additional source nat validations
+ if ($rule->target !== null) {
+ $target_is_addr = Util::isSubnet($rule->target) || Util::isIpAddress($rule->target);
+ $target_proto = strpos($rule->target, ':') === false ? "inet" : "inet6";
+ if ($target_is_addr && $target_proto != $rule->ipprotocol) {
+ $messages->appendMessage(new Message(
+ gettext("Target address type should match selected TCP/IP protocol version."),
+ $rule->target->__reference
+ ));
+ }
+ if (!empty((string)$rule->target_port) && !in_array($rule->protocol, ['TCP', 'UDP'])) {
+ $messages->appendMessage(new Message(
+ gettext("Target ports are only valid for tcp or udp type rules."),
+ $rule->target_port->__reference
+ ));
+ }
+ }
+ }
+ }
+ }
+ foreach ($this->npt->rule->iterateItems() as $rule) {
+ if ($validateFullModel || $rule->isFieldChanged()) {
+ if (!empty((string)$rule->destination_net) && !empty((string)$rule->trackif)) {
+ $messages->appendMessage(new Message(
+ gettext("A track interface is only allowed without an extrenal prefix."),
+ $rule->trackif->__reference
+ ));
+ }
+ if (!empty((string)$rule->destination_net) && !empty((string)$rule->source_net)) {
+ $dparts = explode('/', (string)$rule->destination_net);
+ $sparts = explode('/', (string)$rule->source_net);
+ if (count($dparts) == 2 && count($sparts) == 2 && $dparts[1] != $sparts[1]) {
+ $messages->appendMessage(new Message(
+ gettext("External subnet should match internal subnet."),
+ $rule->destination_net->__reference
+ ));
+ }
+ }
+ }
+ }
+ return $messages;
+ }
+
+ /**
+ * Rollback this model to a previous version.
+ * Make sure to remove this object afterwards, since its contents won't be updated.
+ * @param $revision float|string revision number
+ * @return bool action performed (backup revision existed)
+ */
+ public function rollback($revision)
+ {
+ $filename = Config::getInstance()->getBackupFilename($revision);
+ if ($filename) {
+ // fiddle with the dom, copy OPNsense->Firewall->Filter from backup to current config
+ $sourcexml = simplexml_load_file($filename);
+ if ($sourcexml->OPNsense->Firewall->Filter) {
+ $sourcedom = dom_import_simplexml($sourcexml->OPNsense->Firewall->Filter);
+ $targetxml = Config::getInstance()->object();
+ $targetdom = dom_import_simplexml($targetxml->OPNsense->Firewall->Filter);
+ $node = $targetdom->ownerDocument->importNode($sourcedom, true);
+ $targetdom->parentNode->replaceChild($node, $targetdom);
+ Config::getInstance()->save();
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.xml b/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.xml
new file mode 100644
index 000000000..3e93bded0
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/Filter.xml
@@ -0,0 +1,270 @@
+
+ //OPNsense/Firewall/Filter
+ 1.0.2
+ MFP
+
+ OPNsense firewall filter rules
+
+
+
+
+
+ 1
+ Y
+
+
+ 1
+ 99999
+ provide a valid sequence for sorting
+ Y
+ 1
+
+
+ Y
+ pass
+
+ Pass
+ Block
+ Reject
+
+
+
+ 1
+ Y
+
+
+ N
+ Y
+ Y
+
+
+ Y
+ in
+
+ In
+ Out
+
+
+
+ Y
+ inet
+
+ IPv4
+ IPv6
+
+
+
+ Y
+ any
+
+
+
+ any
+ Y
+
+
+ 0
+ Y
+
+
+ N
+ Y
+ Y
+ Y
+ Please specify a valid portnumber, name, alias or range
+
+
+
+ any
+ Y
+
+
+ 0
+ Y
+
+
+ N
+ Y
+ Y
+ Y
+ Please specify a valid portnumber, name, alias or range
+
+
+ N
+ interface gateways list
+ /tmp/gateway_list.json
+ 20
+ Specify a valid gateway from the list matching the networks ip protocol.
+
+
+ 0
+ Y
+
+
+
+
+ OPNsense.Firewall.Category
+ categories.category
+ name
+
+
+ Y
+ Related category not found.
+
+
+ N
+ /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){0,255}$/u
+ Description should be a string between 1 and 255 characters
+
+
+
+
+
+
+ 1
+ Y
+
+
+ 0
+ Y
+
+
+ 1
+ 99999
+ provide a valid sequence for sorting
+ Y
+ 1
+
+
+ Y
+ lan
+ Y
+
+
+ Y
+ inet
+
+ IPv4
+ IPv6
+
+
+
+ Y
+ any
+
+
+ any
+ Y
+
+
+ 0
+ Y
+
+
+ N
+ Y
+ Y
+ Y
+ Please specify a valid portnumber, name, alias or range
+
+
+ any
+ Y
+
+
+ 0
+ Y
+
+
+ N
+ Y
+ Y
+ Y
+ Please specify a valid portnumber, name, alias or range
+
+
+ wanip
+ Y
+
+
+ N
+ Y
+ Y
+
+
+ 0
+ Y
+
+
+
+
+ OPNsense.Firewall.Category
+ categories.category
+ name
+
+
+ Y
+ Related category not found.
+
+
+ N
+ /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){0,255}$/u
+ Description should be a string between 1 and 255 characters
+
+
+
+
+
+
+ 1
+ Y
+
+
+ 0
+ Y
+
+
+ 1
+ 99999
+ provide a valid sequence for sorting
+ Y
+ 1
+
+
+ Y
+ lan
+ Y
+
+
+ Y
+ ipv6
+ Y
+ N
+
+
+ ipv6
+ Y
+ N
+
+
+
+
+
+
+ OPNsense.Firewall.Category
+ categories.category
+ name
+
+
+ Y
+ Related category not found.
+
+
+ N
+ /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){0,255}$/u
+ Description should be a string between 1 and 255 characters
+
+
+
+
+
diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Firewall/Menu/Menu.xml
new file mode 100644
index 000000000..476e1f38b
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/Menu/Menu.xml
@@ -0,0 +1,15 @@
+
diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/Migrations/MFP1_0_0.php b/src/opnsense/mvc/app/models/OPNsense/Firewall/Migrations/MFP1_0_0.php
new file mode 100644
index 000000000..2e33d2f7c
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/Migrations/MFP1_0_0.php
@@ -0,0 +1,59 @@
+Firewall->FilterRule ---> OPNsense->Firewall->Filter
+ $cfgObj = Config::getInstance()->object();
+ if (
+ !empty($cfgObj->OPNsense) && !empty($cfgObj->OPNsense->Firewall)
+ && !empty($cfgObj->OPNsense->Firewall->FilterRule)
+ ) {
+ // model migration created a new, empty rules section
+ if (empty($cfgObj->OPNsense->Firewall->Filter->rules)) {
+ unset($cfgObj->OPNsense->Firewall->Filter->rules);
+ $targetdom = dom_import_simplexml($cfgObj->OPNsense->Firewall->Filter);
+ foreach ($cfgObj->OPNsense->Firewall->FilterRule->children() as $child) {
+ $sourcedom = dom_import_simplexml($child);
+ $targetdom->appendChild($sourcedom);
+ }
+ unset($cfgObj->OPNsense->Firewall->FilterRule);
+ Config::getInstance()->save();
+ }
+ }
+ }
+}
diff --git a/src/opnsense/mvc/app/views/OPNsense/Firewall/filter.volt b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter.volt
new file mode 100644
index 000000000..717453fe9
--- /dev/null
+++ b/src/opnsense/mvc/app/views/OPNsense/Firewall/filter.volt
@@ -0,0 +1,250 @@
+
+
+
+
+
+
+
+
+
+
+
+ | {{ lang._('ID') }} |
+{% for fieldlist in gridFields %}
+ {{fieldlist['heading']|default('')}} |
+{% endfor %}
+ {{ lang._('Commands') }} |
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ {{ lang._('After changing settings, please remember to apply them with the button below') }}
+
+
+
+{% if not hideSavePointBtns|default(false) %}
+
+
+
+
+{% endif %}
+
+
+
+
+
+
+
+{{ partial("layout_partials/base_dialog",['fields':formDialogFilterRule,'id':'DialogFilterRule','label':lang._('Edit rule')])}}
diff --git a/src/opnsense/scripts/pfplugin/rollback_cancel b/src/opnsense/scripts/pfplugin/rollback_cancel
new file mode 100755
index 000000000..c47f74b07
--- /dev/null
+++ b/src/opnsense/scripts/pfplugin/rollback_cancel
@@ -0,0 +1,40 @@
+#!/usr/local/bin/php
+= 2) {
+ $revision = preg_replace("/[^0-9.]/", "", $argv[1]);
+ if (!empty($revision)) {
+ $lckfile = "/tmp/pfplugin_{$revision}.lock";
+ if (file_exists($lckfile)) {
+ unlink($lckfile);
+ exit(0);
+ }
+ }
+}
+exit(1);
diff --git a/src/opnsense/scripts/pfplugin/rollback_timer b/src/opnsense/scripts/pfplugin/rollback_timer
new file mode 100755
index 000000000..819a1f12c
--- /dev/null
+++ b/src/opnsense/scripts/pfplugin/rollback_timer
@@ -0,0 +1,54 @@
+#!/usr/local/bin/php
+= 2) {
+ $revision = preg_replace("/[^0-9.]/", "", $argv[1]);
+ if (!empty($revision)) {
+ $lckfile = "/tmp/pfplugin_{$revision}.lock";
+ file_put_contents($lckfile, "");
+ // give the api 60 seconds to callback
+ for ($i=0; $i < 60 ; ++$i) {
+ if (!file_exists($lckfile)) {
+ // got feedback
+ exit(0);
+ }
+ sleep(1);
+ }
+ @unlink($lckfile);
+ // no feedback, revert
+ $mdlFilter = new OPNsense\Firewall\Filter();
+ if ($mdlFilter->rollback($revision)) {
+ (new OPNsense\Core\Backend())->configdRun('filter reload');
+ } else {
+ syslog(LOG_WARNING, "unable to revert to unexisting revision : {$revision}");
+ }
+ }
+}
diff --git a/src/opnsense/service/conf/actions.d/actions_pfplugin.conf b/src/opnsense/service/conf/actions.d/actions_pfplugin.conf
new file mode 100644
index 000000000..61a13f477
--- /dev/null
+++ b/src/opnsense/service/conf/actions.d/actions_pfplugin.conf
@@ -0,0 +1,11 @@
+[rollback_timer]
+command:/usr/local/bin/flock -n -E 0 -o /tmp/pfplugin_rollback_timer.lock /usr/local/opnsense/scripts/pfplugin/rollback_timer
+parameters: %s
+type:script
+message:wait for api feedback or revert to previous filter plugin config
+
+[cancel_rollback]
+command: /usr/local/opnsense/scripts/pfplugin/rollback_cancel
+parameters: %s
+type:script_output
+message:cancel pfplugin rollback