dnsmasq: Backend migration and add dhcp support for https://github.com/opnsense/core/issues/8329 (#8355)

This rather large commit implements most relevant dhcp options and rewrites dnsmasq's backend.

By default dnsmasq is disabled, eventually we do want dnsmasq enabled for dhcp services by default, but dns itself disabled. For this reason we support port "0" as implemented at dnsmasq (not listening for dns).

For cases where users want to integrate dns and dhcp services, the advise is to make dnsmasq listen on a non standard port and point unbound to the zones where dnsmasq is responsible for. This has the advantage of a direct connection between dhcp registered hosts and the requesting service. In these cases dnsmasq's dns service acts like a "connector".

In the long run we should deprecate `regdhcpstatic` and `regdhcp` as these either belong to legacy isc-dhcp or hook kea entries (which are better served via unbound).

The first mvc migration phase implemented IndexController.php, which we rename to SettingsController.php now as these results in more logical ui endpoints.

Since we don't bind to addresses directly (unless specifically configured and adviced only for static setups), we can skip the newwanip event which means we don't restart the service on interface changes. dnsmasq is able to filter the relevant networks on the fly, which is the advised scenario and can cope more easily with changes.

When different clients need to receive different options, we can use "tags" now. Requests can add tags to filter options which will be offered to the client, in the most simple scenario one would tag on a range or a host reservation, but more advanced choices can also be achieved using match statements (for example architecture [client-arch])
This commit is contained in:
Ad Schellevis 2025-02-19 16:40:55 +00:00 committed by GitHub
parent c3994d14c6
commit bcf8f9ae75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1421 additions and 252 deletions

16
plist
View File

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

View File

@ -1,6 +1,7 @@
<?php
/*
* Copyright (C) 2025 Deciso B.V.
* Copyright (C) 2014-2023 Franco Fichtner <franco@opnsense.org>
* Copyright (C) 2010 Ermal Luçi
* Copyright (C) 2005-2006 Colin Smith <ethethlay@gmail.com>
@ -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'])) {

View File

@ -0,0 +1,79 @@
<?php
/*
* 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.
*/
namespace OPNsense\Dnsmasq\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
class LeasesController extends ApiControllerBase
{
public function searchAction()
{
$selected_interfaces = $this->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;
}
}

View File

@ -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';
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* 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.
*/
namespace OPNsense\Dnsmasq;
class SettingsController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->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');
}
}

View File

@ -0,0 +1,29 @@
<form>
<field>
<id>boot.tag</id>
<label>Tag</label>
<type>dropdown</type>
<help>Only offer this boot image to the clients matched by the given tag.</help>
</field>
<field>
<id>boot.filename</id>
<label>Filename</label>
<type>text</type>
</field>
<field>
<id>boot.servername</id>
<label>Servername</label>
<type>text</type>
</field>
<field>
<id>boot.address</id>
<label>Server address</label>
<type>text</type>
</field>
<field>
<id>option.description</id>
<label>Description</label>
<type>text</type>
<help>You may enter a description here for your reference (not parsed).</help>
</field>
</form>

View File

@ -0,0 +1,26 @@
<form>
<field>
<id>match.option</id>
<label>Option</label>
<type>dropdown</type>
<help>Option to offer to the client.</help>
</field>
<field>
<id>match.set_tag</id>
<label>Tag [set]</label>
<type>dropdown</type>
<help>Tag to set for requests matching this range which can be used to selectively match dhcp options</help>
</field>
<field>
<id>match.value</id>
<label>Value</label>
<type>text</type>
<help>Value to match, leave empty to match on the option only</help>
</field>
<field>
<id>match.description</id>
<label>Description</label>
<type>text</type>
<help>You may enter a description here for your reference (not parsed).</help>
</field>
</form>

View File

