diff --git a/app/Actions/Shared/CheckDomainDns.php b/app/Actions/Shared/CheckDomainDns.php index d0cea0fb1c..2a34c34a1d 100644 --- a/app/Actions/Shared/CheckDomainDns.php +++ b/app/Actions/Shared/CheckDomainDns.php @@ -66,6 +66,7 @@ class CheckDomainDns } $type = dnsRecordTypeForIp($expectedIp) === 'AAAA' ? DNSTypes::NAME_AAAA : DNSTypes::NAME_A; + $receivedAddressRecord = false; foreach ($dnsServers as $dnsServer) { $remainingNanoseconds = $deadline - hrtime(true); @@ -90,6 +91,7 @@ class CheckDomainDns continue; } + $receivedAddressRecord = true; if (isCloudflareIp($record->getData()) || ($expectedIp && $record->getData() === $expectedIp)) { return $this->result('ok', $this->successMessage($server, $expectedIp), $expectedIp); } @@ -99,9 +101,42 @@ class CheckDomainDns } } + if (! $receivedAddressRecord && hrtime(true) < $deadline) { + foreach ($this->resolveWithSystemDns($host, $type) as $resolvedIp) { + if (isCloudflareIp($resolvedIp) || ($expectedIp && $resolvedIp === $expectedIp)) { + return $this->result('ok', $this->successMessage($server, $expectedIp), $expectedIp); + } + } + } + return $this->result('failed', dnsMismatchGuidanceMessage($expectedIp, $expectedIp), $expectedIp); } + /** + * @return array + */ + protected function resolveWithSystemDns(string $host, string $type): array + { + $recordType = $type === DNSTypes::NAME_AAAA ? DNS_AAAA : DNS_A; + $addressKey = $type === DNSTypes::NAME_AAAA ? 'ipv6' : 'ip'; + + try { + $records = @dns_get_record($host, $recordType); + } catch (\Throwable) { + return []; + } + + if (! is_array($records)) { + return []; + } + + return collect($records) + ->pluck($addressKey) + ->filter(fn ($address) => is_string($address) && filter_var($address, FILTER_VALIDATE_IP) !== false) + ->values() + ->all(); + } + private function successMessage(Server $server, ?string $expectedIp): string { if ( diff --git a/tests/Feature/DnsValidationTest.php b/tests/Feature/DnsValidationTest.php index a9ac104c71..1e03332b7f 100644 --- a/tests/Feature/DnsValidationTest.php +++ b/tests/Feature/DnsValidationTest.php @@ -110,3 +110,81 @@ it('does not start another resolver query after the total dns budget is exhauste ->and($result['example']['message'])->toBe('Could not validate DNS for this domain.') ->and($queryCount)->toHaveCount(0); }); + +it('falls back to the system resolver when custom dns servers return no answers', function () { + InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate( + ['id' => 0], + [ + 'is_dns_validation_enabled' => true, + 'custom_dns_servers' => '192.0.2.1', + ] + )); + + app()->bind(DNSQuery::class, fn () => new class('192.0.2.1') extends DNSQuery + { + public function query(string $question, string $typeName = DNSTypes::NAME_A): false + { + return false; + } + + public function hasError(): bool + { + return true; + } + }); + + $action = new class extends CheckDomainDns + { + protected function resolveWithSystemDns(string $host, string $type): array + { + return ['203.0.113.10']; + } + }; + + $result = $action->handle( + ['example' => 'https://example.com'], + new Server(['ip' => '203.0.113.10']), + '203.0.113.10', + ); + + expect($result['example']['status'])->toBe('ok'); +}); + +it('does not use the system resolver after a custom dns server returns an address', function () { + InstanceSettings::unguarded(fn () => InstanceSettings::query()->updateOrCreate( + ['id' => 0], + [ + 'is_dns_validation_enabled' => true, + 'custom_dns_servers' => '192.0.2.1', + ] + )); + + app()->bind(DNSQuery::class, fn () => new class('192.0.2.1') extends DNSQuery + { + public function query(string $question, string $typeName = DNSTypes::NAME_A): array + { + return [new DNSResult($typeName, 1, 'IN', 60, '198.51.100.20', $question, '', [])]; + } + + public function hasError(): bool + { + return false; + } + }); + + $action = new class extends CheckDomainDns + { + protected function resolveWithSystemDns(string $host, string $type): array + { + throw new RuntimeException('The system resolver must not be used.'); + } + }; + + $result = $action->handle( + ['example' => 'https://example.com'], + new Server(['ip' => '203.0.113.10']), + '203.0.113.10', + ); + + expect($result['example']['status'])->toBe('failed'); +});