Verificare la configurazione del record PTR di un dominio con Node.js
Il record PTR (Pointer Record) rappresenta uno degli elementi più importanti e spesso trascurati nella configurazione DNS di un dominio, soprattutto quando si gestisce un'infrastruttura self-hosted che include servizi come server di posta elettronica, applicazioni web pubbliche o API esposte su internet. A differenza dei record DNS più comuni come A, MX o CNAME, che traducono un nome di dominio in un indirizzo IP, il record PTR opera nella direzione opposta: associa un indirizzo IP al nome di dominio corrispondente, realizzando quella che tecnicamente viene definita risoluzione inversa o reverse DNS lookup.
In questo articolo vedremo come implementare un sistema completo in Node.js per verificare la corretta configurazione del record PTR di un dominio, sfruttando il modulo nativo dns e costruendo strumenti pratici che possono essere integrati in script di monitoraggio, applicazioni di uptime checking o pipeline di validazione dell'infrastruttura.
Cos'è il record PTR e perché è importante
Il record PTR è un tipo di record DNS che mappa un indirizzo IP a un nome di dominio. La sua particolarità risiede nel fatto che viene memorizzato in zone DNS speciali chiamate reverse zones, che utilizzano i domini in-addr.arpa per gli indirizzi IPv4 e ip6.arpa per quelli IPv6. Quando si effettua una query inversa per un indirizzo IPv4 come 93.184.216.34, il sistema DNS interroga in realtà il dominio 34.216.184.93.in-addr.arpa, ottenendo come risposta il nome di dominio associato.
La corretta configurazione del record PTR è particolarmente critica nei seguenti scenari:
- Server di posta elettronica: la maggior parte dei provider di email moderni rifiuta o classifica come spam i messaggi provenienti da server il cui indirizzo IP non ha un record PTR coerente con il nome di dominio dichiarato.
- Sicurezza e logging: molti sistemi di sicurezza utilizzano la risoluzione inversa per identificare la provenienza delle connessioni e produrre log più leggibili.
- Verifica dell'autenticità: la corrispondenza tra forward e reverse DNS (FCrDNS, Forward-Confirmed reverse DNS) è un meccanismo di base per stabilire la legittimità di un host.
- Conformità e best practice: protocolli come SMTP, FTP e alcuni servizi enterprise richiedono o raccomandano fortemente una configurazione PTR corretta.
Il modulo dns di Node.js
Node.js fornisce nativamente il modulo dns, che espone un'API completa per effettuare query DNS di vario tipo, incluse quelle di risoluzione inversa. Il modulo offre due interfacce principali: una basata su callback e una basata su Promise, accessibile tramite dns/promises. Quest'ultima è quella che utilizzeremo nei nostri esempi, in quanto permette di scrivere codice asincrono più pulito e moderno.
Le funzioni principali che useremo sono reverse(), che effettua una query PTR a partire da un indirizzo IP, e resolve4() o resolve6(), che risolvono un nome di dominio nei rispettivi indirizzi IPv4 o IPv6. La combinazione di queste funzioni ci permetterà di implementare la verifica FCrDNS.
Implementazione di base
Iniziamo con un esempio semplice che effettua una query PTR per un singolo indirizzo IP. Creiamo un file basic-ptr-check.js con il seguente contenuto:
// Importa il modulo dns con interfaccia Promise-based
import { promises as dns } from 'node:dns';
// Funzione asincrona per ottenere i record PTR di un indirizzo IP
async function getPtrRecords(ipAddress) {
try {
// La funzione reverse restituisce un array di hostname
const hostnames = await dns.reverse(ipAddress);
return {
success: true,
ip: ipAddress,
hostnames: hostnames
};
} catch (error) {
// Gestione degli errori: il record potrebbe non esistere
return {
success: false,
ip: ipAddress,
error: error.code,
message: error.message
};
}
}
// Esecuzione della verifica
const result = await getPtrRecords('8.8.8.8');
console.log(JSON.stringify(result, null, 2));
Eseguendo questo script con node basic-ptr-check.js otterremo un output che mostra l'hostname associato all'indirizzo IP fornito. Nel caso dell'IP 8.8.8.8, che corrisponde al DNS pubblico di Google, il risultato includerà dns.google.
Gestione degli errori comuni
Quando si lavora con query DNS, è fondamentale gestire correttamente i diversi tipi di errore che possono verificarsi. Il modulo dns di Node.js espone codici di errore specifici che ci permettono di distinguere tra le varie situazioni problematiche. Vediamo i principali:
import { promises as dns } from 'node:dns';
// Mappa dei codici di errore DNS più comuni
const dnsErrorMessages = {
'ENOTFOUND': 'Record DNS non trovato',
'ENODATA': 'Nessun dato disponibile per la query',
'ESERVFAIL': 'Errore del server DNS',
'EREFUSED': 'Query rifiutata dal server DNS',
'ETIMEOUT': 'Timeout della query DNS',
'EBADQUERY': 'Query DNS malformata',
'EBADRESP': 'Risposta DNS non valida'
};
// Funzione che effettua la query con gestione dettagliata degli errori
async function checkPtrWithErrorHandling(ipAddress) {
// Validazione preliminare dell'input
if (!ipAddress || typeof ipAddress !== 'string') {
throw new Error('Indirizzo IP non valido');
}
try {
const hostnames = await dns.reverse(ipAddress);
// Verifica che ci sia almeno un hostname
if (hostnames.length === 0) {
return {
status: 'no-records',
ip: ipAddress,
message: 'Nessun record PTR configurato'
};
}
return {
status: 'success',
ip: ipAddress,
hostnames: hostnames,
count: hostnames.length
};
} catch (error) {
// Recupera il messaggio descrittivo o usa quello di default
const friendlyMessage = dnsErrorMessages[error.code] || 'Errore sconosciuto';
return {
status: 'error',
ip: ipAddress,
errorCode: error.code,
message: friendlyMessage,
originalError: error.message
};
}
}
// Test con diversi scenari
const testIps = ['8.8.8.8', '192.168.1.1', '1.2.3.4'];
for (const ip of testIps) {
const result = await checkPtrWithErrorHandling(ip);
console.log(`Risultato per ${ip}:`, result);
console.log('---');
}
Verifica FCrDNS: Forward-Confirmed reverse DNS
La verifica FCrDNS rappresenta il vero standard per validare la corretta configurazione del record PTR di un dominio. Il processo si articola in tre passaggi: prima si risolve il nome di dominio nel suo indirizzo IP, poi si effettua la query PTR su quell'indirizzo IP per ottenere il nome di dominio associato, infine si verifica che i due nomi di dominio coincidano. Solo quando questa corrispondenza è verificata possiamo dire che il dominio ha una configurazione PTR coerente.
import { promises as dns } from 'node:dns';
// Classe che incapsula la logica di verifica FCrDNS
class FcrdnsValidator {
constructor(options = {}) {
// Timeout configurabile per le query DNS
this.timeout = options.timeout || 5000;
// Server DNS personalizzati (opzionale)
this.servers = options.servers || null;
if (this.servers) {
dns.setServers(this.servers);
}
}
// Risolve un dominio in un array di indirizzi IPv4
async resolveForward(domain) {
try {
const addresses = await dns.resolve4(domain);
return { success: true, addresses };
} catch (error) {
return { success: false, error: error.code };
}
}
// Effettua la query PTR per un indirizzo IP
async resolveReverse(ipAddress) {
try {
const hostnames = await dns.reverse(ipAddress);
return { success: true, hostnames };
} catch (error) {
return { success: false, error: error.code };
}
}
// Esegue la verifica FCrDNS completa
async validate(domain) {
const report = {
domain: domain,
timestamp: new Date().toISOString(),
forwardResolution: null,
reverseResolutions: [],
isValid: false,
issues: []
};
// Step 1: Risoluzione forward (dominio -> IP)
const forwardResult = await this.resolveForward(domain);
if (!forwardResult.success) {
report.issues.push(`Impossibile risolvere il dominio: ${forwardResult.error}`);
return report;
}
report.forwardResolution = forwardResult.addresses;
// Step 2: Per ogni IP, effettua la risoluzione inversa
for (const ip of forwardResult.addresses) {
const reverseResult = await this.resolveReverse(ip);
const ipReport = {
ip: ip,
ptrRecords: [],
matchesDomain: false
};
if (reverseResult.success) {
ipReport.ptrRecords = reverseResult.hostnames;
// Step 3: Verifica della corrispondenza
ipReport.matchesDomain = reverseResult.hostnames.some(
hostname => hostname.toLowerCase() === domain.toLowerCase()
);
if (!ipReport.matchesDomain) {
report.issues.push(
`Il record PTR per ${ip} non corrisponde a ${domain}`
);
}
} else {
report.issues.push(
`Nessun record PTR trovato per ${ip}: ${reverseResult.error}`
);
}
report.reverseResolutions.push(ipReport);
}
// La validazione è positiva solo se tutti gli IP hanno PTR corrispondenti
report.isValid = report.reverseResolutions.length > 0 &&
report.reverseResolutions.every(r => r.matchesDomain);
return report;
}
}
// Utilizzo del validator
const validator = new FcrdnsValidator();
const validationReport = await validator.validate('google.com');
console.log('Report di validazione FCrDNS:');
console.log(JSON.stringify(validationReport, null, 2));
Implementazione di un timeout personalizzato
Le query DNS, soprattutto quelle inverse, possono talvolta richiedere tempo o non rispondere affatto. È quindi una buona pratica implementare un meccanismo di timeout che eviti di bloccare l'applicazione in attesa di risposte che potrebbero non arrivare mai. Possiamo realizzarlo combinando Promise.race() con una promise di timeout:
import { promises as dns } from 'node:dns';
// Helper per creare una promise di timeout
function createTimeout(ms, errorMessage = 'Operazione scaduta') {
return new Promise((_, reject) => {
setTimeout(() => {
const timeoutError = new Error(errorMessage);
timeoutError.code = 'ETIMEOUT';
reject(timeoutError);
}, ms);
});
}
// Wrapper per query PTR con timeout
async function reverseWithTimeout(ipAddress, timeoutMs = 3000) {
return Promise.race([
dns.reverse(ipAddress),
createTimeout(timeoutMs, `Timeout PTR per ${ipAddress}`)
]);
}
// Wrapper per risoluzione forward con timeout
async function resolve4WithTimeout(domain, timeoutMs = 3000) {
return Promise.race([
dns.resolve4(domain),
createTimeout(timeoutMs, `Timeout risoluzione per ${domain}`)
]);
}
// Esempio di utilizzo
async function checkDomainWithTimeout(domain) {
try {
const startTime = Date.now();
const ips = await resolve4WithTimeout(domain, 2000);
const ptrChecks = await Promise.allSettled(
ips.map(ip => reverseWithTimeout(ip, 2000))
);
const elapsedMs = Date.now() - startTime;
return {
domain,
ips,
ptrChecks: ptrChecks.map((result, index) => ({
ip: ips[index],
status: result.status,
value: result.status === 'fulfilled' ? result.value : null,
reason: result.status === 'rejected' ? result.reason.message : null
})),
elapsedMs
};
} catch (error) {
return {
domain,
error: error.message,
code: error.code
};
}
}
const checkResult = await checkDomainWithTimeout('cloudflare.com');
console.log(JSON.stringify(checkResult, null, 2));
Verifica multi-dominio in parallelo
In scenari reali, soprattutto quando si gestisce un'infrastruttura con molteplici domini, è utile poter verificare lo stato dei record PTR di più domini contemporaneamente. Sfruttando Promise.all() o Promise.allSettled() possiamo parallelizzare le query, ottenendo tempi di esecuzione molto ridotti rispetto a un approccio sequenziale.
import { promises as dns } from 'node:dns';
// Verifica PTR per un singolo dominio
async function checkSingleDomain(domain) {
const result = {
domain,
forwardIps: [],
ptrMappings: {},
fcrdnsValid: false,
errors: []
};
try {
// Risoluzione forward
result.forwardIps = await dns.resolve4(domain);
// Risoluzione inversa per ciascun IP, in parallelo
const reverseQueries = result.forwardIps.map(async (ip) => {
try {
const hostnames = await dns.reverse(ip);
return { ip, hostnames, error: null };
} catch (error) {
return { ip, hostnames: [], error: error.code };
}
});
const reverseResults = await Promise.all(reverseQueries);
// Costruzione del report e verifica FCrDNS
let allValid = true;
for (const reverse of reverseResults) {
result.ptrMappings[reverse.ip] = reverse.hostnames;
if (reverse.error) {
result.errors.push(`${reverse.ip}: ${reverse.error}`);
allValid = false;
} else {
const matches = reverse.hostnames.some(
h => h.toLowerCase() === domain.toLowerCase()
);
if (!matches) allValid = false;
}
}
result.fcrdnsValid = allValid && result.forwardIps.length > 0;
} catch (error) {
result.errors.push(`Forward resolution failed: ${error.code}`);
}
return result;
}
// Verifica batch con limite di concorrenza
async function batchCheckDomains(domains, concurrency = 5) {
const results = [];
// Suddivide i domini in chunk per limitare la concorrenza
for (let i = 0; i < domains.length; i += concurrency) {
const chunk = domains.slice(i, i + concurrency);
const chunkResults = await Promise.all(
chunk.map(domain => checkSingleDomain(domain))
);
results.push(...chunkResults);
}
return results;
}
// Esempio di utilizzo con una lista di domini
const domainsToCheck = [
'google.com',
'cloudflare.com',
'github.com',
'mozilla.org',
'wikipedia.org'
];
const batchResults = await batchCheckDomains(domainsToCheck, 3);
// Generazione di un sommario
const summary = {
total: batchResults.length,
valid: batchResults.filter(r => r.fcrdnsValid).length,
invalid: batchResults.filter(r => !r.fcrdnsValid).length,
details: batchResults
};
console.log('Sommario verifica FCrDNS:');
console.log(`Totali: ${summary.total}`);
console.log(`Validi: ${summary.valid}`);
console.log(`Non validi: ${summary.invalid}`);
Supporto per IPv6
Una verifica completa del record PTR non può ignorare il supporto per IPv6, soprattutto considerando la crescente diffusione di questo protocollo. Le query DNS per IPv6 utilizzano la stessa API del modulo dns, ma richiedono l'uso di resolve6() per la risoluzione forward. La funzione reverse(), invece, accetta indistintamente indirizzi IPv4 e IPv6, gestendo internamente la conversione nella zona ip6.arpa appropriata.
import { promises as dns } from 'node:dns';
// Verifica PTR completa per IPv4 e IPv6
async function fullStackPtrCheck(domain) {
const report = {
domain,
ipv4: { addresses: [], ptrRecords: {}, valid: false },
ipv6: { addresses: [], ptrRecords: {}, valid: false },
overallValid: false
};
// Risoluzione IPv4
try {
report.ipv4.addresses = await dns.resolve4(domain);
for (const ip of report.ipv4.addresses) {
try {
report.ipv4.ptrRecords[ip] = await dns.reverse(ip);
} catch (error) {
report.ipv4.ptrRecords[ip] = { error: error.code };
}
}
// Verifica corrispondenza per IPv4
report.ipv4.valid = report.ipv4.addresses.length > 0 &&
report.ipv4.addresses.every(ip => {
const records = report.ipv4.ptrRecords[ip];
return Array.isArray(records) &&
records.some(h => h.toLowerCase() === domain.toLowerCase());
});
} catch (error) {
report.ipv4.error = error.code;
}
// Risoluzione IPv6
try {
report.ipv6.addresses = await dns.resolve6(domain);
for (const ip of report.ipv6.addresses) {
try {
report.ipv6.ptrRecords[ip] = await dns.reverse(ip);
} catch (error) {
report.ipv6.ptrRecords[ip] = { error: error.code };
}
}
// Verifica corrispondenza per IPv6
report.ipv6.valid = report.ipv6.addresses.length > 0 &&
report.ipv6.addresses.every(ip => {
const records = report.ipv6.ptrRecords[ip];
return Array.isArray(records) &&
records.some(h => h.toLowerCase() === domain.toLowerCase());
});
} catch (error) {
report.ipv6.error = error.code;
}
// Valutazione complessiva: deve essere valida almeno una versione
report.overallValid = report.ipv4.valid || report.ipv6.valid;
return report;
}
// Esecuzione della verifica completa
const fullReport = await fullStackPtrCheck('google.com');
console.log(JSON.stringify(fullReport, null, 2));
Utilizzo di server DNS personalizzati
Per ottenere risultati più affidabili o per testare la propagazione DNS attraverso resolver diversi, è possibile configurare server DNS personalizzati. Questa funzionalità è particolarmente utile quando si vuole verificare che un record PTR sia stato propagato correttamente o quando si sospetta che il resolver di sistema stia restituendo dati cached non aggiornati.
import { Resolver } from 'node:dns/promises';
// Crea un resolver indipendente con server DNS specifici
function createCustomResolver(dnsServers) {
const resolver = new Resolver();
resolver.setServers(dnsServers);
return resolver;
}
// Confronta i risultati PTR ottenuti da diversi resolver
async function compareResolvers(ipAddress, resolverConfigs) {
const results = {};
for (const [name, servers] of Object.entries(resolverConfigs)) {
const resolver = createCustomResolver(servers);
try {
const startTime = Date.now();
const hostnames = await resolver.reverse(ipAddress);
const responseTime = Date.now() - startTime;
results[name] = {
success: true,
hostnames,
responseTime,
servers
};
} catch (error) {
results[name] = {
success: false,
error: error.code,
servers
};
}
}
return results;
}
// Definizione dei resolver da confrontare
const resolverConfigs = {
google: ['8.8.8.8', '8.8.4.4'],
cloudflare: ['1.1.1.1', '1.0.0.1'],
quad9: ['9.9.9.9', '149.112.112.112'],
opendns: ['208.67.222.222', '208.67.220.220']
};
// Esegue il confronto
const comparison = await compareResolvers('8.8.8.8', resolverConfigs);
console.log('Confronto tra resolver DNS:');
console.log(JSON.stringify(comparison, null, 2));
// Verifica della consistenza dei risultati
const allHostnames = Object.values(comparison)
.filter(r => r.success)
.map(r => r.hostnames.sort().join(','));
const isConsistent = new Set(allHostnames).size === 1;
console.log(`Risultati consistenti tra i resolver: ${isConsistent}`);
Integrazione in uno script di monitoraggio
Tutto quello che abbiamo costruito finora può essere integrato in uno script di monitoraggio che verifica periodicamente lo stato dei record PTR di una lista di domini, segnalando eventuali anomalie. Questo tipo di strumento si rivela prezioso per chi gestisce server di posta o infrastrutture critiche, dove un PTR errato può causare problemi di consegna delle email o di reputazione del dominio.
import { promises as dns } from 'node:dns';
import { EventEmitter } from 'node:events';
// Monitor di record PTR che emette eventi sui cambiamenti
class PtrMonitor extends EventEmitter {
constructor(options = {}) {
super();
this.interval = options.interval || 60000; // Default 1 minuto
this.domains = new Map();
this.history = new Map();
this.timer = null;
}
// Aggiunge un dominio al monitoraggio
addDomain(domain, expectedHostname = null) {
this.domains.set(domain, {
domain,
expectedHostname: expectedHostname || domain,
addedAt: new Date()
});
this.history.set(domain, []);
}
// Rimuove un dominio dal monitoraggio
removeDomain(domain) {
this.domains.delete(domain);
this.history.delete(domain);
}
// Verifica un singolo dominio
async checkDomain(domainConfig) {
const { domain, expectedHostname } = domainConfig;
const checkResult = {
domain,
timestamp: new Date(),
ips: [],
ptrRecords: {},
valid: false,
issues: []
};
try {
checkResult.ips = await dns.resolve4(domain);
for (const ip of checkResult.ips) {
try {
const hostnames = await dns.reverse(ip);
checkResult.ptrRecords[ip] = hostnames;
const matches = hostnames.some(
h => h.toLowerCase() === expectedHostname.toLowerCase()
);
if (!matches) {
checkResult.issues.push(
`${ip} -> PTR non corrisponde a ${expectedHostname}`
);
}
} catch (error) {
checkResult.ptrRecords[ip] = null;
checkResult.issues.push(
`${ip} -> errore PTR: ${error.code}`
);
}
}
checkResult.valid = checkResult.issues.length === 0 &&
checkResult.ips.length > 0;
} catch (error) {
checkResult.issues.push(
`Risoluzione forward fallita: ${error.code}`
);
}
return checkResult;
}
// Confronta il risultato corrente con quello precedente
detectChanges(domain, currentResult) {
const history = this.history.get(domain);
if (!history || history.length === 0) {
history.push(currentResult);
return null;
}
const previousResult = history[history.length - 1];
const changes = {
domain,
statusChanged: previousResult.valid !== currentResult.valid,
previousValid: previousResult.valid,
currentValid: currentResult.valid,
timestamp: currentResult.timestamp
};
// Mantiene solo gli ultimi 100 risultati
history.push(currentResult);
if (history.length > 100) history.shift();
return changes.statusChanged ? changes : null;
}
// Avvia il monitoraggio periodico
start() {
if (this.timer) return;
const runChecks = async () => {
const domains = Array.from(this.domains.values());
const results = await Promise.all(
domains.map(d => this.checkDomain(d))
);
for (const result of results) {
this.emit('check', result);
if (!result.valid) {
this.emit('invalid', result);
}
const changes = this.detectChanges(result.domain, result);
if (changes) {
this.emit('change', changes);
}
}
};
// Esegue subito la prima verifica e poi a intervalli regolari
runChecks();
this.timer = setInterval(runChecks, this.interval);
}
// Ferma il monitoraggio
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
}
// Esempio di utilizzo del monitor
const monitor = new PtrMonitor({ interval: 30000 });
monitor.addDomain('mail.example.com', 'mail.example.com');
monitor.addDomain('smtp.example.com', 'smtp.example.com');
monitor.on('check', (result) => {
console.log(`[CHECK] ${result.domain}: ${result.valid ? 'OK' : 'FAIL'}`);
});
monitor.on('invalid', (result) => {
console.error(`[ALERT] ${result.domain} non valido:`);
result.issues.forEach(issue => console.error(` - ${issue}`));
});
monitor.on('change', (changes) => {
console.warn(
`[CHANGE] ${changes.domain} è passato da ` +
`${changes.previousValid ? 'valido' : 'non valido'} a ` +
`${changes.currentValid ? 'valido' : 'non valido'}`
);
});
monitor.start();
Considerazioni sulla cache DNS
Un aspetto spesso sottovalutato quando si lavora con query DNS è il comportamento della cache. Sia il sistema operativo che Node.js stesso possono mantenere risultati DNS in cache per ridurre il carico sui server DNS e migliorare le prestazioni. Tuttavia, in scenari di monitoraggio attivo o di verifica della propagazione di nuove configurazioni, questo comportamento può fornire risultati obsoleti. L'uso di un Resolver personalizzato con server DNS specifici aiuta a bypassare la cache di sistema, ma occorre comunque essere consapevoli che i server DNS interrogati possono a loro volta avere cache attive con TTL variabili.
Per ottenere risultati sempre freschi durante lo sviluppo o il debug, è possibile interrogare direttamente i name server autoritativi del dominio, identificandoli prima tramite query NS. Questo approccio garantisce di lavorare con i dati più aggiornati possibili, anche se richiede un maggior numero di query e tempi di risposta leggermente superiori.
Conclusioni
La verifica del record PTR è un'operazione tecnica relativamente semplice da implementare con Node.js, ma rappresenta un controllo fondamentale per garantire il corretto funzionamento di servizi critici come quelli di posta elettronica. Il modulo nativo dns offre tutti gli strumenti necessari per effettuare query inverse, validare la coerenza FCrDNS e costruire sistemi di monitoraggio sofisticati. La combinazione di Promise, gestione degli errori, timeout personalizzati e parallelizzazione permette di realizzare strumenti robusti e performanti, integrabili in pipeline di monitoraggio, dashboard di osservabilità o script di automazione dell'infrastruttura.
L'aspetto più importante da ricordare è che la configurazione del record PTR non è mai isolata: deve essere coerente con la configurazione forward del dominio e supportata da una corretta gestione delle zone DNS inverse, che spesso richiede la collaborazione con il provider che assegna gli indirizzi IP. Una volta verificata e mantenuta nel tempo, una configurazione PTR corretta diventa un fondamentale tassello di affidabilità per qualsiasi servizio internet pubblico.