@ -0,0 +1,36 @@
<form>
<field>
<id>option.option</id>
<label>Option</label>
<type>dropdown</type>
<help>Option to offer to the client.</help>
</field>
<field>
<id>option.tag</id>
<label>Tag</label>
<type>select_multiple</type>
<help>If the optional tags are given then this option is only sent when all the tags are matched.</help>
</field>
<field>
<id>option.value</id>
<label>Value</label>
<type>text</type>
<help>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"</help>
</field>
<field>
<id>option.force</id>
<label>Force</label>
<type>checkbox</type>
<help>Always send the option, also when the client does not ask for it in the parameter request list.</help>
<grid_view>
<type>boolean</type>
<formatter>boolean</formatter>
</grid_view>
</field>
<field>
<id>option.description</id>
<label>Description</label>
<type>text</type>
<help>You may enter a description here for your reference (not parsed).</help>
</field>
</form>

View File

@ -0,0 +1,57 @@
<form>
<field>
<id>range.interface</id>
<label>Interface</label>
<type>dropdown</type>
<help>Interface to serve this range</help>
</field>
<field>
<id>range.set_tag</id>
<label>Tag [set]</label>
<type>dropdown</type>
<help>Optional tag to set for requests matching this range which can be used to selectively match dhcp options</help>
</field>
<field>
<id>range.start_addr</id>
<label>Start address</label>
<type>text</type>
<help>Start of the range.</help>
</field>
<field>
<id>range.end_addr</id>
<label>End address</label>
<type>text</type>
<help>End of the range.</help>
</field>
<field>
<id>range.static</id>
<label>Static</label>
<type>checkbox</type>
<help>Enable DHCP for the network specified, but do not to dynamically allocate IP addresses: only hosts which have static addresses will receive one</help>
<grid_view>
<type>boolean</type>
<formatter>boolean</formatter>
</grid_view>
</field>
<field>
<id>range.lease_time</id>
<label>Lease time</label>
<type>text</type>
<help>Defines how long the addresses (leases) given out by the server are valid (in seconds)</help>
<grid_view>
<visible>false</visible>
</grid_view>
</field>
<field>
<id>range.domain</id>
<label>Domain</label>
<type>text</type>
<help>Offer the specified domain to machines in this range.</help>
</field>
<field>
<id>range.description</id>
<label>Description</label>
<type>text</type>
<help>You may enter a description here for your reference (not parsed).</help>
</field>
</form>

View File

@ -0,0 +1,8 @@
<form>
<field>
<id>tag.tag</id>
<label>Tag</label>
<type>text</type>
<help>An alphanumeric label which marks a network so that DHCP options may be specified on a per-network basis.</help>
</field>
</form>

View File

@ -1,4 +1,8 @@
<form>
<field>
<type>header</type>
<label>DNS</label>
</field>
<field>
<id>host.host</id>
<label>Host</label>
@ -27,6 +31,26 @@
<visible>false</visible>
</grid_view>
</field>
<field>
<type>header</type>
<label>DHCP</label>
</field>
<field>
<id>host.hwaddr</id>
<label>Hardware address</label>
<type>text</type>
<help>When offered and the client requests an address via dhcp, assign the address provided here</help>
</field>
<field>
<id>host.set_tag</id>
<label>Tag [set]</label>
<type>dropdown</type>
<help>Optional tag to set for requests matching this range which can be used to selectively match dhcp options</help>
</field>
<field>
<type>header</type>
<label>Informational</label>
</field>
<field>
<id>host.descr</id>
<label>Description</label>

View File

