From 9c50cbfcb8970d846c1b0ffdc137616339cb7c4f Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Tue, 22 Apr 2025 17:43:27 +0200 Subject: [PATCH] Services: Kea DHCP: Kea DHCPv6 - add new option based on v4 (#8571) This contains roughly the same configuration items as our current isc-dhcp6 alternative, with the exception of not trying to implement dynamic ranges based on data received from dhclient6. In terms of target audience, dynamic environments (receiving their "wan" type addressess via dhcp), should logically use dnsmasq for client configuration. Large (enterprise) setups usually are static by nature and may require prefix deligation to routers behind the primary one. In these cases Kea will be the tool of choice. Both v4 and v6 share the same rc scripts underneath, which means reconfiguration happens per package (eventhough two services are registered). Existing hooks for v4 have been extended with v6 data (firewall rules and staticmaps). Advanced configurations can still opt out of config file generation and supply their own json config, same as implemented for v4. The lease view still needs to be implemented, but that's likely a minor addition. --- plist | 9 + src/etc/inc/plugins.inc.d/kea.inc | 110 +++++++- .../OPNsense/Kea/Api/Dhcpv6Controller.php | 188 ++++++++++++++ .../OPNsense/Kea/Api/ServiceController.php | 10 +- .../OPNsense/Kea/DhcpController.php | 18 ++ .../OPNsense/Kea/forms/dialogPDPool6.xml | 32 +++ .../OPNsense/Kea/forms/dialogPeer6.xml | 21 ++ .../OPNsense/Kea/forms/dialogReservation6.xml | 44 ++++ .../OPNsense/Kea/forms/dialogSubnet6.xml | 47 ++++ .../OPNsense/Kea/forms/generalSettings6.xml | 69 +++++ .../mvc/app/models/OPNsense/Kea/ACL/ACL.xml | 11 + .../mvc/app/models/OPNsense/Kea/KeaDhcpv6.php | 239 ++++++++++++++++++ .../mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml | 181 +++++++++++++ .../mvc/app/models/OPNsense/Kea/Menu/Menu.xml | 1 + .../mvc/app/views/OPNsense/Kea/dhcpv6.volt | 201 +++++++++++++++ .../templates/OPNsense/Kea/keactrl.conf | 2 +- .../service/templates/OPNsense/Kea/rc.conf.d | 6 +- 17 files changed, 1172 insertions(+), 17 deletions(-) create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Kea/Api/Dhcpv6Controller.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPDPool6.xml create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPeer6.xml create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation6.xml create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogSubnet6.xml create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/generalSettings6.xml create mode 100644 src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml create mode 100644 src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv6.volt diff --git a/plist b/plist index 11ab1c638..b6d0a5244 100644 --- a/plist +++ b/plist @@ -391,14 +391,20 @@ /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogVxlan.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/Api/CtrlAgentController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/Api/Dhcpv4Controller.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/Api/Dhcpv6Controller.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/Api/Leases4Controller.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/Api/ServiceController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/DhcpController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/agentSettings.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPDPool6.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPeer4.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPeer6.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation4.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation6.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogSubnet4.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogSubnet6.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/generalSettings4.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Kea/forms/generalSettings6.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Monit/Api/ServiceController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Monit/Api/SettingsController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Monit/Api/StatusController.php @@ -789,6 +795,8 @@ /usr/local/opnsense/mvc/app/models/OPNsense/Kea/KeaCtrlAgent.xml /usr/local/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.php /usr/local/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.xml +/usr/local/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php +/usr/local/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml /usr/local/opnsense/mvc/app/models/OPNsense/Kea/Menu/Menu.xml /usr/local/opnsense/mvc/app/models/OPNsense/Monit/ACL/ACL.xml /usr/local/opnsense/mvc/app/models/OPNsense/Monit/Menu/Menu.xml @@ -933,6 +941,7 @@ /usr/local/opnsense/mvc/app/views/OPNsense/Interface/vxlan.volt /usr/local/opnsense/mvc/app/views/OPNsense/Kea/ctrl_agent.volt /usr/local/opnsense/mvc/app/views/OPNsense/Kea/dhcpv4.volt +/usr/local/opnsense/mvc/app/views/OPNsense/Kea/dhcpv6.volt /usr/local/opnsense/mvc/app/views/OPNsense/Kea/leases4.volt /usr/local/opnsense/mvc/app/views/OPNsense/Monit/index.volt /usr/local/opnsense/mvc/app/views/OPNsense/Monit/status.volt diff --git a/src/etc/inc/plugins.inc.d/kea.inc b/src/etc/inc/plugins.inc.d/kea.inc index c403d0eac..10f126bfd 100644 --- a/src/etc/inc/plugins.inc.d/kea.inc +++ b/src/etc/inc/plugins.inc.d/kea.inc @@ -42,6 +42,23 @@ function kea_services() 'name' => 'kea-dhcpv4', ]; } + if (!empty((string)(new \OPNsense\Kea\KeaDhcpv6())->general->enabled)) { + /** + * Although kea's backend operation is a single package, various services do offer their own pid files. + * Ideally we should have a list of pids to check for the services and only return a single item, + * but showing both with the same action below is the second best we can achieve. + */ + $services[] = [ + 'description' => gettext('KEA DHCPv6 server'), + 'pidfile' => '/var/run/kea/kea-dhcp6.kea-dhcp6.pid', + 'configd' => [ + 'restart' => ['kea restart'], + 'start' => ['kea start'], + 'stop' => ['kea stop'], + ], + 'name' => 'kea-dhcpv6', + ]; + } return $services; } @@ -55,14 +72,18 @@ function kea_run() function kea_staticmap($proto = null, $valid_addresses = true, $ifconfig_details = null) { $staticmap = []; - $keav4 = new \OPNsense\Kea\KeaDhcpv4(); + if ($proto == 6) { + $keamdl = new \OPNsense\Kea\KeaDhcpv6(); + } else { + $keamdl = new \OPNsense\Kea\KeaDhcpv4(); + } - if ($proto == 6 || empty((string)$keav4->general->enabled)) { - /* unsupported protocol or not enabled */ + if (empty((string)$keamdl->general->enabled)) { + /* not enabled */ return $staticmap; } - foreach ($keav4->reservations->reservation->iterateItems() as $reservation) { + foreach ($keamdl->reservations->reservation->iterateItems() as $reservation) { $hostname = !empty((string)$reservation->hostname) ? (string)$reservation->hostname : null; $ip_address = (string)$reservation->ip_address; if ($valid_addresses) { @@ -81,11 +102,13 @@ function kea_staticmap($proto = null, $valid_addresses = true, $ifconfig_details $description = !empty((string)$reservation->description) ? (string)$reservation->description : null; - $subnet_node = $keav4->getNodeByReference("subnets.subnet4.{$reservation->subnet}"); $domain = null; - if ($subnet_node) { - if (!empty((string)$subnet_node->option_data->domain_name)) { - $domain = (string)$subnet_node->option_data->domain_name; + if ($proto == 4) { + $subnet_node = $keamdl->getNodeByReference("subnets.subnet4.{$reservation->subnet}"); + if ($subnet_node) { + if (!empty((string)$subnet_node->option_data->domain_name)) { + $domain = (string)$subnet_node->option_data->domain_name; + } } } @@ -113,12 +136,17 @@ function kea_configure() function kea_configure_do($verbose = false) { $keaDhcpv4 = new \OPNsense\Kea\KeaDhcpv4(); - if ($keaDhcpv4->isEnabled()) { + $keaDhcpv6 = new \OPNsense\Kea\KeaDhcpv6(); + if ($keaDhcpv4->isEnabled() || $keaDhcpv6->isEnabled()) { service_log('Sync KEA DHCP config...', $verbose); - if ($keaDhcpv4->general->manual_config->isEmpty()) { + if ($keaDhcpv4->isEnabled() && $keaDhcpv4->general->manual_config->isEmpty()) { /* skip kea-dhcp4.conf when configured manually */ $keaDhcpv4->generateConfig(); } + if ($keaDhcpv6->isEnabled() && $keaDhcpv6->general->manual_config->isEmpty()) { + /* skip kea-dhcp6.conf when configured manually */ + $keaDhcpv6->generateConfig(); + } (new \OPNsense\Kea\KeaCtrlAgent())->generateConfig(); service_log("done.\n", $verbose); } @@ -136,6 +164,7 @@ function kea_firewall($fw) { global $config; $keav4 = new \OPNsense\Kea\KeaDhcpv4(); + $keav6 = new \OPNsense\Kea\KeaDhcpv6(); if ($keav4->fwrulesEnabled()) { // automatic (IPv4) rules enabled foreach (explode(',', $keav4->general->interfaces) as $intf) { @@ -169,6 +198,65 @@ function kea_firewall($fw) ); } } + + if ($keav6->fwrulesEnabled()) { + foreach (explode(',', $keav6->general->interfaces) as $intf) { + $default_opts = [ + 'protocol' => 'udp', + 'ipprotocol' => 'inet6', + 'interface' => $intf, + '#ref' => 'ui/kea/dhcp/v6', + 'descr' => 'allow access to DHCPv6 server', + 'log' => !isset($config['syslog']['nologdefaultpass']) + ]; + $fw->registerFilterRule( + 1, + [ + 'from' => 'fe80::/10', + 'to' => 'fe80::/10,ff02::/16', + 'to_port' => 546 + ], + $default_opts + ); + $fw->registerFilterRule( + 1, + [ + 'from' => 'fe80::/10', + 'to' => 'ff02::/16', + 'to_port' => 547 + ], + $default_opts + ); + $fw->registerFilterRule( + 1, + [ + 'from' => 'ff02::/16', + 'to' => 'fe80::/10', + 'to_port' => 547 + ], + $default_opts + ); + $fw->registerFilterRule( + 1, + [ + 'from' => 'fe80::/10', + 'to' => '(self)', + 'to_port' => 546 + ], + $default_opts + ); + $fw->registerFilterRule( + 1, + [ + 'from' => '(self)', + 'to' => 'fe80::/10', + 'from_port' => 547, + 'direction' => 'out' + ], + $default_opts + ); + } + } } function kea_xmlrpc_sync() @@ -179,7 +267,7 @@ function kea_xmlrpc_sync() 'description' => gettext('Kea DHCP'), 'section' => 'OPNsense.Kea', 'id' => 'kea', - 'services' => ["kea-dhcpv4"], + 'services' => ["kea-dhcpv4", "kea-dhcpv6"], ]; return $result; diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Kea/Api/Dhcpv6Controller.php b/src/opnsense/mvc/app/controllers/OPNsense/Kea/Api/Dhcpv6Controller.php new file mode 100644 index 000000000..08ac9a976 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Kea/Api/Dhcpv6Controller.php @@ -0,0 +1,188 @@ + [ + 'general' => $data[self::$internalModelName]['general'], + 'ha' => $data[self::$internalModelName]['ha'], + 'this_hostname' => (string)Config::getInstance()->object()->system->hostname + ] + ]; + } + + public function searchSubnetAction() + { + return $this->searchBase("subnets.subnet6", null, "subnet"); + } + + public function setSubnetAction($uuid) + { + return $this->setBase("subnet6", "subnets.subnet6", $uuid); + } + + public function addSubnetAction() + { + return $this->addBase("subnet6", "subnets.subnet6"); + } + + public function getSubnetAction($uuid = null) + { + return $this->getBase("subnet6", "subnets.subnet6", $uuid); + } + + public function delSubnetAction($uuid) + { + return $this->delBase("subnets.subnet6", $uuid); + } + + public function searchReservationAction() + { + return $this->searchBase("reservations.reservation", null, "duid"); + } + + public function setReservationAction($uuid) + { + return $this->setBase("reservation", "reservations.reservation", $uuid); + } + + public function addReservationAction() + { + return $this->addBase("reservation", "reservations.reservation"); + } + + public function getReservationAction($uuid = null) + { + return $this->getBase("reservation", "reservations.reservation", $uuid); + } + + public function delReservationAction($uuid) + { + return $this->delBase("reservations.reservation", $uuid); + } + + public function downloadReservationsAction() + { + if ($this->request->isGet()) { + $this->exportCsv($this->getModel()->reservations->reservation->asRecordSet(false, ['subnet'])); + } + } + + public function uploadReservationsAction() + { + if ($this->request->isPost() && $this->request->hasPost('payload')) { + $subnets = []; + foreach ($this->getModel()->subnets->subnet6->iterateItems() as $key => $node) { + $subnets[(string)$node->subnet] = $key; + } + return $this->importCsv( + 'reservations.reservation', + $this->request->getPost('payload'), + ['duid', 'subnet'], + function (&$record) use ($subnets) { + /* seek matching subnet */ + if (!empty($record['ip_address'])) { + foreach ($subnets as $subnet => $uuid) { + if (Util::isIPInCIDR($record['ip_address'], $subnet)) { + $record['subnet'] = $uuid; + } + } + } + } + ); + } else { + return ['status' => 'failed']; + } + } + + public function searchPdPoolAction() + { + return $this->searchBase("pd_pools.pd_pool"); + } + + public function setPdPoolAction($uuid) + { + return $this->setBase("pd_pool", "pd_pools.pd_pool", $uuid); + } + + public function addPdPoolAction() + { + return $this->addBase("pd_pool", "pd_pools.pd_pool"); + } + + public function getPdPoolAction($uuid = null) + { + return $this->getBase("pd_pool", "pd_pools.pd_pool", $uuid); + } + + public function delPdPoolAction($uuid) + { + return $this->delBase("pd_pools.pd_pool", $uuid); + } + + public function searchPeerAction() + { + return $this->searchBase("ha_peers.peer", null, "name"); + } + + public function setPeerAction($uuid) + { + return $this->setBase("peer", "ha_peers.peer", $uuid); + } + + public function addPeerAction() + { + return $this->addBase("peer", "ha_peers.peer"); + } + + public function getPeerAction($uuid = null) + { + return $this->getBase("peer", "ha_peers.peer", $uuid); + } + + public function delPeerAction($uuid) + { + return $this->delBase("ha_peers.peer", $uuid); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Kea/Api/ServiceController.php b/src/opnsense/mvc/app/controllers/OPNsense/Kea/Api/ServiceController.php index 2d200834f..2f21b2a99 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Kea/Api/ServiceController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Kea/Api/ServiceController.php @@ -30,6 +30,8 @@ namespace OPNsense\Kea\Api; use OPNsense\Base\ApiMutableServiceControllerBase; use OPNsense\Core\Backend; +use OPNsense\Kea\KeaDhcpv4; +use OPNsense\Kea\KeaDhcpv6; class ServiceController extends ApiMutableServiceControllerBase { @@ -38,8 +40,8 @@ class ServiceController extends ApiMutableServiceControllerBase protected static $internalServiceEnabled = 'general.enabled'; protected static $internalServiceName = 'kea'; - /** - * TODO: overwrite when implementing KeaDhcpv6 as well. Both services share the same rc script - * protected function serviceEnabled() {} - */ + protected function serviceEnabled() + { + return (new KeaDhcpv4())->general->enabled == '1' || (new KeaDhcpv6())->general->enabled == '1'; + } } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Kea/DhcpController.php b/src/opnsense/mvc/app/controllers/OPNsense/Kea/DhcpController.php index 11efda035..0ce7a0d2e 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Kea/DhcpController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Kea/DhcpController.php @@ -61,6 +61,24 @@ class DhcpController extends \OPNsense\Base\IndexController $this->view->formGridPeer = $this->getFormGrid("dialogPeer4"); } + public function v6Action() + { + $this->view->pick('OPNsense/Kea/dhcpv6'); + $this->view->formGeneralSettings = $this->getForm("generalSettings6"); + + $this->view->formDialogSubnet = $this->getForm("dialogSubnet6"); + $this->view->formGridSubnet = $this->getFormGrid("dialogSubnet6"); + + $this->view->formDialogReservation = $this->getForm("dialogReservation6"); + $this->view->formGridReservation = $this->getFormGrid("dialogReservation6"); + + $this->view->formDialogPDPool = $this->getForm("dialogPDPool6"); + $this->view->formGridPDPool = $this->getFormGrid("dialogPDPool6"); + + $this->view->formDialogPeer = $this->getForm("dialogPeer6"); + $this->view->formGridPeer = $this->getFormGrid("dialogPeer6"); + } + public function leases4Action() { $this->view->pick('OPNsense/Kea/leases4'); diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPDPool6.xml b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPDPool6.xml new file mode 100644 index 000000000..d43544f51 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPDPool6.xml @@ -0,0 +1,32 @@ +
+ + pd_pool.subnet + + dropdown + Subnet this reservation belongs to + + + pd_pool.prefix + + text + + + + pd_pool.prefix_len + + text + + + + pd_pool.delegated_len + + text + + + + pd_pool.description + + text + You may enter a description here for your reference (not parsed). + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPeer6.xml b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPeer6.xml new file mode 100644 index 000000000..b81e8213c --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPeer6.xml @@ -0,0 +1,21 @@ +
+ + peer.name + + text + Peer name, there should be one entry matching this machines "This server name" + + + peer.role + + dropdown + This peers role + + + peer.url + + text + This specifies the URL of our server instance, which should use a different port than the control agent. + For example http://my.host:8001/ + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation6.xml b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation6.xml new file mode 100644 index 000000000..b850f4b81 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation6.xml @@ -0,0 +1,44 @@ +
+ + reservation.subnet + + dropdown + Subnet this reservation belongs to + + + reservation.ip_address + + text + IP address to offer to the client + + + reservation.duid + + text + duid of the client in question + + + reservation.hostname + + text + Offer a hostname to the client + + + reservation.domain_search + + select_multiple + + true + , + Specifies a ´search list´ of Domain Names to be used by the client to locate not-fully-qualified domain names. + + false + + + + reservation.description + + text + You may enter a description here for your reference (not parsed). + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogSubnet6.xml b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogSubnet6.xml new file mode 100644 index 000000000..fc7b17a8f --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogSubnet6.xml @@ -0,0 +1,47 @@ +
+ + subnet6.subnet + + text + Subnet to use, should be large enough to hold the specified pools and reservations + + + subnet6.description + + text + You may enter a description here for your reference (not parsed). + + + subnet6.pools + + textbox + List of pools, one per line in range or subnet format (e.g. 2001:db8:1::-2001:db8:1::100, 2001:db8:1::/80 + + + header + + + + subnet6.option_data.dns_servers + + select_multiple + + true + DNS servers to offer to the clients + + false + + + + subnet6.option_data.domain_search + + select_multiple + + true + , + Specifies a ´search list´ of Domain Names to be used by the client to locate not-fully-qualified domain names. + + false + + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/generalSettings6.xml b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/generalSettings6.xml new file mode 100644 index 000000000..47178e023 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/generalSettings6.xml @@ -0,0 +1,69 @@ +
+ + header + + + + dhcpv6.general.enabled + + checkbox + Enable DHCPv6 server. + + + dhcpv6.general.manual_config + + checkbox + true + Disable configuration file generation and manage the file (/usr/local/etc/kea/kea-dhcp4.conf) manually. + + + header + + + + dhcpv6.general.interfaces + + select_multiple + Select interfaces to listen on. + + + dhcpv6.general.valid_lifetime + + text + Defines how long the addresses (leases) given out by the server are valid (in seconds) + + + dhcpv6.general.fwrules + + checkbox + Automatically add a basic set of firewall rules to allow dhcp traffic, more fine grained controls can be offered manually when disabling this option. + + + header + + + + dhcpv6.ha.enabled + + checkbox + Enable High availability hook, requires the Control Agent to be enabled as well. + + + dhcpv6.ha.this_server_name + + text + The name of this server, should match with one of the entries in the HA peers. + Leave empty to use this machines hostname + + + + dhcpv6.ha.max_unacked_clients + + text + + This specifies the number of clients which send messages to the partner but appear to not receive any response. + A higher value needs a busier environment in order to consider a member down, when set to 0, + any network disruption will cause a failover to happen. + + +
diff --git a/src/opnsense/mvc/app/models/OPNsense/Kea/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Kea/ACL/ACL.xml index 2416e1544..1e6aa8960 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Kea/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Kea/ACL/ACL.xml @@ -10,6 +10,17 @@ api/kea/service/* + + Services: DHCP: Kea(v6) + Allow access to the KEA dhcp6 server + + ui/kea/dhcp/v6 + ui/kea/dhcp/leases6 + api/kea/dhcpv6/* + api/kea/leases6/* + api/kea/service/* + + Services: DHCP: Kea Ctrl Agent Allow access to the KEA Ctrl Agent diff --git a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php new file mode 100644 index 000000000..ae03e90aa --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php @@ -0,0 +1,239 @@ +reservations->reservation->iterateItems() as $reservation) { + if (!$validateFullModel && !$reservation->isFieldChanged()) { + continue; + } + $key = $reservation->__reference; + $subnet = ""; + $subnet_node = $this->getNodeByReference("subnets.subnet6.{$reservation->subnet}"); + if ($subnet_node) { + $subnet = (string)$subnet_node->subnet; + } + if (!Util::isIPInCIDR((string)$reservation->ip_address, $subnet)) { + $messages->appendMessage(new Message(gettext("Address not in specified subnet"), $key . ".ip_address")); + } + } + + return $messages; + } + + public function isEnabled() + { + return (string)$this->general->enabled == '1' && !empty((string)(string)$this->general->interfaces); + } + + /** + * should filter rules be enabled + * @return bool + */ + public function fwrulesEnabled() + { + return (string)$this->general->enabled == '1' && + (string)$this->general->fwrules == '1' && + !empty((string)$this->general->interfaces); + } + + /** + * + */ + private function getConfigPhysicalInterfaces() + { + $result = []; + $cfg = Config::getInstance()->object(); + foreach (explode(',', $this->general->interfaces) as $if) { + if (isset($cfg->interfaces->$if) && !empty($cfg->interfaces->$if->if)) { + $result[] = (string)$cfg->interfaces->$if->if; + } + } + return $result; + } + + private function getConfigThisServerHostname() + { + $hostname = (string)$this->ha->this_server_name; + if (empty($hostname)) { + $hostname = (string)Config::getInstance()->object()->system->hostname; + } + return $hostname; + } + + private function getConfigSubnets() + { + $result = []; + $subnet_id = 1; + foreach ($this->subnets->subnet6->iterateItems() as $subnet_uuid => $subnet) { + $record = [ + 'id' => $subnet_id++, + 'subnet' => (string)$subnet->subnet, + 'option-data' => [], + 'pools' => [], + 'pd-pools' => [], + 'reservations' => [] + ]; + /* standard option-data elements */ + foreach ($subnet->option_data->iterateItems() as $key => $value) { + $target_fieldname = str_replace('_', '-', $key); + if ((string)$value != '') { + $record['option-data'][] = [ + 'name' => $target_fieldname, + 'data' => (string)$value + ]; + } elseif ($key == 'domain_name') { + $record['option-data'][] = [ + 'name' => $target_fieldname, + 'data' => (string)Config::getInstance()->object()->system->domain + ]; + } + } + /* add pools */ + foreach (array_filter(explode("\n", $subnet->pools)) as $pool) { + $record['pools'][] = ['pool' => $pool]; + } + /* add pd-pools */ + foreach ($this->pd_pools->pd_pool->iterateItems() as $key => $pdpool) { + if ($pdpool->subnet != $subnet_uuid) { + continue; + } + $record['pd-pools'][] = [ + 'prefix' => (string)$pdpool->prefix, + 'prefix-len' => (int)$pdpool->prefix_len->getCurrentValue(), + 'delegated-len' => (int)$pdpool->delegated_len->getCurrentValue() + ]; + } + /* static reservations */ + foreach ($this->reservations->reservation->iterateItems() as $key => $reservation) { + if ($reservation->subnet != $subnet_uuid) { + continue; + } + $res = ['option-data' => []]; + foreach (['duid', 'hostname'] as $key) { + if (!empty((string)$reservation->$key)) { + $res[str_replace('_', '-', $key)] = (string)$reservation->$key; + } + } + $res['ip-addresses'] = explode(',', (string)$reservation->ip_address); + if (!$reservation->domain_search->isEmpty()) { + $res['option-data'][] = [ + 'name' => 'domain-search', + 'data' => (string)$reservation->domain_search + ]; + } + $record['reservations'][] = $res; + } + $result[] = $record; + } + return $result; + } + + public function generateConfig($target = '/usr/local/etc/kea/kea-dhcp6.conf') + { + $cnf = [ + 'Dhcp6' => [ + 'valid-lifetime' => (int)$this->general->valid_lifetime->__toString(), + 'interfaces-config' => [ + 'interfaces' => $this->getConfigPhysicalInterfaces() + ], + 'lease-database' => [ + 'type' => 'memfile', + 'persist' => true, + ], + 'control-socket' => [ + 'socket-type' => 'unix', + 'socket-name' => '/var/run/kea6-ctrl-socket' + ], + 'loggers' => [ + [ + 'name' => 'kea-dhcp6', + 'output_options' => [ + [ + 'output' => 'syslog' + ] + ], + 'severity' => 'INFO', + ] + ], + 'subnet6' => $this->getConfigSubnets(), + ] + ]; + if (!empty((string)(new KeaCtrlAgent())->general->enabled)) { + $cnf['Dhcp6']['hooks-libraries'] = []; + $cnf['Dhcp6']['hooks-libraries'][] = [ + 'library' => '/usr/local/lib/kea/hooks/libdhcp_lease_cmds.so' + ]; + if (!empty((string)$this->ha->enabled)) { + $record = [ + 'library' => '/usr/local/lib/kea/hooks/libdhcp_ha.so', + 'parameters' => [ + 'high-availability' => [ + [ + 'this-server-name' => $this->getConfigThisServerHostname(), + 'mode' => 'hot-standby', + 'heartbeat-delay' => 10000, + 'max-response-delay' => 60000, + 'max-ack-delay' => 5000, + 'max-unacked-clients' => (int)((string)$this->ha->max_unacked_clients), + 'sync-timeout' => 60000, + ] + ] + ] + ]; + foreach ($this->ha_peers->peer->iterateItems() as $peer) { + if (!isset($record['parameters']['high-availability'][0]['peers'])) { + $record['parameters']['high-availability'][0]['peers'] = []; + } + $record['parameters']['high-availability'][0]['peers'][] = array_map( + fn($x) => (string)$x, + iterator_to_array($peer->iterateItems()) + ); + } + $cnf['Dhcp6']['hooks-libraries'][] = $record; + } + } + File::file_put_contents($target, json_encode($cnf, JSON_PRETTY_PRINT), 0600); + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml new file mode 100644 index 000000000..ce580e126 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml @@ -0,0 +1,181 @@ + + //OPNsense/Kea/dhcp6 + 1.0.0 + Kea DHCPv6 configuration + + + + 0 + Y + + + + Y + + + 4000 + Y + + + Y + 1 + + + + + 0 + Y + + + /^([0-9a-zA-Z.\:\-,_]){0,1024}$/u + + + 0 + 65535 + 2 + Y + + + + + + Y + ipv6 + Y + + + + N + ipv6 + Y + , + + + N + , + Y + Please specify a valid list of domains + + + + + + + + + + + + + OPNsense.Kea.KeaDhcpv6 + subnets.subnet6 + subnet + + + Related subnet not found + + + duid.check001 + + + Y + + + N + ipv6 + + + Duplicate entry exists + UniqueConstraint + + + + + Y + /^(?:[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2})+)$/ + Value must be a colon-separated hexadecimal sequence (e.g., 01:02:f3). + + + Duplicate entry exists + UniqueConstraint + + subnet + + + + + + Y + + + N + , + Y + Please specify a valid list of domains + + + + + + + + + + OPNsense.Kea.KeaDhcpv6 + subnets.subnet6 + subnet + + + Related subnet not found + Y + + + ipv6 + + + 1 + 128 + 56 + Y + + + 1 + 128 + Y + 64 + + + + + + + + Y + + + Duplicate entry exists + UniqueConstraint + + + + + primary + Y + + primary + standby + + + + Y + + + Duplicate entry exists + UniqueConstraint + + + + + + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Kea/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Kea/Menu/Menu.xml index 9a0a460e2..32a3adf2a 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Kea/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Kea/Menu/Menu.xml @@ -3,6 +3,7 @@ + diff --git a/src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv6.volt b/src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv6.volt new file mode 100644 index 000000000..3c3917846 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv6.volt @@ -0,0 +1,201 @@ +{# + # Copyright (c) 2025 Deciso B.V. + # 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. + #} + + + + +
+ +
+ {{ partial("layout_partials/base_form",['fields':formGeneralSettings,'id':'frm_generalsettings'])}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridSubnet)}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridPDPool)}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridReservation + {'command_width': '8em'} )}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridPeer)}} +
+
+ +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/kea/service/reconfigure'}) }} +{{ partial("layout_partials/base_dialog",['fields':formDialogSubnet,'id':formGridSubnet['edit_dialog_id'],'label':lang._('Edit Subnet')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogPDPool,'id':formGridPDPool['edit_dialog_id'],'label':lang._('Edit PD Pool')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogReservation,'id':formGridReservation['edit_dialog_id'],'label':lang._('Edit Reservation')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogPeer,'id':formGridPeer['edit_dialog_id'],'label':lang._('Edit Peer')])}} diff --git a/src/opnsense/service/templates/OPNsense/Kea/keactrl.conf b/src/opnsense/service/templates/OPNsense/Kea/keactrl.conf index 45f511fe6..bf8bc8098 100644 --- a/src/opnsense/service/templates/OPNsense/Kea/keactrl.conf +++ b/src/opnsense/service/templates/OPNsense/Kea/keactrl.conf @@ -31,7 +31,7 @@ netconf_srv="${exec_prefix}/sbin/kea-netconf" dhcp4={% if not helpers.empty('OPNsense.Kea.dhcp4.general.enabled') %}yes{% else %}no{% endif %} # Start DHCPv6 server? -dhcp6=no +dhcp6={% if not helpers.empty('OPNsense.Kea.dhcp6.general.enabled') %}yes{% else %}no{% endif %} # Start DHCP DDNS server? dhcp_ddns=no diff --git a/src/opnsense/service/templates/OPNsense/Kea/rc.conf.d b/src/opnsense/service/templates/OPNsense/Kea/rc.conf.d index bb7cedf3b..f8d5acdde 100644 --- a/src/opnsense/service/templates/OPNsense/Kea/rc.conf.d +++ b/src/opnsense/service/templates/OPNsense/Kea/rc.conf.d @@ -1,4 +1,8 @@ -{% if not helpers.empty('OPNsense.Kea.dhcp4.general.interfaces') and not helpers.empty('OPNsense.Kea.dhcp4.general.enabled') %} +{% if ( + not helpers.empty('OPNsense.Kea.dhcp4.general.interfaces') and not helpers.empty('OPNsense.Kea.dhcp4.general.enabled') + ) or ( + not helpers.empty('OPNsense.Kea.dhcp6.general.interfaces') and not helpers.empty('OPNsense.Kea.dhcp6.general.enabled') +) %} kea_enable="YES" kea_setup="/usr/local/sbin/pluginctl -c kea_sync" {% else %}