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 @@ +
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 @@ + 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 @@ + 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 @@ + 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 @@ + 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 @@