diff --git a/src/etc/inc/plugins.inc.d/dnsmasq.inc b/src/etc/inc/plugins.inc.d/dnsmasq.inc index 63fe8600f..6d80f3dde 100644 --- a/src/etc/inc/plugins.inc.d/dnsmasq.inc +++ b/src/etc/inc/plugins.inc.d/dnsmasq.inc @@ -180,19 +180,22 @@ function _dnsmasq_add_host_entries() } foreach ($dnsmasqcfg['hosts'] as $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"; - } - if (!empty($host['aliases'])) { - foreach (explode(",", $host['aliases']) as $alias) { - /** - * XXX: pre migration all hosts where added here as alias, when we combine host.domain we - * miss some information, which is likely not a very good idea anyway. - */ - $lhosts .= "{$host['ip']}\t{$alias}\n"; + /* The host.ip field supports multiple IPv4 and IPv6 addresses */ + foreach (explode(',', $host['ip']) as $ip) { + if (!empty($host['host']) && empty($host['domain'])) { + $lhosts .= "{$ip}\t{$host['host']}\n"; + } elseif (!empty($host['host'])) { + /* XXX: The question is if we do want "host" as a global alias */ + $lhosts .= "{$ip}\t{$host['host']}.{$host['domain']} {$host['host']}\n"; + } + if (!empty($host['aliases'])) { + foreach (explode(",", $host['aliases']) as $alias) { + /** + * XXX: pre migration all hosts where added here as alias, when we combine host.domain we + * miss some information, which is likely not a very good idea anyway. + */ + $lhosts .= "{$ip}\t{$alias}\n"; + } } } } 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 7947aa0b3..70966cc9b 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogHostOverride.xml +++ b/src/opnsense/mvc/app/controllers/OPNsense/Dnsmasq/forms/dialogHostOverride.xml @@ -17,15 +17,18 @@ host.ip - - text - IP address of the host, e.g. 192.168.100.100 or fd00:abcd::1 + + select_multiple + + true + IP addresses of the host, e.g. 192.168.100.100 or fd00:abcd::1. Can be multiple IPv4 and IPv6 addresses for dual stack configurations. Setting multiple addresses will automatically assign the best match based on the subnet of the interface receiving the DHCP Discover. host.aliases select_multiple + true list of aliases (fqdn) false @@ -36,16 +39,48 @@ - host.hwaddr - + host.client_id + text - When offered and the client requests an address via dhcp, assign the address provided here + Match the identifier of the client, e.g., DUID for DHCPv6. Setting the special character "*" will ignore the client identifier for DHCPv4 leases if a client offers both as choice. + + false + + + + host.hwaddr + + select_multiple + + true + Match the hardware address of the client. Can be multiple addresses, e.g., if the client has multiple network cards. Though keep in mind that DNSmasq cannot assume which address is the correct one when multiple send DHCP Discover at the same time. + + + host.lease_time + + text + 86400 + Defines how long the addresses (leases) given out by the server are valid (in seconds) + + false + host.set_tag dropdown - Optional tag to set for requests matching this range which can be used to selectively match dhcp options + Optional tag to set for requests matching this range which can be used to selectively match dhcp options. Can be left empty if options with an interface tag exist, since the client automatically receives this tag based on the interface receiving the DHCP Discover. + + + host.ignore + + checkbox + Ignore any DHCP packets of this host. Useful if it should get served by a different DHCP server. + + false + boolean + boolean + header diff --git a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.php b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.php index ba30a3c2d..e5ca70703 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.php +++ b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.php @@ -55,24 +55,46 @@ class Dnsmasq extends BaseModel $messages = parent::performValidation($validateFullModel); + $usedDhcpIpAddresses = []; + foreach ($this->hosts->iterateItems() as $host) { + if (!$host->hwaddr->isEmpty() || !$host->client_id->isEmpty()) { + foreach (explode(',', (string)$host->ip) as $ip) { + $usedDhcpIpAddresses[$ip] = isset($usedDhcpIpAddresses[$ip]) ? $usedDhcpIpAddresses[$ip] + 1 : 1; + } + } + } + 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" - ) - ); + + // all dhcp-host IP addresses must be unique, host overrides can have duplicate IP addresses + if (!$host->hwaddr->isEmpty() || !$host->client_id->isEmpty()) { + foreach (explode(',', (string)$host->ip) as $ip) { + if ($usedDhcpIpAddresses[$ip] > 1) { + $messages->appendMessage( + new Message( + sprintf(gettext("'%s' is already used in another DHCP host entry."), $ip), + $key . ".ip" + ) + ); + } + } } - if ($host->host->isEmpty() && $host->hwaddr->isEmpty()) { + if ( + $host->host->isEmpty() && + $host->hwaddr->isEmpty() && + $host->client_id->isEmpty() + ) { $messages->appendMessage( new Message( - gettext("Hostnames my only be omitted when a hardware address is offered."), + gettext( + "Hostnames may only be omitted when either a hardware address " . + "or a client identifier is provided." + ), $key . ".host" ) ); diff --git a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml index beb909583..2ec432bc4 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Dnsmasq/Dnsmasq.xml @@ -82,8 +82,21 @@ Y N + , + Y - + + /^(?:\*|(?:[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2})+))$/ + Value must be a colon-separated hexadecimal sequence (e.g., 01:02:f3) or "*". + + + , + Y + + + 1 + + diff --git a/src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf b/src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf index 896cb4d1c..5caba277f 100644 --- a/src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf +++ b/src/opnsense/service/templates/OPNsense/Dnsmasq/dnsmasq.conf @@ -186,9 +186,32 @@ ra-param={{ helpers.physical_interface(dhcp_range.interface) }} {% 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 %} +{# Skip if MAC is missing or invalid (no colon), unless client_id is present #} +{% if not host.client_id and (not host.hwaddr or host.hwaddr.find(':') == -1) %} +{% continue %} +{% endif %} +dhcp-host= +{%- if host.client_id -%} +id:{{ host.client_id }}, +{%- endif -%} +{%- if host.hwaddr -%} +{{ host.hwaddr }}, +{%- endif -%} +{%- if host.set_tag -%} +set:{{ host.set_tag|replace('-', '') }}, +{%- endif -%} +{%- for ip in host.ip.split(',')|map('trim') if host.ip -%} +{{ '[' ~ ip ~ ']' if ':' in ip else ip }}{%- if not loop.last %},{% endif %} +{%- endfor -%} +{%- if host.host -%} +,{{ host.host }} +{%- endif -%} +{%- if host.lease_time -%} +,{{ host.lease_time }}{% else %},86400 +{%- endif -%} +{%- if host.ignore|default('0') == '1' -%} +,ignore +{%- endif +%} {% endfor %} {% set has_default=[] %}