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 %}