feat: forcer le resolver DNS 1.1.1.1 via dig + fallback dns_get_record
src/Service/DnsCheckService.php: - Constante RESOLVER = '1.1.1.1' (Cloudflare DNS) - Methode dig() utilise la commande dig @1.1.1.1 pour toutes les requetes DNS afin d'avoir des resultats coherents quel que soit le resolver local du serveur - isDigAvailable(): detecte si dig est installe (cache static) - fallbackDnsGetRecord(): quand dig n'est pas installe, utilise dns_get_record() PHP natif et formate la sortie au format dig +noall +answer pour que le parsing reste identique - getTxtRecords(), getCnameRecord(), getMxRecords(), getSrvRecords() utilisent tous dig() en interne - getCnameRecord() et getSrvRecords() rendues publiques pour utilisation par la commande src/Command/CheckDnsCommand.php: - Suppression du check DKIM generique (DKIM verifie uniquement via AWS SES avec les 3 CNAME individuels par domaine) - checkDnsRecordExists(), checkMxExists(), checkTxtContains() utilisent maintenant $this->dnsCheck au lieu de dns_get_record() direct - getCnameRecord() supprimee de la commande (delegue au service) - getMxValues() et getTxtSpfValue() utilisent le service docker/php/dev/Dockerfile: - Ajout du paquet dnsutils (fournit la commande dig) docker/php/prod/Dockerfile: - Ajout du paquet dnsutils (fournit la commande dig) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
unzip \
|
||||
qpdf \
|
||||
curl \
|
||||
dnsutils \
|
||||
python3 \
|
||||
python3-pip \
|
||||
git \
|
||||
|
||||
@@ -12,6 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libfreetype-dev \
|
||||
libmagickwand-dev \
|
||||
unzip \
|
||||
dnsutils \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install \
|
||||
|
||||
@@ -70,7 +70,7 @@ class CheckDnsCommand extends Command
|
||||
$this->dnsCheck->checkDmarc($domain, $checks, $errors, $successes);
|
||||
$this->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords);
|
||||
|
||||
$this->dnsCheck->checkDkim($domain, $checks, $errors, $warnings, $successes);
|
||||
// DKIM verifie via AWS SES (3 CNAME individuels), pas de check generique
|
||||
|
||||
$this->dnsCheck->checkMx($domain, self::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes);
|
||||
$this->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
|
||||
@@ -220,7 +220,7 @@ class CheckDnsCommand extends Command
|
||||
foreach ($dkim['tokens'] as $token) {
|
||||
$expectedCname = $token.'.dkim.amazonses.com';
|
||||
$dkimFqdn = $token.'._domainkey.'.$domain;
|
||||
$actualCname = $this->getCnameRecord($dkimFqdn);
|
||||
$actualCname = $this->dnsCheck->getCnameRecord($dkimFqdn);
|
||||
|
||||
$found = null !== $actualCname && str_contains($actualCname, 'dkim.amazonses.com');
|
||||
$checks[] = DnsCheckService::check(
|
||||
@@ -310,10 +310,9 @@ class CheckDnsCommand extends Command
|
||||
|
||||
private function getMxValues(string $domain): string
|
||||
{
|
||||
$records = dns_get_record($domain, \DNS_MX) ?: [];
|
||||
$values = [];
|
||||
foreach ($records as $mx) {
|
||||
$values[] = rtrim($mx['target'] ?? '', '.').' (pri: '.($mx['pri'] ?? '?').')';
|
||||
foreach ($this->dnsCheck->getMxRecords($domain) as $mx) {
|
||||
$values[] = $mx['target'].' (pri: '.$mx['pri'].')';
|
||||
}
|
||||
|
||||
return implode(', ', $values);
|
||||
@@ -321,13 +320,16 @@ class CheckDnsCommand extends Command
|
||||
|
||||
private function getTxtSpfValue(string $domain): string
|
||||
{
|
||||
$records = dns_get_record($domain, \DNS_TXT) ?: [];
|
||||
foreach ($records as $r) {
|
||||
$txt = $r['txt'] ?? '';
|
||||
$output = $this->dnsCheck->dig($domain, 'TXT');
|
||||
|
||||
foreach (explode("\n", $output) as $line) {
|
||||
if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) {
|
||||
$txt = str_replace('" "', '', $m[1]);
|
||||
if (str_starts_with($txt, 'v=spf1')) {
|
||||
return $txt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
@@ -403,16 +405,16 @@ class CheckDnsCommand extends Command
|
||||
return match ($type) {
|
||||
'MX' => $this->checkMxExists($name, $expectedContent),
|
||||
'TXT' => $this->checkTxtContains($name, $expectedContent),
|
||||
'CNAME' => null !== $this->getCnameRecord($name),
|
||||
'SRV' => [] !== (dns_get_record($name, \DNS_SRV) ?: []),
|
||||
'CNAME' => null !== $this->dnsCheck->getCnameRecord($name),
|
||||
'SRV' => [] !== $this->dnsCheck->getSrvRecords($name),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
private function checkMxExists(string $name, string $target): bool
|
||||
{
|
||||
foreach (dns_get_record($name, \DNS_MX) ?: [] as $mx) {
|
||||
if (str_contains(rtrim($mx['target'] ?? '', '.'), $target)) {
|
||||
foreach ($this->dnsCheck->getMxRecords($name) as $mx) {
|
||||
if (str_contains($mx['target'], $target)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -422,22 +424,20 @@ class CheckDnsCommand extends Command
|
||||
|
||||
private function checkTxtContains(string $name, string $start): bool
|
||||
{
|
||||
foreach (dns_get_record($name, \DNS_TXT) ?: [] as $r) {
|
||||
if (str_starts_with($r['txt'] ?? '', $start)) {
|
||||
$output = $this->dnsCheck->dig($name, 'TXT');
|
||||
|
||||
foreach (explode("\n", $output) as $line) {
|
||||
if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) {
|
||||
$txt = str_replace('" "', '', $m[1]);
|
||||
if (str_starts_with($txt, $start)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getCnameRecord(string $domain): ?string
|
||||
{
|
||||
$records = @dns_get_record($domain, \DNS_CNAME) ?: [];
|
||||
|
||||
return [] !== $records ? rtrim($records[0]['target'] ?? '', '.') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $errors
|
||||
* @param list<string> $warnings
|
||||
|
||||
@@ -150,7 +150,7 @@ class DnsCheckService
|
||||
*/
|
||||
public function checkMx(string $domain, string $expectedMx, array &$checks, array &$errors, array &$successes): void
|
||||
{
|
||||
$mxRecords = dns_get_record($domain, \DNS_MX) ?: [];
|
||||
$mxRecords = $this->getMxRecords($domain);
|
||||
|
||||
if ([] === $mxRecords) {
|
||||
$errors[] = "[$domain] MX : absent";
|
||||
@@ -162,9 +162,8 @@ class DnsCheckService
|
||||
$found = false;
|
||||
$digValues = [];
|
||||
foreach ($mxRecords as $mx) {
|
||||
$target = rtrim($mx['target'] ?? '', '.');
|
||||
$digValues[] = $target.' (pri: '.($mx['pri'] ?? '?').')';
|
||||
if (str_contains($target, $expectedMx)) {
|
||||
$digValues[] = $mx['target'].' (pri: '.$mx['pri'].')';
|
||||
if (str_contains($mx['target'], $expectedMx)) {
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
@@ -189,10 +188,10 @@ class DnsCheckService
|
||||
{
|
||||
$bounceDomain = 'bounce.'.$domain;
|
||||
$expected = 'feedback-smtp.*.amazonses.com';
|
||||
$mxRecords = dns_get_record($bounceDomain, \DNS_MX) ?: [];
|
||||
$mxRecords = $this->getMxRecords($bounceDomain);
|
||||
|
||||
if ([] !== $mxRecords) {
|
||||
$target = rtrim($mxRecords[0]['target'] ?? '', '.');
|
||||
$target = $mxRecords[0]['target'];
|
||||
$successes[] = "[$domain] Bounce MX : $target";
|
||||
$checks[] = self::check('Bounce', 'MX '.$bounceDomain, 'ok', $target, $expected, $target);
|
||||
} else {
|
||||
@@ -207,31 +206,120 @@ class DnsCheckService
|
||||
}
|
||||
}
|
||||
|
||||
private const RESOLVER = '1.1.1.1';
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function getTxtRecords(string $domain): array
|
||||
{
|
||||
$records = dns_get_record($domain, \DNS_TXT) ?: [];
|
||||
$output = $this->dig($domain, 'TXT');
|
||||
$records = [];
|
||||
|
||||
return array_map(fn (array $r) => $r['txt'] ?? '', $records);
|
||||
foreach (explode("\n", $output) as $line) {
|
||||
if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) {
|
||||
$records[] = str_replace('" "', '', $m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getCnameRecord(string $domain): ?string
|
||||
return $records;
|
||||
}
|
||||
|
||||
public function getCnameRecord(string $domain): ?string
|
||||
{
|
||||
$records = @dns_get_record($domain, \DNS_CNAME) ?: [];
|
||||
$output = $this->dig($domain, 'CNAME');
|
||||
|
||||
return [] !== $records ? rtrim($records[0]['target'] ?? '', '.') : null;
|
||||
}
|
||||
|
||||
private function getDkimTxtRecord(string $domain): ?string
|
||||
{
|
||||
foreach ($this->getTxtRecords($domain) as $record) {
|
||||
if (str_contains($record, 'v=DKIM1') || str_contains($record, 'k=rsa')) {
|
||||
return $record;
|
||||
}
|
||||
if (preg_match('/\bIN\s+CNAME\s+(\S+)/', $output, $m)) {
|
||||
return rtrim($m[1], '.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{target: string, pri: int}>
|
||||
*/
|
||||
public function getMxRecords(string $domain): array
|
||||
{
|
||||
$output = $this->dig($domain, 'MX');
|
||||
$records = [];
|
||||
|
||||
foreach (explode("\n", $output) as $line) {
|
||||
if (preg_match('/\bIN\s+MX\s+(\d+)\s+(\S+)/', $line, $m)) {
|
||||
$records[] = ['target' => rtrim($m[2], '.'), 'pri' => (int) $m[1]];
|
||||
}
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{target: string, port: int, priority: int}>
|
||||
*/
|
||||
public function getSrvRecords(string $domain): array
|
||||
{
|
||||
$output = $this->dig($domain, 'SRV');
|
||||
$records = [];
|
||||
|
||||
foreach (explode("\n", $output) as $line) {
|
||||
if (preg_match('/\bIN\s+SRV\s+(\d+)\s+\d+\s+(\d+)\s+(\S+)/', $line, $m)) {
|
||||
$records[] = ['target' => rtrim($m[3], '.'), 'port' => (int) $m[2], 'priority' => (int) $m[1]];
|
||||
}
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
private function isDigAvailable(): bool
|
||||
{
|
||||
static $available = null;
|
||||
if (null === $available) {
|
||||
$available = null !== @shell_exec('which dig 2>/dev/null');
|
||||
}
|
||||
|
||||
return $available;
|
||||
}
|
||||
|
||||
public function dig(string $domain, string $type): string
|
||||
{
|
||||
if (!$this->isDigAvailable()) {
|
||||
return $this->fallbackDnsGetRecord($domain, $type);
|
||||
}
|
||||
|
||||
$cmd = sprintf('dig @%s %s %s +noall +answer 2>/dev/null', self::RESOLVER, escapeshellarg($domain), escapeshellarg($type));
|
||||
$output = @shell_exec($cmd);
|
||||
|
||||
return $output ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback quand dig n'est pas installe : utilise dns_get_record()
|
||||
* et formate la sortie comme dig +noall +answer.
|
||||
*/
|
||||
private function fallbackDnsGetRecord(string $domain, string $type): string
|
||||
{
|
||||
$dnsType = match (strtoupper($type)) {
|
||||
'TXT' => \DNS_TXT,
|
||||
'MX' => \DNS_MX,
|
||||
'CNAME' => \DNS_CNAME,
|
||||
'SRV' => \DNS_SRV,
|
||||
default => \DNS_ANY,
|
||||
};
|
||||
|
||||
$records = @dns_get_record($domain, $dnsType) ?: [];
|
||||
$lines = [];
|
||||
|
||||
foreach ($records as $r) {
|
||||
$t = $r['type'] ?? '';
|
||||
match ($t) {
|
||||
'TXT' => $lines[] = "$domain.\tIN\tTXT\t\"".($r['txt'] ?? '')."\"",
|
||||
'MX' => $lines[] = "$domain.\tIN\tMX\t".($r['pri'] ?? 0).' '.($r['target'] ?? ''),
|
||||
'CNAME' => $lines[] = "$domain.\tIN\tCNAME\t".($r['target'] ?? ''),
|
||||
'SRV' => $lines[] = "$domain.\tIN\tSRV\t".($r['pri'] ?? 0).' '.($r['weight'] ?? 0).' '.($r['port'] ?? 0).' '.($r['target'] ?? ''),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user