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); ?> -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - -
- -
- -
- - - - - -
- - - -
- -
- /> - -
- - -
- /> - -
- - -
- - - -
- : - - : - - - -
- - -
  - - - - - -
-
-
-
-
-
-
-