From 4c6ebde58c402a11011be6cc1db9a360d004b698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurice=20Preu=C3=9F=20=28envoyr=29?= Date: Wed, 18 Jan 2023 13:57:47 +0100 Subject: [PATCH] adding new dns resolver setting for let's encrypt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maurice Preuß (envoyr) Co-authored-by: Michael Kaufmann --- actions/admin/settings/131.ssl.php | 10 +++ composer.json | 3 +- composer.lock | 53 +++++++++++- install/froxlor.sql.php | 3 +- install/updates/froxlor/update_2.x.inc.php | 9 +++ .../updates/preconfig/preconfig_2.x.inc.php | 18 ++++- lib/Froxlor/Api/Commands/Domains.php | 4 +- lib/Froxlor/Api/Commands/SubDomains.php | 4 +- lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php | 2 +- lib/Froxlor/Froxlor.php | 2 +- lib/Froxlor/Install/Preconfig.php | 2 +- lib/Froxlor/PhpHelper.php | 81 +++++++++++-------- lng/de.lng.php | 4 + lng/en.lng.php | 4 + templates/Froxlor/form/formfields.html.twig | 10 ++- 15 files changed, 165 insertions(+), 44 deletions(-) diff --git a/actions/admin/settings/131.ssl.php b/actions/admin/settings/131.ssl.php index 85f8c0be8a..05ef2fa397 100644 --- a/actions/admin/settings/131.ssl.php +++ b/actions/admin/settings/131.ssl.php @@ -241,6 +241,16 @@ 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' + ], + 'system_le_domain_dnscheck_resolver' => [ + 'label' => lng('serversettings.le_domain_dnscheck_resolver'), + 'settinggroup' => 'system', + 'varname' => 'le_domain_dnscheck_resolver', + 'type' => 'text', + 'string_regexp' => '/^(([0-9]+ [a-z0-9\-\._]+, ?)*[0-9]+ [a-z0-9\-\._]+)?$/i', + 'string_emptyallowed' => true, + 'default' => '', + 'save_method' => 'storeSettingField' ] ] ] diff --git a/composer.json b/composer.json index cf2e19424d..67ab5f7038 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,8 @@ "voku/anti-xss": "^4.1", "twig/twig": "^3.3", "erusev/parsedown": "^1.7", - "symfony/console": "^5.4" + "symfony/console": "^5.4", + "pear/net_dns2": "^1.5" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/composer.lock b/composer.lock index bacbbad280..f5e0d5fb27 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f8370edea3c85bcb7b681926a1fff04e", + "content-hash": "41e7a3bc0e13b47c4f245334b113c3be", "packages": [ { "name": "erusev/parsedown", @@ -198,6 +198,57 @@ ], "time": "2022-06-09T08:53:42+00:00" }, + { + "name": "pear/net_dns2", + "version": "v1.5.3", + "source": { + "type": "git", + "url": "https://github.com/mikepultz/netdns2.git", + "reference": "dc8053772132a855b8bb6193422a959995f3a773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikepultz/netdns2/zipball/dc8053772132a855b8bb6193422a959995f3a773", + "reference": "dc8053772132a855b8bb6193422a959995f3a773", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-0": { + "Net_DNS2": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Mike Pultz", + "email": "mike@mikepultz.com", + "homepage": "https://mikepultz.com/", + "role": "lead" + } + ], + "description": "Native PHP DNS Resolver and Updater Library", + "homepage": "https://netdns2.com/", + "keywords": [ + "PEAR", + "dns", + "network" + ], + "support": { + "issues": "https://github.com/mikepultz/netdns2/issues", + "source": "https://github.com/mikepultz/netdns2" + }, + "time": "2022-11-28T19:16:31+00:00" + }, { "name": "phpmailer/phpmailer", "version": "v6.6.3", diff --git a/install/froxlor.sql.php b/install/froxlor.sql.php index c82e6ad64b..11657149aa 100644 --- a/install/froxlor.sql.php +++ b/install/froxlor.sql.php @@ -670,6 +670,7 @@ ('system', 'leaccount', ''), ('system', 'nssextrausers', '1'), ('system', 'le_domain_dnscheck', '1'), + ('system', 'le_domain_dnscheck_resolver', '1.1.1.1'), ('system', 'ssl_protocols', 'TLSv1.2'), ('system', 'tlsv13_cipher_list', ''), ('system', 'honorcipherorder', '0'), @@ -741,7 +742,7 @@ ('panel', 'logo_overridecustom', '0'), ('panel', 'settings_mode', '0'), ('panel', 'version', '2.0.8'), - ('panel', 'db_version', '202301120'); + ('panel', 'db_version', '202301180'); DROP TABLE IF EXISTS `panel_tasks`; diff --git a/install/updates/froxlor/update_2.x.inc.php b/install/updates/froxlor/update_2.x.inc.php index 246185e0bf..b74bca0ea3 100644 --- a/install/updates/froxlor/update_2.x.inc.php +++ b/install/updates/froxlor/update_2.x.inc.php @@ -363,3 +363,12 @@ Froxlor::updateToVersion('2.0.8'); } + +if (Froxlor::isDatabaseVersion('202301120')) { + Update::showUpdateStep("Adding new setting for DNS resolver when using Let's Encrypt"); + $system_le_domain_dnscheck_resolver = isset($_POST['system_le_domain_dnscheck_resolver']) ? $_POST['system_le_domain_dnscheck_resolver'] : '1.1.1.1'; + Settings::AddNew("system.le_domain_dnscheck_resolver", $system_le_domain_dnscheck_resolver); + Update::lastStepStatus(0); + + Froxlor::updateToDbVersion('202301180'); +} diff --git a/install/updates/preconfig/preconfig_2.x.inc.php b/install/updates/preconfig/preconfig_2.x.inc.php index af89668ab0..d299f694b3 100644 --- a/install/updates/preconfig/preconfig_2.x.inc.php +++ b/install/updates/preconfig/preconfig_2.x.inc.php @@ -79,7 +79,7 @@ if ((int) Settings::Get('system.leenabled') == 1 && $acmesh_challenge_dir != $recommended) { $has_preconfig = true; $description = 'ACME challenge docroot from settings differs from the current installation directory.'; - $question = 'Validate Let\'s Encrypt challenge path (recommended value: ' . $recommended . ') '; + $question = 'Validate Let\'s Encrypt challenge path (recommended value: ' . $recommended . ')'; $return['system_letsencryptchallengepath_upd'] = [ 'type' => 'text', 'value' => $recommended, @@ -91,5 +91,21 @@ } } +if (Update::versionInUpdate($current_db_version, '202301180')) { + //if ((int) Settings::Get('system.leenabled') == 1) { + $has_preconfig = true; + $description = 'Froxlor now supports to set a external DNS resolver for the Let\'s Encrypt pre-check.'; + $question = 'Specify a DNS resolver IP (recommended value: 1.1.1.1 or similar)'; + $return['system_le_domain_dnscheck_resolver'] = [ + 'type' => 'text', + 'pattern' => '^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$|^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$|^\s*$', + 'value' => '1.1.1.1', + 'placeholder' => '1.1.1.1', + 'label' => $question, + 'prior_infotext' => $description, + ]; + //} +} + $preconfig['fields'] = $return; return $preconfig; diff --git a/lib/Froxlor/Api/Commands/Domains.php b/lib/Froxlor/Api/Commands/Domains.php index ee31f6002c..93d853f4c8 100644 --- a/lib/Froxlor/Api/Commands/Domains.php +++ b/lib/Froxlor/Api/Commands/Domains.php @@ -559,7 +559,7 @@ public function add() // validate dns if lets encrypt is enabled to check whether we can use it at all if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') { - $domain_ips = PhpHelper::gethostbynamel6($domain); + $domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver')); $selected_ips = $this->getIpsFromIdArray($ssl_ipandports); if ($domain_ips == false || count(array_intersect($selected_ips, $domain_ips)) <= 0) { Response::standardError('invaliddnsforletsencrypt', '', true); @@ -1523,7 +1523,7 @@ public function update() // validate dns if lets encrypt is enabled to check whether we can use it at all if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') { - $domain_ips = PhpHelper::gethostbynamel6($result['domain']); + $domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver')); $selected_ips = $this->getIpsFromIdArray($ssl_ipandports); if ($domain_ips == false || count(array_intersect($selected_ips, $domain_ips)) <= 0) { Response::standardError('invaliddnsforletsencrypt', '', true); diff --git a/lib/Froxlor/Api/Commands/SubDomains.php b/lib/Froxlor/Api/Commands/SubDomains.php index 6097caf05b..6389650d3e 100644 --- a/lib/Froxlor/Api/Commands/SubDomains.php +++ b/lib/Froxlor/Api/Commands/SubDomains.php @@ -262,7 +262,7 @@ public function add() // validate dns if lets encrypt is enabled to check whether we can use it at all if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') { $our_ips = Domain::getIpsOfDomain($domain_check['id']); - $domain_ips = PhpHelper::gethostbynamel6($completedomain); + $domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver')); if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) { Response::standardError('invaliddnsforletsencrypt', '', true); } @@ -738,7 +738,7 @@ public function update() // validate dns if lets encrypt is enabled to check whether we can use it at all if ($result['letsencrypt'] != $letsencrypt && $letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') { $our_ips = Domain::getIpsOfDomain($result['parentdomainid']); - $domain_ips = PhpHelper::gethostbynamel6($result['domain']); + $domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver')); if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) { Response::standardError('invaliddnsforletsencrypt', '', true); } diff --git a/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php index e44c0d35d7..122d644308 100644 --- a/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php +++ b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php @@ -521,7 +521,7 @@ private static function validateDns(array &$domains, $domain_id, &$cronlog) foreach ($loop_domains as $idx => $domain) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Validating DNS of " . $domain); // ips according to NS - $domain_ips = PhpHelper::gethostbynamel6($domain); + $domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver')); if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) { // no common ips... $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $domain . " due to no system known IP address via DNS check"); diff --git a/lib/Froxlor/Froxlor.php b/lib/Froxlor/Froxlor.php index 98ce9e7860..b886fecee3 100644 --- a/lib/Froxlor/Froxlor.php +++ b/lib/Froxlor/Froxlor.php @@ -34,7 +34,7 @@ final class Froxlor const VERSION = '2.0.8'; // Database version (YYYYMMDDC where C is a daily counter) - const DBVERSION = '202301120'; + const DBVERSION = '202301180'; // Distribution branding-tag (used for Debian etc.) const BRANDING = ''; diff --git a/lib/Froxlor/Install/Preconfig.php b/lib/Froxlor/Install/Preconfig.php index da5c943a49..43e9942e9a 100644 --- a/lib/Froxlor/Install/Preconfig.php +++ b/lib/Froxlor/Install/Preconfig.php @@ -101,7 +101,7 @@ public static function getPreConfig(): array $agree = [ 'title' => 'Check', 'fields' => [ - 'update_changesagreed' => ['type' => 'checkbox', 'value' => 1, 'label' => 'I have read the update notifications above and I am aware of the changes made to my system.'], + 'update_changesagreed' => ['mandatory' => true, 'type' => 'checkrequired', 'value' => 1, 'label' => 'I have read the update notifications above and I am aware of the changes made to my system.'], 'update_preconfig' => ['type' => 'hidden', 'value' => 1] ] ]; diff --git a/lib/Froxlor/PhpHelper.php b/lib/Froxlor/PhpHelper.php index 82dc70a9b0..2250df314c 100644 --- a/lib/Froxlor/PhpHelper.php +++ b/lib/Froxlor/PhpHelper.php @@ -27,6 +27,8 @@ use Exception; use Froxlor\UI\Panel\UI; +use Net_DNS2_Exception; +use Net_DNS2_Resolver; use Throwable; use voku\helper\AntiXSS; @@ -244,45 +246,60 @@ public static function loadConfigArrayDir(...$configdirs) * ipv6 aware gethostbynamel function * * @param string $host - * @param boolean $try_a - * default true + * @param boolean $try_a default true + * @param string|null $nameserver set additional resolver nameserver to use (e.g. 1.1.1.1) * @return boolean|array */ - public static function gethostbynamel6($host, $try_a = true) + public static function gethostbynamel6(string $host, bool $try_a = true, string $nameserver = null) { - $dns6 = @dns_get_record($host, DNS_AAAA); - if (!is_array($dns6)) { - // no record or failed to check - $dns6 = []; - } - if ($try_a == true) { - $dns4 = @dns_get_record($host, DNS_A); - if (!is_array($dns4)) { - // no record or failed to check - $dns4 = []; - } - $dns = array_merge($dns4, $dns6); - } else { - $dns = $dns6; - } $ips = []; - foreach ($dns as $record) { - if ($record["type"] == "A") { - // always use compressed ipv6 format - $ip = inet_ntop(inet_pton($record["ip"])); - $ips[] = $ip; + + try { + // set the default nameservers to use, use the system default if none are provided + $resolver = new Net_DNS2_Resolver($nameserver ? ['nameservers' => [$nameserver]] : []); + + // get all ip addresses from the A record + if ($try_a) { + try { + $answer = $resolver->query($host, 'A')->answer; + foreach ($answer as $rr) { + $ips[] = $rr->address; + } + } catch (Net_DNS2_Exception $e) { + // we can't do anything here, just continue + } } - if ($record["type"] == "AAAA") { - // always use compressed ipv6 format - $ip = inet_ntop(inet_pton($record["ipv6"])); - $ips[] = $ip; + + // get all ip addresses from the AAAA record + try { + $answer = $resolver->query($host, 'AAAA')->answer; + foreach ($answer as $rr) { + $ips[] = $rr->address; + } + } catch (Net_DNS2_Exception $e) { + // we can't do anything here, just continue + } + } catch (Net_DNS2_Exception $e) { + // fallback to php's dns_get_record if Net_DNS2 has no resolver available, but this may cause + // problems if the system's dns is not configured correctly; for example, the acme pre-check + // will fail because some providers put a local ip in /etc/hosts + + // get all ip addresses from the A record + if ($try_a) { + $answer = @dns_get_record($host, DNS_A); + foreach ($answer as $rr) { + $ips[] = $rr['ip']; + } + } + + // get all ip addresses from the AAAA record + $answer = @dns_get_record($host, DNS_AAAA); + foreach ($answer as $rr) { + $ips[] = $rr['ipv6']; } } - if (count($ips) < 1) { - return false; - } else { - return $ips; - } + + return count($ips) > 0 ? $ips : false; } /** diff --git a/lng/de.lng.php b/lng/de.lng.php index 3c38970124..25282a0d47 100644 --- a/lng/de.lng.php +++ b/lng/de.lng.php @@ -1965,6 +1965,10 @@ 'title' => 'Validiere DNS der Domains wenn Let\'s Encrypt genutzt wird', 'description' => 'Wenn aktiviert wird froxlor überprüfen ob die DNS Einträge der Domains, welche ein Let\'s Encrypt Zertifikat beantragt, mindestens auf eine der System IP Adressen auflöst.', ], + 'le_domain_dnscheck_resolver' => [ + 'title' => 'DNS Resolver für die DNS Überprüfung', + 'description' => 'IP Adresse des DNS Servers, welcher für die DNS Überprüfung genutzt werden soll. Wenn leer, wird der Standard DNS Resolver des Systems genutzt.', + ], 'phpsettingsforsubdomains' => [ 'description' => 'Wenn ja, wird die gewählte PHP-Config für alle Subdomains übernommen', ], diff --git a/lng/en.lng.php b/lng/en.lng.php index d7f7eca7e7..776dcddf19 100644 --- a/lng/en.lng.php +++ b/lng/en.lng.php @@ -2084,6 +2084,10 @@ 'title' => 'Validate DNS of domains when using Let\'s Encrypt', 'description' => 'If activated, froxlor will validate whether the domain which requests a Let\'s Encrypt certificate resolves to at least one of the system ip addresses.', ], + 'le_domain_dnscheck_resolver' => [ + 'title' => 'Use a external nameserver for DNS validation', + 'description' => 'If set, froxlor will use this DNS to validate the DNS of domains when using Let\'s Encrypt. If empty, the system\'s default DNS resolver will be used.', + ], 'phpsettingsforsubdomains' => [ 'description' => 'If yes the chosen php-config will be updated to all subdomains', ], diff --git a/templates/Froxlor/form/formfields.html.twig b/templates/Froxlor/form/formfields.html.twig index c7549e1e4e..0491a6a356 100644 --- a/templates/Froxlor/form/formfields.html.twig +++ b/templates/Froxlor/form/formfields.html.twig @@ -54,6 +54,8 @@ {{ _self.input_ul(id, field) }} {% elseif field.type == 'checkbox' %} {{ _self.bool(id, field) }} + {% elseif field.type == 'checkrequired' %} + {{ _self.chk_required(id, field) }} {% elseif field.type == 'select' %} {{ _self.select(id, field) }} {% elseif field.type == 'textarea' %} @@ -119,6 +121,12 @@ {% endif %} {% endmacro %} +{% macro chk_required(id, field) %} +
+ +
+{% endmacro %} + {% macro infotext(id, field) %} {% if field.next_to is defined %}
@@ -151,7 +159,7 @@ {% if field.next_to is defined %}
{% endif %} - + {% if field.type == 'hidden' and field.display is defined %} {% endif %}