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 @@
+
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') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ lang._('After changing settings, please remember to apply them with the button below') }}
+
+
+
+
+
+
+
+
+{{ 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."));
- ?>
-
-
-
-
-
-
-
-
-
-
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");
-
-?>
-
-
-
-
-
-
-
-
-
-