@ -1,48 +1,45 @@
<form>
<field>
<type>header</type>
<label>Default</label>
</field>
<field>
<id>dnsmasq.enable</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable Dnsmasq.</help>
</field>
<field>
<id>dnsmasq.port</id>
<label>Listen Port</label>
<type>text</type>
<help>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.</help>
</field>
<field>
<id>dnsmasq.interface</id>
<label>Interface</label>
<type>select_multiple</type>
<hint>All (recommended)</hint>
<hint>All</hint>
<help>
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.
</help>
</field>
<field>
<id>dnsmasq.strictbind</id>
<label>Strict Interface Binding</label>
<type>checkbox</type>
<help>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.</help>
<advanced>true</advanced>
<help>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.</help>
</field>
<field>
<type>header</type>
<label>DNS</label>
</field>
<field>
<id>dnsmasq.port</id>
<label>Listen Port</label>
<type>text</type>
<help>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</help>
</field>
<field>
<id>dnsmasq.dnssec</id>
<label>DNSSEC</label>
<type>checkbox</type>
</field>
<field>
<id>dnsmasq.regdhcpstatic</id>
<label>Register DHCP Static Mappings</label>
<type>checkbox</type>
<help>If this option is set, then DHCP static mappings will be registered in Dnsmasq, so that their name can be resolved.</help>
</field>
<field>
<id>dnsmasq.dhcpfirst</id>
<label>Prefer DHCP</label>
<type>checkbox</type>
<help>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).</help>
</field>
<field>
<id>dnsmasq.no_hosts</id>
<label>No Hosts Lookup</label>
@ -52,6 +49,7 @@
<field>
<id>dnsmasq.log_queries</id>
<label>Log the results of DNS queries</label>
<advanced>true</advanced>
<type>checkbox</type>
</field>
<field>
@ -59,6 +57,7 @@
<label>Maximum concurrent queries</label>
<type>text</type>
<hint>5000</hint>
<advanced>true</advanced>
<help>Set the maximum number of concurrent DNS queries. On configurations with tight resources, this value may need to be reduced.</help>
</field>
<field>
@ -66,6 +65,7 @@
<label>Cache size</label>
<type>text</type>
<hint>10000</hint>
<advanced>true</advanced>
<help>Set the size of the cache. Setting the cache size to zero disables caching. Please note that huge cache size impacts performance.</help>
</field>
<field>
@ -73,8 +73,16 @@
<label>Local DNS entry TTL</label>
<type>text</type>
<hint>1</hint>
<advanced>true</advanced>
<help>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.</help>
</field>
<field>
<id>dnsmasq.no_ident</id>
<label>No ident</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>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.</help>
</field>
<field>
<type>header</type>
<label>DNS Query Forwarding</label>
@ -83,29 +91,68 @@
<id>dnsmasq.strict_order</id>
<label>Query DNS servers sequentially</label>
<type>checkbox</type>
<help>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.</help>
<help>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.</help>
</field>
<field>
<id>dnsmasq.domain_needed</id>
<label>Require domain</label>
<type>checkbox</type>
<help>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.</help>
<help>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.</help>
</field>
<field>
<id>dnsmasq.no_private_reverse</id>
<label>Do not forward private reverse lookups</label>
<type>checkbox</type>
<help>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.</help>
<help>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.</help>
</field>
<field>
<type>header</type>
<label>ISC DHCP</label>
<label>DHCP</label>
</field>
<field>
<id>dnsmasq.no_dhcp_interface</id>
<label>Interface [no dhcp]</label>
<type>select_multiple</type>
<advanced>true</advanced>
<help>
Do not provide DHCP, TFTP or router advertisement on the specified interfaces, but do provide DNS service.
</help>
</field>
<field>
<id>dnsmasq.dhcp_fqdn</id>
<label>DHCP fqdn</label>
<type>checkbox</type>
<help>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.</help>
</field>
<field>
<id>dnsmasq.dhcp_lease_max</id>
<label>DHCP max leases</label>
<type>text</type>
<hint>1000</hint>
<advanced>true</advanced>
<help>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.</help>
</field>
<field>
<id>dnsmasq.dhcp_authoritative</id>
<label>DHCP authoritative</label>
<type>checkbox</type>
<help>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.</help>
</field>
<field>
<id>dnsmasq.dhcp_default_fw_rules</id>
<label>DHCP register firewall rules</label>
<type>checkbox</type>
<help>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).</help>
</field>
<field>
<type>header</type>
<label>ISC / KEA DHCP (legacy)</label>
</field>
<field>
<id>dnsmasq.regdhcp</id>
<label>Register ISC DHCP4 Leases</label>
<type>checkbox</type>
<help>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.</help>
<help>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.</help>
</field>
<field>
<id>dnsmasq.regdhcpdomain</id>
@ -113,4 +160,16 @@
<type>text</type>
<help>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.</help>
</field>
<field>
<id>dnsmasq.regdhcpstatic</id>
<label>Register DHCP Static Mappings</label>
<type>checkbox</type>
<help>If this option is set, then DHCP static mappings will be registered, so that their name can be resolved.</help>
</field>
<field>
<id>dnsmasq.dhcpfirst</id>
<label>Prefer DHCP</label>
<type>checkbox</type>
<help>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).</help>
</field>
</form>

