diff --git a/plist b/plist index 4bef2c62f..323c58c40 100644 --- a/plist +++ b/plist @@ -319,9 +319,16 @@ /usr/local/opnsense/mvc/app/controllers/OPNsense/Diagnostics/forms/ping.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Diagnostics/forms/portprobe.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Diagnostics/forms/traceroute.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/LeasesController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/ServiceController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php -/usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/IndexController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/LeasesController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/SettingsController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPboot.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPmatch.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPoption.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPrange.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPtag.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDomainOverride.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogHostOverride.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/general.xml @@ -924,7 +931,8 @@ /usr/local/opnsense/mvc/app/views/OPNsense/Diagnostics/traffic.volt /usr/local/opnsense/mvc/app/views/OPNsense/Diagnostics/treeview.volt /usr/local/opnsense/mvc/app/views/OPNsense/Diagnostics/vip.volt -/usr/local/opnsense/mvc/app/views/OPNsense/Dnsmasq/index.volt +/usr/local/opnsense/mvc/app/views/OPNsense/Dnsmasq/leases.volt +/usr/local/opnsense/mvc/app/views/OPNsense/Dnsmasq/settings.volt /usr/local/opnsense/mvc/app/views/OPNsense/Firewall/alias.volt /usr/local/opnsense/mvc/app/views/OPNsense/Firewall/alias_util.volt /usr/local/opnsense/mvc/app/views/OPNsense/Firewall/category.volt @@ -1075,12 +1083,14 @@ /usr/local/opnsense/scripts/dhcp/cleanup_leases4.php /usr/local/opnsense/scripts/dhcp/cleanup_leases6.php /usr/local/opnsense/scripts/dhcp/dnsmasq_watcher.py +/usr/local/opnsense/scripts/dhcp/get_dnsmasq_leases.py /usr/local/opnsense/scripts/dhcp/get_kea_leases.py /usr/local/opnsense/scripts/dhcp/get_leases.py /usr/local/opnsense/scripts/dhcp/get_leases6.py /usr/local/opnsense/scripts/dhcp/prefixes.php /usr/local/opnsense/scripts/dhcp/prefixes.sh /usr/local/opnsense/scripts/dhcp/unbound_watcher.py +/usr/local/opnsense/scripts/dns/dnsmasq_dhcp_options.py /usr/local/opnsense/scripts/dns/query_dns.py /usr/local/opnsense/scripts/filter/delete_table.py /usr/local/opnsense/scripts/filter/download_geoip.py @@ -1383,6 +1393,8 @@ /usr/local/opnsense/service/templates/OPNsense/Captiveportal/rc.conf.d /usr/local/opnsense/service/templates/OPNsense/Cron/+TARGETS /usr/local/opnsense/service/templates/OPNsense/Cron/user.cron +/usr/local/opnsense/service/templates/OPNsense/Dnsmasq/+TARGETS +/usr/local/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf /usr/local/opnsense/service/templates/OPNsense/Filter/+TARGETS /usr/local/opnsense/service/templates/OPNsense/Filter/filter_geoip.conf /usr/local/opnsense/service/templates/OPNsense/Filter/filter_tables.conf diff --git a/src/etc/inc/plugins.inc.d/dnsmasq.inc b/src/etc/inc/plugins.inc.d/dnsmasq.inc index 8c17728da..28a1f0c8f 100644 --- a/src/etc/inc/plugins.inc.d/dnsmasq.inc +++ b/src/etc/inc/plugins.inc.d/dnsmasq.inc @@ -1,6 +1,7 @@ * Copyright (C) 2010 Ermal Luçi * Copyright (C) 2005-2006 Colin Smith @@ -40,8 +41,7 @@ function dnsmasq_configure() { return [ 'dns' => ['dnsmasq_configure_do'], - 'local' => ['dnsmasq_configure_do'], - 'newwanip' => ['dnsmasq_configure_do'], + 'local' => ['dnsmasq_configure_do'] ]; } @@ -90,6 +90,48 @@ function dnsmasq_xmlrpc_sync() return $result; } +/** + * Register automatic firewall rules + */ +function dnsmasq_firewall(\OPNsense\Firewall\Plugin $fw) +{ + global $config; + + $mdl = new \OPNsense\Dnsmasq\Dnsmasq(); + if (!$mdl->enable->isEmpty() && !$mdl->dhcp_default_fw_rules->isEmpty()) { + $dhcp_ifs = $mdl->getDhcpInterfaces(); + if (empty($dhcp_ifs)) { + return; + } + foreach ($fw->getInterfaceMapping() as $intf => $intfinfo) { + if (in_array($intf, $dhcp_ifs)) { + $baserule = [ + 'interface' => $intf, + 'protocol' => 'udp', + 'log' => !isset($config['syslog']['nologdefaultpass']), + '#ref' => "ui/dnsmasq/settings#general", + 'descr' => 'allow access to DHCP server' + ]; + $fw->registerFilterRule( + 1, + ['direction' => 'in', 'from_port' => 68, 'to' => '255.255.255.255', 'to_port' => 67], + $baserule + ); + $fw->registerFilterRule( + 1, + ['direction' => 'in', 'from_port' => 68, 'to' => '(self)', 'to_port' => 67], + $baserule + ); + $fw->registerFilterRule( + 1, + ['direction' => 'out', 'from_port' => 67, 'from' => '(self)', 'to_port' => 68], + $baserule + ); + } + } + } +} + function dnsmasq_configure_do($verbose = false) { global $config; @@ -103,102 +145,12 @@ function dnsmasq_configure_do($verbose = false) service_log('Starting Dnsmasq DNS...', $verbose); - $args = ''; - if (!isset($config['system']['webgui']['nodnsrebindcheck'])) { - $args .= '--rebind-localhost-ok --stop-dns-rebind'; - } - - $args .= ' -H /var/etc/dnsmasq-hosts '; - $args .= ' -H /var/etc/dnsmasq-leases '; - - /* Setup listen port, if non-default */ - if (isset($config['dnsmasq']['port']) && is_port($config['dnsmasq']['port'])) { - $args .= " --port={$config['dnsmasq']['port']} "; - } - - if (!empty($config['dnsmasq']['interface'])) { - $ifs = []; - foreach (explode(',', $config['dnsmasq']['interface']) as $ifname) { - if (!empty($config['interfaces'][$ifname]) && !empty($config['interfaces'][$ifname]['if'])) { - $ifs[] = $config['interfaces'][$ifname]['if']; - } - } - $args .= " --interface=" . implode(',', $ifs) . " "; - - if (!empty($addresses) && !empty($config['dnsmasq']['strictbind'])) { - $args .= ' --bind-interfaces '; - } - } - - if (!empty($config['dnsmasq']['no_private_reverse'])) { - $args .= ' --bogus-priv '; - } - - foreach (config_read_array('dnsmasq', 'domainoverrides') as $override) { - $ip = $override['ip']; - if (!empty($ip) && !empty($override['port'])) { - $ip .= '#' . $override['port']; - } - if (!empty($ip) && !empty($override['srcip'])) { - $ip .= '@' . $override['srcip']; - } - - $args .= ' --server=' . escapeshellarg('/' . $override['domain'] . '/' . $ip); - - if (!isset($config['system']['webgui']['nodnsrebindcheck'])) { - $args .= ' --rebind-domain-ok=' . escapeshellarg('/' . $override['domain'] . '/') . ' '; - } - } - - if (!empty($config['dnsmasq']['strict_order'])) { - $args .= ' --strict-order '; - } else { - $args .= ' --all-servers '; - } - - if (!empty($config['dnsmasq']['domain_needed'])) { - $args .= ' --domain-needed '; - } - - if (!empty($config['dnsmasq']['dnssec'])) { - $args .= ' --dnssec '; - $args .= ' --trust-anchor=.,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D '; - $args .= ' --trust-anchor=.,38696,8,2,683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16 '; - } - - if (!empty($config['dnsmasq']['log_queries'])) { - $args .= ' --log-queries=extra '; - } - - if (!empty($config['dnsmasq']['no_hosts'])) { - $args .= ' --no-hosts '; - } - - if (!empty($config['dnsmasq']['dns_forward_max'])) { - $args .= " --dns-forward-max={$config['dnsmasq']['dns_forward_max']} "; - } else { - $args .= ' --dns-forward-max=5000 '; - } - - if (!empty($config['dnsmasq']['cache_size'])) { - $args .= " --cache-size={$config['dnsmasq']['cache_size']} "; - } else { - $args .= ' --cache-size=10000 '; - } - - if (!empty($config['dnsmasq']['local_ttl'])) { - $args .= " --local-ttl={$config['dnsmasq']['local_ttl']} "; - } else { - $args .= ' --local-ttl=1 '; - } - - $args .= ' --conf-dir=/usr/local/etc/dnsmasq.conf.d,\*.conf '; - _dnsmasq_add_host_entries(); - mwexec("/usr/local/sbin/dnsmasq {$args}"); + mwexec("/usr/local/sbin/dnsmasq"); if (!empty($config['dnsmasq']['regdhcp'])) { + /* XXX: deprecate when moving ISC to plugins ? */ $domain = $config['system']['domain']; if (!empty($config['dnsmasq']['regdhcpdomain'])) { $domain = $config['dnsmasq']['regdhcpdomain']; @@ -222,11 +174,11 @@ function _dnsmasq_add_host_entries() } foreach ($dnsmasqcfg['hosts'] as $host) { - if (!empty($host['host'])) { + if (!empty($host['host']) && empty($host['domain'])) { + $lhosts .= "{$host['ip']}\t{$host['host']}\n"; + } elseif (!empty($host['host'])) { /* XXX: The question is if we do want "host" as a global alias */ $lhosts .= "{$host['ip']}\t{$host['host']}.{$host['domain']} {$host['host']}\n"; - } else { - $lhosts .= "{$host['ip']}\t{$host['domain']}\n"; } if (!empty($host['aliases'])) { foreach (explode(",", $host['aliases']) as $alias) { @@ -240,6 +192,7 @@ function _dnsmasq_add_host_entries() } if (!empty($dnsmasqcfg['regdhcpstatic'])) { + /* XXX: deprecate when ISC is moved to plugins? It doesn't make much sense to offer these registrations for KEA*/ foreach (plugins_run('static_mapping', [null, true, legacy_interfaces_details()]) as $map) { foreach ($map as $host) { if (empty($host['hostname'])) { diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/LeasesController.php b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/LeasesController.php new file mode 100644 index 000000000..63990eece --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/LeasesController.php @@ -0,0 +1,79 @@ +request->get('selected_interfaces'); + $backend = new Backend(); + $interfaces = []; + + $leases = json_decode($backend->configdpRun('dnsmasq list leases'), true) ?? []; + $ifconfig = json_decode($backend->configdRun('interface list ifconfig'), true); + $mac_db = json_decode($backend->configdRun('interface list macdb'), true) ?? []; + + $ifmap = []; + foreach (Config::getInstance()->object()->interfaces->children() as $if => $if_props) { + $ifmap[(string)$if_props->if] = [ + 'descr' => (string)$if_props->descr ?: strtoupper($if), + 'key' => $if + ]; + } + + if (!empty($leases) && isset($leases['records'])) { + $records = $leases['records']; + foreach ($records as &$record) { + $record['if_descr'] = ''; + $record['if_name'] = ''; + if (!empty($record['if']) && isset($ifmap[$record['if']])) { + $record['if_descr'] = $ifmap[$record['if']]['descr']; + $record['if_name'] = $ifmap[$record['if']]['key']; + $interfaces[$ifmap[$record['if']]['key']] = $ifmap[$record['if']]['descr']; + } + $mac = strtoupper(substr(str_replace(':', '', $record['hwaddr']), 0, 6)); + $record['mac_info'] = isset($mac_db[$mac]) ? $mac_db[$mac] : ''; + } + } else { + $records = []; + } + + $response = $this->searchRecordsetBase($records, null, 'address', function ($key) use ($selected_interfaces) { + return empty($selected_interfaces) || in_array($key['if_name'], $selected_interfaces); + }); + + $response['interfaces'] = $interfaces; + return $response; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/ServiceController.php b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/ServiceController.php index ade9b9f2e..c3f89794a 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/ServiceController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/ServiceController.php @@ -37,7 +37,7 @@ use OPNsense\Base\ApiMutableServiceControllerBase; class ServiceController extends ApiMutableServiceControllerBase { protected static $internalServiceClass = '\OPNsense\Dnsmasq\Dnsmasq'; - //protected static $internalServiceTemplate = 'OPNsense/Dnsmasq'; + protected static $internalServiceTemplate = 'OPNsense/Dnsmasq'; protected static $internalServiceEnabled = 'enable'; protected static $internalServiceName = 'dnsmasq'; } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php index 9c08f4813..998ab467e 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/Api/SettingsController.php @@ -35,6 +35,7 @@ class SettingsController extends ApiMutableModelControllerBase protected static $internalModelName = 'dnsmasq'; protected static $internalModelClass = '\OPNsense\Dnsmasq\Dnsmasq'; + /* hosts */ public function searchHostAction() { return $this->searchBase('hosts'); @@ -60,6 +61,41 @@ class SettingsController extends ApiMutableModelControllerBase return $this->delBase('hosts', $uuid); } + public function downloadHostsAction() + { + if ($this->request->isGet()) { + $this->exportCsv($this->getModel()->hosts->asRecordSet(false, ['comments'])); + } + } + + public function uploadHostsAction() + { + if ($this->request->isPost() && $this->request->hasPost('payload')) { + /* fields used by kea */ + $map = [ + 'ip_address' => 'ip', + 'hw_address' => 'hwaddr', + 'hostname' => 'host', + 'description' => 'descr', + ]; + return $this->importCsv( + 'hosts', + $this->request->getPost('payload'), + ['host', 'domain', 'ip'], + function (&$record) use ($map) { + foreach ($map as $from => $to) { + if (isset($record[$from])) { + $record[$to] = $record[$from]; + } + } + } + ); + } else { + return ['status' => 'failed']; + } + } + + /* domains */ public function searchDomainAction() { return $this->searchBase('domainoverrides'); @@ -84,4 +120,134 @@ class SettingsController extends ApiMutableModelControllerBase { return $this->delBase('domainoverrides', $uuid); } + + /* dhcp tags */ + public function searchTagAction() + { + return $this->searchBase('dhcp_tags'); + } + + public function getTagAction($uuid = null) + { + return $this->getBase('tag', 'dhcp_tags', $uuid); + } + + public function setTagAction($uuid) + { + return $this->setBase('tag', 'dhcp_tags', $uuid); + } + + public function addTagAction() + { + return $this->addBase('tag', 'dhcp_tags'); + } + + public function delTagAction($uuid) + { + return $this->delBase('dhcp_tags', $uuid); + } + + /* dhcp ranges */ + public function searchRangeAction() + { + return $this->searchBase('dhcp_ranges'); + } + + public function getRangeAction($uuid = null) + { + return $this->getBase('range', 'dhcp_ranges', $uuid); + } + + public function setRangeAction($uuid) + { + return $this->setBase('range', 'dhcp_ranges', $uuid); + } + + public function addRangeAction() + { + return $this->addBase('range', 'dhcp_ranges'); + } + + public function delRangeAction($uuid) + { + return $this->delBase('dhcp_ranges', $uuid); + } + + /* dhcp options */ + public function searchOptionAction() + { + return $this->searchBase('dhcp_options'); + } + + public function getOptionAction($uuid = null) + { + return $this->getBase('option', 'dhcp_options', $uuid); + } + + public function setOptionAction($uuid) + { + return $this->setBase('option', 'dhcp_options', $uuid); + } + + public function addOptionAction() + { + return $this->addBase('option', 'dhcp_options'); + } + + public function delOptionAction($uuid) + { + return $this->delBase('dhcp_options', $uuid); + } + + /* dhcp match options */ + public function searchMatchAction() + { + return $this->searchBase('dhcp_options_match'); + } + + public function getMatchAction($uuid = null) + { + return $this->getBase('match', 'dhcp_options_match', $uuid); + } + + public function setMatchAction($uuid) + { + return $this->setBase('match', 'dhcp_options_match', $uuid); + } + + public function addMatchAction() + { + return $this->addBase('match', 'dhcp_options_match'); + } + + public function delMatchAction($uuid) + { + return $this->delBase('dhcp_options_match', $uuid); + } + + /* dhcp boot options */ + public function searchBootAction() + { + return $this->searchBase('dhcp_boot'); + } + + public function getBootAction($uuid = null) + { + return $this->getBase('boot', 'dhcp_boot', $uuid); + } + + public function setBootAction($uuid) + { + return $this->setBase('boot', 'dhcp_boot', $uuid); + } + + public function addBootAction() + { + return $this->addBase('boot', 'dhcp_boot'); + } + + public function delBootAction($uuid) + { + return $this->delBase('dhcp_boot', $uuid); + } } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/IndexController.php b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/LeasesController.php similarity index 72% rename from src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/IndexController.php rename to src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/LeasesController.php index fdde11306..2f633db39 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/IndexController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/LeasesController.php @@ -28,15 +28,20 @@ namespace OPNsense\Dnsmasq; -class IndexController extends \OPNsense\Base\IndexController +class LeasesController extends \OPNsense\Base\IndexController { + /** + * {@inheritdoc} + */ + protected function templateJSIncludes() + { + return array_merge(parent::templateJSIncludes(), [ + '/ui/js/moment-with-locales.min.js' + ]); + } + public function indexAction() { - $this->view->generalForm = $this->getForm("general"); - $this->view->formDialogEditHostOverride = $this->getForm("dialogHostOverride"); - $this->view->formGridHostOverride = $this->getFormGrid("dialogHostOverride"); - $this->view->formDialogEditDomainOverride = $this->getForm("dialogDomainOverride"); - $this->view->formGridDomainOverride = $this->getFormGrid("dialogDomainOverride"); - $this->view->pick('OPNsense/Dnsmasq/index'); + $this->view->pick('OPNsense/Dnsmasq/leases'); } } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/SettingsController.php b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/SettingsController.php new file mode 100644 index 000000000..020116ca7 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/SettingsController.php @@ -0,0 +1,53 @@ +view->generalForm = $this->getForm("general"); + $this->view->formDialogEditHostOverride = $this->getForm("dialogHostOverride"); + $this->view->formGridHostOverride = $this->getFormGrid("dialogHostOverride", "host"); + $this->view->formDialogEditDomainOverride = $this->getForm("dialogDomainOverride"); + $this->view->formGridDomainOverride = $this->getFormGrid("dialogDomainOverride", "domain"); + $this->view->formDialogEditDHCPtag = $this->getForm("dialogDHCPtag"); + $this->view->formGridDHCPtag = $this->getFormGrid("dialogDHCPtag", "tag"); + $this->view->formDialogEditDHCPrange = $this->getForm("dialogDHCPrange"); + $this->view->formGridDHCPrange = $this->getFormGrid("dialogDHCPrange", "range"); + $this->view->formDialogEditDHCPoption = $this->getForm("dialogDHCPoption"); + $this->view->formGridDHCPoption = $this->getFormGrid("dialogDHCPoption", "option"); + $this->view->formDialogEditDHCPmatch = $this->getForm("dialogDHCPmatch"); + $this->view->formGridDHCPmatch = $this->getFormGrid("dialogDHCPmatch", "match"); + $this->view->formDialogEditDHCPboot = $this->getForm("dialogDHCPboot"); + $this->view->formGridDHCPboot = $this->getFormGrid("dialogDHCPboot", "boot"); + + $this->view->pick('OPNsense/Dnsmasq/settings'); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPboot.xml b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPboot.xml new file mode 100644 index 000000000..e858e0856 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPboot.xml @@ -0,0 +1,29 @@ +
+ + boot.tag + + dropdown + Only offer this boot image to the clients matched by the given tag. + + + boot.filename + + text + + + boot.servername + + text + + + boot.address + + text + + + option.description + + text + You may enter a description here for your reference (not parsed). + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPmatch.xml b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPmatch.xml new file mode 100644 index 000000000..aae97dcd5 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPmatch.xml @@ -0,0 +1,26 @@ +
+ + match.option + + dropdown + Option to offer to the client. + + + match.set_tag + + dropdown + Tag to set for requests matching this range which can be used to selectively match dhcp options + + + match.value + + text + Value to match, leave empty to match on the option only + + + match.description + + text + You may enter a description here for your reference (not parsed). + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPoption.xml b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPoption.xml new file mode 100644 index 000000000..d9c46cc46 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPoption.xml @@ -0,0 +1,36 @@ +
+ + option.option + + dropdown + Option to offer to the client. + + + option.tag + + select_multiple + If the optional tags are given then this option is only sent when all the tags are matched. + + + option.value + + text + Value (or values) to send to the client. The special address 0.0.0.0 is taken to mean "the address of the machine running dnsmasq" + + + option.force + + checkbox + Always send the option, also when the client does not ask for it in the parameter request list. + + boolean + boolean + + + + option.description + + text + You may enter a description here for your reference (not parsed). + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPrange.xml b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPrange.xml new file mode 100644 index 000000000..159ff091d --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPrange.xml @@ -0,0 +1,57 @@ +
+ + range.interface + + dropdown + Interface to serve this range + + + range.set_tag + + dropdown + Optional tag to set for requests matching this range which can be used to selectively match dhcp options + + + range.start_addr + + text + Start of the range. + + + range.end_addr + + text + End of the range. + + + range.static + + checkbox + Enable DHCP for the network specified, but do not to dynamically allocate IP addresses: only hosts which have static addresses will receive one + + boolean + boolean + + + + range.lease_time + + text + Defines how long the addresses (leases) given out by the server are valid (in seconds) + + false + + + + range.domain + + text + Offer the specified domain to machines in this range. + + + range.description + + text + You may enter a description here for your reference (not parsed). + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPtag.xml b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPtag.xml new file mode 100644 index 000000000..cfe664dc3 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogDHCPtag.xml @@ -0,0 +1,8 @@ +
+ + tag.tag + + text + An alphanumeric label which marks a network so that DHCP options may be specified on a per-network basis. + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogHostOverride.xml b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogHostOverride.xml index 9e12e4ea6..7947aa0b3 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogHostOverride.xml +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogHostOverride.xml @@ -1,4 +1,8 @@
+ + header + + host.host @@ -27,6 +31,26 @@ false + + header + + + + host.hwaddr + + text + When offered and the client requests an address via dhcp, assign the address provided here + + + host.set_tag + + dropdown + Optional tag to set for requests matching this range which can be used to selectively match dhcp options + + + header + + host.descr diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/general.xml b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/general.xml index 32c7ecea7..208ce7bfa 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/general.xml +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/general.xml @@ -1,48 +1,45 @@ + + header + + dnsmasq.enable checkbox Enable Dnsmasq. - - dnsmasq.port - - text - The port used for responding to DNS queries. It should normally be left blank unless another service needs to bind to TCP/UDP port 53. - dnsmasq.interface select_multiple - All (recommended) + All - Interface IPs used by Dnsmasq for responding to queries from clients. If an interface has both IPv4 and IPv6 IPs, both are used. Queries to other interface IPs not selected below are discarded. The default behavior is to respond to queries on every available IPv4 and IPv6 address. + Interface IPs used to responding to queries from clients. If an interface has both IPv4 and IPv6 IPs, both are used. Queries to other interface IPs not selected below are discarded. The default behavior is to respond to queries on every available IPv4 and IPv6 address. dnsmasq.strictbind checkbox - If this option is set, Dnsmasq will only bind to the interfaces containing the IP addresses selected above, rather than binding to all interfaces and discarding queries to other addresses. This option does not work with IPv6. If set, Dnsmasq will not bind to IPv6 addresses. + true + By default we bind the wildcard address, even when listening on some interfaces. Requests that shouldn't be handled are discarded, this has the advantage of working even when interfaces come and go and change address. This option forces binding to only the interfaces we are listening on, which is less stable in non static environments. + + + header + + + + dnsmasq.port + + text + The port used for responding to DNS queries. It should normally be left blank unless another service needs to bind to TCP/UDP port 53. Setting this to zero (0) completely disables DNS function dnsmasq.dnssec checkbox - - dnsmasq.regdhcpstatic - - checkbox - If this option is set, then DHCP static mappings will be registered in Dnsmasq, so that their name can be resolved. - - - dnsmasq.dhcpfirst - - checkbox - If this option is set, then DHCP mappings will be resolved before the manual list of names below. This only affects the name given for a reverse lookup (PTR). - dnsmasq.no_hosts @@ -52,6 +49,7 @@ dnsmasq.log_queries + true checkbox @@ -59,6 +57,7 @@ text 5000 + true Set the maximum number of concurrent DNS queries. On configurations with tight resources, this value may need to be reduced. @@ -66,6 +65,7 @@ text 10000 + true Set the size of the cache. Setting the cache size to zero disables caching. Please note that huge cache size impacts performance. @@ -73,8 +73,16 @@ text 1 + true This option allows a time-to-live (in seconds) to be given for local DNS entries, i.e. /etc/hosts or DHCP leases. This will reduce the load on the server at the expense of clients using stale data under some circumstances. A value of zero will disable client-side caching. + + dnsmasq.no_ident + + checkbox + true + Do not respond to class CHAOS and type TXT in domain bind queries. Without this option being set, the cache statistics are also available in the DNS as answers to queries of class CHAOS and type TXT in domain bind. + header @@ -83,29 +91,68 @@ dnsmasq.strict_order checkbox - If this option is set, Dnsmasq will query the DNS servers sequentially in the order specified (System: General Setup: DNS Servers), rather than all at once in parallel. + If this option is set, we will query the DNS servers sequentially in the order specified (System: General Setup: DNS Servers), rather than all at once in parallel. dnsmasq.domain_needed checkbox - If this option is set, Dnsmasq will not forward A or AAAA queries for plain names, without dots or domain parts, to upstream name servers. If the name is not known from /etc/hosts or DHCP then a "not found" answer is returned. + If this option is set, we will not forward A or AAAA queries for plain names, without dots or domain parts, to upstream name servers. If the name is not known from /etc/hosts or DHCP then a "not found" answer is returned. dnsmasq.no_private_reverse checkbox - If this option is set, Dnsmasq will not forward reverse DNS lookups (PTR) for private addresses (RFC 1918) to upstream name servers. Any entries in the Domain Overrides section forwarding private "n.n.n.in-addr.arpa" names to a specific server are still forwarded. If the IP to name is not known from /etc/hosts, DHCP or a specific domain override then a "not found" answer is immediately returned. + If this option is set, we will not forward reverse DNS lookups (PTR) for private addresses (RFC 1918) to upstream name servers. Any entries in the Domain Overrides section forwarding private "n.n.n.in-addr.arpa" names to a specific server are still forwarded. If the IP to name is not known from /etc/hosts, DHCP or a specific domain override then a "not found" answer is immediately returned. header - + + + + dnsmasq.no_dhcp_interface + + select_multiple + true + + Do not provide DHCP, TFTP or router advertisement on the specified interfaces, but do provide DNS service. + + + + dnsmasq.dhcp_fqdn + + checkbox + In the default mode, we insert the unqualified names of DHCP clients into the DNS, in which case they have to be unique. Using this option the unqualified name is no longer put in the DNS, only the qualified name. + + + dnsmasq.dhcp_lease_max + + text + 1000 + true + Limits dnsmasq to the specified maximum number of DHCP leases. This limit is to prevent DoS attacks from hosts which create thousands of leases and use lots of memory in the dnsmasq process. + + + dnsmasq.dhcp_authoritative + + checkbox + Should be set when dnsmasq is definitely the only DHCP server on a network. For DHCPv4, it changes the behaviour from strict RFC compliance so that DHCP requests on unknown leases from unknown hosts are not ignored. + + + dnsmasq.dhcp_default_fw_rules + + checkbox + Automatically register firewall rules to allow dhcp traffic for all explicitly selected interfaces, can be disabled for more fine grained control if needed. Changes are only effective after a firewall service restart (see system diagnostics). + + + header + dnsmasq.regdhcp checkbox - If this option is set, then machines that specify their hostname when requesting a DHCP lease will be registered in Dnsmasq, so that their name can be resolved. + If this option is set, then machines that specify their hostname when requesting a DHCP lease will be registered, so that their name can be resolved. dnsmasq.regdhcpdomain @@ -113,4 +160,16 @@ text The domain name to use for DHCP hostname registration. If empty, the default system domain is used. Note that all DHCP leases will be assigned to the same domain. If this is undesired, static DHCP lease registration is able to provide coherent mappings. + + dnsmasq.regdhcpstatic + + checkbox + If this option is set, then DHCP static mappings will be registered, so that their name can be resolved. + + + dnsmasq.dhcpfirst + + checkbox + If this option is set, then DHCP mappings will be resolved before the manual list of names below. This only affects the name given for a reverse lookup (PTR). + diff --git a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/ACL/ACL.xml index 4893f34d0..3cc2cfdcf 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/ACL/ACL.xml @@ -1,13 +1,13 @@ - Services: Dnsmasq DNS: Settings + Services: Dnsmasq DNS/DHCP: Settings - ui/dnsmasq + ui/dnsmasq/* api/dnsmasq/* - Services: Dnsmasq DNS: Log File + Services: Dnsmasq DNS/DHCP: Log File ui/diagnostics/log/core/dnsmasq/* api/diagnostics/log/core/dnsmasq/* diff --git a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.php b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.php index 21b200fcc..ee91d9f13 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.php +++ b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.php @@ -29,6 +29,7 @@ namespace OPNsense\Dnsmasq; use OPNsense\Base\BaseModel; +use OPNsense\Base\Messages\Message; /** * Class Dnsmasq @@ -36,4 +37,68 @@ use OPNsense\Base\BaseModel; */ class Dnsmasq extends BaseModel { + /** + * {@inheritdoc} + */ + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + foreach ($this->hosts->iterateItems() as $host) { + if (!$validateFullModel && !$host->isFieldChanged()) { + continue; + } + $key = $host->__reference; + if (!$host->hwaddr->isEmpty() && strpos($host->ip->getCurrentValue(), ':') !== false) { + $messages->appendMessage( + new Message( + gettext("Only IPv4 reservations are currently supported"), + $key . ".ip" + ) + ); + } + + if ($host->host->isEmpty() && $host->hwaddr->isEmpty()) { + $messages->appendMessage( + new Message( + gettext("Hostnames my only be omitted when a hardware address is offered."), + $key . ".host" + ) + ); + } + } + + foreach ($this->dhcp_ranges->iterateItems() as $range) { + if (!$validateFullModel && !$range->isFieldChanged()) { + continue; + } + $key = $range->__reference; + if (!$range->domain->isEmpty() && $range->end_addr->isEmpty()) { + $messages->appendMessage( + new Message( + gettext("Can only configure a domain when a full range (including end) is specified."), + $key . ".domain" + ) + ); + } + } + + return $messages; + } + + public function getDhcpInterfaces() + { + $result = []; + if (!empty($this->dhcp_ranges->iterateItems()->current())) { + $exclude = []; + foreach (explode(',', $this->no_dhcp_interface) as $item) { + $exclude[] = $item; + } + foreach (explode(',', $this->interface) as $item) { + if (!empty($item) && !in_array($item, $exclude)){ + $result[] = $item; + } + } + } + return $result; + } } diff --git a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml index 6b95c503b..e32e08df8 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml @@ -1,6 +1,6 @@ /dnsmasq - 1.0.0 + 1.0.1 @@ -19,7 +19,10 @@ Y - + + 0 + 65535 + 0 @@ -29,14 +32,28 @@ 0 + + Y + + + + 0 + + + + Y + 1 + + + Y + 1 + Y N - Y - Y /^(?:(?:[a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*(?:[a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$/i A valid domain must be specified. @@ -44,6 +61,16 @@ Y N + + + + + OPNsense.Dnsmasq.Dnsmasq + dhcp_tags + tag + + + @@ -70,5 +97,105 @@ + + + /^([0-9a-zA-Z$]){1,1024}$/u + + + UniqueConstraint + Tag names should be unique. + + + Y + + + + + Any + + /^(?!lo0$).*/ + + Y + + + + + OPNsense.Dnsmasq.Dnsmasq + dhcp_tags + tag + + + + + N + ipv4 + Y + + + N + ipv4 + + + + 1 + + + Y + N + + + + + + + + + OPNsense.Dnsmasq.Dnsmasq + dhcp_tags + tag + + + Y + + + + + + + + + + + OPNsense.Dnsmasq.Dnsmasq + dhcp_tags + tag + + + Y + + + + + + + + + OPNsense.Dnsmasq.Dnsmasq + dhcp_tags + tag + + + + + +
+ + diff --git a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Menu/Menu.xml index 3edeb648d..b8b322141 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Menu/Menu.xml @@ -1,8 +1,15 @@ - - - + + + + + + + + + + diff --git a/src/opnsense/mvc/app/views/OPNsense/Dnsmasq/index.volt b/src/opnsense/mvc/app/views/OPNsense/Dnsmasq/index.volt deleted file mode 100644 index b409bf851..000000000 --- a/src/opnsense/mvc/app/views/OPNsense/Dnsmasq/index.volt +++ /dev/null @@ -1,105 +0,0 @@ -{# - # 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':generalForm,'id':'frm_settings'])}} -
- -
- {{ partial('layout_partials/base_bootgrid_table', formGridHostOverride)}} -
- -
- {{ partial('layout_partials/base_bootgrid_table', formGridDomainOverride)}} -
-
- -
-
-
-
- -

-
-
-
- - -{{ partial("layout_partials/base_dialog",['fields':formDialogEditHostOverride,'id':formGridHostOverride['edit_dialog_id'],'label':lang._('Edit Host Override')])}} -{{ partial("layout_partials/base_dialog",['fields':formDialogEditDomainOverride,'id':formGridDomainOverride['edit_dialog_id'],'label':lang._('Edit Domain Override')])}} diff --git a/src/opnsense/mvc/app/views/OPNsense/Dnsmasq/leases.volt b/src/opnsense/mvc/app/views/OPNsense/Dnsmasq/leases.volt new file mode 100644 index 000000000..d00476442 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Dnsmasq/leases.volt @@ -0,0 +1,113 @@ +{# + # 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. + #} + + + + + + +
+
+ +
+ + + + + + + + + + + + +
{{ lang._('Interface') }}{{ lang._('IP Address') }}{{ lang._('MAC Address') }}{{ lang._('Expire') }}{{ lang._('Hostname') }}
+
diff --git a/src/opnsense/mvc/app/views/OPNsense/Dnsmasq/settings.volt b/src/opnsense/mvc/app/views/OPNsense/Dnsmasq/settings.volt new file mode 100644 index 000000000..33419de64 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Dnsmasq/settings.volt @@ -0,0 +1,207 @@ +{# + # 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':generalForm,'id':'frm_settings'])}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridHostOverride + {'command_width': '8em'} )}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridDomainOverride)}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridDHCPtag)}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridDHCPrange)}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridDHCPoption)}} +
+ {{ partial('layout_partials/base_bootgrid_table', formGridDHCPboot)}} +
+ +
+ {{ partial('layout_partials/base_bootgrid_table', formGridDHCPmatch)}} +
+
+ +
+
+
+
+ +

+
+
+
+ + +{{ partial("layout_partials/base_dialog",['fields':formDialogEditHostOverride,'id':formGridHostOverride['edit_dialog_id'],'label':lang._('Edit Host Override')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditDomainOverride,'id':formGridDomainOverride['edit_dialog_id'],'label':lang._('Edit Domain Override')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditDHCPtag,'id':formGridDHCPtag['edit_dialog_id'],'label':lang._('Edit DHCP tag')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditDHCPrange,'id':formGridDHCPrange['edit_dialog_id'],'label':lang._('Edit DHCP range')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditDHCPoption,'id':formGridDHCPoption['edit_dialog_id'],'label':lang._('Edit DHCP option')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditDHCPboot,'id':formGridDHCPboot['edit_dialog_id'],'label':lang._('Edit DHCP boot')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditDHCPmatch,'id':formGridDHCPmatch['edit_dialog_id'],'label':lang._('Edit DHCP match / option')])}} diff --git a/src/opnsense/scripts/dhcp/get_dnsmasq_leases.py b/src/opnsense/scripts/dhcp/get_dnsmasq_leases.py new file mode 100755 index 000000000..1cef46dac --- /dev/null +++ b/src/opnsense/scripts/dhcp/get_dnsmasq_leases.py @@ -0,0 +1,65 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2025 Ad Schellevis + 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. +""" + +import ipaddress +import subprocess +import os +import ujson + + +if __name__ == '__main__': + filename = '/var/db/dnsmasq.leases' + ranges = {} + leases = [] + this_interface = None + ifconfig = subprocess.run(['/sbin/ifconfig', '-f', 'inet:cidr,inet6:cidr'], capture_output=True, text=True).stdout + for line in ifconfig.split('\n'): + if not line.startswith("\t") and line.find(':') > -1: + this_interface = line.strip().split(':')[0] + elif this_interface is not None and line.startswith("\tinet") and line.find('-->') == -1: + ranges[ipaddress.ip_network(line.split()[1], strict=False)] = this_interface + + if os.path.isfile(filename): + with open(filename, 'r') as leasefile: + for line in leasefile: + parts = line.split() + if len(parts) > 4 and parts[0].isdigit(): + lease = { + 'expire': int(parts[0]), + 'hwaddr': parts[1], + 'address': parts[2], + 'hostname': parts[3], + 'client_id': parts[4] + } + for net in ranges: + if net.overlaps(ipaddress.ip_network(lease['address'])): + lease['if'] = ranges[net] + break + leases.append(lease) + + print (ujson.dumps({'records': leases})) diff --git a/src/opnsense/scripts/dns/dnsmasq_dhcp_options.py b/src/opnsense/scripts/dns/dnsmasq_dhcp_options.py new file mode 100755 index 000000000..0f2b926c1 --- /dev/null +++ b/src/opnsense/scripts/dns/dnsmasq_dhcp_options.py @@ -0,0 +1,40 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2025 Ad Schellevis + 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. + +""" +import json +import subprocess + +result = {} +sp = subprocess.run(['/usr/local/sbin/dnsmasq', '--help','dhcp'], capture_output=True, text=True) +for line in sp.stdout.split("\n"): + parts = line.split(maxsplit=1) + if len(parts) == 2 and parts[0].isdigit(): + result[parts[0]] = "%s [%s]" % (parts[1], parts[0]) + +print(json.dumps(result)) + diff --git a/src/opnsense/service/conf/actions.d/actions_dnsmasq.conf b/src/opnsense/service/conf/actions.d/actions_dnsmasq.conf index 3f453067e..6657603f9 100644 --- a/src/opnsense/service/conf/actions.d/actions_dnsmasq.conf +++ b/src/opnsense/service/conf/actions.d/actions_dnsmasq.conf @@ -1,24 +1,33 @@ [start] command:/usr/local/sbin/pluginctl -s dnsmasq start -parameters: type:script message:Starting Dnsmasq [stop] command:/usr/local/sbin/pluginctl -s dnsmasq stop -parameters: type:script message:Stopping Dnsmasq [restart] command:/usr/local/sbin/pluginctl -s dnsmasq restart -parameters: type:script message:Restarting Dnsmasq description:Restart Dnsmasq DNS service [status] command:/usr/local/sbin/pluginctl -s dnsmasq status -parameters: type:script_output message:Request Dnsmasq status + +[list.dhcp_options] +command:/usr/local/opnsense/scripts/dns/dnsmasq_dhcp_options.py +type:script_output +message:request dhcp options +cache_ttl:86400 + +[list.leases] +command:/usr/local/opnsense/scripts/dhcp/get_dnsmasq_leases.py +parameters: +type:script_output +message:list dnsmasq dhcp leases + diff --git a/src/opnsense/service/templates/OPNsense/Dnsmasq/+TARGETS b/src/opnsense/service/templates/OPNsense/Dnsmasq/+TARGETS new file mode 100644 index 000000000..aec11b7ba --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Dnsmasq/+TARGETS @@ -0,0 +1 @@ +dnsmasq.conf:/usr/local/etc/dnsmasq.conf diff --git a/src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf b/src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf new file mode 100644 index 000000000..b5e5dfb96 --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf @@ -0,0 +1,133 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +# + +{% if helpers.exists('system.webgui.nodnsrebindcheck') %} +rebind-localhost-ok +stop-dns-rebind +{% endif %} + +{% if dnsmasq.port %} +port={{ dnsmasq.port }} +{% endif %} + +{% if dnsmasq.interface %} +# If you want dnsmasq to listen for DHCP and DNS requests only on +# specified interfaces (and the loopback) give the name of the +# interface (eg eth0) here. +# Repeat the line for more than one interface. +interface={{helpers.physical_interfaces(dnsmasq.interface.split(','))|join(',')}} +{% endif %} + +{% if dnsmasq.no_dhcp_interface %} +# If you want dnsmasq to provide only DNS service on an interface, +# configure it as shown above, and then use the following line to +# disable DHCP and TFTP on it. +no-dhcp-interface={{helpers.physical_interfaces(dnsmasq.no_dhcp_interface.split(','))|join(',')}} +{% endif %} + +{% if dnsmasq.strictbind == '1' %} +# On systems which support it, dnsmasq binds the wildcard address, +# even when it is listening on only some interfaces. It then discards +# requests that it shouldn't reply to. This has the advantage of +# working even when interfaces come and go and change address. If you +# want dnsmasq to really bind only the interfaces it is listening on, +# uncomment this option. About the only time you may need this is when +# running another nameserver on the same machine. +bind-interfaces +{% endif %} + + +{% if dnsmasq.no_private_reverse == '1' %} +# Never forward addresses in the non-routed address spaces. +bogus-priv +{% endif %} + +{% for override in helpers.toList('dnsmasq.domainoverrides') %} +server=/{{override.domain}}/{{override.ip}}{%if override.port %}#{{override.port}}{% endif %}{%if override.srcip %}@{{override.srcip}}{% endif %} +{% if helpers.exists('system.webgui.nodnsrebindcheck') %} +rebind-domain-ok=/{{override.domain}}/ +{% endif %} +{% endfor %} + +{% if dnsmasq.strict_order == '1' %} +# By default, dnsmasq will send queries to any of the upstream +# servers it knows about and tries to favour servers to are known +# to be up. Uncommenting this forces dnsmasq to try each query +# with each server strictly in the order they appear in +# /etc/resolv.conf +strict-order +{% endif %} + +{% if dnsmasq.domain_needed == '1' %} +# Never forward plain names (without a dot or domain part) +domain-needed +{% endif %} + + +{% if dnsmasq.dnssec == '1' %} +# Uncomment these to enable DNSSEC validation and caching: +# (Requires dnsmasq to be built with DNSSEC option.) +conf-file=/usr/local/share/dnsmasq/trust-anchors.conf +dnssec +{% endif %} + +{% if dnsmasq.log_queries == '1' %} +# For debugging purposes, log each DNS query as it passes through +# dnsmasq. +log-queries=extra +{% endif %} + +{% if dnsmasq.no_hosts == '1' %} +# If you don't want dnsmasq to read /etc/hosts, uncomment the +# following line. +no-hosts +{% endif %} + +# host entries flushed via dnsmasq_watcher.py [isc] and a dump of the static reservations +addn-hosts=/var/etc/dnsmasq-hosts +addn-hosts=/var/etc/dnsmasq-leases + +dns-forward-max={{dnsmasq.dns_forward_max|default('5000')}} +cache-size={{dnsmasq.cache_size|default('10000')}} +local-ttl={{dnsmasq.local_ttl|default('1')}} + +conf-dir=/usr/local/etc/dnsmasq.conf.d,\*.conf + + +{% for dhcp_range in helpers.toList('dnsmasq.dhcp_ranges') %} + +dhcp-range={% + if dhcp_range.interface +%}{{helpers.physical_interface(dhcp_range.interface)}},{% endif %}{% + if dhcp_range.set_tag +%}set:{{dhcp_range.set_tag|replace('-','')}},{% + endif +%}{{dhcp_range.start_addr}},{% + if dhcp_range.static == '1' +%}static,{% + elif dhcp_range.end_addr +%}{{ dhcp_range.end_addr }},{% + endif +%}{{dhcp_range.lease_time|default('86400')}} +{% if dhcp_range.domain %} +domain={{ dhcp_range.domain }},{{dhcp_range.start_addr}},{{dhcp_range.end_addr}} +{% endif %} +{% endfor %} + +{% for host in helpers.toList('dnsmasq.hosts') %} +{% if host.hwaddr and host.hwaddr.find(':') == -1%} +dhcp-host={{host.hwaddr}}{% if host.set_tag%},set:{{host.set_tag|replace('-','')}}{% endif %},{{host.ip}} +{% endif %} +{% endfor %} + +{% for option in helpers.toList('dnsmasq.dhcp_options') %} +dhcp-option{% if option.force == '1' %}-force{% endif %}={% if option.tag %}tag:{{option.tag.replace('-','').split(',')|join(',tag:')}},{% endif %}{{ option.option }},{{ option.value }} +{% endfor %} + +{% for match in helpers.toList('dnsmasq.dhcp_options_match') %} +dhcp-match=set:{{match.set_tag.replace('-','')}},{{match.option}}{%if match.value %},{{match.value}}{% endif +%} +{% endfor %} + +{% if dnsmasq.no_ident == '1' %} +no-ident +{% endif %}