diff --git a/plist b/plist index 62f281022..417de6b18 100644 --- a/plist +++ b/plist @@ -385,16 +385,19 @@ /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogVTI.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/LaggSettingsController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/LoopbackSettingsController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/NeighborSettingsController.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/LaggController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/LoopbackController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/NeighborController.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/dialogLagg.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogLoopback.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogNeighbor.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 @@ -503,6 +506,7 @@ /usr/local/opnsense/mvc/app/library/OPNsense/Core/Config.php /usr/local/opnsense/mvc/app/library/OPNsense/Core/ConfigException.php /usr/local/opnsense/mvc/app/library/OPNsense/Core/File.php +/usr/local/opnsense/mvc/app/library/OPNsense/Core/FileObject.php /usr/local/opnsense/mvc/app/library/OPNsense/Core/Routing.php /usr/local/opnsense/mvc/app/library/OPNsense/Core/Shell.php /usr/local/opnsense/mvc/app/library/OPNsense/Core/Singleton.php @@ -570,6 +574,7 @@ /usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/InterfaceField.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/JsonKeyValueStoreField.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/LegacyLinkField.php +/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/MacAddressField.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/ModelRelationField.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/NetworkAliasField.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/NetworkField.php @@ -675,6 +680,7 @@ /usr/local/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.xml /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/ACL/ACL.xml /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/LaggInterfaceField.php +/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/NeighborField.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipField.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipInterfaceField.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/VipNetworkField.php @@ -684,6 +690,9 @@ /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/Neighbor.php +/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor.xml +/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor/dhcpd.php /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 @@ -800,6 +809,7 @@ /usr/local/opnsense/mvc/app/views/OPNsense/IPsec/vti.volt /usr/local/opnsense/mvc/app/views/OPNsense/Interface/lagg.volt /usr/local/opnsense/mvc/app/views/OPNsense/Interface/loopback.volt +/usr/local/opnsense/mvc/app/views/OPNsense/Interface/neighbor.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 @@ -1002,6 +1012,7 @@ /usr/local/opnsense/scripts/interfaces/ppp-rename.sh /usr/local/opnsense/scripts/interfaces/ppp-uptime.sh /usr/local/opnsense/scripts/interfaces/reconfigure_laggs.php +/usr/local/opnsense/scripts/interfaces/reconfigure_neighbors.php /usr/local/opnsense/scripts/interfaces/reconfigure_vips.php /usr/local/opnsense/scripts/interfaces/reconfigure_vlans.php /usr/local/opnsense/scripts/interfaces/rtsold_resolvconf.sh diff --git a/src/etc/inc/interfaces.inc b/src/etc/inc/interfaces.inc index dc640a519..5fd565299 100644 --- a/src/etc/inc/interfaces.inc +++ b/src/etc/inc/interfaces.inc @@ -3865,17 +3865,67 @@ function interfaces_staticarp_configure($if, $ifconfig_details = null) mwexecf('/usr/sbin/arp -d -i %s -a', [$ifcfg['if']]); } - if (isset($config['dhcpd'][$if]['staticmap'])) { - foreach ($config['dhcpd'][$if]['staticmap'] as $arpent) { - if (!$want && !isset($arpent['arp_table_static_entry'])) { - continue; + interfaces_neighbors_configure($ifcfg['if'], $ifconfig_details); +} + +function interfaces_neighbors_configure($device = null, $ifconfig_details = null) +{ + $subnets = []; + + if (!empty($device)) { + if (empty($ifconfig_details) || empty($ifconfig_details[$device])) { + /* when called with an interface, require $ifconfig_details being passed */ + return; + } + foreach (['ipv4', 'ipv6'] as $proto) { + if (!empty($ifconfig_details[$device])) { + foreach ($ifconfig_details[$device][$proto] as $item) { + $subnets[] = $item['ipaddr'] . '/' . $item['subnetbits']; + } } - if (!isset($arpent['ipaddr'])) { - continue; - } - mwexecf('/usr/sbin/arp -s %s %s', [$arpent['ipaddr'], $arpent['mac']]); } } + + $current_neightbors = []; + foreach ((new \OPNsense\Interfaces\Neighbor())->neighbor->iterateItems() as $key => $node) { + $found = empty($if); /* unfiltered when no $if provided */ + foreach ($subnets as $subnet) { + $found = ip_in_subnet((string)$node->ipaddress, $subnet); + if ($found) { + break; + } + } + if ($found) { + // IPv4 [arp] or IPv6 [ndp] + if (strpos($node->ipaddress, ":") === false) { + mwexecf('/usr/sbin/arp -s %s %s', [$node->ipaddress, $node->etheraddr]); + } else { + mwexecf('/usr/sbin/ndp -s %s %s', [$node->ipaddress, $node->etheraddr]); + } + } + $current_neightbors[] = (string)$node->ipaddress; + } + + /* persist accounted addresses, without a cleanup that would be all seen since last cleanup */ + $fobj = new \OPNsense\Core\FileObject('/tmp/interfaces_neighbors.json', 'a+'); + $current = $fobj->readJson() ?? []; + $fobj->truncate(0)->writeJson( + !empty($device) ? array_unique(array_merge($current_neightbors, $current)) : $current_neightbors + ); + unset($fobj); + /* only cleanup when applying all interfaces */ + if (empty($device) && is_array($current)) { + foreach ($current as $item) { + if (is_string($item) && is_ipaddr($item) && !in_array($item, $current_neightbors)) { + if (strpos($item, ":") === false) { + mwexecf('/usr/sbin/arp -d %s', [$item]); + } else { + mwexecf('/usr/sbin/ndp -d %s', [$item]); + } + } + } + } + } function get_interfaces_info($include_unlinked = false) diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/NeighborSettingsController.php b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/NeighborSettingsController.php new file mode 100644 index 000000000..1c09ee84d --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/NeighborSettingsController.php @@ -0,0 +1,72 @@ +searchBase("neighbor", ['etheraddr', 'ipaddress', 'descr', 'origin'], "vxlanid"); + } + + public function setItemAction($uuid) + { + return $this->setBase("neighbor", "neighbor", $uuid); + } + + public function addItemAction() + { + return $this->addBase("neighbor", "neighbor"); + } + + public function getItemAction($uuid = null) + { + return $this->getBase("neighbor", "neighbor", $uuid); + } + + public function delItemAction($uuid) + { + return $this->delBase("neighbor", $uuid); + } + + public function reconfigureAction() + { + $result = ["status" => "failed"]; + if ($this->request->isPost()) { + $result['status'] = strtolower(trim((new Backend())->configdRun('interface neighbor configure'))); + } + return $result; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/NeighborController.php b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/NeighborController.php new file mode 100644 index 000000000..c6398a8b2 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/NeighborController.php @@ -0,0 +1,38 @@ +view->pick('OPNsense/Interface/neighbor'); + $this->view->formDialogEdit = $this->getForm("dialogNeighbor"); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogNeighbor.xml b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogNeighbor.xml new file mode 100644 index 000000000..d901f9b44 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogNeighbor.xml @@ -0,0 +1,20 @@ +
+ + neighbor.etheraddr + + text + Hardware MAC address of the cllient (format xx:xx:xx:xx:xx:xx) + + + neighbor.ipaddress + + text + IP address to assign to the provided MAC address, which will either end up in the arp (IPv4) or ndp (IPv6) table + + + neighbor.descr + + text + You may enter a description here for your reference (not parsed). + +
diff --git a/src/opnsense/mvc/app/library/OPNsense/Core/FileObject.php b/src/opnsense/mvc/app/library/OPNsense/Core/FileObject.php new file mode 100644 index 000000000..27905af03 --- /dev/null +++ b/src/opnsense/mvc/app/library/OPNsense/Core/FileObject.php @@ -0,0 +1,147 @@ +fhandle = fopen($filename, $mode . 'e'); /* always add close-on-exec flag to prevent fork inherit */ + + if ($permissions != null) { + @chmod($filename, $permissions); + } + if ($operation != null) { + if (!flock($this->fhandle, $operation)) { + fclose($this->fhandle); + $this->fhandle = null; + throw new Exception('Unable to open file in requested mode.'); + } + } + } + + /** + * close and unlock filehandle + */ + function __destruct() + { + if ($this->fhandle) { + fclose($this->fhandle); + } + } + + /** + * Unlock when locked + * @return this + */ + public function unlock() + { + flock($this->fhandle, LOCK_UN); + return $this; + } + + /** + * seek + * @param int $offset offset to use + * @param int $whence start position + * @return this + */ + public function seek(int $offset, int $whence = SEEK_SET) + { + fseek($this->fhandle, $whence); + return $this; + } + + /** + * truncate this file + * @param int $whence start position + * @return this + */ + public function truncate(int $size) + { + ftruncate($this->fhandle, $size); + return $this; + } + + /** + * read this file + * @param int $whence start position + * @return payload + */ + public function read(int $length = -1) + { + if ($length == -1) { + $length = fstat($this->fhandle)['size']; + } + if ($length === 0) { + return null; + } + return fread($this->fhandle, $length); + } + + /** + * write contents to this file + * @param string $data start position + * @param int $length length to write + * @return this + */ + public function write(string $data, ?int $length = null) + { + fwrite($this->fhandle, $data, $length); + return $this; + } + + /** + * read and parse json content + * @return array + */ + public function readJson() + { + return json_decode($this->read(), true); + } + + /** + * write array as json data + * @return this + */ + public function writeJson(array $data) + { + return $this->write(json_encode($data)); + } +} \ No newline at end of file diff --git a/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/MacAddressField.php b/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/MacAddressField.php new file mode 100644 index 000000000..88e5a6b89 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/MacAddressField.php @@ -0,0 +1,63 @@ +internalValue != null) { + $validators[] = new CallbackValidator(["callback" => function ($data) { + if (empty(filter_var($data, FILTER_VALIDATE_MAC))) { + return [$this->getValidationMessage()]; + } + return []; + } + ]); + } + return $validators; + } +} 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 03d613f1d..737893c20 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml @@ -103,6 +103,7 @@ + diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/NeighborField.php b/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/NeighborField.php new file mode 100644 index 000000000..ab93fb176 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/NeighborField.php @@ -0,0 +1,98 @@ +newInstance(); + $dynamic_data = $obj->collect(); + if (is_array($dynamic_data)) { + $seq = 1; + foreach ($dynamic_data as $record) { + if (is_array($record) && !empty($record['etheraddr']) && !empty($record['ipaddress'])) { + $itemKey = sprintf('%s-%s', $origin, $seq); + $result[$itemKey] = [ + 'etheraddr' => $record['etheraddr'], + 'ipaddress' => $record['ipaddress'], + 'descr' => $record['descr'] ?? '' + ]; + self::$internalSourcemap[$itemKey] = $record['source'] ?? $origin; + $seq++; + } + } + } + } catch (\Error | \Exception | ReflectionException $e) { + syslog(LOG_ERR, sprintf( + "Invalid neightbor object %s in %s (%s)", $classname, realpath($filename), $e->getMessage() + )); + } + } + return $result; + } + + /** + * {@inheritdoc} + */ + protected function actionPostLoadingEvent() + { + parent::actionPostLoadingEvent(); + foreach ($this->iterateItems() as $key => $node) { + $type_node = new TextField(); + $type_node->setInternalIsVirtual(); + if (isset(self::$internalSourcemap[$key])) { + $type_node->setValue(self::$internalSourcemap[$key]); + } else { + $type_node->setValue('manual'); + } + $node->addChildNode('origin', $type_node); + } + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor.php b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor.php new file mode 100644 index 000000000..f4c16509d --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor.php @@ -0,0 +1,35 @@ + + //OPNsense/Interfaces/neighbors + 1.0.0 + Neighbor configuration + + + + Y + + + Y + N + + + + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor/dhcpd.php b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor/dhcpd.php new file mode 100644 index 000000000..fd4aba1d5 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor/dhcpd.php @@ -0,0 +1,61 @@ +object(); + if ($config->dhcpd->count() > 0) { + foreach ($config->dhcpd->children() as $intf => $node) { + foreach ($node->children() as $key => $data) { + if ($key == 'staticmap') { + if (!empty($data->arp_table_static_entry) || !empty($node->staticarp) ) { + $result[] = [ + 'etheraddr' => (string)$data->mac, + 'ipaddress' => (string)$data->ipaddr, + 'descr' => (string)$data->descr, + 'source' => sprintf('dhcpd-%s', $intf) + ]; + } + } + } + } + } + return $result; + } +} \ No newline at end of file diff --git a/src/opnsense/mvc/app/views/OPNsense/Interface/neighbor.volt b/src/opnsense/mvc/app/views/OPNsense/Interface/neighbor.volt new file mode 100644 index 000000000..4e2f880bb --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Interface/neighbor.volt @@ -0,0 +1,66 @@ + +
+ + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Origin') }}{{ lang._('Mac') }}{{ lang._('IP address') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ + +
+
+ +
+ +

+
+
+ + +{{ partial("layout_partials/base_dialog",['fields':formDialogEdit,'id':'DialogEdit','label':lang._('Edit Neighbor')])}} diff --git a/src/opnsense/scripts/interfaces/reconfigure_neighbors.php b/src/opnsense/scripts/interfaces/reconfigure_neighbors.php new file mode 100755 index 000000000..1313bbfde --- /dev/null +++ b/src/opnsense/scripts/interfaces/reconfigure_neighbors.php @@ -0,0 +1,34 @@ +#!/usr/local/bin/php +