View File

@ -1,13 +1,13 @@
<acl>
<page-services-dnsforwarder>
<name>Services: Dnsmasq DNS: Settings</name>
<name>Services: Dnsmasq DNS/DHCP: Settings</name>
<patterns>
<pattern>ui/dnsmasq</pattern>
<pattern>ui/dnsmasq/*</pattern>
<pattern>api/dnsmasq/*</pattern>
</patterns>
</page-services-dnsforwarder>
<page-diagnostics-logs-dnsmasq>
<name>Services: Dnsmasq DNS: Log File</name>
<name>Services: Dnsmasq DNS/DHCP: Log File</name>
<patterns>
<pattern>ui/diagnostics/log/core/dnsmasq/*</pattern>
<pattern>api/diagnostics/log/core/dnsmasq/*</pattern>

View File

@ -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;
}
}

View File

@ -1,6 +1,6 @@
<model>
<mount>/dnsmasq</mount>
<version>1.0.0</version>
<version>1.0.1</version>
<items>
<enable type="BooleanField"/>
<regdhcp type="BooleanField"/>
@ -19,7 +19,10 @@
<interface type="InterfaceField">
<Multiple>Y</Multiple>
</interface>
<port type="PortField"/>
<port type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>65535</MaximumValue>
</port>
<dns_forward_max type="IntegerField">
<MinimumValue>0</MinimumValue>
</dns_forward_max>
@ -29,14 +32,28 @@
<local_ttl type="IntegerField">
<MinimumValue>0</MinimumValue>
</local_ttl>
<no_dhcp_interface type="InterfaceField">
<Multiple>Y</Multiple>
</no_dhcp_interface>
<dhcp_fqdn type="BooleanField"/>
<dhcp_lease_max type="IntegerField">
<MinimumValue>0</MinimumValue>
</dhcp_lease_max>
<dhcp_authoritative type="BooleanField"/>
<dhcp_default_fw_rules type="BooleanField">
<Required>Y</Required>
<Default>1</Default>
</dhcp_default_fw_rules>
<no_ident type="BooleanField">
<Required>Y</Required>
<Default>1</Default>
</no_ident>
<hosts type="ArrayField">
<host type="HostnameField">
<IsDNSName>Y</IsDNSName>
<IpAllowed>N</IpAllowed>
<Required>Y</Required>
</host>
<domain type="TextField">
<Required>Y</Required>
<Mask>/^(?:(?:[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</Mask>
<ValidationMessage>A valid domain must be specified.</ValidationMessage>
</domain>
@ -44,6 +61,16 @@
<Required>Y</Required>
<NetMaskAllowed>N</NetMaskAllowed>
</ip>
<hwaddr type="MacAddressField"/>
<set_tag type="ModelRelationField">
<Model>
<tag>
<source>OPNsense.Dnsmasq.Dnsmasq</source>
<items>dhcp_tags</items>
<display>tag</display>
</tag>
</Model>
</set_tag>
<descr type="TextField"/>
<comments type="TextField"/>
<!-- Intentionally last, AliasesField back references other fields (which should already be there) -->
@ -70,5 +97,105 @@
</ip>
<descr type="TextField"/>
</domainoverrides>
<dhcp_tags type="ArrayField">
<tag type="TextField">
<Mask>/^([0-9a-zA-Z$]){1,1024}$/u</Mask>
<Constraints>
<check001>
<type>UniqueConstraint</type>
<ValidationMessage>Tag names should be unique.</ValidationMessage>
</check001>
</Constraints>
<Required>Y</Required>
</tag>
</dhcp_tags>
<dhcp_ranges type="ArrayField">
<interface type="InterfaceField">
<BlankDesc>Any</BlankDesc>
<Filters>
<if>/^(?!lo0$).*/</if>
</Filters>
<AllowDynamic>Y</AllowDynamic>
</interface>
<set_tag type="ModelRelationField">
<Model>
<tag>
<source>OPNsense.Dnsmasq.Dnsmasq</source>
<items>dhcp_tags</items>
<display>tag</display>
</tag>
</Model>
</set_tag>
<start_addr type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<AddressFamily>ipv4</AddressFamily>
<Required>Y</Required>
</start_addr>
<end_addr type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<AddressFamily>ipv4</AddressFamily>
</end_addr>
<static type="BooleanField"/>
<lease_time type="IntegerField">
<MinimumValue>1</MinimumValue>
</lease_time>
<domain type="HostnameField">
<IsDNSName>Y</IsDNSName>
<IpAllowed>N</IpAllowed>
</domain>
<description type="DescriptionField"/>
</dhcp_ranges>
<dhcp_options type="ArrayField">
<option type="JsonKeyValueStoreField">
<ConfigdPopulateAct>dnsmasq list dhcp_options</ConfigdPopulateAct>
<Required>Y</Required>
</option>
<tag type="ModelRelationField">
<Model>
<tag>
<source>OPNsense.Dnsmasq.Dnsmasq</source>
<items>dhcp_tags</items>
<display>tag</display>
</tag>
</Model>
<Multiple>Y</Multiple>
</tag>
<value type="TextField"/>
<force type="BooleanField"/>
<description type="DescriptionField"/>
</dhcp_options>
<dhcp_options_match type="ArrayField">
<option type="JsonKeyValueStoreField">
<ConfigdPopulateAct>dnsmasq list dhcp_options</ConfigdPopulateAct>
<Required>Y</Required>
</option>
<set_tag type="ModelRelationField">
<Model>
<tag>
<source>OPNsense.Dnsmasq.Dnsmasq</source>
<items>dhcp_tags</items>
<display>tag</display>
</tag>
</Model>
<Required>Y</Required>
</set_tag>
<value type="TextField"/>
<description type="DescriptionField"/>
</dhcp_options_match>
<dhcp_boot type="ArrayField">
<tag type="ModelRelationField">
<Model>
<tag>
<source>OPNsense.Dnsmasq.Dnsmasq</source>
<items>dhcp_tags</items>
<display>tag</display>
</tag>
</Model>
</tag>
<filename type="TextField"/>
<servername type="TextField"/>
<address type="TextField"/>
<description type="DescriptionField"/>
</dhcp_boot>
</items>
</model>

