diff --git a/lib/functions/dns/function.createDomainZone.php b/lib/functions/dns/function.createDomainZone.php index 424c6ceb..b99f193a 100644 --- a/lib/functions/dns/function.createDomainZone.php +++ b/lib/functions/dns/function.createDomainZone.php @@ -14,7 +14,7 @@ * @package Functions * */ -function createDomainZone($domain_id, $froxlorhostname = false) +function createDomainZone($domain_id, $froxlorhostname = false, $isMainButSubTo = false) { if (!$froxlorhostname) { @@ -49,9 +49,18 @@ function createDomainZone($domain_id, $froxlorhostname = false) addRequiredEntry('@', 'A', $required_entries); addRequiredEntry('@', 'AAAA', $required_entries); - addRequiredEntry('@', 'NS', $required_entries); + if (! $isMainButSubTo) { + addRequiredEntry('@', 'NS', $required_entries); + } if ($domain['isemaildomain'] === '1') { addRequiredEntry('@', 'MX', $required_entries); + if (Settings::Get('system.dns_createmailentry')) { + foreach(['imap', 'pop3', 'mail', 'smtp'] as $record) { + foreach(['AAAA', 'A'] as $type) { + addRequiredEntry($record, $type, $required_entries); + } + } + } } // additional required records by setting @@ -67,7 +76,7 @@ function createDomainZone($domain_id, $froxlorhostname = false) { // additional required records for subdomains $subdomains_stmt = Database::prepare(" - SELECT `domain` FROM `" . TABLE_PANEL_DOMAINS . "` + SELECT `domain`, `iswildcarddomain`, `wwwserveralias` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `parentdomainid` = :domainid "); Database::pexecute($subdomains_stmt, array( @@ -81,28 +90,14 @@ function createDomainZone($domain_id, $froxlorhostname = false) addRequiredEntry(str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'AAAA', $required_entries); // Check whether to add a www.-prefix - if ($domain['iswildcarddomain'] == '1') { + if ($subdomain['iswildcarddomain'] == '1') { addRequiredEntry('*.' . str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'A', $required_entries); addRequiredEntry('*.' . str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'AAAA', $required_entries); - } elseif ($domain['wwwserveralias'] == '1') { + } elseif ($subdomain['wwwserveralias'] == '1') { addRequiredEntry('www.' . str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'A', $required_entries); addRequiredEntry('www.' . str_replace('.' . $domain['domain'], '', $subdomain['domain']), 'AAAA', $required_entries); } } - - // additional required records for main-but-subdomain-to - $mainbutsub_stmt = Database::prepare(" - SELECT `domain` FROM `" . TABLE_PANEL_DOMAINS . "` - WHERE `ismainbutsubto` = :domainid - "); - Database::pexecute($mainbutsub_stmt, array( - 'domainid' => $domain_id - )); - - while ($mainbutsubtodomain = $mainbutsub_stmt->fetch(PDO::FETCH_ASSOC)) { - // Add NS entry for subdomain-records of "main-but-subdomain-to"-domains, they get their own Zone - addRequiredEntry(str_replace('.' . $domain['domain'], '', $mainbutsubtodomain['domain']), 'NS', $required_entries); - } } // additional required records for SPF and DKIM if activated @@ -265,16 +260,31 @@ function createDomainZone($domain_id, $froxlorhostname = false) $primary_ns = Settings::Get('system.hostname'); } - // TODO for now, dummy time-periods - $soa_content = $primary_ns . " " . escapeSoaAdminMail(Settings::Get('panel.adminmail')) . " (" . PHP_EOL; - $soa_content .= $domain['bindserial'] . "\t; serial" . PHP_EOL; - $soa_content .= "1800\t; refresh (30 mins)" . PHP_EOL; - $soa_content .= "900\t; retry (15 mins)" . PHP_EOL; - $soa_content .= "604800\t; expire (7 days)" . PHP_EOL; - $soa_content .= "1200\t)\t; minimum (20 mins)"; + if (! $isMainButSubTo) { + $date = date('Ymd'); + $domain['bindserial'] = (preg_match('/^' . $date . '/', $domain['bindserial']) ? + $domain['bindserial'] + 1 : + $date . '00'); + if (!$froxlorhostname) { + $upd_stmt = Database::prepare(" + UPDATE `" . TABLE_PANEL_DOMAINS . "` SET + `bindserial` = :serial + WHERE `id` = :id + "); + Database::pexecute($upd_stmt, array('serial' => $domain['bindserial'], 'id' => $domain['id'])); + } - $soa_record = new DnsEntry('@', 'SOA', $soa_content); - array_unshift($zonerecords, $soa_record); + $soa_content = $primary_ns . " " . escapeSoaAdminMail(Settings::Get('panel.adminmail')) . " (" . PHP_EOL; + $soa_content .= $domain['bindserial'] . "\t; serial" . PHP_EOL; + // TODO for now, dummy time-periods + $soa_content .= "1800\t; refresh (30 mins)" . PHP_EOL; + $soa_content .= "900\t; retry (15 mins)" . PHP_EOL; + $soa_content .= "604800\t; expire (7 days)" . PHP_EOL; + $soa_content .= "1200\t)\t; minimum (20 mins)"; + + $soa_record = new DnsEntry('@', 'SOA', $soa_content); + array_unshift($zonerecords, $soa_record); + } $zone = new DnsZone((int) Settings::Get('system.defaultttl'), $domain['domain'], $domain['bindserial'], $zonerecords); diff --git a/scripts/classes/class.DnsBase.php b/scripts/classes/class.DnsBase.php index ba7a8506..65753b59 100644 --- a/scripts/classes/class.DnsBase.php +++ b/scripts/classes/class.DnsBase.php @@ -69,14 +69,38 @@ abstract class DnsBase protected function getDomainList() { - // get all Domains - $result_domains_stmt = Database::query(" - SELECT `d`.`id`, `d`.`domain`, `d`.`customerid`, `d`.`zonefile`, `c`.`loginname`, `c`.`guid` - FROM `" . TABLE_PANEL_DOMAINS . "` `d` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`) - WHERE `d`.`isbinddomain` = '1' ORDER BY `d`.`domain` ASC + $result_domains_stmt = Database::query( + " + SELECT + `d`.`id`, + `d`.`domain`, + `d`.`isemaildomain`, + `d`.`iswildcarddomain`, + `d`.`wwwserveralias`, + `d`.`customerid`, + `d`.`zonefile`, + `d`.`bindserial`, + `d`.`dkim`, + `d`.`dkim_id`, + `d`.`dkim_pubkey`, + `d`.`ismainbutsubto`, + `c`.`loginname`, + `c`.`guid` + FROM + `" . TABLE_PANEL_DOMAINS . "` `d` + LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`) + WHERE + `d`.`isbinddomain` = '1' + ORDER BY + `d`.`domain` ASC "); - $domains = $result_domains_stmt->fetchAll(PDO::FETCH_ASSOC); + $domains = array(); + // don't use fetchall() to be able to set the first column to the domain id and use it later on to set the rows' + // array of direct children without having to search the outer array + while ($domain = $result_domains_stmt->fetch(PDO::FETCH_ASSOC)) { + $domains[$domain["id"]] = $domain; + } // frolxor-hostname (#1090) if (Settings::get('system.dns_createhostnameentry') == 1) { @@ -98,11 +122,58 @@ abstract class DnsBase } if (empty($domains)) { - $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'No domains found for nameserver-config, skipping...'); return null; } - return $domains; + // collect domain IDs of direct child domains as arrays in ['children'] column + foreach (array_keys($domains) as $key) { + if (! isset($domains[$key]['children'])) { + $domains[$key]['children'] = array(); + } + if ($domains[$key]['ismainbutsubto'] > 0) { + if (isset($domains[ $domains[$key]['ismainbutsubto'] ])) { + $domains[ $domains[$key]['ismainbutsubto'] ]['children'][] = $domains[$key]['id']; + } else { + $this->_logger->logAction(CRON_ACTION, LOG_ERR, + 'Database inconsistency: domain ' . $domain['domain'] . ' (ID #' . $key . + ') is set to to be subdomain to non-existent domain ID #' . + $domains[$key]['ismainbutsubto'] . + '. No DNS record(s) will be created for this domain.'); + } + } + } + + $this->_logger->logAction(CRON_ACTION, LOG_DEBUG, + str_pad('domId', 9, ' ') . str_pad('domain', 40, ' ') . + 'ismainbutsubto ' . str_pad('parent domain', 40, ' ') . + "list of child domain ids"); + foreach ($domains as $domain) { + $logLine = + str_pad($domain['id'], 9, ' ') . + str_pad($domain['domain'], 40, ' ') . + str_pad($domain['ismainbutsubto'], 15, ' ') . + str_pad(((isset($domains[ $domain['ismainbutsubto'] ])) ? + $domains[ $domain['ismainbutsubto'] ]['domain'] : + '-'), 40, ' ') . + join(', ', $domain['children']); + $this->_logger->logAction(CRON_ACTION, LOG_DEBUG, $logLine); + } + + return $domains; + } + + public function reloadDaemon() + { + // reload DNS daemon + $cmd = Settings::Get('system.bindreload_command'); + $cmdStatus = 1; + safe_exec(escapeshellcmd($cmd), $cmdStatus); + if ($cmdStatus === 0) { + $this->_logger->logAction(CRON_ACTION, LOG_INFO, Settings::Get('system.dns_server') . ' daemon reloaded'); + } else { + $this->_logger->logAction(CRON_ACTION, LOG_ERR, 'Error while running `' . $cmd . + '`: exit code (' . $cmdStatus . ') - please check your system logs'); + } } public function writeDKIMconfigs() diff --git a/scripts/jobs/cron_tasks.inc.dns.10.bind.php b/scripts/jobs/cron_tasks.inc.dns.10.bind.php index 89120e98..06fbc9c8 100644 --- a/scripts/jobs/cron_tasks.inc.dns.10.bind.php +++ b/scripts/jobs/cron_tasks.inc.dns.10.bind.php @@ -19,6 +19,8 @@ if (! defined('MASTER_CRONJOB')) class bind extends DnsBase { + private $_bindconf_file = ""; + public function writeConfigs() { // tell the world what we are doing @@ -35,39 +37,71 @@ class bind extends DnsBase $domains = $this->getDomainList(); - if (! empty($domains)) { - $bindconf_file = '# ' . Settings::Get('system.bindconf_directory') . 'froxlor_bind.conf' . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n\n"; + if (empty($domains)) { + $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'No domains found for nameserver-config, skipping...'); + return; + } - foreach ($domains as $domain) { - // check for system-hostname - $isFroxlorHostname = false; - if (isset($domain['froxlorhost']) && $domain['froxlorhost'] == 1) { - $isFroxlorHostname = true; - } - // create zone-file - $this->_logger->logAction(CRON_ACTION, LOG_DEBUG, 'Generating dns zone for ' . $domain['domain']); - $zone = createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname); - $zonefile = (string)$zone; - $domain['zonefile'] = 'domains/' . $domain['domain'] . '.zone'; - $zonefile_name = makeCorrectFile(Settings::Get('system.bindconf_directory') . '/' . $domain['zonefile']); - $zonefile_handler = fopen($zonefile_name, 'w'); - fwrite($zonefile_handler, $zonefile); - fclose($zonefile_handler); - $this->_logger->logAction(CRON_ACTION, LOG_INFO, '`' . $zonefile_name . '` zone written'); + $bindconf_file = '# ' . Settings::Get('system.bindconf_directory') . 'froxlor_bind.conf' . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n\n"; - // generate config - $bindconf_file .= $this->_generateDomainConfig($domain); + foreach ($domains as $domain) { + if ($domain['ismainbutsubto'] > 0) { + // domains with ismainbutsubto>0 are handled by recursion within walkDomainList() + continue; + } + $this->walkDomainList($domain, $domains); + } + + $bindconf_file_handler = fopen(makeCorrectFile(Settings::Get('system.bindconf_directory') . '/froxlor_bind.conf'), 'w'); + fwrite($bindconf_file_handler, $this->_bindconf_file); + fclose($bindconf_file_handler); + $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'froxlor_bind.conf written'); + $this->reloadDaemon(); + $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'Task4 finished'); + } + + + private function walkDomainList($domain, $domains) + { + $zoneContent = ''; + $subzones = ''; + + foreach ($domain['children'] as $child_domain_id) { + $subzones .= $this->walkDomainList($domains[$child_domain_id], $domains); + } + + if ($domain['zonefile'] == '') { + // check for system-hostname + $isFroxlorHostname = false; + if (isset($domain['froxlorhost']) && $domain['froxlorhost'] == 1) { + $isFroxlorHostname = true; } - // write config - $bindconf_file_handler = fopen(makeCorrectFile(Settings::Get('system.bindconf_directory') . '/froxlor_bind.conf'), 'w'); - fwrite($bindconf_file_handler, $bindconf_file); - fclose($bindconf_file_handler); - $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'froxlor_bind.conf written'); - - // reload Bind - safe_exec(escapeshellcmd(Settings::Get('system.bindreload_command'))); - $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'Bind9 reloaded'); + if ($domain['ismainbutsubto'] == 0) { + $zoneContent = (string) createDomainZone(($domain['id'] == 'none') ? + $domain : + $domain['id'], + $isFroxlorHostname); + $domain['zonefile'] = 'domains/' . $domain['domain'] . '.zone'; + $zonefile_name = makeCorrectFile(Settings::Get('system.bindconf_directory') . '/' . + $domain['zonefile']); + $zonefile_handler = fopen($zonefile_name, 'w'); + fwrite($zonefile_handler, $zoneContent . $subzones); + fclose($zonefile_handler); + $this->_logger->logAction(CRON_ACTION, LOG_INFO, '`' . $zonefile_name . '` written'); + $this->_bindconf_file .= $this->_generateDomainConfig($domain); + } else { + return (string) createDomainZone(($domain['id'] == 'none') ? + $domain : + $domain['id'], + $isFroxlorHostname, + true); + } + } else { + $this->_logger->logAction(CRON_ACTION, LOG_INFO, + 'Added zonefile ' . $domain['zonefile'] . ' for domain ' . $domain['domain'] . + ' - Note that you will also have to handle ALL records for ALL subdomains.'); + $this->_bindconf_file .= $this->_generateDomainConfig($domain); } } diff --git a/scripts/jobs/cron_tasks.inc.dns.20.pdns.php b/scripts/jobs/cron_tasks.inc.dns.20.pdns.php index 3847657d..15844c71 100644 --- a/scripts/jobs/cron_tasks.inc.dns.20.pdns.php +++ b/scripts/jobs/cron_tasks.inc.dns.20.pdns.php @@ -30,38 +30,72 @@ class pdns extends DnsBase $this->_connectToPdnsDb(); // clean up - $this->_cleanZonefiles(); + $this->_clearZoneTables(); $domains = $this->getDomainList(); - if (! empty($domains)) { + if (empty($domains)) { + $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'No domains found for nameserver-config, skipping...'); + return; + } - foreach ($domains as $domain) { - // check for system-hostname - $isFroxlorHostname = false; - if (isset($domain['froxlorhost']) && $domain['froxlorhost'] == 1) { - $isFroxlorHostname = true; - } - // create zone-file - $this->_logger->logAction(CRON_ACTION, LOG_DEBUG, 'Generating dns zone for ' . $domain['domain']); - $zone = createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname); + foreach ($domains as $domain) { + if ($domain['ismainbutsubto'] > 0) { + // domains with ismainbutsubto>0 are handled by recursion within walkDomainList() + continue; + } + $this->walkDomainList($domain, $domains); + } - $dom_id = $this->_insertZone($zone->origin, $zone->serial); - $this->_insertRecords($dom_id, $zone->records, $zone->origin); - $this->_insertAllowedTransfers($dom_id); + $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'PowerDNS database updated'); + $this->reloadDaemon(); + $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'Task4 finished'); + } - $this->_logger->logAction(CRON_ACTION, LOG_INFO, '`' . $domain['domain'] . '` zone written'); + private function walkDomainList($domain, $domains) + { + $zoneContent = ''; + $subzones = array(); + + foreach ($domain['children'] as $child_domain_id) { + $subzones[] = $this->walkDomainList($domains[$child_domain_id], $domains); + } + + if ($domain['zonefile'] == '') { + // check for system-hostname + $isFroxlorHostname = false; + if (isset($domain['froxlorhost']) && $domain['froxlorhost'] == 1) { + $isFroxlorHostname = true; } - $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'Database updated'); - - // reload Bind - safe_exec(escapeshellcmd(Settings::Get('system.bindreload_command'))); - $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'pdns reloaded'); + if ($domain['ismainbutsubto'] == 0) { + $zoneContent = createDomainZone(($domain['id'] == 'none') ? + $domain : + $domain['id'], + $isFroxlorHostname); + if (count($subzones)) { + array_push($zoneContent->records, ...$subzones); + } + $pdnsDomId = $this->_insertZone($zoneContent->origin, $zoneContent->serial); + $this->_insertRecords($pdnsDomId, $zoneContent->records, $zoneContent->origin); + $this->_insertAllowedTransfers($pdnsDomId); + $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'DB entries stored for zone `' . $domain['domain'] . '`'); + } else { + return createDomainZone(($domain['id'] == 'none') ? + $domain : + $domain['id'], + $isFroxlorHostname, + true); + } + } else { + $this->_logger->logAction(CRON_ACTION, LOG_ERROR, + 'Zonefiles are NOT supported when PowerDNS is selected as DNS daemon (triggered by: ' . + $domain['domain'] . ')'); + $this->_bindconf_file .= $this->_generateDomainConfig($domain); } } - private function _cleanZonefiles() + private function _clearZoneTables() { $this->_logger->logAction(CRON_ACTION, LOG_INFO, 'Cleaning dns zone entries from database'); @@ -96,6 +130,11 @@ class pdns extends DnsBase foreach ($records as $record) { + if ($record instanceof DnsZone) { + $this->_insertRecords($domainid, $record->records, $record->origin); + continue; + } + if ($record->record == '@') { $_record = $origin; }