From 1bec000c1e6eaad039c14b207ae3998952c77d16 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Tue, 10 Oct 2023 20:43:01 +0200 Subject: [PATCH] Interfaces/neighbor - implement new neighbor configuration for arp/ndp entries closes https://github.com/opnsense/core/issues/6917 This commit adds a new component linked in Interfaces/Neighbors which offers the ability to manually register static leases and provides application control from other modules such as dhcpd. To minimize the risk, we're reusing the existing interfaces_staticarp_configure() hooks while only adjusting how static arp entries are being attached to the interface (match on addresses assigned when triggering with an interface). Entries registered via dhcp will be visible from the ui as well together with its origin. The previous version didn't cleanup old static entries, this version triggers a cleanup when executed for all interfaces using all earlier modifications processed via the same function (interfaces_neighbors_configure()). --- plist | 11 ++ src/etc/inc/interfaces.inc | 66 +++++++- .../Api/NeighborSettingsController.php | 72 +++++++++ .../Interfaces/NeighborController.php | 38 +++++ .../Interfaces/forms/dialogNeighbor.xml | 20 +++ .../app/library/OPNsense/Core/FileObject.php | 147 ++++++++++++++++++ .../Base/FieldTypes/MacAddressField.php | 63 ++++++++ .../app/models/OPNsense/Core/Menu/Menu.xml | 1 + .../Interfaces/FieldTypes/NeighborField.php | 98 ++++++++++++ .../models/OPNsense/Interfaces/Neighbor.php | 35 +++++ .../models/OPNsense/Interfaces/Neighbor.xml | 17 ++ .../OPNsense/Interfaces/Neighbor/dhcpd.php | 61 ++++++++ .../views/OPNsense/Interface/neighbor.volt | 66 ++++++++ .../interfaces/reconfigure_neighbors.php | 34 ++++ .../conf/actions.d/actions_interface.conf | 5 + 15 files changed, 726 insertions(+), 8 deletions(-) create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/NeighborSettingsController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Interfaces/NeighborController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogNeighbor.xml create mode 100644 src/opnsense/mvc/app/library/OPNsense/Core/FileObject.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/MacAddressField.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/NeighborField.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor.xml create mode 100644 src/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor/dhcpd.php create mode 100644 src/opnsense/mvc/app/views/OPNsense/Interface/neighbor.volt create mode 100755 src/opnsense/scripts/interfaces/reconfigure_neighbors.php 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 +