View File

@ -1,8 +1,15 @@
<menu>
<Services>
<Dnsmasq VisibleName="Dnsmasq DNS" cssClass="fa fa-tags fa-fw">
<Settings order="10" url="/ui/dnsmasq"/>
<LogFile VisibleName="Log File" order="50" url="/ui/diagnostics/log/core/dnsmasq"/>
<Dnsmasq VisibleName="Dnsmasq DNS &amp; DHCP" cssClass="fa fa-tags fa-fw">
<General order="10" url="/ui/dnsmasq/settings#general"/>
<Hosts order="20" url="/ui/dnsmasq/settings#hosts"/>
<Domains order="30" url="/ui/dnsmasq/settings#domains"/>
<Dhcptags VisibleName="DHCP tags" order="40" url="/ui/dnsmasq/settings#dhcptags"/>
<Dhcpranges VisibleName="DHCP ranges" order="50" url="/ui/dnsmasq/settings#dhcpranges"/>
<Dhcpoptions VisibleName="DHCP options" order="60" url="/ui/dnsmasq/settings#dhcpoptions"/>
<Dhcpmatches VisibleName="DHCP matches" order="70" url="/ui/dnsmasq/settings#dhcpmatches"/>
<leases order="80" url="/ui/dnsmasq/leases"/>
<LogFile VisibleName="Log File" order="90" url="/ui/diagnostics/log/core/dnsmasq"/>
</Dnsmasq>
</Services>
</menu>

View File

