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 @@ +
+ + rule.enabled + + checkbox + Enable this rule + + + rule.sequence + + text + + + rule.action + + dropdown + 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. + + + + rule.quick + + checkbox + + 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. + + + + rule.interface + + select_multiple + + + rule.direction + + dropdown + + Direction of the traffic. The default policy is to filter inbound traffic, which sets the policy to the interface originally receiving the traffic. + + + + rule.ipprotocol + + dropdown + + + rule.protocol + + dropdown + + + rule.source_net + + text + + + rule.source_port + + text + true + Source port number or well known name (imap, imaps, http, https, ...), for ranges use a dash + + + rule.source_not + + checkbox + Use this option to invert the sense of the match. + + + rule.destination_net + + text + + + rule.destination_not + + checkbox + Use this option to invert the sense of the match. + + + rule.destination_port + + text + Destination port number or well known name (imap, imaps, http, https, ...), for ranges use a dash + + + rule.gateway + + dropdown + + Leave as 'default' to use the system routing table. Or choose a gateway to utilize policy based routing. + + + + rule.log + + checkbox + Log packets that are handled by this rule + + + rule.categories + + select_multiple + + For grouping purposes you may select multiple groups here to organize items. + + + rule.description + + text + +
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 @@ +
+ + rule.enabled + + checkbox + Enable this rule + + + rule.sequence + + text + + + rule.log + + checkbox + Log packets that are handled by this rule + + + rule.interface + + dropdown + + + rule.source_net + + text + + + rule.destination_net + + text + Enter the external IPv6 prefix for this network prefix translation. Leave empty to auto-detect the prefix address using the specified tracking interface instead. The prefix size specified for the internal prefix will also be applied to the external prefix. + + + rule.trackif + + dropdown + Use prefix defined on the selected interface instead of the interface this rule applies to when target prefix is not provided. + + + rule.categories + + select_multiple + + For grouping purposes you may select multiple groups here to organize items. + + + rule.description + + text + +
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 @@ +
+ + rule.enabled + + checkbox + Enable this rule + + + rule.nonat + + checkbox + Enabling this option will disable NAT for traffic matching this rule and stop processing Outbound NAT rules. + + + rule.sequence + + text + + + rule.interface + + dropdown + + + rule.ipprotocol + + dropdown + + + rule.protocol + + dropdown + + + rule.source_net + + text + + + rule.source_port + + text + true + Source port number or well known name (imap, imaps, http, https, ...), for ranges use a dash + + + rule.source_not + + checkbox + Use this option to invert the sense of the match. + + + rule.destination_net + + text + + + rule.destination_not + + checkbox + Use this option to invert the sense of the match. + + + rule.destination_port + + text + Destination port number or well known name (imap, imaps, http, https, ...), for ranges use a dash + + + rule.target + + text + + Packets matching this rule will be mapped to the IP address given here. + + + + rule.target_port + + text + Destination port number or well known name (imap, imaps, http, https, ...) + + + rule.log + + checkbox + Log packets that are handled by this rule + + + rule.categories + + select_multiple + + For grouping purposes you may select multiple groups here to organize items. + + + rule.description + + text + +
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 @@ + + + + +
+
+ + + + + + +{% for fieldlist in gridFields %} + +{% endfor %} + + + + + + + + + + + +
{{ lang._('ID') }}{{fieldlist['heading']|default('')}}{{ lang._('Commands') }}
+ + +
+
+ +
+ +{% 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