%PDF- %PDF-
Direktori : /backups/router/usr/local/etc/inc/plugins.inc.d/ |
Current File : //backups/router/usr/local/etc/inc/plugins.inc.d/dhcpd.inc |
<?php /* * Copyright (C) 2014-2024 Franco Fichtner <franco@opnsense.org> * Copyright (C) 2010 Ermal Luçi * Copyright (C) 2005-2006 Colin Smith <ethethlay@gmail.com> * Copyright (C) 2003-2004 Manuel Kasper <mk@neon1.net> * 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. */ function dhcpd_configure() { return [ 'dhcp' => ['dhcpd_dhcp_configure:3'], 'local' => ['dhcpd_dhcp_configure'], ]; } function dhcpd_run() { return [ 'static_mapping' => 'dhcpd_staticmap', ]; } function dhcpd_radvd_enabled() { global $config; /* handle manually configured DHCP6 server settings first */ foreach (config_read_array('dhcpdv6') as $dhcpv6if => $dhcpv6ifconf) { if (isset($config['interfaces'][$dhcpv6if]['enable']) && isset($dhcpv6ifconf['ramode']) && $dhcpv6ifconf['ramode'] != 'disabled') { return true; } } /* handle DHCP-PD prefixes and 6RD dynamic interfaces */ foreach (legacy_config_get_interfaces(array('virtual' => false)) as $ifcfg) { if (isset($ifcfg['enable']) && !empty($ifcfg['track6-interface']) && !isset($ifcfg['dhcpd6track6allowoverride'])) { return true; } } return false; } function dhcpd_dhcpv6_enabled() { global $config; /* handle manually configured DHCP6 server settings first */ foreach (config_read_array('dhcpdv6') as $dhcpv6if => $dhcpv6ifconf) { if (isset($config['interfaces'][$dhcpv6if]['enable']) && isset($dhcpv6ifconf['enable'])) { return true; } } /* handle DHCP-PD prefixes and 6RD dynamic interfaces */ foreach (legacy_config_get_interfaces(array('virtual' => false)) as $ifcfg) { if (isset($ifcfg['enable']) && !empty($ifcfg['track6-interface']) && !isset($ifcfg['dhcpd6track6allowoverride'])) { return true; } } return false; } function dhcpd_dhcpv4_enabled() { global $config; foreach (config_read_array('dhcpd') as $dhcpif => $dhcpifconf) { if (isset($dhcpifconf['enable']) && !empty($config['interfaces'][$dhcpif])) { return true; } } return false; } function dhcpd_services() { global $config; $services = array(); if (dhcpd_radvd_enabled()) { $pconfig = array(); $pconfig['name'] = "radvd"; $pconfig['description'] = gettext("Router Advertisement Daemon"); $pconfig['php']['restart'] = array('dhcpd_radvd_configure'); $pconfig['php']['start'] = array('dhcpd_radvd_configure'); $pconfig['pidfile'] = '/var/run/radvd.pid'; $services[] = $pconfig; } if (dhcpd_dhcpv4_enabled()) { $pconfig = array(); $pconfig['name'] = 'dhcpd'; $pconfig['description'] = gettext("DHCPv4 Server"); $pconfig['php']['restart'] = array('dhcpd_dhcp4_configure'); $pconfig['php']['start'] = array('dhcpd_dhcp4_configure'); $pconfig['pidfile'] = '/var/dhcpd/var/run/dhcpd.pid'; $services[] = $pconfig; } if (dhcpd_dhcpv6_enabled()) { $pconfig = array(); $pconfig['name'] = 'dhcpd6'; $pconfig['description'] = gettext("DHCPv6 Server"); $pconfig['php']['restart'] = array('dhcpd_dhcp6_configure'); $pconfig['php']['start'] = array('dhcpd_dhcp6_configure'); $pconfig['pidfile'] = '/var/dhcpd/var/run/dhcpdv6.pid'; $services[] = $pconfig; } return $services; } function dhcpd_radvd_configure($verbose = false, $blacklist = []) { global $config; if (!dhcpd_radvd_enabled()) { killbypid('/var/run/radvd.pid'); return; } service_log('Starting router advertisement service...', $verbose); $ifconfig_details = legacy_interfaces_details(); $radvdconf = "# Automatically generated, do not edit\n"; /* Process all links which need the router advertise daemon */ $radvdifs = array(); /* handle manually configured DHCP6 server settings first */ foreach (config_read_array('dhcpdv6') as $dhcpv6if => $dhcpv6ifconf) { if (isset($config['interfaces'][$dhcpv6if]['track6-interface']) && !isset($config['interfaces'][$dhcpv6if]['dhcpd6track6allowoverride'])) { continue; } elseif (!isset($config['interfaces'][$dhcpv6if]['enable'])) { $radvdconf .= "# Skipping disabled interface {$dhcpv6if}\n"; continue; } elseif (isset($blacklist[$dhcpv6if])) { $radvdconf .= "# Skipping blacklisted interface {$dhcpv6if}\n"; continue; } elseif (!isset($dhcpv6ifconf['ramode']) || $dhcpv6ifconf['ramode'] == 'disabled') { $radvdconf .= "# Skipping unset interface {$dhcpv6if}\n"; continue; } $carp_mode = false; $src_addr = false; $ifcfgipv6 = get_interface_ipv6(!empty($dhcpv6ifconf['rainterface']) ? $dhcpv6ifconf['rainterface'] : $dhcpv6if); if (!is_ipaddrv6($ifcfgipv6) && !isset($config['interfaces'][$dhcpv6if]['dhcpd6track6allowoverride'])) { $radvdconf .= "# Skipping addressless interface {$dhcpv6if}\n"; continue; } $realif = get_real_interface($dhcpv6if, 'inet6'); $radvdifs[$realif] = 1; $mtu = legacy_interface_stats($realif)['mtu']; if (isset($config['interfaces'][$dhcpv6if]['track6-interface'])) { $realtrackif = get_real_interface($config['interfaces'][$dhcpv6if]['track6-interface'], 'inet6'); $trackmtu = legacy_interface_stats($realtrackif)['mtu']; if (!empty($trackmtu) && !empty($mtu)) { if ($trackmtu < $mtu) { $mtu = $trackmtu; } } } if (!empty($dhcpv6ifconf['AdvLinkMTU']) && !empty($mtu)) { if ($dhcpv6ifconf['AdvLinkMTU'] < $mtu) { $mtu = $dhcpv6ifconf['AdvLinkMTU']; } else { log_msg("Skipping AdvLinkMTU configuration since it cannot be applied on {$dhcpv6if}", LOG_WARNING); } } $radvdconf .= "# Generated RADVD config for manual assignment on {$dhcpv6if}\n"; $radvdconf .= "interface {$realif} {\n"; $radvdconf .= "\tAdvSendAdvert on;\n"; $radvdconf .= sprintf("\tMinRtrAdvInterval %s;\n", !empty($dhcpv6ifconf['ramininterval']) ? $dhcpv6ifconf['ramininterval'] : '200'); $radvdconf .= sprintf("\tMaxRtrAdvInterval %s;\n", !empty($dhcpv6ifconf['ramaxinterval']) ? $dhcpv6ifconf['ramaxinterval'] : '600'); if (!empty($dhcpv6ifconf['AdvDefaultLifetime'])) { $radvdconf .= sprintf("\tAdvDefaultLifetime %s;\n", $dhcpv6ifconf['AdvDefaultLifetime']); } $radvdconf .= sprintf("\tAdvLinkMTU %s;\n", !empty($mtu) ? $mtu : 0); switch ($dhcpv6ifconf['rapriority']) { case "low": $radvdconf .= "\tAdvDefaultPreference low;\n"; break; case "high": $radvdconf .= "\tAdvDefaultPreference high;\n"; break; default: $radvdconf .= "\tAdvDefaultPreference medium;\n"; break; } switch ($dhcpv6ifconf['ramode']) { case 'assist': case 'managed': $radvdconf .= "\tAdvManagedFlag on;\n"; $radvdconf .= "\tAdvOtherConfigFlag on;\n"; break; case 'stateless': $radvdconf .= "\tAdvManagedFlag off;\n"; $radvdconf .= "\tAdvOtherConfigFlag on;\n"; break; default: break; } if (!empty($dhcpv6ifconf['ranodefault'])) { $radvdconf .= "\tAdvDefaultLifetime 0;\n"; } $stanzas = []; list (, $networkv6) = interfaces_primary_address6($dhcpv6if, $ifconfig_details); if (is_subnetv6($networkv6)) { $stanzas[] = $networkv6; } foreach (config_read_array('virtualip', 'vip') as $vip) { if ($vip['interface'] != $dhcpv6if || !is_ipaddrv6($vip['subnet'])) { continue; } if (is_linklocal($vip['subnet'])) { if ($ifcfgipv6 == $vip['subnet']) { $carp_mode = !empty($vip['vhid']); $src_addr = true; } continue; } if ($vip['subnet_bits'] == '128' || !empty($vip['nobind'])) { continue; } /* force subnet to 64 as per radvd complaint "prefix length should be 64 for xzy" */ $subnetv6 = gen_subnetv6($vip['subnet'], 64); $stanzas[] = "{$subnetv6}/64"; } if ($src_addr) { /* inject configured link-local address into the RA message */ $radvdconf .= "\tAdvRASrcAddress {\n"; $radvdconf .= "\t\t{$ifcfgipv6};\n"; $radvdconf .= "\t};\n"; } if ($carp_mode) { /* to avoid wrong MAC being stuck during failover */ $radvdconf .= "\tAdvSourceLLAddress off;\n"; /* to avoid final advertisement with zero router lifetime */ $radvdconf .= "\tRemoveAdvOnExit off;\n"; } /* VIPs may duplicate readings from system */ $stanzas = array_unique($stanzas); foreach ($stanzas as $stanza) { $radvdconf .= "\tprefix {$stanza} {\n"; $radvdconf .= "\t\tDeprecatePrefix " . (!empty($dhcpv6ifconf['AdvDeprecatePrefix']) ? $dhcpv6ifconf['AdvDeprecatePrefix'] : ($carp_mode ? 'off' : 'on')) . ";\n"; switch ($dhcpv6ifconf['ramode']) { case 'assist': case 'stateless': case 'unmanaged': $radvdconf .= "\t\tAdvOnLink on;\n"; $radvdconf .= "\t\tAdvAutonomous on;\n"; break; case 'managed': $radvdconf .= "\t\tAdvOnLink on;\n"; $radvdconf .= "\t\tAdvAutonomous off;\n"; break; case 'router': $radvdconf .= "\t\tAdvOnLink off;\n"; $radvdconf .= "\t\tAdvAutonomous off;\n"; break; default: break; } if (!empty($dhcpv6ifconf['AdvValidLifetime'])) { $radvdconf .= sprintf("\t\tAdvValidLifetime %s;\n", $dhcpv6ifconf['AdvValidLifetime']); } if (!empty($dhcpv6ifconf['AdvPreferredLifetime'])) { $radvdconf .= sprintf("\t\tAdvPreferredLifetime %s;\n", $dhcpv6ifconf['AdvPreferredLifetime']); } $radvdconf .= "\t};\n"; } if (!empty($dhcpv6ifconf['raroutes'])) { foreach (explode(',', $dhcpv6ifconf['raroutes']) as $raroute) { $radvdconf .= "\troute {$raroute} {\n"; $radvdconf .= "\t\tRemoveRoute " . (!empty($dhcpv6ifconf['AdvRemoveRoute']) ? $dhcpv6ifconf['AdvRemoveRoute'] : ($carp_mode ? 'off' : 'on')) . ";\n"; if (!empty($dhcpv6ifconf['AdvRouteLifetime'])) { $radvdconf .= "\t\tAdvRouteLifetime {$dhcpv6ifconf['AdvRouteLifetime']};\n"; } $radvdconf .= "\t};\n"; } } $dnslist = []; $dnssl = null; /* advertise both DNS servers and domains via RA (RFC 8106) if allowed */ if (!isset($dhcpv6ifconf['radisablerdnss'])) { $dnslist_tmp = []; if (isset($dhcpv6ifconf['rasamednsasdhcp6']) && !empty($dhcpv6ifconf['dnsserver'][0])) { $dnslist_tmp = $dhcpv6ifconf['dnsserver']; } elseif (!isset($dhcpv6ifconf['rasamednsasdhcp6']) && !empty($dhcpv6ifconf['radnsserver'][0])) { $dnslist_tmp = $dhcpv6ifconf['radnsserver']; } elseif (!empty(service_by_filter(['dns_ports' => '53']))) { if (is_ipaddrv6($ifcfgipv6)) { $dnslist_tmp[] = $ifcfgipv6; } else { log_msg("dhcpd_radvd_configure(manual) found no suitable IPv6 address on {$dhcpv6if}({$realif})", LOG_WARNING); } } elseif (!empty($config['system']['dnsserver'][0])) { $dnslist_tmp = $config['system']['dnsserver']; } foreach ($dnslist_tmp as $server) { if (!is_ipaddrv6($server)) { continue; } if (count($dnslist) >= 3) { log_msg("The radvd RDNSS entry $server cannot be added due to too many addresses.", LOG_WARNING); continue; } $dnslist[] = $server; } if (isset($dhcpv6ifconf['rasamednsasdhcp6']) && !empty($dhcpv6ifconf['domainsearchlist'])) { $dnssl = implode(' ', explode(';', $dhcpv6ifconf['domainsearchlist'])); } elseif (!isset($dhcpv6ifconf['rasamednsasdhcp6']) && !empty($dhcpv6ifconf['radomainsearchlist'])) { $dnssl = implode(' ', explode(';', $dhcpv6ifconf['radomainsearchlist'])); } elseif (!empty($config['system']['domain'])) { $dnssl = $config['system']['domain']; } } if (count($dnslist) > 0) { $radvdconf .= "\tRDNSS " . implode(" ", $dnslist) . " {\n"; if (!empty($dhcpv6ifconf['AdvRDNSSLifetime'])) { $radvdconf .= "\t\tAdvRDNSSLifetime {$dhcpv6ifconf['AdvRDNSSLifetime']};\n"; } $radvdconf .= "\t};\n"; } if (!empty($dnssl)) { $radvdconf .= "\tDNSSL {$dnssl} {\n"; if (!empty($dhcpv6ifconf['AdvDNSSLLifetime'])) { $radvdconf .= "\t\tAdvDNSSLLifetime {$dhcpv6ifconf['AdvDNSSLLifetime']};\n"; } $radvdconf .= "\t};\n"; } $radvdconf .= "};\n"; } /* handle DHCP-PD prefixes and 6RD dynamic interfaces */ foreach (array_keys(get_configured_interface_with_descr()) as $if) { if (!isset($config['interfaces'][$if]['track6-interface'])) { continue; } elseif (empty($config['interfaces'][$config['interfaces'][$if]['track6-interface']])) { $radvdconf .= "# Skipping defunct interface {$if}\n"; continue; } elseif (!isset($config['interfaces'][$if]['enable'])) { $radvdconf .= "# Skipping disabled interface {$if}\n"; continue; } elseif (isset($blacklist[$if])) { $radvdconf .= "# Skipping blacklisted interface {$if}\n"; continue; } $trackif = $config['interfaces'][$if]['track6-interface']; $realif = get_real_interface($if, 'inet6'); /* prevent duplicate entries, manual overrides */ if (isset($radvdifs[$realif])) { continue; } $radvdifs[$realif] = 1; $autotype = 'unknown'; if (isset($config['interfaces'][$trackif]['ipaddrv6'])) { $autotype = $config['interfaces'][$trackif]['ipaddrv6']; } $realtrackif = get_real_interface($trackif, 'inet6'); $mtu = legacy_interface_stats($realif)['mtu']; $trackmtu = legacy_interface_stats($realtrackif)['mtu']; if (!empty($trackmtu) && !empty($mtu)) { if ($trackmtu < $mtu) { $mtu = $trackmtu; } } $dnslist = []; list ($ifcfgipv6, $networkv6) = interfaces_primary_address6($if, $ifconfig_details); if ($autotype == 'slaac') { /* XXX this may be incorrect and needs an override or revisit */ $networkv6 = '2000::/64'; } if (!empty(service_by_filter(['dns_ports' => '53']))) { if (is_ipaddrv6($ifcfgipv6)) { $dnslist[] = $ifcfgipv6; } else { log_msg("dhcpd_radvd_configure(auto) found no suitable IPv6 address on {$if}({$realif})", LOG_WARNING); } } elseif (!empty($config['system']['dnsserver'])) { foreach ($config['system']['dnsserver'] as $server) { if (!is_ipaddrv6($server)) { continue; } if (count($dnslist) >= 3) { log_msg("The radvd RDNSS entry $server cannot be added due to too many addresses.", LOG_WARNING); continue; } $dnslist[] = $server; } } $radvdconf .= "# Generated RADVD config for {$autotype} assignment from {$trackif} on {$if}\n"; $radvdconf .= "interface {$realif} {\n"; $radvdconf .= "\tAdvSendAdvert on;\n"; $radvdconf .= sprintf("\tAdvLinkMTU %s;\n", !empty($mtu) ? $mtu : 0); $radvdconf .= "\tAdvManagedFlag on;\n"; $radvdconf .= "\tAdvOtherConfigFlag on;\n"; if (!empty($networkv6)) { $radvdconf .= "\tprefix {$networkv6} {\n"; if ($autotype == 'slaac') { /* XXX also of interest in the future, see hardcoded prefix above */ $radvdconf .= "\t\tBase6Interface $realtrackif;\n"; } $radvdconf .= "\t\tDeprecatePrefix on;\n"; $radvdconf .= "\t\tAdvOnLink on;\n"; $radvdconf .= "\t\tAdvAutonomous on;\n"; $radvdconf .= "\t};\n"; } foreach (config_read_array('virtualip', 'vip') as $vip) { if ($vip['interface'] != $if || !is_ipaddrv6($vip['subnet']) || $vip['subnet_bits'] == '128') { continue; } if (is_linklocal($vip['subnet']) || !empty($vip['nobind'])) { continue; } /* force subnet to 64 as per radvd complaint "prefix length should be 64 for xzy" */ $subnetv6 = gen_subnetv6($vip['subnet'], 64); $vipnetv6 = "{$subnetv6}/64"; if ($vipnetv6 == $networkv6) { continue; } $radvdconf .= "\tprefix {$vipnetv6} {\n"; $radvdconf .= "\t\tDeprecatePrefix on;\n"; $radvdconf .= "\t\tAdvOnLink on;\n"; $radvdconf .= "\t\tAdvAutonomous on;\n"; $radvdconf .= "\t};\n"; } if (count($dnslist) > 0) { $radvdconf .= "\tRDNSS " . implode(" ", $dnslist) . " { };\n"; } if (!empty($config['system']['domain'])) { $radvdconf .= "\tDNSSL {$config['system']['domain']} { };\n"; } $radvdconf .= "};\n"; } $radvd_conf_file = '/var/etc/radvd.conf'; file_put_contents($radvd_conf_file, $radvdconf); if (count($radvdifs)) { $last_version = @file_get_contents("{$radvd_conf_file}.last"); $this_version = shell_safe('/bin/cat %s | sha256', $radvd_conf_file); if (isvalidpid('/var/run/radvd.pid') && $last_version == $this_version) { killbypid('/var/run/radvd.pid', 'HUP'); } else { killbypid('/var/run/radvd.pid'); file_put_contents("{$radvd_conf_file}.last", $this_version); mwexec('/usr/local/sbin/radvd -p /var/run/radvd.pid -C /var/etc/radvd.conf -m syslog'); } } else { /* stop on invalid configuration for legacy condition above */ killbypid('/var/run/radvd.pid'); @unlink("{$radvd_conf_file}.last"); } service_log("done.\n", $verbose); } function dhcpd_dhcp_configure($verbose = false, $family = null, $blacklist = []) { $dirs = ['/dev', '/etc', '/lib', '/run', '/usr', '/usr/local/sbin', '/var/db', '/var/run']; foreach ($dirs as $dir) { mwexecf('/bin/mkdir -p %s', "/var/dhcpd{$dir}"); } if (mwexecf('/sbin/mount -uw %s', '/var/dhcpd/dev', true)) { mwexecf('/sbin/mount -t devfs devfs %s', '/var/dhcpd/dev'); } mwexecf('/usr/sbin/chown -R dhcpd:dhcpd %s', '/var/dhcpd'); if ($family == null || $family == 'inet') { dhcpd_dhcp4_configure($verbose); } if ($family == null || $family == 'inet6') { dhcpd_dhcp6_configure($verbose, $blacklist); dhcpd_radvd_configure($verbose, $blacklist); } } function dhcpd_dhcp4_configure($verbose = false) { global $config; $need_ddns_updates = false; $ddns_zones = []; if (!dhcpd_dhcpv4_enabled()) { /* * XXX If there is no enabled server interfaces_staticarp_configure() * is not executed as documented. This likely does not matter, but * since it came up as odd in testing make a note here for the future. */ killbypid('/var/dhcpd/var/run/dhcpd.pid'); return; } service_log('Starting DHCPv4 service...', $verbose); killbypid('/var/dhcpd/var/run/dhcpd.pid'); /* Only consider DNS servers with IPv4 addresses for the IPv4 DHCP server. */ $dns_arrv4 = []; if (!empty($config['system']['dnsserver'][0])) { foreach ($config['system']['dnsserver'] as $dnsserver) { if (is_ipaddrv4($dnsserver)) { $dns_arrv4[] = $dnsserver; } } } $custoptions = ""; $ifconfig_details = legacy_interfaces_details(); foreach ($config['dhcpd'] as $dhcpif => $dhcpifconf) { if (isset($dhcpifconf['numberoptions']['item'])) { foreach ($dhcpifconf['numberoptions']['item'] as $itemidx => $item) { if (!empty($item['type'])) { $itemtype = $item['type']; } else { $itemtype = "text"; } $custoptions .= "option custom-{$dhcpif}-{$itemidx} code {$item['number']} = {$itemtype};\n"; } } } $dhcpdconf = <<<EOD option domain-name "{$config['system']['domain']}"; option ldap-server code 95 = text; option arch code 93 = unsigned integer 16; # RFC4578 option pac-webui code 252 = text; {$custoptions} default-lease-time 7200; max-lease-time 86400; log-facility local7; one-lease-per-client true; deny duplicates; ping-check true; update-conflict-detection false; authoritative; EOD; $omapi_added = false; $dhcpdifs = []; /* * loop through and determine if we need * to setup failover peer "bleh" entries */ foreach ($config['dhcpd'] as $dhcpif => $dhcpifconf) { /* certainly odd but documented and side effect populates ARP table */ interfaces_staticarp_configure($dhcpif, $ifconfig_details); if (!isset($dhcpifconf['enable'])) { continue; } if (!empty($dhcpifconf['failover_peerip'])) { $intip = get_interface_ip($dhcpif, $ifconfig_details); $failover_primary = false; if (!empty($config['virtualip']['vip'])) { foreach ($config['virtualip']['vip'] as $vipent) { if ($vipent['interface'] == $dhcpif) { $carp_nw = gen_subnet($vipent['subnet'], $vipent['subnet_bits']); if (ip_in_subnet($dhcpifconf['failover_peerip'], "{$carp_nw}/{$vipent['subnet_bits']}")) { /* this is the interface! */ if (is_numeric($vipent['advskew']) && (intval($vipent['advskew']) < 20)) { $failover_primary = true; } break; } } } } else { log_msg("DHCP failover set up on {$dhcpif} but no CARP virtual IPs defined!", LOG_WARNING); } $dhcpdconf_pri = ""; if ($failover_primary) { $my_port = "519"; $peer_port = "520"; $type = "primary"; $dhcpdconf_pri = "split 128;\n"; if (isset($dhcpifconf['failover_split'])) { $dhcpdconf_pri = "split {$dhcpifconf['failover_split']};\n"; } $dhcpdconf_pri .= " mclt 600;\n"; } else { $type = "secondary"; $my_port = "520"; $peer_port = "519"; } if (is_ipaddrv4($intip)) { $dhcpdconf .= <<<EOPP failover peer "dhcp_{$dhcpif}" { {$type}; address {$intip}; port {$my_port}; peer address {$dhcpifconf['failover_peerip']}; peer port {$peer_port}; max-response-delay 10; max-unacked-updates 10; {$dhcpdconf_pri} load balance max seconds 3; } \n EOPP; } } } $iflist = get_configured_interface_with_descr(); foreach ($config['dhcpd'] as $dhcpif => $dhcpifconf) { if (!isset($dhcpifconf['enable']) || !isset($iflist[$dhcpif])) { continue; } list ($ifcfgip, $ifcfgnet, $ifcfgsn) = interfaces_primary_address($dhcpif, $ifconfig_details); if (!is_ipaddrv4($ifcfgip) || !is_subnetv4($ifcfgnet)) { $realif = get_real_interface($dhcpif); log_msg("dhcpd_dhcp4_configure() found no suitable IPv4 address on {$dhcpif}({$realif})", LOG_WARNING); continue; } $subnetmask = gen_subnet_mask($ifcfgsn); $subnet = explode('/', $ifcfgnet)[0]; $all_pools = []; $all_pools[] = $dhcpifconf; if (!empty($dhcpifconf['pool'])) { $all_pools = array_merge($all_pools, $dhcpifconf['pool']); } $dnscfg = ""; if (!empty($dhcpifconf['domain'])) { $dnscfg .= " option domain-name \"{$dhcpifconf['domain']}\";\n"; } if (!empty($dhcpifconf['domainsearchlist'])) { $dnscfg .= " option domain-search \"" . join("\",\"", preg_split("/[ ;]+/", $dhcpifconf['domainsearchlist'])) . "\";\n"; } $newzone = []; if (isset($dhcpifconf['ddnsupdate'])) { $need_ddns_updates = true; if (!empty($dhcpifconf['ddnsdomain'])) { $newzone['domain-name'] = $dhcpifconf['ddnsdomain']; $dnscfg .= " ddns-domainname \"{$dhcpifconf['ddnsdomain']}\";\n"; } else { $newzone['domain-name'] = $config['system']['domain']; } $revsubnet = array_reverse(explode(".", $subnet)); $subnetmask_rev = array_reverse(explode('.', $subnetmask)); foreach ($subnetmask_rev as $octet) { if ($octet == "0") { array_shift($revsubnet); } } $newzone['ptr-domain'] = implode(".", $revsubnet) . ".in-addr.arpa"; } if (!empty($dhcpifconf['dnsserver'][0])) { $dnscfg .= " option domain-name-servers " . join(",", $dhcpifconf['dnsserver']) . ";"; if (!empty($newzone['domain-name'])) { $newzone['dns-servers'] = $dhcpifconf['dnsserver']; } } elseif (!empty(service_by_filter(['dns_ports' => '53']))) { $dnscfg .= " option domain-name-servers {$ifcfgip};"; if (!empty($newzone['domain-name'])) { $newzone['dns-servers'] = [$ifcfgip]; } } elseif (!empty($dns_arrv4)) { $dnscfg .= " option domain-name-servers " . join(",", $dns_arrv4) . ";"; if (!empty($newzone['domain-name'])) { $newzone['dns-servers'] = $dns_arrv4; } } /* * Create classes - These all contain comma-separated lists. * Join them into one big comma-separated string then split * them all up. */ $all_mac_strings = []; foreach ($all_pools as $poolconf) { if (!empty($poolconf['mac_allow'])) { $all_mac_strings[] = $poolconf['mac_allow']; } if (!empty($poolconf['mac_deny'])) { $all_mac_strings[] = $poolconf['mac_deny']; } } $all_mac_list = array_unique(explode(',', implode(',', $all_mac_strings))); foreach ($all_mac_list as $mac) { if (!empty($mac)) { $dhcpdconf .= 'class "' . str_replace(':', '', $mac) . '" {' . "\n"; // Skip the first octet of the MAC address - for media type, typically Ethernet ("01") and match the rest. $dhcpdconf .= ' match if substring (hardware, 1, ' . (substr_count($mac, ':') + 1) . ') = ' . $mac . ';' . "\n"; $dhcpdconf .= '}' . "\n"; } } $dhcpdconf .= "\nsubnet {$subnet} netmask {$subnetmask} {\n"; // Setup pool options foreach ($all_pools as $poolconf) { $dhcpdconf .= " pool {\n"; if (!empty($poolconf['dnsserver'][0])) { $dhcpdconf .= ' option domain-name-servers ' . join(',', $poolconf['dnsserver']) . ";\n"; } /* allow/deny MACs */ if (!empty($poolconf['mac_allow'])) { $mac_allow_list = array_unique(explode(',', $poolconf['mac_allow'])); foreach ($mac_allow_list as $mac) { if (!empty($mac)) { $dhcpdconf .= " allow members of \"" . str_replace(':', '', $mac) . "\";\n"; } } } if (!empty($poolconf['mac_deny'])) { $mac_deny_list = array_unique(explode(',', $poolconf['mac_deny'])); foreach ($mac_deny_list as $mac) { if (!empty($mac)) { $dhcpdconf .= " deny members of \"" . str_replace(':', '', $mac) . "\";\n"; } } } if (!empty($poolconf['failover_peerip'])) { $dhcpdconf .= " deny dynamic bootp clients;\n"; } if (isset($poolconf['denyunknown'])) { $dhcpdconf .= " deny unknown-clients;\n"; } if (isset($poolconf['ignoreuids'])) { $dhcpdconf .= " ignore-client-uids true;\n"; } if ( !empty($poolconf['gateway']) && $poolconf['gateway'] != "none" && (empty($dhcpifconf['gateway']) || $poolconf['gateway'] != $dhcpifconf['gateway']) ) { $dhcpdconf .= " option routers {$poolconf['gateway']};\n"; } if (!empty($dhcpifconf['failover_peerip'])) { $dhcpdconf .= " failover peer \"dhcp_{$dhcpif}\";\n"; } if ( !empty($poolconf['domain']) && (empty($dhcpifconf['domain']) || $poolconf['domain'] != $dhcpifconf['domain']) ) { $dhcpdconf .= " option domain-name \"{$poolconf['domain']}\";\n"; } if ( !empty($poolconf['domainsearchlist']) && (empty($dhcpifconf['domainsearchlist']) || $poolconf['domainsearchlist'] != $dhcpifconf['domainsearchlist']) ) { $dhcpdconf .= " option domain-search \"" . join("\",\"", preg_split("/[ ;]+/", $poolconf['domainsearchlist'])) . "\";\n"; } if (isset($poolconf['ddnsupdate'])) { if ( !empty($poolconf['ddnsdomain']) && (empty($dhcpifconf['ddnsdomain']) || $poolconf['ddnsdomain'] != $dhcpifconf['ddnsdomain']) ) { $dhcpdconf .= " ddns-domainname \"{$poolconf['ddnsdomain']}\";\n"; $newddnszone = []; $newddnszone['domain-name'] = $poolconf['ddnsdomain']; $newddnszone['dns-servers'] = array($poolconf['ddnsdomainprimary']); $newddnszone['ddnsdomainkeyname'] = $poolconf['ddnsdomainkeyname'] ?? ''; $newddnszone['ddnsdomainkey'] = $poolconf['ddnsdomainkey'] ?? ''; $newddnszone['ddnsdomainalgorithm'] = !empty($poolconf['ddnsdomainalgorithm']) ? $poolconf['ddnsdomainalgorithm'] : "hmac-md5"; $ddns_zones[] = $newddnszone; } } // default-lease-time if ( !empty($poolconf['defaultleasetime']) && (empty($dhcpifconf['defaultleasetime']) || $poolconf['defaultleasetime'] != $dhcpifconf['defaultleasetime']) ) { $dhcpdconf .= " default-lease-time {$poolconf['defaultleasetime']};\n"; } // max-lease-time if ( !empty($poolconf['maxleasetime']) && (empty($dhcpifconf['maxleasetime']) || $poolconf['maxleasetime'] != $dhcpifconf['maxleasetime']) ) { $dhcpdconf .= " max-lease-time {$poolconf['maxleasetime']};\n"; } // interface MTU if ( !empty($poolconf['interface_mtu']) && (empty($dhcpifconf['interface_mtu']) || $poolconf['interface_mtu'] != $dhcpifconf['interface_mtu']) ) { $dhcpdconf .= " option interface-mtu {$poolconf['interface_mtu']};\n"; } // netbios-name* if ( !empty($poolconf['winsserver'][0]) && (empty($dhcpifconf['winsserver'][0]) || $poolconf['winsserver'][0] != $dhcpifconf['winsserver'][0]) ) { $dhcpdconf .= " option netbios-name-servers " . join(",", $poolconf['winsserver']) . ";\n"; $dhcpdconf .= " option netbios-node-type 8;\n"; } // ntp-servers if ( !empty($poolconf['ntpserver'][0]) && (empty($dhcpifconf['ntpserver'][0]) || $poolconf['ntpserver'][0] != $dhcpifconf['ntpserver'][0]) ) { $dhcpdconf .= " option ntp-servers " . join(",", $poolconf['ntpserver']) . ";\n"; } // tftp-server-name if (!empty($poolconf['tftp']) && (empty($dhcpifconf['tftp']) || $poolconf['tftp'] != $dhcpifconf['tftp'])) { $dhcpdconf .= " option tftp-server-name \"{$poolconf['tftp']}\";\n"; // bootfile-name if (!empty($poolconf['bootfilename']) && (empty($dhcpifconf['bootfilename']) || $poolconf['bootfilename'] != $dhcpifconf['bootfilename'])) { $dhcpdconf .= " option bootfile-name \"{$poolconf['bootfilename']}\";\n"; } } // ldap-server if (!empty($poolconf['ldap']) && (empty($dhcpifconf['ldap']) || $poolconf['ldap'] != $dhcpifconf['ldap'])) { $dhcpdconf .= " option ldap-server \"{$poolconf['ldap']}\";\n"; } // net boot information if (isset($poolconf['netboot'])) { if (!empty($poolconf['nextserver']) && (empty($dhcpifconf['nextserver']) || $poolconf['nextserver'] != $dhcpifconf['nextserver'])) { $dhcpdconf .= " next-server {$poolconf['nextserver']};\n"; } if (!empty($poolconf['filename']) && (empty($dhcpifconf['filename']) || $poolconf['filename'] != $dhcpifconf['filename'])) { $dhcpdconf .= " filename \"{$poolconf['filename']}\";\n"; } if (!empty($poolconf['rootpath']) && (empty($dhcpifconf['rootpath']) || $poolconf['rootpath'] != $dhcpifconf['rootpath'])) { $dhcpdconf .= " option root-path \"{$poolconf['rootpath']}\";\n"; } } $dhcpdconf .= " range {$poolconf['range']['from']} {$poolconf['range']['to']};\n"; $dhcpdconf .= " }\n\n"; } // End of settings inside pools if (!empty($dhcpifconf['gateway'])) { $routers = $dhcpifconf['gateway'] != "none" ? $dhcpifconf['gateway'] : null; } else { // by default, add interface address in "option routers" $routers = $ifcfgip; } if (!empty($routers)) { $dhcpdconf .= " option routers {$routers};\n"; } $dhcpdconf .= <<<EOD $dnscfg EOD; // default-lease-time if (!empty($dhcpifconf['defaultleasetime'])) { $dhcpdconf .= " default-lease-time {$dhcpifconf['defaultleasetime']};\n"; } // max-lease-time if (!empty($dhcpifconf['maxleasetime'])) { $dhcpdconf .= " max-lease-time {$dhcpifconf['maxleasetime']};\n"; } // min-secs if (!empty($dhcpifconf['minsecs'])) { $dhcpdconf .= " min-secs {$dhcpifconf['minsecs']};\n"; } // interface MTU if (!empty($dhcpifconf['interface_mtu'])) { $dhcpdconf .= " option interface-mtu {$dhcpifconf['interface_mtu']};\n"; } // netbios-name* if (!empty($dhcpifconf['winsserver'][0])) { $dhcpdconf .= " option netbios-name-servers " . join(",", $dhcpifconf['winsserver']) . ";\n"; $dhcpdconf .= " option netbios-node-type 8;\n"; } // ntp-servers if (!empty($dhcpifconf['ntpserver'][0])) { $dhcpdconf .= " option ntp-servers " . join(",", $dhcpifconf['ntpserver']) . ";\n"; } // tftp-server-name if (!empty($dhcpifconf['tftp'])) { $dhcpdconf .= " option tftp-server-name \"{$dhcpifconf['tftp']}\";\n"; // bootfile-name if (!empty($dhcpifconf['bootfilename'])) { $dhcpdconf .= " option bootfile-name \"{$dhcpifconf['bootfilename']}\";\n"; } } // add pac url if it applies if (isset($dhcpifconf['wpad']) && !empty($config['system']['hostname']) && !empty($config['system']['domain'])) { $protocol = !empty($config['system']['webgui']['protocol']) ? $config['system']['webgui']['protocol'] : 'https'; // take hostname from system settings - it can be used to be resolved to anything based on client IP $host = implode('.', array('wpad', $config['system']['domain'])); $default_port = (isset($config['system']['webgui']['protocol']) && $config['system']['webgui']['protocol'] == 'https') ? 443 : 80; $port = !empty($config['system']['webgui']['port']) ? $config['system']['webgui']['port'] : $default_port; $webui_url = "$protocol://$host:$port/wpad.dat"; $dhcpdconf .= " option pac-webui \"$webui_url\";\n"; } // Handle option, number rowhelper values if (isset($dhcpifconf['numberoptions']['item'])) { $dhcpdconf .= "\n"; foreach ($dhcpifconf['numberoptions']['item'] as $itemidx => $item) { if (empty($item['type']) || $item['type'] == "text") { $dhcpdconf .= " option custom-{$dhcpif}-{$itemidx} \"{$item['value']}\";\n"; } else { $dhcpdconf .= " option custom-{$dhcpif}-{$itemidx} {$item['value']};\n"; } } } // ldap-server if (!empty($dhcpifconf['ldap'])) { $dhcpdconf .= " option ldap-server \"{$dhcpifconf['ldap']}\";\n"; } // net boot information if (isset($dhcpifconf['netboot'])) { if (!empty($dhcpifconf['nextserver'])) { $dhcpdconf .= " next-server {$dhcpifconf['nextserver']};\n"; } $conditional = false; $filemap = [ '00:06' => 'filename32', '00:07' => 'filename64', '00:09' => 'filename64', '00:0a' => 'filename32arm', '00:0b' => 'filename64arm', ]; if (!empty($dhcpifconf['filenameipxe'])) { $dhcpdconf .= " if exists user-class and option user-class = \"iPXE\" {\n"; $dhcpdconf .= " filename \"{$dhcpifconf['filenameipxe']}\";\n"; $dhcpdconf .= " }"; $conditional = true; } foreach ($filemap as $arch => $file) { if (empty($dhcpifconf[$file])) { continue; } $dhcpdconf .= $conditional ? ' else ' : ' '; $dhcpdconf .= "if option arch = {$arch} {\n"; $dhcpdconf .= " filename \"{$dhcpifconf[$file]}\";\n"; $dhcpdconf .= " }"; $conditional = true; } if ($conditional) { if (!empty($dhcpifconf['filename'])) { $dhcpdconf .= " else {\n"; $dhcpdconf .= " filename \"{$dhcpifconf['filename']}\";\n"; $dhcpdconf .= " }"; } $dhcpdconf .= "\n"; } elseif (!empty($dhcpifconf['filename'])) { $dhcpdconf .= " filename \"{$dhcpifconf['filename']}\";\n"; } if (!empty($dhcpifconf['rootpath'])) { $dhcpdconf .= " option root-path \"{$dhcpifconf['rootpath']}\";\n"; } } $dhcpdconf .= <<<EOD } EOD; /* add static mappings */ if (!empty($dhcpifconf['staticmap'])) { foreach ($dhcpifconf['staticmap'] as $i => $sm) { $dhcpdconf .= "\nhost s_{$dhcpif}_{$i} {\n"; if (!empty($sm['mac'])) { $dhcpdconf .= " hardware ethernet {$sm['mac']};\n"; } if (!empty($sm['cid'])) { $dhcpdconf .= " option dhcp-client-identifier \"{$sm['cid']}\";\n"; } if (!empty($sm['ipaddr'])) { $dhcpdconf .= " fixed-address {$sm['ipaddr']};\n"; } if (!empty($sm['hostname'])) { $dhhostname = str_replace(" ", "_", $sm['hostname']); $dhhostname = str_replace(".", "_", $dhhostname); $dhcpdconf .= " option host-name \"{$dhhostname}\";\n"; if ($need_ddns_updates) { $dhcpdconf .= " ddns-hostname \"{$dhhostname}\";\n"; } $dhcpdconf .= " set hostname-override = config-option host-name;\n"; } if (!empty($sm['filename'])) { $dhcpdconf .= " filename \"{$sm['filename']}\";\n"; } if (!empty($sm['rootpath'])) { $dhcpdconf .= " option root-path \"{$sm['rootpath']}\";\n"; } if (!empty($sm['gateway']) && $sm['gateway'] != "none" && (empty($dhcpifconf['gateway']) || $sm['gateway'] != $dhcpifconf['gateway'])) { $dhcpdconf .= " option routers {$sm['gateway']};\n"; } $smdnscfg = ""; if (!empty($sm['domain']) && ($sm['domain'] != $dhcpifconf['domain'])) { $smdnscfg .= " option domain-name \"{$sm['domain']}\";\n"; } if (!empty($sm['domainsearchlist']) && ($sm['domainsearchlist'] != $dhcpifconf['domainsearchlist'])) { $smdnscfg .= " option domain-search \"" . join("\",\"", preg_split("/[ ;]+/", $sm['domainsearchlist'])) . "\";\n"; } if ($need_ddns_updates) { if (!empty($sm['ddnsdomain']) && ($sm['ddnsdomain'] != $dhcpifconf['ddnsdomain'])) { $smdnscfg .= " ddns-domainname \"{$sm['ddnsdomain']}\";\n"; } } if (!empty($sm['dnsserver']) && ($sm['dnsserver'][0]) && ($sm['dnsserver'][0] != $dhcpifconf['dnsserver'][0])) { $smdnscfg .= " option domain-name-servers " . join(",", $sm['dnsserver']) . ";\n"; } $dhcpdconf .= "{$smdnscfg}"; // default-lease-time if (!empty($sm['defaultleasetime']) && ($sm['defaultleasetime'] != $dhcpifconf['defaultleasetime'])) { $dhcpdconf .= " default-lease-time {$sm['defaultleasetime']};\n"; } // max-lease-time if (!empty($sm['maxleasetime']) && ($sm['maxleasetime'] != $dhcpifconf['maxleasetime'])) { $dhcpdconf .= " max-lease-time {$sm['maxleasetime']};\n"; } // netbios-name* if (!empty($sm['winsserver']) && $sm['winsserver'][0] && ($sm['winsserver'][0] != $dhcpifconf['winsserver'][0])) { $dhcpdconf .= " option netbios-name-servers " . join(",", $sm['winsserver']) . ";\n"; $dhcpdconf .= " option netbios-node-type 8;\n"; } // ntp-servers if (!empty($sm['ntpserver']) && $sm['ntpserver'][0] && ($sm['ntpserver'][0] != $dhcpifconf['ntpserver'][0])) { $dhcpdconf .= " option ntp-servers " . join(",", $sm['ntpserver']) . ";\n"; } // tftp-server-name if (!empty($sm['tftp']) && ($sm['tftp'] != $dhcpifconf['tftp'])) { $dhcpdconf .= " option tftp-server-name \"{$sm['tftp']}\";\n"; // bootfile-name if (!empty($sm['bootfilename']) && ($sm['bootfilename'] != $dhcpifconf['bootfilename'])) { $dhcpdconf .= " option bootfile-name \"{$sm['bootfilename']}\";\n"; } } $dhcpdconf .= "}\n"; } } $dhcpdifs[] = get_real_interface($dhcpif); if (!empty($newzone['domain-name']) && isset($dhcpifconf['ddnsupdate']) && is_ipaddrv4($dhcpifconf['ddnsdomainprimary'])) { $newzone['dns-servers'] = array($dhcpifconf['ddnsdomainprimary']); $newzone['ddnsdomainkeyname'] = $dhcpifconf['ddnsdomainkeyname'] ?? ''; $newzone['ddnsdomainkey'] = $dhcpifconf['ddnsdomainkey'] ?? ''; $newzone['ddnsdomainalgorithm'] = !empty($dhcpifconf['ddnsdomainalgorithm']) ? $dhcpifconf['ddnsdomainalgorithm'] : "hmac-md5"; $ddns_zones[] = $newzone; } if (isset($dhcpifconf['omapi']) && !$omapi_added) { $dhcpdconf .= "\nomapi-port {$dhcpifconf['omapiport']};\n"; if (isset($dhcpifconf['omapialgorithm']) && isset($dhcpifconf['omapikey'])) { $dhcpdconf .= "key omapi_key {\n"; $dhcpdconf .= " algorithm {$dhcpifconf['omapialgorithm']};\n"; $dhcpdconf .= " secret \"{$dhcpifconf['omapikey']}\";\n"; $dhcpdconf .= "};\nomapi-key omapi_key;\n\n"; /* make sure we only add this OMAPI block once */ $omapi_added = true; } } } if ($need_ddns_updates) { $dhcpdconf .= "\nddns-update-style interim;\n"; $dhcpdconf .= "update-static-leases on;\n"; $dhcpdconf .= dhcpd_zones($ddns_zones); } foreach (glob('/usr/local/etc/dhcpd.opnsense.d/*.conf') as $file) { $dhcpdconf .= "\n\n# including custom file {$file}\n" . file_get_contents($file); } @file_put_contents('/var/dhcpd/etc/dhcpd.conf', $dhcpdconf); @touch('/var/dhcpd/var/db/dhcpd.leases'); if (count($dhcpdifs) > 0) { mwexec('/usr/local/opnsense/scripts/dhcp/cleanup_leases4.php -m'); mwexec('/usr/local/sbin/dhcpd -user dhcpd -group dhcpd -chroot /var/dhcpd -cf /etc/dhcpd.conf -pf /var/run/dhcpd.pid ' . join(' ', $dhcpdifs)); } service_log("done.\n", $verbose); } function dhcpd_zones($ddns_zones, $ipproto = 'inet') { $dhcpdconf = ''; if (is_array($ddns_zones)) { $added_zones = []; $added_keys = []; foreach ($ddns_zones as $zone) { $versionsuffix = $ipproto == "inet6" ? "6" : ""; // We don't need to add zones multiple times. foreach ([$zone['domain-name'], $zone['ptr-domain']] as $domain) { if (!empty($domain) && !in_array($domain, $added_zones)) { /* dhcpdconf2 is injected *after* the key */ $dhcpdconf2 = "zone {$domain}. {\n"; // XXX: $zone['dns-servers'] only contains one item, ref $newzone['dns-servers'] $dhcpdconf2 .= " primary{$versionsuffix} {$zone['dns-servers'][0]};\n"; if (!empty($zone['ddnsdomainkeyname']) && !empty($zone['ddnsdomainkey'])) { if (!in_array($zone['ddnsdomainkeyname'], $added_keys)) { $dhcpdconf .= "\nkey {$zone['ddnsdomainkeyname']} {\n"; $dhcpdconf .= " algorithm {$zone['ddnsdomainalgorithm']};\n"; $dhcpdconf .= " secret {$zone['ddnsdomainkey']};\n"; $dhcpdconf .= "}\n"; $added_keys[] = $zone['ddnsdomainkeyname']; } $dhcpdconf2 .= " key {$zone['ddnsdomainkeyname']};\n"; } $dhcpdconf2 .= "}\n"; $dhcpdconf .= $dhcpdconf2; $added_zones[] = $domain; } } } } return $dhcpdconf; } function dhcpd_dhcp6_configure($verbose = false, $blacklist = []) { global $config; if (!dhcpd_dhcpv6_enabled()) { killbypid('/var/dhcpd/var/run/dhcpdv6.pid'); killbypid('/var/run/dhcpleases6.pid'); return; } service_log('Starting DHCPv6 service...', $verbose); killbypid('/var/dhcpd/var/run/dhcpdv6.pid'); killbypid('/var/run/dhcpleases6.pid'); $iflist = get_configured_interface_with_descr(); $ifconfig_details = legacy_interfaces_details(); $dhcpdv6cfg = config_read_array('dhcpdv6'); /* Only consider DNS servers with IPv6 addresses for the IPv6 DHCP server. */ $dns_arrv6 = array(); if (!empty($config['system']['dnsserver'][0])) { foreach ($config['system']['dnsserver'] as $dnsserver) { if (is_ipaddrv6($dnsserver)) { $dns_arrv6[] = $dnsserver; } } } /* we add a fake entry for interfaces that are set to track6 another WAN */ foreach (array_keys($iflist) as $ifname) { /* Do not put in the config an interface which is down */ if (isset($blacklist[$ifname])) { continue; } if (!empty($config['interfaces'][$ifname]['track6-interface'])) { list ($ifcfgipv6) = interfaces_primary_address6($ifname, $ifconfig_details); if (!is_ipaddrv6($ifcfgipv6)) { continue; } $ifcfgipv6 = Net_IPv6::getNetmask($ifcfgipv6, 64); $ifcfgipv6arr = explode(':', $ifcfgipv6); if (!isset($config['interfaces'][$ifname]['dhcpd6track6allowoverride'])) { /* mock a real server */ $dhcpdv6cfg[$ifname] = array(); $dhcpdv6cfg[$ifname]['enable'] = true; /* fixed range */ $ifcfgipv6arr[7] = '1000'; $dhcpdv6cfg[$ifname]['range'] = array(); $dhcpdv6cfg[$ifname]['range']['from'] = Net_IPv6::compress(implode(':', $ifcfgipv6arr)); $ifcfgipv6arr[7] = '2000'; $dhcpdv6cfg[$ifname]['range']['to'] = Net_IPv6::compress(implode(':', $ifcfgipv6arr)); /* with enough room we can add dhcp6 prefix delegation */ $pdlen = calculate_ipv6_delegation_length($config['interfaces'][$ifname]['track6-interface']); if ($pdlen > 2) { /* XXX calculation is probably out of whack, please fix */ $pdlenmax = $pdlen; $pdlenhalf = $pdlenmax - 1; $pdlenmin = 64 - ceil($pdlenhalf / 4); $dhcpdv6cfg[$ifname]['prefixrange'] = array(); $dhcpdv6cfg[$ifname]['prefixrange']['prefixlength'] = $pdlenmin; /* set the delegation start to half the current address block */ $range = Net_IPv6::parseAddress($ifcfgipv6, (64 - $pdlenmax)); $range['start'] = Net_IPv6::getNetmask($range['end'], (64 - $pdlenhalf)); /* set the end range to a multiple of the prefix delegation size, required by dhcpd */ $range = Net_IPv6::parseAddress($range['end'], (64 - $pdlenhalf)); $range['end'] = Net_IPv6::getNetmask($range['end'], (64 - round($pdlen / 2))); $dhcpdv6cfg[$ifname]['prefixrange']['from'] = Net_IPv6::compress($range['start']); $dhcpdv6cfg[$ifname]['prefixrange']['to'] = Net_IPv6::compress($range['end']); } } else { if (!empty($dhcpdv6cfg[$ifname]['range']['from']) && !empty($dhcpdv6cfg[$ifname]['range']['to'])) { /* get config entry and marry it to the live prefix */ $dhcpdv6cfg[$ifname]['range']['from'] = merge_ipv6_address($ifcfgipv6, $dhcpdv6cfg[$ifname]['range']['from']); $dhcpdv6cfg[$ifname]['range']['to'] = merge_ipv6_address($ifcfgipv6, $dhcpdv6cfg[$ifname]['range']['to']); } if (!empty($dhcpdv6cfg[$ifname]['prefixrange']['from']) && !empty($dhcpdv6cfg[$ifname]['prefixrange']['to'])) { /* XXX $pdlen is never validated against prefixlenght setting, but must be smaller or equal */ $pdlen = 64 - calculate_ipv6_delegation_length($config['interfaces'][$ifname]['track6-interface']); $range_from = $dhcpdv6cfg[$ifname]['prefixrange']['from']; if (merge_ipv6_address($range_from, '::') == '::') { log_msg("'{$dhcpdv6cfg[$ifname]['prefixrange']['from']}' is not a valid prefix range value", LOG_WARNING); /* XXX previously it was suggested to use suffix but it was actually infix so shift 64 bits if possible */ $range_from = $dhcpdv6cfg[$ifname]['prefixrange']['from'] . ':0:0:0:0'; } $range_to = $dhcpdv6cfg[$ifname]['prefixrange']['to']; if (merge_ipv6_address($range_to, '::') == '::') { log_msg("'{$dhcpdv6cfg[$ifname]['prefixrange']['to']}' is not a valid prefix range value", LOG_WARNING); /* XXX previously it was suggested to use suffix but it was actually infix so shift 64 bits if possible */ $range_to = $dhcpdv6cfg[$ifname]['prefixrange']['to'] . ':0:0:0:0'; } $dhcpdv6cfg[$ifname]['prefixrange']['from'] = merge_ipv6_address($ifcfgipv6, $range_from, $pdlen); $dhcpdv6cfg[$ifname]['prefixrange']['to'] = merge_ipv6_address($ifcfgipv6, $range_to, $pdlen); } } } } $custoptionsv6 = ""; foreach ($dhcpdv6cfg as $dhcpv6if => $dhcpv6ifconf) { if (isset($dhcpv6ifconf['numberoptions']['item'])) { foreach ($dhcpv6ifconf['numberoptions']['item'] as $itemv6idx => $itemv6) { if (!empty($itemv6['type'])) { $itemtype = $itemv6['type']; } else { $itemtype = "text"; } $custoptionsv6 .= "option custom-{$dhcpv6if}-{$itemv6idx} code {$itemv6['number']} = {$itemtype};\n"; } } } if (isset($dhcpv6ifconf['netboot']) && !empty($dhcpv6ifconf['bootfile_url'])) { $custoptionsv6 .= "option dhcp6.bootfile-url code 59 = string;\n"; } $dhcpdv6conf = <<<EOD option dhcp6.domain-search "{$config['system']['domain']}"; option dhcp6.rapid-commit; {$custoptionsv6} default-lease-time 7200; max-lease-time 86400; log-facility local7; one-lease-per-client true; deny duplicates; ping-check true; update-conflict-detection false; authoritative; EOD; $dhcpdv6ifs = []; $ddns_zones = []; $need_ddns_updates = false; foreach ($dhcpdv6cfg as $dhcpv6if => $dhcpv6ifconf) { if (!isset($dhcpv6ifconf['enable']) || !isset($iflist[$dhcpv6if])) { continue; } if (isset($blacklist[$dhcpv6if])) { continue; } list ($ifcfgipv6, $networkv6) = interfaces_primary_address6($dhcpv6if, $ifconfig_details); if (!is_ipaddrv6($ifcfgipv6) || !is_subnetv6($networkv6)) { $realif = get_real_interface($dhcpv6if, 'inet6'); log_msg("dhcpd_dhcp6_configure() found no suitable IPv6 address on {$dhcpv6if}({$realif})", LOG_WARNING); continue; } $dnscfgv6 = ""; if (!empty($dhcpv6ifconf['domainsearchlist'])) { $dnscfgv6 .= " option dhcp6.domain-search \"" . join("\",\"", preg_split("/[ ;]+/", $dhcpv6ifconf['domainsearchlist'])) . "\";\n"; } $newzone = array(); if (isset($dhcpv6ifconf['ddnsupdate'])) { $need_ddns_updates = true; if (!empty($dhcpv6ifconf['ddnsdomain'])) { $dnscfgv6 .= " ddns-domainname \"{$dhcpv6ifconf['ddnsdomain']}\";\n"; $newzone['domain-name'] = $dhcpv6ifconf['ddnsdomain']; } else { $newzone['domain-name'] = $config['system']['domain']; } $subnetv6 = explode("/", $networkv6)[0]; $addr = inet_pton($subnetv6); $addr_unpack = unpack('H*hex', $addr); $addr_hex = $addr_unpack['hex']; $revsubnet = array_reverse(str_split($addr_hex)); foreach ($revsubnet as $octet) { if ($octet == "0") { array_shift($revsubnet); } else { break; } } $newzone['ptr-domain'] = implode(".", $revsubnet) . ".ip6.arpa"; } if (isset($dhcpv6ifconf['dnsserver'][0])) { $dnscfgv6 .= " option dhcp6.name-servers " . join(",", $dhcpv6ifconf['dnsserver']) . ";"; } elseif (!empty(service_by_filter(['dns_ports' => '53']))) { $dnscfgv6 .= " option dhcp6.name-servers {$ifcfgipv6};"; } elseif (!empty($dns_arrv6)) { $dnscfgv6 .= " option dhcp6.name-servers " . join(",", $dns_arrv6) . ";"; } $dhcpdv6conf .= "\nsubnet6 {$networkv6} {\n"; if (!empty($dhcpv6ifconf['range']['from'])) { $dhcpdv6conf .= " range6 {$dhcpv6ifconf['range']['from']} {$dhcpv6ifconf['range']['to']};\n"; } $dhcpdv6conf .= "{$dnscfgv6}\n"; if (!empty($dhcpv6ifconf['prefixrange']['from']) && is_ipaddrv6($dhcpv6ifconf['prefixrange']['from']) && is_ipaddrv6($dhcpv6ifconf['prefixrange']['to'])) { $dhcpdv6conf .= " prefix6 {$dhcpv6ifconf['prefixrange']['from']} {$dhcpv6ifconf['prefixrange']['to']}/{$dhcpv6ifconf['prefixrange']['prefixlength']};\n"; } // default-lease-time if (!empty($dhcpv6ifconf['defaultleasetime'])) { $dhcpdv6conf .= " default-lease-time {$dhcpv6ifconf['defaultleasetime']};\n"; } // max-lease-time if (!empty($dhcpv6ifconf['maxleasetime'])) { $dhcpdv6conf .= " max-lease-time {$dhcpv6ifconf['maxleasetime']};\n"; } // min-secs if (!empty($dhcpv6ifconf['minsecs'])) { $dhcpdv6conf .= " min-secs {$dhcpv6ifconf['minsecs']};\n"; } // ntp-servers if (isset($dhcpv6ifconf['ntpserver'][0])) { $ntpservers = array(); foreach ($dhcpv6ifconf['ntpserver'] as $ntpserver) { if (is_ipaddrv6($ntpserver)) { $ntpservers[] = $ntpserver; } } if (count($ntpservers) > 0) { $dhcpdv6conf .= " option dhcp6.sntp-servers " . join(",", $dhcpv6ifconf['ntpserver']) . ";\n"; } } // Handle option, number rowhelper values if (isset($dhcpv6ifconf['numberoptions']['item'])) { $dhcpdv6conf .= "\n"; foreach ($dhcpv6ifconf['numberoptions']['item'] as $itemv6idx => $itemv6) { if (empty($itemv6['type']) || $itemv6['type'] == "text") { $dhcpdv6conf .= " option custom-{$dhcpv6if}-{$itemv6idx} \"{$itemv6['value']}\";\n"; } else { $dhcpdv6conf .= " option custom-{$dhcpv6if}-{$itemv6idx} {$itemv6['value']};\n"; } } } // net boot information if (isset($dhcpv6ifconf['netboot'])) { if (!empty($dhcpv6ifconf['bootfile_url'])) { $dhcpdv6conf .= " option dhcp6.bootfile-url \"{$dhcpv6ifconf['bootfile_url']}\";\n"; } } $dhcpdv6conf .= "}\n"; /* add static mappings */ /* Needs to use DUID */ if (isset($dhcpv6ifconf['staticmap'])) { $i = 0; foreach ($dhcpv6ifconf['staticmap'] as $sm) { $dhcpdv6conf .= <<<EOD host s_{$dhcpv6if}_{$i} { host-identifier option dhcp6.client-id {$sm['duid']}; EOD; if (!empty($sm['ipaddrv6'])) { if (isset($config['interfaces'][$dhcpv6if]['dhcpd6track6allowoverride'])) { $sm['ipaddrv6'] = merge_ipv6_address($ifcfgipv6, $sm['ipaddrv6']); } $dhcpdv6conf .= " fixed-address6 {$sm['ipaddrv6']};\n"; } if (!empty($sm['hostname'])) { $dhhostname = str_replace(" ", "_", $sm['hostname']); $dhhostname = str_replace(".", "_", $dhhostname); $dhcpdv6conf .= " option host-name \"{$dhhostname}\";\n"; if ($need_ddns_updates) { $dhcpdv6conf .= " ddns-hostname \"{$dhhostname}\";\n"; } } if (!empty($sm['filename'])) { $dhcpdv6conf .= " filename \"{$sm['filename']}\";\n"; } if (!empty($sm['rootpath'])) { $dhcpdv6conf .= " option root-path \"{$sm['rootpath']}\";\n"; } if (!empty($sm['domainsearchlist']) && ($sm['domainsearchlist'] != $dhcpv6ifconf['domainsearchlist'])) { $dhcpdv6conf .= " option dhcp6.domain-search \"" . join("\",\"", preg_split("/[ ;]+/", $sm['domainsearchlist'])) . "\";\n"; } $dhcpdv6conf .= "}\n"; $i++; } } if (!empty($newzone['domain-name']) && isset($dhcpv6ifconf['ddnsupdate']) && is_ipaddrv6($dhcpv6ifconf['ddnsdomainprimary'])) { $newzone['dns-servers'] = array($dhcpv6ifconf['ddnsdomainprimary']); $newzone['ddnsdomainkeyname'] = $dhcpv6ifconf['ddnsdomainkeyname']; $newzone['ddnsdomainkey'] = $dhcpv6ifconf['ddnsdomainkey']; $newzone['ddnsdomainalgorithm'] = !empty($dhcpv6ifconf['ddnsdomainalgorithm']) ? $dhcpv6ifconf['ddnsdomainalgorithm'] : "hmac-md5"; $ddns_zones[] = $newzone; } $dhcpdv6ifs[] = escapeshellcmd(get_real_interface($dhcpv6if, 'inet6')); } if ($need_ddns_updates) { $dhcpdv6conf .= "\nddns-update-style interim;\n"; $dhcpdv6conf .= "update-static-leases on;\n"; $dhcpdv6conf .= dhcpd_zones($ddns_zones, "inet6"); } else { $dhcpdv6conf .= "\nddns-update-style none;\n"; } foreach (glob('/usr/local/etc/dhcpd6.opnsense.d/*.conf') as $file) { $dhcpdv6conf .= "\n\n# including custom file {$file}\n" . file_get_contents($file); } @file_put_contents('/var/dhcpd/etc/dhcpdv6.conf', $dhcpdv6conf); @touch('/var/dhcpd/var/db/dhcpd6.leases'); if (count($dhcpdv6ifs) > 0) { mwexec('/usr/local/sbin/dhcpd -6 -user dhcpd -group dhcpd -chroot /var/dhcpd -cf /etc/dhcpdv6.conf -pf /var/run/dhcpdv6.pid ' . join(' ', $dhcpdv6ifs)); mwexecf('/usr/sbin/daemon -m0 -f -p %s %s', ['/var/run/dhcpleases6.pid', '/usr/local/opnsense/scripts/dhcp/prefixes.sh']); } service_log("done.\n", $verbose); } function dhcpd_staticmap($proto = null, $valid_addresses = true, $ifconfig_details = null) { $staticmap = []; foreach (empty($proto) ? [4, 6] : [$proto] as $inet) { $ipaddr = $inet == 6 ? 'ipaddrv6' : 'ipaddr'; foreach (config_read_array($inet == 6 ? 'dhcpdv6' : 'dhcpd') as $dhcpif => $dhcpifconf) { if (!isset($dhcpifconf['staticmap']) || !isset($dhcpifconf['enable'])) { continue; } $ifconf = config_read_array('interfaces', $dhcpif); list ($ipaddrv6) = $inet == 6 && isset($ifconf['dhcpd6track6allowoverride']) ? interfaces_primary_address6($dhcpif, $ifconfig_details) : [null]; foreach ($dhcpifconf['staticmap'] as $host) { if (empty($host[$ipaddr]) && $valid_addresses) { /* we only return proper entries with an IP address */ continue; } if (!empty($ipaddrv6)) { /* expand IPv6 suffix address, but only allow user-given compressed suffix */ $host['ipaddrv6'] = merge_ipv6_address($ipaddrv6, $host['ipaddrv6']); } if (!empty($host['ipaddrv6'])) { /* avoid sloppy input by recompressing the address correctly */ $host['ipaddrv6'] = Net_IPv6::compress(Net_IPv6::uncompress($host['ipaddrv6'])); } $domain = null; // XXX: dhcpdv6 domain entries have been superseded by domainsearchlist, // for backward compatibility support both here. if ($inet == 6 && !empty($host['domainsearchlist'])) { $domain = $host['domainsearchlist']; } elseif (!empty($host['domain'])) { $domain = $host['domain']; } elseif ($inet == 6 && !empty($dhcpifconf['domainsearchlist'])) { $domain = $dhcpifconf['domainsearchlist']; } elseif (!empty($dhcpifconf['domain'])) { $domain = $dhcpifconf['domain']; } if ($domain !== null) { /* first entry only */ $domain = preg_split('/[ ;]+/', $domain)[0]; } $entry = [ 'descr' => $host['descr'] ?? null, 'domain' => $domain, 'hostname' => $host['hostname'] ?? null, 'interface' => $dhcpif, $ipaddr => $host[$ipaddr], ]; foreach (['mac', 'duid'] as $property) { if (isset($host[$property])) { $entry[$property] = $host[$property]; } } $staticmap[] = $entry; } } } return $staticmap; } function dhcpd_parse_duid($duid_string) { $parsed_duid = []; for ($i = 0; $i < strlen($duid_string); $i++) { $s = substr($duid_string, $i, 1); if ($s == '\\') { $n = substr($duid_string, $i + 1, 1); if ($n == '\\' || $n == '"') { $parsed_duid[] = sprintf('%02x', ord($n)); $i += 1; } elseif (is_numeric($n)) { $parsed_duid[] = sprintf('%02x', octdec(substr($duid_string, $i + 1, 3))); $i += 3; } } else { $parsed_duid[] = sprintf('%02x', ord($s)); } } $iaid = array_slice($parsed_duid, 0, 4); $duid = array_slice($parsed_duid, 4); return [$iaid, $duid]; }