@ -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.
#}
<script>
$( document ).ready(function() {
let data_get_map = {'frm_settings':"/api/dnsmasq/settings/get"};
mapDataToFormUI(data_get_map).done(function(data){
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
updateServiceControlUI('dnsmasq');
});
$("#{{formGridHostOverride['table_id']}}").UIBootgrid({
'search':'/api/dnsmasq/settings/search_host',
'get':'/api/dnsmasq/settings/get_host/',
'set':'/api/dnsmasq/settings/set_host/',
'add':'/api/dnsmasq/settings/add_host/',
'del':'/api/dnsmasq/settings/del_host/'
});
$("#{{formGridDomainOverride['table_id']}}").UIBootgrid({
'search':'/api/dnsmasq/settings/search_domain',
'get':'/api/dnsmasq/settings/get_domain/',
'set':'/api/dnsmasq/settings/set_domain/',
'add':'/api/dnsmasq/settings/add_domain/',
'del':'/api/dnsmasq/settings/del_domain/'
});
$("#reconfigureAct").SimpleActionButton({
onPreAction: function() {
const dfObj = new $.Deferred();
saveFormToEndpoint("/api/dnsmasq/settings/set", 'frm_settings', function () { dfObj.resolve(); }, true, function () { dfObj.reject(); });
return dfObj;
},
onAction: function(data, status) {
updateServiceControlUI('dnsmasq');
}
});
});
</script>
<!-- Navigation bar -->
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#general">{{ lang._('General') }}</a></li>
<li><a data-toggle="tab" href="#hosts">{{ lang._('Hosts') }}</a></li>
<li><a data-toggle="tab" href="#domains">{{ lang._('Domains') }}</a></li>
</ul>
<div class="tab-content content-box">
<!-- general settings -->
<div id="general" class="tab-pane fade in active">
{{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_settings'])}}
</div>
<!-- Tab: Hosts -->
<div id="hosts" class="tab-pane fade in">
{{ partial('layout_partials/base_bootgrid_table', formGridHostOverride)}}
</div>
<!-- Tab: Domains -->
<div id="domains" class="tab-pane fade in">
{{ partial('layout_partials/base_bootgrid_table', formGridDomainOverride)}}
</div>
</div>
<section class="page-content-main">
<div class="content-box">
<div class="col-md-12">
<br/>
<button class="btn btn-primary" id="reconfigureAct"
data-endpoint='/api/dnsmasq/service/reconfigure'
data-label="{{ lang._('Apply') }}"
data-error-title="{{ lang._('Error reconfiguring Dnsmasq') }}"
type="button"
></button>
<br/><br/>
</div>
</div>
</section>
{{ 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')])}}

View File

@ -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.
#}
<script>
$( document ).ready(function() {
let selected_interfaces = [];
$("#interface-selection").on("changed.bs.select", function (e) {
selected_interfaces = $(this).val();
$("#grid-leases").bootgrid('reload');
})
$("#grid-leases").UIBootgrid({
search:'/api/dnsmasq/leases/search/',
options: {
selection: false,
multiSelect: false,
useRequestHandlerOnGet: true,
requestHandler: function(request) {
request['selected_interfaces'] = selected_interfaces;
return request;
},
responseHandler: function (response) {
if (response.interfaces !== undefined) {
let intfsel = $("#interface-selection > option").map(function () {
return $(this).val();
}).get();
for ([intf, descr] of Object.entries(response['interfaces'])) {
if (!intfsel.includes(intf)) {
$("#interface-selection").append($('<option>', {
value: intf,
text: descr
}));
}
}
$("#interface-selection").selectpicker('refresh');
}
return response;
},
formatters: {
"overflowformatter": function (column, row) {
return '<span class="overflow">' + row[column.id] + '</span><br/>'
},
"macformatter": function (column, row) {
let mac = '<span class="overflow">' + row.hwaddr + '</span>';
if (row.mac_info != '') {
mac = mac + '<br/>' + '<small class="overflow"><i>' + row.mac_info + '</i></small>';
}
return mac;
},
"timestamp": function (column, row) {
return moment.unix(row[column.id]).local().format('YYYY-MM-DD HH:mm:ss');
},
}
}
});
$("#interface-selection-wrapper").detach().prependTo('#grid-leases-header > .row > .actionBar > .actions');
updateServiceControlUI('dnsmasq');
});
</script>
<style>
.overflow {
text-overflow: clip;
white-space: normal;
word-break: break-word;
}
</style>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs"></ul>
<div class="tab-content content-box col-xs-12 __mb">
<div class="btn-group" id="interface-selection-wrapper">
<select class="selectpicker" multiple="multiple" data-live-search="true" id="interface-selection" data-width="auto" title="All Interfaces">
</select>
</div>
<table id="grid-leases" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="if_descr" data-type="string">{{ lang._('Interface') }}</th>
<th data-column-id="address" data-identifier="true" data-type="string" data-formatter="overflowformatter">{{ lang._('IP Address') }}</th>
<th data-column-id="hwaddr" data-type="string" data-formatter="macformatter" data-width="9em">{{ lang._('MAC Address') }}</th>
<th data-column-id="expire" data-type="string" data-formatter="timestamp">{{ lang._('Expire') }}</th>
<th data-column-id="hostname" data-type="string" data-formatter="overflowformatter">{{ lang._('Hostname') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>

View File

@ -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.
#}
<script>
$( document ).ready(function() {
let data_get_map = {'frm_settings':"/api/dnsmasq/settings/get"};
mapDataToFormUI(data_get_map).done(function(data){
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
updateServiceControlUI('dnsmasq');
});
let all_grids = {};
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let grid_ids = null;
switch (e.target.hash) {
case '#hosts':
grid_ids = ["{{formGridHostOverride['table_id']}}"];
break;
case '#domains':
grid_ids = ["{{formGridDomainOverride['table_id']}}"];
break;
case '#dhcptags':
grid_ids = ["{{formGridDHCPtag['table_id']}}"];
break;
case '#dhcpranges':
grid_ids = ["{{formGridDHCPrange['table_id']}}"];
break;
case '#dhcpoptions':
grid_ids = ["{{formGridDHCPoption['table_id']}}", "{{formGridDHCPboot['table_id']}}"];
break;
case '#dhcpmatches':
grid_ids = ["{{formGridDHCPmatch['table_id']}}"];
break;
}
/* grid action selected, load or refresh target grid */
if (grid_ids !== null) {
grid_ids.forEach(function (grid_id, index) {
if (all_grids[grid_id] === undefined) {
all_grids[grid_id] = $("#"+grid_id).UIBootgrid({
'search':'/api/dnsmasq/settings/search_' + grid_id,
'get':'/api/dnsmasq/settings/get_' + grid_id + '/',
'set':'/api/dnsmasq/settings/set_' + grid_id + '/',
'add':'/api/dnsmasq/settings/add_' + grid_id + '/',
'del':'/api/dnsmasq/settings/del_' + grid_id + '/'
});
/* insert headers when multiple grids exist on a single tab */
let header = $("#" + grid_id + "-header");
if (grid_id === 'option' ) {
header.find('div.actionBar').parent().prepend(
$('<td id="heading-wrapper" class="col-sm-2 theading-text">{{ lang._('Options') }}</div>')
);
} else if (grid_id == 'boot') {
header.find('div.actionBar').parent().prepend(
$('<td id="heading-wrapper" class="col-sm-2 theading-text">{{ lang._('Boot') }}</div>')
);
} else if (grid_id == 'host') {
all_grids[grid_id].find("tfoot td:last").append($("#hosts_tfoot_append > button").detach());
}
} else {
all_grids[grid_id].bootgrid('reload');
}
});
}
});
$("#reconfigureAct").SimpleActionButton({
onPreAction: function() {
const dfObj = new $.Deferred();
saveFormToEndpoint("/api/dnsmasq/settings/set", 'frm_settings', function () { dfObj.resolve(); }, true, function () { dfObj.reject(); });
return dfObj;
},
onAction: function(data, status) {
updateServiceControlUI('dnsmasq');
}
});
$("#download_hosts").click(function(e){
e.preventDefault();
window.open("/api/dnsmasq/settings/download_hosts");
});
$("#upload_hosts").SimpleFileUploadDlg({
onAction: function(){
$("#{{formGridHostOverride['table_id']}}").bootgrid('reload');
}
});
$('.nav-tabs a').on('shown.bs.tab', function (e) {
history.pushState(null, null, e.target.hash);
});
$(window).on('hashchange', function(e) {
$('a[href="' + window.location.hash + '"]').click()
});
let selected_tab = window.location.hash != "" ? window.location.hash : "#general";
$('a[href="' +selected_tab + '"]').click();
});
</script>
<style>
tbody.collapsible > tr > td:first-child {
padding-left: 30px;
}
</style>
<div style="display: none;" id="hosts_tfoot_append">
<button
id="upload_hosts"
type="button"
data-title="{{ lang._('Import hosts') }}"
data-endpoint='/api/dnsmasq/settings/upload_hosts'
title="{{ lang._('Import csv') }}"
data-toggle="tooltip"
class="btn btn-xs"
><span class="fa fa-fw fa-upload"></span></button>
<button id="download_hosts" type="button" title="{{ lang._('Export as csv') }}" data-toggle="tooltip" class="btn btn-xs"><span class="fa fa-fw fa-table"></span></button>
</div>
<!-- Navigation bar -->
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li><a data-toggle="tab" href="#general">{{ lang._('General') }}</a></li>
<li><a data-toggle="tab" href="#hosts">{{ lang._('Hosts') }}</a></li>
<li><a data-toggle="tab" href="#domains">{{ lang._('Domains') }}</a></li>
<li><a data-toggle="tab" href="#dhcptags">{{ lang._('DHCP tags') }}</a></li>
<li><a data-toggle="tab" href="#dhcpranges">{{ lang._('DHCP ranges') }}</a></li>
<li><a data-toggle="tab" href="#dhcpoptions">{{ lang._('DHCP options') }}</a></li>
<li><a data-toggle="tab" href="#dhcpmatches">{{ lang._('DHCP options / match') }}</a></li>
</ul>
<div class="tab-content content-box">
<!-- general settings -->
<div id="general" class="tab-pane fade in active">
{{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_settings'])}}
</div>
<!-- Tab: Hosts -->
<div id="hosts" class="tab-pane fade in">
{{ partial('layout_partials/base_bootgrid_table', formGridHostOverride + {'command_width': '8em'} )}}
</div>
<!-- Tab: Domains -->
<div id="domains" class="tab-pane fade in">
{{ partial('layout_partials/base_bootgrid_table', formGridDomainOverride)}}
</div>
<!-- Tab: DHCP Tags -->
<div id="dhcptags" class="tab-pane fade in">
{{ partial('layout_partials/base_bootgrid_table', formGridDHCPtag)}}
</div>
<!-- Tab: DHCP Ranges -->
<div id="dhcpranges" class="tab-pane fade in">
{{ partial('layout_partials/base_bootgrid_table', formGridDHCPrange)}}
</div>
<!-- Tab: DHCP [boot] Options -->
<div id="dhcpoptions" class="tab-pane fade in">
{{ partial('layout_partials/base_bootgrid_table', formGridDHCPoption)}}
<hr/>
{{ partial('layout_partials/base_bootgrid_table', formGridDHCPboot)}}
</div>
<!-- Tab: DHCP Options / Match -->
<div id="dhcpmatches" class="tab-pane fade in">
{{ partial('layout_partials/base_bootgrid_table', formGridDHCPmatch)}}
</div>
</div>
<section class="page-content-main">
<div class="content-box">
<div class="col-md-12">
<br/>
<button class="btn btn-primary" id="reconfigureAct"
data-endpoint='/api/dnsmasq/service/reconfigure'
data-label="{{ lang._('Apply') }}"
data-error-title="{{ lang._('Error reconfiguring Dnsmasq') }}"
type="button"
></button>
<br/><br/>
</div>
</div>
</section>
{{ 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')])}}

View File

@ -0,0 +1,65 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2025 Ad Schellevis <ad@opnsense.org>
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}))

View File

@ -0,0 +1,40 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2025 Ad Schellevis <ad@opnsense.org>
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))

View File

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

View File

@ -0,0 +1 @@
dnsmasq.conf:/usr/local/etc/dnsmasq.conf

View File

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