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 \
|
unzip \
|
||||||
qpdf \
|
qpdf \
|
||||||
curl \
|
curl \
|
||||||
|
dnsutils \
|
||||||
python3 \
|
python3 \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
git \
|
git \
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libfreetype-dev \
|
libfreetype-dev \
|
||||||
libmagickwand-dev \
|
libmagickwand-dev \
|
||||||
unzip \
|
unzip \
|
||||||
|
dnsutils \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||||
&& docker-php-ext-install \
|
&& docker-php-ext-install \
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class CheckDnsCommand extends Command
|
|||||||
$this->dnsCheck->checkDmarc($domain, $checks, $errors, $successes);
|
$this->dnsCheck->checkDmarc($domain, $checks, $errors, $successes);
|
||||||
$this->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords);
|
$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->dnsCheck->checkMx($domain, self::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes);
|
||||||
$this->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
|
$this->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
|
||||||
@@ -220,7 +220,7 @@ class CheckDnsCommand extends Command
|
|||||||
foreach ($dkim['tokens'] as $token) {
|
foreach ($dkim['tokens'] as $token) {
|
||||||
$expectedCname = $token.'.dkim.amazonses.com';
|
$expectedCname = $token.'.dkim.amazonses.com';
|
||||||
$dkimFqdn = $token.'._domainkey.'.$domain;
|
$dkimFqdn = $token.'._domainkey.'.$domain;
|
||||||
$actualCname = $this->getCnameRecord($dkimFqdn);
|
$actualCname = $this->dnsCheck->getCnameRecord($dkimFqdn);
|
||||||
|
|
||||||
$found = null !== $actualCname && str_contains($actualCname, 'dkim.amazonses.com');
|
$found = null !== $actualCname && str_contains($actualCname, 'dkim.amazonses.com');
|
||||||
$checks[] = DnsCheckService::check(
|
$checks[] = DnsCheckService::check(
|
||||||
@@ -310,10 +310,9 @@ class CheckDnsCommand extends Command
|
|||||||
|
|
||||||
private function getMxValues(string $domain): string
|
private function getMxValues(string $domain): string
|
||||||
{
|
{
|
||||||
$records = dns_get_record($domain, \DNS_MX) ?: [];
|
|
||||||
$values = [];
|
$values = [];
|
||||||
foreach ($records as $mx) {
|
foreach ($this->dnsCheck->getMxRecords($domain) as $mx) {
|
||||||
$values[] = rtrim($mx['target'] ?? '', '.').' (pri: '.($mx['pri'] ?? '?').')';
|
$values[] = $mx['target'].' (pri: '.$mx['pri'].')';
|
||||||
}
|
}
|
||||||
|
|
||||||
return implode(', ', $values);
|
return implode(', ', $values);
|
||||||
@@ -321,11 +320,14 @@ class CheckDnsCommand extends Command
|
|||||||
|
|
||||||
private function getTxtSpfValue(string $domain): string
|
private function getTxtSpfValue(string $domain): string
|
||||||
{
|
{
|
||||||
$records = dns_get_record($domain, \DNS_TXT) ?: [];
|
$output = $this->dnsCheck->dig($domain, 'TXT');
|
||||||
foreach ($records as $r) {
|
|
||||||
$txt = $r['txt'] ?? '';
|
foreach (explode("\n", $output) as $line) {
|
||||||
if (str_starts_with($txt, 'v=spf1')) {
|
if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) {
|
||||||
return $txt;
|
$txt = str_replace('" "', '', $m[1]);
|
||||||
|
if (str_starts_with($txt, 'v=spf1')) {
|
||||||
|
return $txt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,16 +405,16 @@ class CheckDnsCommand extends Command
|
|||||||
return match ($type) {
|
return match ($type) {
|
||||||
'MX' => $this->checkMxExists($name, $expectedContent),
|
'MX' => $this->checkMxExists($name, $expectedContent),
|
||||||
'TXT' => $this->checkTxtContains($name, $expectedContent),
|
'TXT' => $this->checkTxtContains($name, $expectedContent),
|
||||||
'CNAME' => null !== $this->getCnameRecord($name),
|
'CNAME' => null !== $this->dnsCheck->getCnameRecord($name),
|
||||||
'SRV' => [] !== (dns_get_record($name, \DNS_SRV) ?: []),
|
'SRV' => [] !== $this->dnsCheck->getSrvRecords($name),
|
||||||
default => false,
|
default => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkMxExists(string $name, string $target): bool
|
private function checkMxExists(string $name, string $target): bool
|
||||||
{
|
{
|
||||||
foreach (dns_get_record($name, \DNS_MX) ?: [] as $mx) {
|
foreach ($this->dnsCheck->getMxRecords($name) as $mx) {
|
||||||
if (str_contains(rtrim($mx['target'] ?? '', '.'), $target)) {
|
if (str_contains($mx['target'], $target)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,22 +424,20 @@ class CheckDnsCommand extends Command
|
|||||||
|
|
||||||
private function checkTxtContains(string $name, string $start): bool
|
private function checkTxtContains(string $name, string $start): bool
|
||||||
{
|
{
|
||||||
foreach (dns_get_record($name, \DNS_TXT) ?: [] as $r) {
|
$output = $this->dnsCheck->dig($name, 'TXT');
|
||||||
if (str_starts_with($r['txt'] ?? '', $start)) {
|
|
||||||
return true;
|
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;
|
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> $errors
|
||||||
* @param list<string> $warnings
|
* @param list<string> $warnings
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class DnsCheckService
|
|||||||
*/
|
*/
|
||||||
public function checkMx(string $domain, string $expectedMx, array &$checks, array &$errors, array &$successes): void
|
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) {
|
if ([] === $mxRecords) {
|
||||||
$errors[] = "[$domain] MX : absent";
|
$errors[] = "[$domain] MX : absent";
|
||||||
@@ -162,9 +162,8 @@ class DnsCheckService
|
|||||||
$found = false;
|
$found = false;
|
||||||
$digValues = [];
|
$digValues = [];
|
||||||
foreach ($mxRecords as $mx) {
|
foreach ($mxRecords as $mx) {
|
||||||
$target = rtrim($mx['target'] ?? '', '.');
|
$digValues[] = $mx['target'].' (pri: '.$mx['pri'].')';
|
||||||
$digValues[] = $target.' (pri: '.($mx['pri'] ?? '?').')';
|
if (str_contains($mx['target'], $expectedMx)) {
|
||||||
if (str_contains($target, $expectedMx)) {
|
|
||||||
$found = true;
|
$found = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,10 +188,10 @@ class DnsCheckService
|
|||||||
{
|
{
|
||||||
$bounceDomain = 'bounce.'.$domain;
|
$bounceDomain = 'bounce.'.$domain;
|
||||||
$expected = 'feedback-smtp.*.amazonses.com';
|
$expected = 'feedback-smtp.*.amazonses.com';
|
||||||
$mxRecords = dns_get_record($bounceDomain, \DNS_MX) ?: [];
|
$mxRecords = $this->getMxRecords($bounceDomain);
|
||||||
|
|
||||||
if ([] !== $mxRecords) {
|
if ([] !== $mxRecords) {
|
||||||
$target = rtrim($mxRecords[0]['target'] ?? '', '.');
|
$target = $mxRecords[0]['target'];
|
||||||
$successes[] = "[$domain] Bounce MX : $target";
|
$successes[] = "[$domain] Bounce MX : $target";
|
||||||
$checks[] = self::check('Bounce', 'MX '.$bounceDomain, 'ok', $target, $expected, $target);
|
$checks[] = self::check('Bounce', 'MX '.$bounceDomain, 'ok', $target, $expected, $target);
|
||||||
} else {
|
} else {
|
||||||
@@ -207,31 +206,120 @@ class DnsCheckService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const RESOLVER = '1.1.1.1';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
*/
|
*/
|
||||||
private function getTxtRecords(string $domain): array
|
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
|
|
||||||
{
|
|
||||||
$records = @dns_get_record($domain, \DNS_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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $records;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCnameRecord(string $domain): ?string
|
||||||
|
{
|
||||||
|
$output = $this->dig($domain, 'CNAME');
|
||||||
|
|
||||||
|
if (preg_match('/\bIN\s+CNAME\s+(\S+)/', $output, $m)) {
|
||||||
|
return rtrim($m[1], '.');
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
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