Skip to content

Commit

Permalink
adding new dns resolver setting for let's encrypt
Browse files Browse the repository at this point in the history
Signed-off-by: Maurice Preuß (envoyr) <[email protected]>
Co-authored-by: Michael Kaufmann <[email protected]>
  • Loading branch information
envoyr and d00p committed Jan 18, 2023
1 parent 1e013d9 commit 4c6ebde
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 44 deletions.
10 changes: 10 additions & 0 deletions actions/admin/settings/131.ssl.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
]
]
]
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 52 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion install/froxlor.sql.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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`;
Expand Down
9 changes: 9 additions & 0 deletions install/updates/froxlor/update_2.x.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
18 changes: 17 additions & 1 deletion install/updates/preconfig/preconfig_2.x.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<strong>Validate Let\'s Encrypt challenge path (recommended value: ' . $recommended . ')&nbsp;';
$question = '<strong>Validate Let\'s Encrypt challenge path (recommended value: ' . $recommended . ')</strong>';
$return['system_letsencryptchallengepath_upd'] = [
'type' => 'text',
'value' => $recommended,
Expand All @@ -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 = '<strong>Specify a DNS resolver IP (recommended value: 1.1.1.1 or similar)</strong>';
$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;
4 changes: 2 additions & 2 deletions lib/Froxlor/Api/Commands/Domains.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions lib/Froxlor/Api/Commands/SubDomains.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion lib/Froxlor/Froxlor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down
2 changes: 1 addition & 1 deletion lib/Froxlor/Install/Preconfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public static function getPreConfig(): array
$agree = [
'title' => 'Check',
'fields' => [
'update_changesagreed' => ['type' => 'checkbox', 'value' => 1, 'label' => '<strong>I have read the update notifications above and I am aware of the changes made to my system.</strong>'],
'update_changesagreed' => ['mandatory' => true, 'type' => 'checkrequired', 'value' => 1, 'label' => '<strong>I have read the update notifications above and I am aware of the changes made to my system.</strong>'],
'update_preconfig' => ['type' => 'hidden', 'value' => 1]
]
];
Expand Down
81 changes: 49 additions & 32 deletions lib/Froxlor/PhpHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions lng/de.lng.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
Expand Down
4 changes: 4 additions & 0 deletions lng/en.lng.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
Expand Down
10 changes: 9 additions & 1 deletion templates/Froxlor/form/formfields.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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' %}
Expand Down Expand Up @@ -119,6 +121,12 @@
{% endif %}
{% endmacro %}

{% macro chk_required(id, field) %}
<div class="form-check form-switch">
<input type="checkbox" value="{{ field.value }}" id="{{ id }}" name="{{ id }}" class="form-check-input" {% if field.mandatory is defined and field.mandatory == 1 %} required {% endif %} />
</div>
{% endmacro %}

{% macro infotext(id, field) %}
{% if field.next_to is defined %}
<div class="input-group">
Expand Down Expand Up @@ -151,7 +159,7 @@
{% if field.next_to is defined %}
<div class="input-group">
{% endif %}
<input type="{{ field.type }}" {% if field.visible is defined and field.visible == false %} disabled {% endif %} {% if field.type == 'number' and field.min is defined %} min="{{ field.min }}" {% endif %} {% if field.type == 'number' and field.max is defined %} max="{{ field.max }}" {% endif %} {% if field.type != 'number' and field.maxlength is defined %} maxlength="{{ field.maxlength }}" {% endif %} id="{{ id }}" name="{{ id }}" value="{{ field.value|raw }}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.autocomplete is defined %} autocomplete="{{ field.autocomplete }}" {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %} {% if field.type == 'file' and field.accept is defined %} accept="{{ field.accept }}" {% endif %}/>
<input type="{{ field.type }}" {% if field.visible is defined and field.visible == false %} disabled {% endif %} {% if field.type == 'number' and field.min is defined %} min="{{ field.min }}" {% endif %} {% if field.type == 'number' and field.max is defined %} max="{{ field.max }}" {% endif %} {% if field.type != 'number' and field.maxlength is defined %} maxlength="{{ field.maxlength }}" {% endif %} id="{{ id }}" name="{{ id }}" value="{{ field.value|raw }}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.autocomplete is defined %} autocomplete="{{ field.autocomplete }}" {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %} {% if field.type == 'file' and field.accept is defined %} accept="{{ field.accept }}" {% endif %} {% if field.pattern is defined %} pattern="{{ field.pattern }}" {% endif %}/>
{% if field.type == 'hidden' and field.display is defined %}
<input type="text" readonly class="form-control-plaintext" value="{{ field.display|raw }}">
{% endif %}
Expand Down

0 comments on commit 4c6ebde

Please sign in to comment.