From 7d4597efe5e85dcf2d5df18def7521ee8d3edd09 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Wed, 26 Oct 2022 16:42:46 +0200 Subject: [PATCH] Virtual IP MVC/API conversion (#6105) closes #5984 refactors legacy pages, includes the following: o remove type field as this seems to be redundant and confusing o input form additions (show hide related fields) o add button for carp type to select first unused vhid o implement configure action, caching removed addresses in /tmp/delete_vip_{$uuid}.todo files (by the controller) o add mode filter to search action and complete with relevant fields for our grid o fix warning in interfaces.inc (interface_proxyarp_configure()), array creation issue o add validation for addresses used in port forwards and outbound nat rules. previous version tried to rename forwards, we choose to be consistent when it comes to edit/delete. o change ACL to use the new endpoints, remove "show only" ACL. we can always consider putting it back later, but the experience of only able to reach the grid likely won't be practical. o remove old firewall_virtual_ip*.php files --- plist | 11 +- src/etc/inc/interfaces.inc | 2 +- .../Interfaces/Api/VipSettingsController.php | 202 +++++++ .../OPNsense/Interfaces/VipController.php | 38 ++ .../OPNsense/Interfaces/forms/dialogVip.xml | 78 +++ .../mvc/app/models/OPNsense/Core/ACL/ACL.xml | 9 +- .../app/models/OPNsense/Core/Menu/Menu.xml | 4 +- .../FieldTypes/VipInterfaceField.php | 58 ++ .../Interfaces/FieldTypes/VipNetworkField.php | 67 +++ .../app/models/OPNsense/Interfaces/Vip.php | 193 +++++++ .../app/models/OPNsense/Interfaces/Vip.xml | 77 +++ .../mvc/app/views/OPNsense/Interface/vip.volt | 129 +++++ .../scripts/interfaces/reconfigure_vips.php | 123 +++++ .../conf/actions.d/actions_interface.conf | 5 + src/www/firewall_nat_edit.php | 2 +- src/www/firewall_nat_out_edit.php | 2 +- src/www/firewall_virtual_ip.php | 329 ----------- src/www/firewall_virtual_ip_edit.php | 517 ------------------ 18 files changed, 985 insertions(+), 861 deletions(-) create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VipSettingsController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Interfaces/VipController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogVip.xml create mode 100644 src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipInterfaceField.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipNetworkField.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.xml create mode 100644 src/opnsense/mvc/app/views/OPNsense/Interface/vip.volt create mode 100755 src/opnsense/scripts/interfaces/reconfigure_vips.php delete mode 100644 src/www/firewall_virtual_ip.php delete mode 100644 src/www/firewall_virtual_ip_edit.php diff --git a/plist b/plist index b5fbf6200..d2a5565ce 100644 --- a/plist +++ b/plist @@ -346,12 +346,15 @@ /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogKeyPair.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPSK.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/LoopbackSettingsController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VipSettingsController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VlanSettingsController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VxlanSettingsController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/LoopbackController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/VipController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/VlanController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/VxlanController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogLoopback.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogVip.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogVlan.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogVxlan.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Monit/Api/ServiceController.php @@ -582,10 +585,14 @@ /usr/local/opnsense/mvc/app/models/OPNsense/IPsec/Migrations/M1_0_0.php /usr/local/opnsense/mvc/app/models/OPNsense/IPsec/Migrations/M1_0_1.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/ACL/ACL.xml +/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipInterfaceField.php +/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipNetworkField.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VlanInterfaceField.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Loopback.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Loopback.xml /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.xml +/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.php +/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.xml /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Vlan.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Vlan.xml /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/VxLan.php @@ -666,6 +673,7 @@ /usr/local/opnsense/mvc/app/views/OPNsense/IPsec/spd.volt /usr/local/opnsense/mvc/app/views/OPNsense/IPsec/tunnels.volt /usr/local/opnsense/mvc/app/views/OPNsense/Interface/loopback.volt +/usr/local/opnsense/mvc/app/views/OPNsense/Interface/vip.volt /usr/local/opnsense/mvc/app/views/OPNsense/Interface/vlan.volt /usr/local/opnsense/mvc/app/views/OPNsense/Interface/vxlan.volt /usr/local/opnsense/mvc/app/views/OPNsense/Monit/index.volt @@ -829,6 +837,7 @@ /usr/local/opnsense/scripts/interfaces/ppp-linkdown.sh /usr/local/opnsense/scripts/interfaces/ppp-linkup.sh /usr/local/opnsense/scripts/interfaces/ppp-uptime.sh +/usr/local/opnsense/scripts/interfaces/reconfigure_vips.php /usr/local/opnsense/scripts/interfaces/reconfigure_vlans.php /usr/local/opnsense/scripts/interfaces/rtsold_resolvconf.sh /usr/local/opnsense/scripts/interfaces/traffic_stats.php @@ -1841,8 +1850,6 @@ /usr/local/www/firewall_schedule_edit.php /usr/local/www/firewall_scrub.php /usr/local/www/firewall_scrub_edit.php -/usr/local/www/firewall_virtual_ip.php -/usr/local/www/firewall_virtual_ip_edit.php /usr/local/www/foot.inc /usr/local/www/getserviceproviders.php /usr/local/www/guiconfig.inc diff --git a/src/etc/inc/interfaces.inc b/src/etc/inc/interfaces.inc index 01e94db56..0fb47514c 100644 --- a/src/etc/inc/interfaces.inc +++ b/src/etc/inc/interfaces.inc @@ -1303,7 +1303,7 @@ function interface_proxyarp_configure($interface = '') if ($vipent['mode'] === "proxyarp") { if (empty($interface) || $interface == $vipent['interface']) { if (empty($paa[$vipent['interface']])) { - $paa[$proxyif] = array(); + $paa[$vipent['interface']] =[]; } $paa[$vipent['interface']][] = $vipent; } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VipSettingsController.php b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VipSettingsController.php new file mode 100644 index 000000000..55a143d90 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VipSettingsController.php @@ -0,0 +1,202 @@ + '']; + $tmp = $this->request->getPost('vip'); + if (!empty($tmp['network'])) { + $parts = explode('/', $tmp['network'], 2); + $overlay['subnet'] = $parts[0]; + if (count($parts) < 2) { + $overlay['subnet_bits'] = strpos($parts[0], ':') !== false ? 128 : 32; + } else { + $overlay['subnet_bits'] = $parts[1]; + } + } + return $overlay; + } + + /** + * retrieve first unused VHID number + */ + public function getUnusedVhidAction() + { + $vhids = []; + foreach ($this->getModel()->vip->iterateItems() as $vip) { + if (!in_array((string)$vip->vhid, $vhids) && !empty((string)$vip->vhid)) { + $vhids[] = (string)$vip->vhid; + } + } + for ($i=1; $i <= 255; $i++) { + if (!in_array((string)$i, $vhids)) { + return ['vhid' => $i, 'status' => 'ok']; + } + } + return ['status' => 'not_found']; + } + + /** + * remap subnet and subnet_bits to network (which represents combined field) + */ + private function handleFormValidations($response) + { + if (!empty($response['validations'])) { + foreach (array_keys($response['validations']) as $fieldname) { + if (in_array($fieldname, ['vip.subnet', 'vip.subnet_bits'])) { + if (empty($response['validations']['vip.network'])) { + $response['validations']['vip.network'] = []; + } + if (is_array($response['validations'][$fieldname])) { + $response['validations']['vip.network'] = array_merge( + $response['validations']['vip.network'], + $response['validations'][$fieldname] + ); + } else { + $response['validations']['vip.network'][] = $response['validations'][$fieldname]; + } + unset($response['validations'][$fieldname]); + } + } + } + return $response; + } + + public function searchItemAction() + { + $mode = $this->request->getPost('mode'); + $filter_funct = null; + if (!empty($mode)) { + $filter_funct = function ($record) use ($mode) { + return in_array($record->mode, $mode); + }; + } + $result = $this->searchBase( + 'vip', + ['interface', 'mode', 'type', 'descr', 'subnet', 'subnet_bits', 'vhid', 'advbase', 'advskew'], + 'descr', + $filter_funct + ); + + if (!empty($result['rows'])) { + foreach ($result['rows'] as &$row) { + $row['address'] = sprintf("%s/%s", $row['subnet'], $row['subnet_bits']); + $row['vhid_txt'] = $row['vhid']; + if ($row['mode'] == 'CARP') { + $row['vhid_txt'] = sprintf( + gettext('%s (freq. %s/%s)'), + $row['vhid'], + $row['advbase'], + $row['advskew'] + ); + } + } + } + return $result; + } + + public function setItemAction($uuid) + { + $node = $this->getModel()->getNodeByReference('vip.' . $uuid); + $validations = []; + if ($node != null && explode('/', $_POST['vip']['network'])[0] != (string)$node->subnet) { + $validations = $this->getModel()->whereUsed((string)$node->subnet); + if (!empty($validations)) { + // XXX a bit unpractical, but we can not validate previous values from the model so + // we are obligated to return this as a single error (even if the form has other issues too) + return [ + 'result' => 'failed', + 'validations' => [ + 'vip.network' => array_slice($validations, 0 , 2) + ] + ]; + } elseif (!file_exists("/tmp/delete_vip_{$uuid}.todo")) { + file_put_contents("/tmp/delete_vip_{$uuid}.todo", (string)$node->subnet); + } + } + + return $this->handleFormValidations($this->setBase('vip', 'vip', $uuid, $this->getVipOverlay())); + } + + public function addItemAction() + { + return $this->handleFormValidations($this->addBase('vip', 'vip', $this->getVipOverlay())); + } + + public function getItemAction($uuid = null) + { + $vip = $this->getBase('vip', 'vip', $uuid); + // Merge subnet + netmask into network field + if (!empty($vip['vip']) && !empty($vip['vip']['subnet'])) { + $vip['vip']['network'] = $vip['vip']['subnet'] . "/" . $vip['vip']['subnet_bits']; + } elseif (!empty($vip['vip'])) { + $vip['vip']['network'] = ''; + } + unset($vip['vip']['subnet']); + unset($vip['vip']['subnet_bits']); + return $vip; + } + + public function delItemAction($uuid) + { + $node = $this->getModel()->getNodeByReference('vip.' . $uuid); + $validations = $this->getModel()->whereUsed((string)$node->subnet); + if (!empty($validations)) { + throw new UserException(implode('
', array_slice($validations, 0 , 5)), gettext("Item in use by")); + } + $response = $this->delBase("vip", $uuid); + if ($response['result'] ?? '' == 'deleted' && !file_exists("/tmp/delete_vip_{$uuid}.todo")) { + file_put_contents("/tmp/delete_vip_{$uuid}.todo", (string)$node->subnet); + } + return $response; + } + + public function reconfigureAction() + { + $result = array("status" => "failed"); + if ($this->request->isPost()) { + $result['status'] = strtolower(trim((new Backend())->configdRun('interface vip configure'))); + } + return $result; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/VipController.php b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/VipController.php new file mode 100644 index 000000000..457e0e33e --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/VipController.php @@ -0,0 +1,38 @@ +view->pick('OPNsense/Interface/vip'); + $this->view->formDialogVip = $this->getForm("dialogVip"); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogVip.xml b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogVip.xml new file mode 100644 index 000000000..be2faa16f --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogVip.xml @@ -0,0 +1,78 @@ +
+ + vip.mode + + dropdown + Proxy ARP and other type Virtual IPs cannot be bound to by anything running on the firewall, such as IPsec, OpenVPN, etc. Use a CARP or IP Alias type address for these cases. + + + vip.interface + + dropdown + + + vip.network + + text + Provide an address and subnet to use. (e.g 192.168.0.1/24) + + + vip.gateway + + text + For some interface types a gateway is required to configure an IP Alias (ppp/pppoe/tun), leave this field empty for all other interface types. + true + + + vip.noexpand + + checkbox + Disable expansion of this entry into IPs on NAT lists (e.g. 192.168.1.0/24 expands to 256 entries. + + + + vip.nobind + + checkbox + Assigning services to the virtual IP's interface will automatically include this address. Check to prevent binding to this address instead. + + + + vip.password + + password + + Enter the VHID group password. + + + vip.vhid + + text + + Enter the VHID group that the machines will share. + + + vip.advbase + + text + + Specifies the base of the advertisement interval in seconds. The acceptable values are 1 to 255. + + + vip.advskew + + text + + + Specifies the skew to add to the base advertisement interval to make one host advertise slower than another host. + It is specified in 1/256 of seconds. The acceptable values are 0 to 254. + + + + vip.descr + + text + You may enter a description here for your reference (not parsed). + + +
diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml index 239f9b117..776bb3081 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml @@ -309,15 +309,10 @@ Firewall: Virtual IP Address: Edit - firewall_virtual_ip_edit.php* + ui/interfaces/vip + api/interfaces/vip_settings/* - - Firewall: Virtual IP Addresses - - firewall_virtual_ip.php* - - Diagnostics: Log: Firewall: General diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml index a0abfe39e..570553ac2 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml @@ -106,9 +106,7 @@ - - - + diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipInterfaceField.php b/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipInterfaceField.php new file mode 100644 index 000000000..70d50ca4a --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipInterfaceField.php @@ -0,0 +1,58 @@ +object(); + if (!empty($configHandle->interfaces)) { + foreach ($configHandle->interfaces->children() as $ifname => $node) { + if (!empty((string)$node->type) && $node->type == 'group') { + continue; + } elseif (!empty((string)$node->if) && $node->if == 'enc0') { + continue; + } + $descr = !empty((string)$node->descr) ? (string)$node->descr : strtoupper($ifname); + self::$interfaces[$ifname] = $descr; + } + } + } + $this->internalOptionList = self::$interfaces; + return parent::actionPostLoadingEvent(); + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipNetworkField.php b/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipNetworkField.php new file mode 100644 index 000000000..eac2f0526 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipNetworkField.php @@ -0,0 +1,67 @@ +getInternalXMLTagName() == 'subnet_bits' && $this->getParentNode()->subnet->isFieldChanged()) { + // no need to validate on subnet_bits as subnet is changed as well, let's trigger on subnet only + return $validators; + } + $validators[] = new CallbackValidator(["callback" => function ($data) { + $parent = $this->getParentNode(); + $subnet_bits = (string)$parent->subnet_bits; + $subnet = (string)$parent->subnet; + $messages = []; + if (!Util::isSubnet($subnet . "/" . $subnet_bits)) { + $messages[] = sprintf( + gettext('Entry "%s/%s" is not a valid network address.'), + $subnet, + $subnet_bits + ); + } + return $messages; + } + ]); + + return $validators; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.php b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.php new file mode 100644 index 000000000..0cdad69ee --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.php @@ -0,0 +1,193 @@ +getFlatNodes() as $key => $node) { + $tagName = $node->getInternalXMLTagName(); + $parentNode = $node->getParentNode(); + if ($validateFullModel || $node->isFieldChanged()) { + if ($parentNode->getInternalXMLTagName() === 'vip' && in_array($tagName, $vip_fields)) { + $parentKey = $parentNode->__reference; + $vips[$parentKey] = $parentNode; + } + } + if ((string)$parentNode->mode == 'carp' && !isset($carp_vhids[(string)$parentNode->vhid])) { + $carp_vhids[(string)$parentNode->vhid] = $parentNode; + } + } + + // validate all changed VIPs + foreach ($vips as $key => $node) { + $subnet_bits = (string)$node->subnet_bits; + $subnet = (string)$node->subnet; + if (in_array((string)$node->mode, ['carp', 'ipalias'])) { + if (Util::isSubnet($subnet . "/" . $subnet_bits) && strpos($subnet, ':') === false) { + $sm = 0; + for ($i = 0; $i < $subnet_bits; $i++) { + $sm >>= 1; + $sm |= 0x80000000; + } + $network_addr = long2ip(ip2long($subnet) & $sm); + $broadcast_addr = long2ip((ip2long($subnet) & 0xFFFFFFFF) | $sm); + if ($subnet == $network_addr && $subnet_bits != '32') { + $messages->appendMessage( + new Message( + gettext("You cannot use the network address for this VIP"), + $key . ".subnet" + ) + ); + } elseif ($subnet == $broadcast_addr && $subnet_bits != '32') { + $messages->appendMessage( + new Message( + gettext("You cannot use the broadcast address for this VIP"), + $key . ".subnet" + ) + ); + } + } + $configHandle = Config::getInstance()->object(); + if (!empty($configHandle->interfaces) && !empty((string)$node->vhid)) { + foreach ($configHandle->interfaces->children() as $ifname => $ifnode) { + if ($ifname === (string)$node->interface && substr($ifnode->if, 0, 2) === 'lo') { + $messages->appendMessage( + new Message( + gettext('For this type of VIP loopback is not allowed.'), + $key . ".interface" + ) + ); + break; + } + } + } + } + if ((string)$node->mode == 'carp') { + if (empty((string)$node->password)) { + $messages->appendMessage( + new Message( + gettext("You must specify a CARP password that is shared between the two VHID members."), + $key . ".password" + ) + ); + } + if (empty((string)$node->vhid)) { + $messages->appendMessage( + new Message( + gettext('A VHID must be selected for this CARP VIP.'), + $key . ".vhid" + ) + ); + } elseif ( + isset($carp_vhids[(string)$node->vhid]) && + $carp_vhids[(string)$node->vhid]->__reference != $node->__reference + ) { + $errmsg = gettext( + "VHID %s is already in use on interface %s. Pick a unique number on this interface." + ); + $messages->appendMessage( + new Message( + sprintf($errmsg, (string)$node->vhid, (string)$carp_vhids[(string)$node->vhid]->interface), + $key . ".vhid" + ) + ); + } + } elseif ( + (string)$node->mode == 'ipalias' && + !empty((string)$node->vhid) && ( + !isset($carp_vhids[(string)$node->vhid]) || + (string)$carp_vhids[(string)$node->vhid]->interface != (string)$node->interface + ) + ) { + $errmsg = gettext("VHID %s must be defined on interface %s as a CARP VIP first."); + $messages->appendMessage( + new Message( + sprintf($errmsg, (string)$node->vhid, (string)$node->interface), + $key . ".vhid" + ) + ); + } + } + + return $messages; + } + + /** + * find relevant references to this address which prevent removal or change of this address. + */ + public function whereUsed($address) + { + $relevant_paths = [ + 'nat.outbound.rule.' => gettext('Address %s referenced by outboud nat rule "%s"'), + 'nat.rule.' => gettext('Address %s referenced by port forward "%s"'), + ]; + $usages = []; + foreach (Config::getInstance()->object()->xpath("//text()[.='{$address}']") as $node) { + $referring_node = $node->xpath("..")[0]; + $item_path = [$node->getName(), $referring_node->getName()]; + $item_description = ""; + $parent_node = $referring_node; + while ($parent_node != null && $parent_node->xpath("../..") != null) { + if (empty($item_description)) { + foreach (["description", "descr", "name"] as $key) { + if (!empty($parent_node->$key)) { + $item_description = (string)$parent_node->$key; + break; + } + } + } + $parent_node = $parent_node->xpath("..")[0]; + $item_path[] = $parent_node->getName(); + } + $item_path = implode('.', array_reverse($item_path))."\n"; + foreach ($relevant_paths as $ref => $msg) { + if (preg_match("/^{$ref}/", $item_path)) { + $usages[] = sprintf($msg, $address, $item_description); + } + } + } + return $usages; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.xml b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.xml new file mode 100644 index 000000000..6601ae603 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Vip.xml @@ -0,0 +1,77 @@ + + /virtualip + 1.0.0 + + Virtual IP configuration + + + + + Y + S + + + Y + ipalias + + IP Alias + CARP + Proxy ARP + + Other + + + + + N + + + Address already assigned. + UniqueConstraint + + + + + + N + + + N + + + 0 + Y + + + 0 + Y + + + N + + + N + 1 + 255 + Invalid VHID number provided, Acceptable values for vhid are 1 to 255. + + + Y + 1 + 1 + 254 + Invalid advertisement interval, acceptable values are 1 to 255. + + + Y + 0 + 0 + 254 + Invalid skew value, acceptable values are 0 to 255. + + + N + + + + diff --git a/src/opnsense/mvc/app/views/OPNsense/Interface/vip.volt b/src/opnsense/mvc/app/views/OPNsense/Interface/vip.volt new file mode 100644 index 000000000..611055a30 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Interface/vip.volt @@ -0,0 +1,129 @@ + +
+ + + + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Address') }}{{ lang._('VHID') }}{{ lang._('Interface') }}{{ lang._('Type') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ + +
+
+ +
+ +

+
+
+ + +{{ partial("layout_partials/base_dialog",['fields':formDialogVip,'id':'DialogVip','label':lang._('Edit Virtual IP')])}} diff --git a/src/opnsense/scripts/interfaces/reconfigure_vips.php b/src/opnsense/scripts/interfaces/reconfigure_vips.php new file mode 100755 index 000000000..186e68b53 --- /dev/null +++ b/src/opnsense/scripts/interfaces/reconfigure_vips.php @@ -0,0 +1,123 @@ +#!/usr/local/bin/php + $ifcnf) { + foreach (['ipv4', 'ipv6'] as $proto) { + if (!empty($ifcnf[$proto])) { + foreach ($ifcnf[$proto] as $address) { + $addresses[$address['ipaddr']] = [ + 'subnetbits' => $address['subnetbits'], + 'if' => $ifname, + 'vhid' => $address['vhid'] ?? '', + 'advbase' => '', + 'advskew' => '' + ]; + if (!empty($address['vhid'])) { + foreach ($ifcnf['carp'] as $vhid) { + if ($vhid['vhid'] == $address['vhid']) { + $addresses[$address['ipaddr']]['advbase'] = $vhid['advbase']; + $addresses[$address['ipaddr']]['advskew'] = $vhid['advskew']; + } + } + } + } + } + } +} + +// remove deleted vips +foreach (glob("/tmp/delete_vip_*.todo") as $filename) { + $address = trim(file_get_contents($filename)); + if (isset($addresses[$address])) { + legacy_interface_deladdress($addresses[$address]['if'], $address, is_ipaddrv6($address) ? 6 : 4); + } else { + // not found, likely proxy arp + $anyproxyarp = true; + } + unlink($filename); +} + +// diff model and actual ifconfig +if (!empty($config['virtualip']['vip'])) { + $interfaces = []; + foreach (legacy_config_get_interfaces() as $interfaceKey => $itf) { + if (!empty($itf['if']) && ($itf['type'] ?? '') != 'group') { + $interfaces[$interfaceKey] = $itf['if']; + } + } + foreach ($config['virtualip']['vip'] as $vipent) { + if (!empty($vipent['interface']) && !empty($interfaces[$vipent['interface']])) { + $if = $interfaces[$vipent['interface']]; + $subnet = $vipent['subnet']; + $subnet_bits = $vipent['subnet_bits']; + $vhid = $vipent['vhid'] ?? ''; + $advbase = !empty($vipent['vhid']) ? $vipent['advbase'] : ''; + $advskew = !empty($vipent['vhid']) ? $vipent['advskew'] : ''; + if ($vipent['mode'] == 'proxyarp') { + $anyproxyarp = true; + } + if (in_array($vipent['mode'], ['proxyarp', 'other'])) { + if (isset($addresses[$subnet])) { + legacy_interface_deladdress($addresses[$subnet]['if'], $subnet, is_ipaddrv6($subnet) ? 6 : 4); + } + continue; + } elseif ( + isset($addresses[$subnet]) && + $addresses[$subnet]['subnetbits'] == $subnet_bits && + $addresses[$subnet]['if'] == $if && + $addresses[$subnet]['vhid'] == $vhid && + $addresses[$subnet]['advbase'] == $advbase && + $addresses[$subnet]['advskew'] == $advskew + ) { + // configured and found equal + continue; + } + // default configure action depending on type + switch ($vipent['mode']) { + case 'ipalias': + interface_ipalias_configure($vipent); + break; + case 'carp': + interface_carp_configure($vipent); + break; + } + } + } +} + +if ($anyproxyarp) { + interface_proxyarp_configure(); +} diff --git a/src/opnsense/service/conf/actions.d/actions_interface.conf b/src/opnsense/service/conf/actions.d/actions_interface.conf index 02538b596..ac2d6ba44 100644 --- a/src/opnsense/service/conf/actions.d/actions_interface.conf +++ b/src/opnsense/service/conf/actions.d/actions_interface.conf @@ -131,6 +131,11 @@ command: /usr/local/sbin/pluginctl -c loopback message: Reconfiguring loopbacks type: script +[vip.configure] +command: /usr/local/opnsense/scripts/interfaces/reconfigure_vips.php +message: Reconfiguring Virtual IPs +type: script + [show.protocol] command:/usr/bin/netstat -s --libxo json parameters: diff --git a/src/www/firewall_nat_edit.php b/src/www/firewall_nat_edit.php index 1ae48f4ee..dc069d720 100644 --- a/src/www/firewall_nat_edit.php +++ b/src/www/firewall_nat_edit.php @@ -766,7 +766,7 @@ $( document ).ready(function() { foreach ($config['virtualip']['vip'] as $sn): if (isset($sn['noexpand'])) continue; - if (in_array($sn['mode'], array("proxyarp", "other")) && $sn['type'] == "network"): + if ($sn['mode'] == "proxyarp"): $start = ip2long32(gen_subnet($sn['subnet'], $sn['subnet_bits'])); $end = ip2long32(gen_subnet_max($sn['subnet'], $sn['subnet_bits'])); $len = $end - $start; diff --git a/src/www/firewall_nat_out_edit.php b/src/www/firewall_nat_out_edit.php index 6605ad65f..933742106 100644 --- a/src/www/firewall_nat_out_edit.php +++ b/src/www/firewall_nat_out_edit.php @@ -47,7 +47,7 @@ function formTranslateAddresses() { if (isset($config['virtualip']['vip'])) { foreach ($config['virtualip']['vip'] as $sn) { if (!isset($sn['noexpand'])) { - if (in_array($sn['mode'], array("proxyarp", "other")) && $sn['type'] == "network") { + if ($sn['mode'] == "proxyarp") { $start = ip2long32(gen_subnet($sn['subnet'], $sn['subnet_bits'])); $end = ip2long32(gen_subnet_max($sn['subnet'], $sn['subnet_bits'])); $len = $end - $start; diff --git a/src/www/firewall_virtual_ip.php b/src/www/firewall_virtual_ip.php deleted file mode 100644 index bccd84443..000000000 --- a/src/www/firewall_virtual_ip.php +++ /dev/null @@ -1,329 +0,0 @@ - - * Copyright (C) 2003-2005 Manuel Kasper - * Copyright (C) 2004-2005 Scott Ullrich - * 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("guiconfig.inc"); -require_once("interfaces.inc"); -require_once("filter.inc"); - -/** - * delete virtual ip - */ -function deleteVIPEntry($id) { - global $config; - $input_errors = array(); - $a_vip = &config_read_array('virtualip', 'vip'); - /* make sure no inbound NAT mappings reference this entry */ - if (isset($config['nat']['rule'])) { - foreach ($config['nat']['rule'] as $rule) { - if(!empty($rule['destination']['address'])) { - if ($rule['destination']['address'] == $a_vip[$id]['subnet']) { - $input_errors[] = gettext("This entry cannot be deleted because it is still referenced by at least one NAT mapping."); - break; - } - } - } - } - - if (is_ipaddrv6($a_vip[$id]['subnet'])) { - list (, $if_subnet) = interfaces_primary_address6($a_vip[$id]['interface']); - $subnet = gen_subnetv6($a_vip[$id]['subnet'], $a_vip[$id]['subnet_bits']); - $is_ipv6 = true; - } else { - list (, $if_subnet) = interfaces_primary_address($a_vip[$id]['interface']); - $subnet = gen_subnet($a_vip[$id]['subnet'], $a_vip[$id]['subnet_bits']); - $is_ipv6 = false; - } - - $subnet .= "/" . $a_vip[$id]['subnet_bits']; - - if (isset($config['gateways']['gateway_item'])) { - foreach($config['gateways']['gateway_item'] as $gateway) { - if ($a_vip[$id]['interface'] != $gateway['interface']) - continue; - if ($is_ipv6 && $gateway['ipprotocol'] == 'inet') - continue; - if (!$is_ipv6 && $gateway['ipprotocol'] == 'inet6') - continue; - if (ip_in_subnet($gateway['gateway'], $if_subnet)) - continue; - - if (ip_in_subnet($gateway['gateway'], $subnet)) { - $input_errors[] = gettext("This entry cannot be deleted because it is still referenced by at least one Gateway."); - break; - } - } - } - - if (count($input_errors) == 0) { - // Special case since every proxyarp vip is handled by the same daemon. - if ($a_vip[$id]['mode'] == "proxyarp") { - $viface = $a_vip[$id]['interface']; - unset($a_vip[$id]); - interface_proxyarp_configure($viface); - } else { - interface_vip_bring_down($a_vip[$id]); - unset($a_vip[$id]); - } - if (count($config['virtualip']['vip']) == 0) { - unset($config['virtualip']['vip']); - } - } - return $input_errors; -} - -$a_vip = &config_read_array('virtualip', 'vip'); - -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $pconfig = $_POST; - if (isset($pconfig['id']) && isset($a_vip[$pconfig['id']])) { - // id found and valid - $id = $pconfig['id']; - } - if (isset($pconfig['apply'])) { - if (file_exists('/tmp/.firewall_virtual_ip.apply')) { - $toapplylist = unserialize(file_get_contents('/tmp/.firewall_virtual_ip.apply')); - foreach ($toapplylist as $vid => $ovip) { - if (!empty($ovip)) { - interface_vip_bring_down($ovip); - } - if (!empty($a_vip[$vid])) { - switch ($a_vip[$vid]['mode']) { - case "ipalias": - interface_ipalias_configure($a_vip[$vid]); - break; - case "proxyarp": - interface_proxyarp_configure($a_vip[$vid]['interface']); - break; - case "carp": - interface_carp_configure($a_vip[$vid]); - break; - default: - break; - } - } - } - @unlink('/tmp/.firewall_virtual_ip.apply'); - } - filter_configure(); - $savemsg = get_std_save_message(); - clear_subsystem_dirty('vip'); - } elseif (isset($pconfig['act']) && $pconfig['act'] == 'del' && isset($id)) { - $input_errors = deleteVIPEntry($id); - if (count($input_errors) == 0) { - write_config(); - header(url_safe('Location: /firewall_virtual_ip.php')); - exit; - } - } elseif (isset($pconfig['act']) && $pconfig['act'] == 'del_x' && isset($pconfig['rule']) && count($pconfig['rule']) > 0) { - // delete selected VIPs, sort rule in reverse order to delete the highest item sequences first - foreach (array_reverse($pconfig['rule']) as $ruleId) { - if (isset($a_vip[$ruleId])) { - deleteVIPEntry($ruleId); - } - } - write_config(); - header(url_safe('Location: /firewall_virtual_ip.php')); - exit; - } elseif (isset($pconfig['act']) && $pconfig['act'] == 'move' && isset($pconfig['rule']) && count($pconfig['rule']) > 0) { - // move selected rules - if (!isset($id)) { - // if rule not set/found, move to end - $id = count($a_vip); - } - $a_vip = legacy_move_config_list_items($a_vip, $id, $pconfig['rule']); - write_config(); - header(url_safe('Location: /firewall_virtual_ip.php')); - exit; - } -} - -include("head.inc"); - -?> - - - -
-
-
- 0) - print_input_errors($input_errors); - else - if (isset($savemsg)) - print_info_box($savemsg); - else - if (is_subsystem_dirty('vip')) - print_info_box_apply(gettext("The VIP configuration has been changed.")."
".gettext("You must apply the changes in order for them to take effect.")); - ?> -
-
-
- - - - - - - - - - - - - - - false)); - $interfaces['lo0'] = array('descr' => 'Loopback'); - $i = 0; - foreach ($a_vip as $vipent): - if(!empty($vipent['subnet']) || !empty($vipent['range']) || !empty($vipent['subnet_bits']) || (isset($vipent['range']['from']) && !empty($vipent['range']['from']))): ?> - - - - - - - - - - -
- - - - " class="act_move btn btn-default btn-xs"> - - - - - -
- - - - - - - - - - - - - - - - - " class="act_move btn btn-default btn-xs"> - - - - - - - - - - - -
-
-
-
-
-
-
- - diff --git a/src/www/firewall_virtual_ip_edit.php b/src/www/firewall_virtual_ip_edit.php deleted file mode 100644 index 3275e25ab..000000000 --- a/src/www/firewall_virtual_ip_edit.php +++ /dev/null @@ -1,517 +0,0 @@ - - * Copyright (C) 2003-2005 Manuel Kasper - * Copyright (C) 2004-2005 Scott Ullrich - * 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("guiconfig.inc"); -require_once("interfaces.inc"); - -/** - * find max vhid - */ -function find_last_used_vhid() { - global $config; - $vhid = 0; - if (isset($config['virtualip']['vip'])) { - foreach($config['virtualip']['vip'] as $vip) { - if(!empty($vip['vhid']) && $vip['vhid'] > $vhid) { - $vhid = $vip['vhid']; - } - } - } - return $vhid; -} - -$a_vip = &config_read_array('virtualip', 'vip'); - -if ($_SERVER['REQUEST_METHOD'] === 'GET') { - if (isset($_GET['dup']) && isset($a_vip[$_GET['dup']])) { - $configId = $_GET['dup']; - $after = $configId; - } elseif (isset($_GET['id']) && isset($a_vip[$_GET['id']])) { - $id = $_GET['id']; - $configId = $id; - } - - $pconfig = []; - $form_fields = [ - 'advbase', - 'advskew', - 'descr', - 'gateway', - 'interface', - 'mode', - 'noexpand', - 'password', - 'subnet', - 'subnet_bits', - 'type', - 'vhid', - ]; - - // initialize empty form fields - foreach ($form_fields as $fieldname) { - $pconfig[$fieldname] = null; - } - $pconfig['bind'] = 'yes'; - - if (isset($configId)) { - // 1-on-1 copy of config data - foreach ($form_fields as $fieldname) { - if (isset($a_vip[$configId][$fieldname])) { - $pconfig[$fieldname] = $a_vip[$configId][$fieldname]; - } - } - if (!empty($a_vip[$configId]['nobind'])) { - $pconfig['bind'] = null; - } - } -} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { - $input_errors = []; - $pconfig = $_POST; - - // input record id, if valid - if (isset($pconfig['id']) && isset($a_vip[$pconfig['id']])) { - $id = $pconfig['id']; - } - if (!empty($config['interfaces'][$pconfig['interface']]) && !empty($config['interfaces'][$pconfig['interface']]['if'])) { - $selected_interface = $config['interfaces'][$pconfig['interface']]['if']; - } else { - $selected_interface = []; - } - // perform form validations - $reqdfields = ['mode']; - $reqdfieldsn = [gettext('Type')]; - do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); - - if (isset($id) && $pconfig['mode'] != $a_vip[$id]['mode']) { - $input_errors[] = gettext("Virtual IP mode may not be changed for an existing entry."); - } else { - if (isset($pconfig['subnet'])) { - $pconfig['subnet'] = trim($pconfig['subnet']); - if (!is_ipaddr($pconfig['subnet'])) { - $input_errors[] = gettext("A valid IP address must be specified."); - } else { - $ignore_if = isset($id) ? $a_vip[$id]['interface'] : $pconfig['interface']; - if (is_ipaddr_configured($pconfig['subnet'], $ignore_if)) { - $input_errors[] = gettext("This IP address is being used by another interface or VIP."); - } - } - } - if (!empty($pconfig['gateway']) && !is_ipaddr($pconfig['gateway'])) { - $input_errors[] = gettext("A valid gateway IP address must be specified."); - } - - /* ipalias and carp should not use network or broadcast address */ - if ($pconfig['mode'] == "ipalias" || $pconfig['mode'] == "carp") { - if (is_ipaddrv4($pconfig['subnet']) && $pconfig['subnet_bits'] != '32' && $pconfig['subnet_bits'] != '31') { - $network_addr = gen_subnet($pconfig['subnet'], $pconfig['subnet_bits']); - $broadcast_addr = gen_subnet_max($pconfig['subnet'], $pconfig['subnet_bits']); - if (isset($network_addr) && $pconfig['subnet'] == $network_addr) { - $input_errors[] = gettext("You cannot use the network address for this VIP"); - } else if (isset($broadcast_addr) && $pconfig['subnet'] == $broadcast_addr) { - $input_errors[] = gettext("You cannot use the broadcast address for this VIP"); - } - } - } - - /* make sure new ip is within the subnet of a valid ip - * on one of our interfaces (wan, lan optX) - */ - if ($pconfig['mode'] == 'carp') { - /* verify against reusage of vhids */ - foreach($config['virtualip']['vip'] as $vipId => $vip) { - if (isset($vip['vhid']) && $vip['vhid'] == $pconfig['vhid'] && $vip['mode'] == 'carp' && $vip['interface'] == $pconfig['interface'] && $vipId != $id) { - $input_errors[] = sprintf(gettext("VHID %s is already in use on interface %s. Pick a unique number on this interface."),$pconfig['vhid'], convert_friendly_interface_to_friendly_descr($pconfig['interface'])); - } - } - if (empty($pconfig['password'])) { - $input_errors[] = gettext("You must specify a CARP password that is shared between the two VHID members."); - } - if (empty($pconfig['vhid'])) { - $input_errors[] = gettext('A VHID must be selected for this CARP VIP.'); - } - if (substr($selected_interface,0, 2) === 'lo') { - $input_errors[] = gettext('For this type of VIP loopback is not allowed.'); - } - } else if ($pconfig['mode'] != 'ipalias' && substr($selected_interface,0, 2) === 'lo') { - $input_errors[] = gettext('For this type of VIP loopback is not allowed.'); - } elseif ($pconfig['mode'] == 'ipalias' && !empty($pconfig['vhid'])) { - $carpvip_found = false; - foreach($config['virtualip']['vip'] as $vipId => $vip) { - if ($vip['interface'] == $pconfig['interface'] && $vip['vhid'] == $pconfig['vhid'] && $vip['mode'] == 'carp') { - $carpvip_found = true ; - } - } - if (!$carpvip_found) { - $input_errors[] = sprintf(gettext("VHID %s must be defined on interface %s as a CARP VIP first."),$pconfig['vhid'], convert_friendly_interface_to_friendly_descr($pconfig['interface'])); - } - } - } - - if (count($input_errors) == 0) { - $vipent = []; - // defaults - $vipent['type'] = "single"; - $vipent['subnet_bits'] = "32"; - // 1-on-1 copy attributes - foreach (['mode', 'interface', 'descr', 'type', 'subnet_bits', 'subnet', 'vhid', - 'advskew','advbase','password', 'gateway'] as $fieldname) { - if (isset($pconfig[$fieldname]) && $pconfig[$fieldname] != "") { - $vipent[$fieldname] = $pconfig[$fieldname]; - } - } - - if (!empty($pconfig['noexpand'])) { - // noexpand, only used for proxyarp - $vipent['noexpand'] = true; - } - if (empty($pconfig['bind']) && ($pconfig['mode'] == 'ipalias' || $pconfig['mode'] == 'carp')) { - // nobind, only used for ipalias/carp - $vipent['nobind'] = true; - } - - // virtual ip UI keeps track of its changes in a separate file - // (which is only use on apply in firewall_virtual_ip) - // add or change this administration here. - // Not the nicest thing to do, but we keep it for now. - if (file_exists('/tmp/.firewall_virtual_ip.apply')) { - $toapplylist = unserialize(file_get_contents('/tmp/.firewall_virtual_ip.apply')); - } else { - $toapplylist = []; - } - if (isset($id)) { - // save existing content before changing it - $toapplylist[$id] = $a_vip[$id]; - } else { - // new entry, no old data - $toapplylist[count($a_vip)] = []; - } - - if (isset($id)) { - /* modify all virtual IP rules with this address */ - for ($i = 0; isset($config['nat']['rule'][$i]); $i++) { - if (isset($config['nat']['rule'][$i]['destination']['address']) && $config['nat']['rule'][$i]['destination']['address'] == $a_vip[$id]['subnet']) { - $config['nat']['rule'][$i]['destination']['address'] = $vipent['subnet']; - } - } - } - - // update or insert item in config - if (isset($id)) { - $a_vip[$id] = $vipent; - } else { - $a_vip[] = $vipent; - } - write_config(); - mark_subsystem_dirty('vip'); - file_put_contents('/tmp/.firewall_virtual_ip.apply', serialize($toapplylist)); - header(url_safe('Location: /firewall_virtual_ip.php')); - exit; - } -} - -legacy_html_escape_form_data($pconfig); - -include("head.inc"); - -?> - - - - - - - - -
-
-
- 0) print_input_errors($input_errors); ?> -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - -
- -
- -
- - - - - -
- - - -
- -
- /> - -
- - -
- /> - -
- - -
- - - -
- : - - : - - - -
- - -
  - - - - - -
-
-
-
-
-
-
-