Effettuare query DNS in Laravel
Il DNS (Domain Name System) è il sistema che traduce i nomi di dominio in indirizzi IP e viceversa, ed è alla base del funzionamento di Internet. In molte applicazioni web è necessario effettuare query DNS direttamente dal backend: per validare indirizzi email, verificare la configurazione di un dominio, controllare la presenza di record SPF, DKIM o MX prima di inviare messaggi, oppure semplicemente per diagnosticare problemi di rete. Laravel non dispone di un facade dedicato esclusivamente alle query DNS, ma offre diversi strumenti — nativi di PHP e integrati nell'ecosistema del framework — che permettono di eseguire queste operazioni in modo pulito ed efficace. In questo articolo vedremo come sfruttarli al meglio, partendo dalle funzioni PHP di base fino ad arrivare a soluzioni più strutturate con classi dedicate e test automatici.
Le funzioni PHP native per il DNS
PHP mette a disposizione alcune funzioni native per interrogare il DNS. Le più rilevanti sono
dns_get_record(), checkdnsrr() e gethostbyname() /
gethostbyaddr(). Queste funzioni sono disponibili in qualsiasi ambiente PHP
senza dipendenze aggiuntive e possono essere usate direttamente in Laravel come in qualsiasi
altro progetto PHP.
La funzione dns_get_record() è la più potente: accetta un nome di dominio,
un tipo di record DNS (o la costante DNS_ALL per recuperarli tutti) e restituisce
un array associativo con i risultati. I tipi di record supportati includono
DNS_A, DNS_AAAA, DNS_CNAME, DNS_MX,
DNS_NS, DNS_TXT, DNS_SOA e altri.
<?php
// Recupera tutti i record MX per un dominio
$records = dns_get_record('example.com', DNS_MX);
foreach ($records as $record) {
// Stampa il target e la priorità di ogni record MX
echo $record['target'] . ' (priorità: ' . $record['pri'] . ')';
}
La funzione checkdnsrr() è più semplice: restituisce true se
esiste almeno un record del tipo specificato per il dominio dato, false
altrimenti. È utile per una verifica rapida senza dover analizzare i dettagli del record.
<?php
// Controlla se esiste un record MX per il dominio
if (checkdnsrr('example.com', 'MX')) {
// Il dominio accetta posta elettronica
echo 'Il dominio ha un record MX.';
} else {
echo 'Nessun record MX trovato.';
}
Le funzioni gethostbyname() e gethostbyaddr() effettuano rispettivamente
una risoluzione diretta (da nome a IP) e inversa (da IP a nome). Sono le più semplici da usare
ma anche le meno flessibili, poiché non permettono di specificare il tipo di record.
<?php
// Risoluzione diretta: nome a IPv4
$ipAddress = gethostbyname('example.com');
// Risoluzione inversa: IP a nome host
$hostname = gethostbyaddr('93.184.216.34');
Creare un servizio DNS in Laravel
Usare le funzioni PHP native direttamente nei controller o nei job non è una buona pratica: rende il codice difficile da testare e da mantenere. La soluzione corretta in Laravel è incapsulare la logica DNS in un servizio dedicato, registrarlo nel container e iniettarlo dove necessario tramite dependency injection.
Creiamo prima la classe del servizio nella directory app/Services:
<?php
namespace App\Services;
class DnsLookupService
{
/**
* Restituisce i record DNS di un dominio per il tipo specificato.
*/
public function getRecords(string $domain, int $type = DNS_ALL): array
{
// Recupera i record dal resolver di sistema
$results = dns_get_record($domain, $type);
// Restituisce un array vuoto in caso di errore
return $results !== false ? $results : [];
}
/**
* Controlla se esiste almeno un record del tipo specificato.
*/
public function hasRecord(string $domain, string $type): bool
{
return checkdnsrr($domain, $type);
}
/**
* Restituisce il primo indirizzo IPv4 associato al dominio.
*/
public function resolveIpv4(string $domain): ?string
{
$records = $this->getRecords($domain, DNS_A);
// Prende il primo risultato disponibile
return $records[0]['ip'] ?? null;
}
/**
* Restituisce il primo indirizzo IPv6 associato al dominio.
*/
public function resolveIpv6(string $domain): ?string
{
$records = $this->getRecords($domain, DNS_AAAA);
// Prende il primo risultato disponibile
return $records[0]['ipv6'] ?? null;
}
/**
* Restituisce i record MX ordinati per priorità crescente.
*/
public function getMxRecords(string $domain): array
{
$records = $this->getRecords($domain, DNS_MX);
// Ordina per priorità (campo pri): valore più basso = priorità più alta
usort($records, fn($a, $b) => $a['pri'] <=> $b['pri']);
return $records;
}
/**
* Restituisce tutti i record TXT del dominio.
*/
public function getTxtRecords(string $domain): array
{
return $this->getRecords($domain, DNS_TXT);
}
/**
* Restituisce i nameserver (NS) del dominio.
*/
public function getNameservers(string $domain): array
{
return $this->getRecords($domain, DNS_NS);
}
/**
* Effettua la risoluzione inversa di un indirizzo IP.
*/
public function reverseResolve(string $ipAddress): ?string
{
$hostname = gethostbyaddr($ipAddress);
// gethostbyaddr restituisce l'IP originale se la risoluzione fallisce
return $hostname !== $ipAddress ? $hostname : null;
}
}
Ora registriamo il servizio in un service provider. Possiamo usare l'AppServiceProvider
già esistente oppure crearne uno dedicato con php artisan make:provider DnsServiceProvider.
<?php
namespace App\Providers;
use App\Services\DnsLookupService;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Registra il servizio come singleton nel container
$this->app->singleton(DnsLookupService::class, function () {
return new DnsLookupService();
});
}
public function boot(): void
{
//
}
}
Utilizzare il servizio in un controller
Una volta registrato nel container, il servizio può essere iniettato automaticamente da Laravel tramite il costruttore del controller o direttamente nel metodo dell'azione (route model binding e dependency injection automatica funzionano anche per i servizi applicativi).
<?php
namespace App\Http\Controllers;
use App\Services\DnsLookupService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DnsController extends Controller
{
public function __construct(
// Iniezione automatica tramite il container di Laravel
private readonly DnsLookupService $dnsService
) {}
/**
* Restituisce un riepilogo DNS completo per il dominio richiesto.
*/
public function lookup(Request $request): JsonResponse
{
$request->validate([
// Il campo domain è obbligatorio e deve essere un dominio valido
'domain' => ['required', 'string', 'max:253'],
]);
$domain = $request->input('domain');
$result = [
'domain' => $domain,
'ipv4' => $this->dnsService->resolveIpv4($domain),
'ipv6' => $this->dnsService->resolveIpv6($domain),
'mx' => $this->dnsService->getMxRecords($domain),
'txt' => $this->dnsService->getTxtRecords($domain),
'nameservers' => $this->dnsService->getNameservers($domain),
];
return response()->json($result);
}
/**
* Verifica la presenza di un record MX per validare un dominio email.
*/
public function checkMx(Request $request): JsonResponse
{
$request->validate([
'domain' => ['required', 'string'],
]);
$domain = $request->input('domain');
$hasMx = $this->dnsService->hasRecord($domain, 'MX');
return response()->json([
'domain' => $domain,
'has_mx' => $hasMx,
]);
}
/**
* Effettua la risoluzione inversa di un indirizzo IP.
*/
public function reverseResolve(Request $request): JsonResponse
{
$request->validate([
'ip' => ['required', 'ip'],
]);
$ip = $request->input('ip');
$hostname = $this->dnsService->reverseResolve($ip);
return response()->json([
'ip' => $ip,
'hostname' => $hostname,
]);
}
}
Le rotte corrispondenti possono essere definite in routes/api.php:
<?php
use App\Http\Controllers\DnsController;
use Illuminate\Support\Facades\Route;
// Gruppo di rotte per le operazioni DNS
Route::prefix('dns')->group(function () {
Route::get('/lookup', [DnsController::class, 'lookup']);
Route::get('/check-mx', [DnsController::class, 'checkMx']);
Route::get('/reverse', [DnsController::class, 'reverseResolve']);
});
Validazione email con verifica DNS
Uno degli utilizzi più comuni delle query DNS in un'applicazione web è la validazione avanzata
degli indirizzi email: oltre a verificare il formato con una regex o con la regola email
di Laravel, è possibile controllare che il dominio dell'indirizzo abbia effettivamente un record
MX, il che significa che è configurato per ricevere posta. Questo riduce drasticamente l'inserimento
di email inesistenti o con errori di digitazione nel dominio.
Creiamo una regola di validazione personalizzata con il comando Artisan:
php artisan make:rule DomainHasMxRecord
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class DomainHasMxRecord implements ValidationRule
{
/**
* Verifica che il dominio dell'email abbia almeno un record MX.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// Estrae il dominio dalla parte destra dell'indirizzo email
$domain = substr(strrchr($value, '@'), 1);
if (empty($domain)) {
$fail("Il campo :attribute non contiene un dominio valido.");
return;
}
// Controlla la presenza di record MX tramite DNS
if (!checkdnsrr($domain, 'MX')) {
$fail("Il dominio '$domain' non ha record MX configurati.");
}
}
}
La regola può essere usata in un form request o direttamente in un controller:
<?php
namespace App\Http\Controllers;
use App\Rules\DomainHasMxRecord;
use Illuminate\Http\Request;
class RegistrationController extends Controller
{
public function store(Request $request)
{
$request->validate([
// Valida il formato e poi controlla il DNS
'email' => ['required', 'email:rfc,dns', new DomainHasMxRecord()],
'name' => ['required', 'string', 'max:255'],
]);
// Procede con la registrazione dell'utente
}
}
Verificare record SPF e DKIM tramite TXT
I record SPF (Sender Policy Framework) e DKIM (DomainKeys Identified Mail) sono entrambi pubblicati come record TXT nel DNS. SPF specifica quali server sono autorizzati a inviare email per conto di un dominio; DKIM pubblica la chiave pubblica usata per verificare la firma crittografica dei messaggi. Verificare questi record è utile per diagnosticare problemi di deliverability nella propria applicazione.
<?php
namespace App\Services;
class EmailAuthDnsService
{
/**
* Recupera il record SPF del dominio, se presente.
*/
public function getSpfRecord(string $domain): ?string
{
$txtRecords = dns_get_record($domain, DNS_TXT);
if ($txtRecords === false) {
return null;
}
foreach ($txtRecords as $record) {
$text = $record['txt'] ?? '';
// I record SPF iniziano sempre con "v=spf1"
if (str_starts_with($text, 'v=spf1')) {
return $text;
}
}
return null;
}
/**
* Recupera la chiave pubblica DKIM per il selettore specificato.
*/
public function getDkimRecord(string $domain, string $selector): ?string
{
// Il record DKIM si trova sempre su un sottodominio con questo formato
$dkimDomain = "{$selector}._domainkey.{$domain}";
$txtRecords = dns_get_record($dkimDomain, DNS_TXT);
if ($txtRecords === false || empty($txtRecords)) {
return null;
}
// Restituisce il testo del primo record trovato
return $txtRecords[0]['txt'] ?? null;
}
/**
* Verifica se il dominio ha un record DMARC configurato.
*/
public function hasDmarcRecord(string $domain): bool
{
// DMARC si trova sempre sul sottodominio _dmarc
$dmarcDomain = "_dmarc.{$domain}";
$txtRecords = dns_get_record($dmarcDomain, DNS_TXT);
if ($txtRecords === false || empty($txtRecords)) {
return false;
}
foreach ($txtRecords as $record) {
// I record DMARC iniziano con "v=DMARC1"
if (str_starts_with($record['txt'] ?? '', 'v=DMARC1')) {
return true;
}
}
return false;
}
}
Eseguire query DNS nei job e nelle code
Le query DNS possono essere lente, soprattutto quando il resolver di sistema deve contattare server remoti o quando si interrogano domini che non esistono (il timeout può richiedere diversi secondi). Per questo motivo, in un'applicazione Laravel è consigliabile spostare le query DNS onerose all'interno di job eseguiti in background tramite il sistema di code.
<?php
namespace App\Jobs;
use App\Models\Domain;
use App\Services\DnsLookupService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CheckDomainDnsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Numero massimo di tentativi in caso di errore
public int $tries = 3;
// Timeout in secondi per ciascun tentativo
public int $timeout = 30;
public function __construct(
private readonly Domain $domain
) {}
/**
* Esegue la verifica DNS del dominio in background.
*/
public function handle(DnsLookupService $dnsService): void
{
// Recupera l'indirizzo IPv4 del dominio
$ipv4 = $dnsService->resolveIpv4($this->domain->name);
// Verifica la presenza di record MX
$hasMx = $dnsService->hasRecord($this->domain->name, 'MX');
// Recupera i record TXT per l'analisi SPF/DKIM/DMARC
$txtRecords = $dnsService->getTxtRecords($this->domain->name);
// Aggiorna il modello con i dati raccolti
$this->domain->update([
'resolved_ip' => $ipv4,
'has_mx' => $hasMx,
'txt_records' => json_encode($txtRecords),
'last_checked' => now(),
]);
}
}
Il job può essere dispatchato da un controller o da un comando Artisan:
<?php
// Dispatch immediato nella coda predefinita
CheckDomainDnsJob::dispatch($domain);
// Dispatch con ritardo di 5 minuti
CheckDomainDnsJob::dispatch($domain)->delay(now()->addMinutes(5));
// Dispatch su una coda specifica
CheckDomainDnsJob::dispatch($domain)->onQueue('dns-checks');
Cache dei risultati DNS
Il DNS dispone già di un meccanismo di caching basato sul TTL (Time To Live) di ciascun record,
ma a livello applicativo è comunque utile introdurre un ulteriore layer di cache per evitare
query ripetute a breve distanza di tempo. Laravel mette a disposizione il facade Cache
che supporta diversi driver (Redis, Memcached, file, database). Integrare la cache nel servizio
DNS è semplice e migliora significativamente le prestazioni quando lo stesso dominio viene
interrogato più volte nella stessa sessione o in sessioni ravvicinate.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
class CachedDnsLookupService extends DnsLookupService
{
// Durata della cache in secondi (corrisponde a un TTL DNS tipico)
private int $cacheTtl = 300;
/**
* Restituisce i record DNS con caching automatico.
*/
public function getRecords(string $domain, int $type = DNS_ALL): array
{
// Genera una chiave univoca basata su dominio e tipo di record
$cacheKey = "dns:{$domain}:{$type}";
return Cache::remember($cacheKey, $this->cacheTtl, function () use ($domain, $type) {
// Esegue la query DNS solo se la cache è scaduta o assente
return parent::getRecords($domain, $type);
});
}
/**
* Invalida la cache per un dominio specifico.
*/
public function flushDomainCache(string $domain): void
{
// Tipi di record comuni da invalidare
$types = [DNS_ALL, DNS_A, DNS_AAAA, DNS_MX, DNS_TXT, DNS_NS, DNS_CNAME];
foreach ($types as $type) {
Cache::forget("dns:{$domain}:{$type}");
}
}
}
Scrivere test per il servizio DNS
Le funzioni PHP native per il DNS non sono facilmente testabili in isolamento perché dipendono
dalla rete e dal resolver di sistema. La soluzione standard in Laravel è creare un'interfaccia
per il servizio DNS e iniettare un mock nei test. In alternativa, si può usare
Mockery direttamente per sostituire il servizio concreto con una versione
controllata che restituisce dati predefiniti.
Definiamo prima un'interfaccia per il servizio:
<?php
namespace App\Contracts;
interface DnsLookupServiceInterface
{
public function getRecords(string $domain, int $type): array;
public function hasRecord(string $domain, string $type): bool;
public function resolveIpv4(string $domain): ?string;
public function getMxRecords(string $domain): array;
public function getTxtRecords(string $domain): array;
}
Aggiorniamo il service provider per registrare l'implementazione concreta legata all'interfaccia:
<?php
use App\Contracts\DnsLookupServiceInterface;
use App\Services\DnsLookupService;
// Lega l'interfaccia all'implementazione concreta
$this->app->bind(DnsLookupServiceInterface::class, DnsLookupService::class);
Ora possiamo scrivere un test che usa un mock dell'interfaccia:
<?php
namespace Tests\Unit;
use App\Contracts\DnsLookupServiceInterface;
use App\Rules\DomainHasMxRecord;
use Mockery;
use Tests\TestCase;
class DnsLookupServiceTest extends TestCase
{
public function test_resolves_ipv4_correctly(): void
{
// Crea un mock dell'interfaccia DNS
$mockService = Mockery::mock(DnsLookupServiceInterface::class);
// Definisce il comportamento atteso
$mockService->shouldReceive('resolveIpv4')
->with('example.com')
->once()
->andReturn('93.184.216.34');
// Inietta il mock nel container di Laravel
$this->app->instance(DnsLookupServiceInterface::class, $mockService);
// Risolve il servizio e verifica il risultato
$service = app(DnsLookupServiceInterface::class);
$ip = $service->resolveIpv4('example.com');
$this->assertEquals('93.184.216.34', $ip);
}
public function test_returns_null_when_no_ipv4_found(): void
{
$mockService = Mockery::mock(DnsLookupServiceInterface::class);
// Simula un dominio senza record A
$mockService->shouldReceive('resolveIpv4')
->with('nonexistent.invalid')
->once()
->andReturn(null);
$this->app->instance(DnsLookupServiceInterface::class, $mockService);
$service = app(DnsLookupServiceInterface::class);
$result = $service->resolveIpv4('nonexistent.invalid');
$this->assertNull($result);
}
public function test_mx_records_are_sorted_by_priority(): void
{
// Record MX non ordinati da restituire come risposta del mock
$unsortedRecords = [
['target' => 'mx2.example.com', 'pri' => 20],
['target' => 'mx1.example.com', 'pri' => 10],
['target' => 'mx3.example.com', 'pri' => 30],
];
$mockService = Mockery::mock(DnsLookupServiceInterface::class);
$mockService->shouldReceive('getMxRecords')
->with('example.com')
->once()
->andReturn($unsortedRecords);
$this->app->instance(DnsLookupServiceInterface::class, $mockService);
$service = app(DnsLookupServiceInterface::class);
$records = $service->getMxRecords('example.com');
// Verifica che il primo record abbia la priorità più alta (valore più basso)
$this->assertEquals('mx2.example.com', $records[0]['target']);
}
protected function tearDown(): void
{
// Chiude i mock di Mockery dopo ogni test
Mockery::close();
parent::tearDown();
}
}
Considerazioni sulle prestazioni e sui timeout
Le query DNS tramite le funzioni PHP native utilizzano il resolver di sistema configurato
nel file /etc/resolv.conf del server. In ambienti di produzione, è consigliabile
verificare che il resolver sia configurato correttamente e che i timeout siano adeguati al
tipo di applicazione. Su server Linux è possibile personalizzare il timeout e il numero di
tentativi aggiungendo le direttive timeout e attempts nel file
di configurazione del resolver:
# /etc/resolv.conf
nameserver 1.1.1.1
nameserver 8.8.8.8
options timeout:2 attempts:3
PHP non espone un'opzione nativa per impostare il timeout delle query DNS a livello di codice.
Se si ha la necessità di controllare il timeout in modo preciso, una soluzione alternativa
consiste nel wrappare la query in un processo con timeout usando proc_open()
oppure delegare le query a un client DNS esterno come il pacchetto
reactphp/dns che supporta query asincrone.
Per applicazioni che effettuano molte query DNS (ad esempio sistemi di monitoraggio o strumenti
di analisi di domini) è fortemente consigliato usare Redis come driver di cache e impostare
un TTL allineato ai TTL reali dei record DNS. Un approccio efficace è recuperare il TTL
direttamente dal campo ttl restituito da dns_get_record() e usarlo
come durata della cache:
<?php
/**
* Recupera i record e li memorizza nella cache usando il TTL del DNS.
*/
public function getRecordsWithDynamicTtl(string $domain, int $type): array
{
$cacheKey = "dns:{$domain}:{$type}";
// Controlla prima se i dati sono già in cache
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
$records = dns_get_record($domain, $type);
if ($records === false || empty($records)) {
return [];
}
// Usa il TTL del primo record come durata della cache
$ttl = $records[0]['ttl'] ?? 300;
Cache::put($cacheKey, $records, $ttl);
return $records;
}
Conclusioni
PHP e Laravel offrono tutti gli strumenti necessari per effettuare query DNS in modo efficace:
le funzioni native come dns_get_record() e checkdnsrr() coprono
la maggior parte dei casi d'uso, mentre l'architettura di Laravel — con il suo container,
le interfacce, le regole di validazione personalizzate e il sistema di code — permette di
strutturare il codice in modo pulito, testabile e scalabile. Che si tratti di validare
indirizzi email, diagnosticare configurazioni DNS o costruire strumenti di monitoraggio,
integrare le query DNS in un'applicazione Laravel richiede poche classi ben organizzate
e una gestione attenta della cache e dei timeout.