Verificare la configurazione del record PTR di un dominio con Java

Il record PTR (Pointer Record) è uno dei componenti fondamentali del DNS, spesso trascurato ma cruciale per il corretto funzionamento di servizi come la posta elettronica, l'autenticazione dei server e la reputazione di un dominio. A differenza dei record A o AAAA, che mappano un nome di dominio a un indirizzo IP, il record PTR svolge l'operazione inversa: associa un indirizzo IP al nome di dominio corrispondente. Questo processo è noto come reverse DNS lookup.

In questo articolo vedremo come verificare la corretta configurazione di un record PTR utilizzando Java, sfruttando le API standard di JNDI (Java Naming and Directory Interface) e analizzando i risultati per determinare se il record è configurato correttamente.

Cos'è un record PTR e perché è importante

Quando un server mail riceve una connessione da un altro server, una delle prime operazioni che esegue è la verifica del record PTR dell'indirizzo IP del mittente. Se il PTR non è configurato o non corrisponde al nome del server dichiarato nel comando HELO/EHLO, il messaggio rischia di essere classificato come spam o rifiutato direttamente.

Il record PTR viene memorizzato in una zona DNS speciale chiamata in-addr.arpa per gli indirizzi IPv4 e ip6.arpa per gli indirizzi IPv6. Per esempio, l'indirizzo IPv4 192.0.2.10 corrisponde al nome 10.2.0.192.in-addr.arpa. Una verifica corretta del record PTR deve quindi tenere conto di questa rappresentazione invertita.

Le API di Java per le query DNS

Java offre diverse strade per effettuare query DNS. La più semplice è l'utilizzo della classe InetAddress, che però si limita a operazioni di base e dipende dal resolver del sistema operativo. Per un controllo più granulare, la scelta migliore è JNDI con il provider DNS, che permette di interrogare direttamente i name server e di richiedere tipi specifici di record, incluso il PTR.

Vediamo subito un esempio di base che mostra come ottenere il record PTR di un indirizzo IP.

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;

public class PtrLookup {

    public static String lookupPtr(String ipAddress) throws Exception {
        // Conversione dell'indirizzo IP nella notazione inversa per la zona in-addr.arpa
        String reverseName = buildReverseName(ipAddress);

        // Configurazione del contesto JNDI con il provider DNS
        Hashtable<String, String> environment = new Hashtable<>();
        environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        environment.put(Context.PROVIDER_URL, "dns://8.8.8.8");

        DirContext context = new InitialDirContext(environment);

        // Richiesta esplicita del record PTR
        Attributes attributes = context.getAttributes(reverseName, new String[] { "PTR" });
        Attribute ptrAttribute = attributes.get("PTR");

        if (ptrAttribute == null) {
            return null;
        }

        return ptrAttribute.get().toString();
    }

    private static String buildReverseName(String ipAddress) {
        String[] octets = ipAddress.split("\\.");
        StringBuilder reversed = new StringBuilder();
        for (int i = octets.length - 1; i >= 0; i--) {
            reversed.append(octets[i]).append(".");
        }
        reversed.append("in-addr.arpa");
        return reversed.toString();
    }
}

Il codice mostra il pattern fondamentale: si costruisce il nome inverso a partire dall'indirizzo IP, si configura un contesto JNDI specificando il provider DNS (in questo caso il resolver di Google), si effettua la query richiedendo solo il tipo PTR e si estrae il valore restituito. Da notare l'uso esplicito del server DNS nella variabile PROVIDER_URL: questo permette di evitare comportamenti imprevedibili dovuti alla configurazione locale.

Verifica della coerenza forward-confirmed reverse DNS

Avere un record PTR configurato non è sufficiente. Il vero controllo di qualità si chiama FCrDNS (Forward-Confirmed reverse DNS) e consiste nel verificare che il nome ottenuto dal lookup PTR, una volta risolto a sua volta tramite un record A, riporti all'indirizzo IP di partenza. Questo doppio controllo è ciò che la maggior parte dei filtri antispam esegue per validare un mittente.

Il flusso logico è il seguente: dato un IP, si ricava il nome tramite PTR; il nome ottenuto viene risolto tramite record A o AAAA; gli IP risultanti devono includere quello iniziale. Se uno qualsiasi di questi passaggi fallisce, la configurazione non è coerente.

import javax.naming.Context;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.NamingEnumeration;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

public class PtrValidator {

    private final DirContext context;

    public PtrValidator(String dnsServer) throws Exception {
        Hashtable<String, String> environment = new Hashtable<>();
        environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        environment.put(Context.PROVIDER_URL, "dns://" + dnsServer);
        // Imposta il timeout per evitare blocchi prolungati
        environment.put("com.sun.jndi.dns.timeout.initial", "2000");
        environment.put("com.sun.jndi.dns.timeout.retries", "3");
        this.context = new InitialDirContext(environment);
    }

    public ValidationResult validate(String ipAddress) throws Exception {
        // Primo passaggio: ottenere il record PTR dall'indirizzo IP
        String hostname = lookupPtr(ipAddress);
        if (hostname == null) {
            return new ValidationResult(ipAddress, null, false, "Record PTR mancante");
        }

        // Rimuove il punto finale tipico delle risposte DNS
        if (hostname.endsWith(".")) {
            hostname = hostname.substring(0, hostname.length() - 1);
        }

        // Secondo passaggio: risolvere il nome tramite record A
        List<String> resolvedAddresses = lookupA(hostname);
        if (resolvedAddresses.isEmpty()) {
            return new ValidationResult(ipAddress, hostname, false, "Nessun record A trovato per " + hostname);
        }

        // Terzo passaggio: verifica della coerenza
        boolean matches = resolvedAddresses.contains(ipAddress);
        String message = matches
                ? "Configurazione FCrDNS valida"
                : "Mismatch: il nome non risolve all'IP originale";

        return new ValidationResult(ipAddress, hostname, matches, message);
    }

    private String lookupPtr(String ipAddress) throws Exception {
        String reverseName = buildReverseName(ipAddress);
        Attributes attributes = context.getAttributes(reverseName, new String[] { "PTR" });
        Attribute ptrAttribute = attributes.get("PTR");
        return ptrAttribute != null ? ptrAttribute.get().toString() : null;
    }

    private List<String> lookupA(String hostname) throws Exception {
        List<String> addresses = new ArrayList<>();
        Attributes attributes = context.getAttributes(hostname, new String[] { "A" });
        Attribute aAttribute = attributes.get("A");
        if (aAttribute != null) {
            NamingEnumeration<?> values = aAttribute.getAll();
            while (values.hasMore()) {
                addresses.add(values.next().toString());
            }
        }
        return addresses;
    }

    private String buildReverseName(String ipAddress) {
        String[] octets = ipAddress.split("\\.");
        StringBuilder reversed = new StringBuilder();
        for (int i = octets.length - 1; i >= 0; i--) {
            reversed.append(octets[i]).append(".");
        }
        reversed.append("in-addr.arpa");
        return reversed.toString();
    }
}

La classe PtrValidator incapsula la logica completa di validazione e restituisce un oggetto ValidationResult che descrive l'esito della verifica. L'introduzione dei timeout è particolarmente importante: senza di essi, una query verso un server DNS non raggiungibile potrebbe bloccare l'applicazione per decine di secondi.

Il modello dei risultati

Per rendere il codice più ordinato, conviene definire una classe dedicata che rappresenti l'esito della validazione. Questo approccio permette di trasportare tutte le informazioni utili in un unico oggetto e facilita la serializzazione, ad esempio in formato JSON, qualora il controllo venga esposto come API.

public class ValidationResult {

    private final String ipAddress;
    private final String hostname;
    private final boolean valid;
    private final String message;

    public ValidationResult(String ipAddress, String hostname, boolean valid, String message) {
        this.ipAddress = ipAddress;
        this.hostname = hostname;
        this.valid = valid;
        this.message = message;
    }

    public String getIpAddress() {
        return ipAddress;
    }

    public String getHostname() {
        return hostname;
    }

    public boolean isValid() {
        return valid;
    }

    public String getMessage() {
        return message;
    }

    @Override
    public String toString() {
        return String.format("PTR check for %s -> %s [%s]: %s",
                ipAddress, hostname, valid ? "OK" : "FAIL", message);
    }
}

Supporto per indirizzi IPv6

Gli indirizzi IPv6 richiedono una gestione differente rispetto a IPv4, poiché la zona di riferimento è ip6.arpa e ciascun nibble (cioè ciascun gruppo di 4 bit, equivalente a una cifra esadecimale) deve essere invertito singolarmente. Un indirizzo IPv6 deve quindi essere espanso nella sua forma completa prima della conversione.

import java.net.InetAddress;
import java.net.Inet6Address;

public class ReverseNameBuilder {

    public static String build(String ipAddress) throws Exception {
        InetAddress address = InetAddress.getByName(ipAddress);
        if (address instanceof Inet6Address) {
            return buildIpv6Reverse(address);
        }
        return buildIpv4Reverse(ipAddress);
    }

    private static String buildIpv4Reverse(String ipAddress) {
        String[] octets = ipAddress.split("\\.");
        StringBuilder reversed = new StringBuilder();
        for (int i = octets.length - 1; i >= 0; i--) {
            reversed.append(octets[i]).append(".");
        }
        reversed.append("in-addr.arpa");
        return reversed.toString();
    }

    private static String buildIpv6Reverse(InetAddress address) {
        byte[] bytes = address.getAddress();
        StringBuilder reversed = new StringBuilder();
        // Itera dall'ultimo byte al primo, separando ciascun byte in due nibble
        for (int i = bytes.length - 1; i >= 0; i--) {
            int unsignedByte = bytes[i] & 0xFF;
            int lowNibble = unsignedByte & 0x0F;
            int highNibble = (unsignedByte >> 4) & 0x0F;
            reversed.append(Integer.toHexString(lowNibble)).append(".");
            reversed.append(Integer.toHexString(highNibble)).append(".");
        }
        reversed.append("ip6.arpa");
        return reversed.toString();
    }
}

Questa utility universale può sostituire il metodo buildReverseName della classe PtrValidator, rendendo il sistema compatibile sia con IPv4 sia con IPv6. La conversione IPv6 sfrutta il fatto che InetAddress restituisce sempre l'indirizzo in formato espanso a 16 byte, evitando così la necessità di gestire manualmente le forme abbreviate con i doppi due punti.

Esempio di utilizzo completo

Mettendo insieme tutti i pezzi, possiamo costruire una piccola applicazione da riga di comando che accetta un indirizzo IP e restituisce l'esito della verifica del record PTR.

public class PtrCheckApp {

    public static void main(String[] args) {
        if (args.length == 0) {
            System.err.println("Uso: java PtrCheckApp <ip-address> [dns-server]");
            System.exit(1);
        }

        String ipAddress = args[0];
        String dnsServer = args.length > 1 ? args[1] : "8.8.8.8";

        try {
            PtrValidator validator = new PtrValidator(dnsServer);
            ValidationResult result = validator.validate(ipAddress);
            System.out.println(result);
            System.exit(result.isValid() ? 0 : 2);
        } catch (Exception exception) {
            System.err.println("Errore durante la verifica: " + exception.getMessage());
            System.exit(3);
        }
    }
}

L'applicazione utilizza i codici di uscita per comunicare l'esito al sistema operativo: 0 per una configurazione valida, 2 per un mismatch e 3 per un errore tecnico. Questo schema rende il programma facilmente integrabile in pipeline di monitoraggio o script di automazione.

Gestione degli errori e casi limite

Nella pratica, una verifica del record PTR può fallire per ragioni diverse. Le più comuni sono il record assente, un timeout di rete, un name server che non risponde, oppure un record presente ma non corrispondente. È importante distinguere tra queste situazioni: un timeout temporaneo non equivale a una configurazione errata, mentre un PTR mancante indica un problema strutturale che richiede un intervento sul DNS.

Va inoltre considerato che alcuni provider configurano record PTR generici, del tipo host-203-0-113-25.example-isp.net, validi tecnicamente ma poco indicativi del servizio reale. Per server di posta dedicati, la prassi raccomanda di personalizzare il PTR in modo che corrisponda al nome dell'MX o al banner SMTP, garantendo così piena coerenza tra le diverse interrogazioni DNS.

Conclusione

La verifica del record PTR è un'operazione semplice ma di grande importanza per la qualità di un servizio web o di posta. Java offre tutti gli strumenti necessari per implementarla in modo robusto, dal lookup di base con JNDI fino al controllo completo FCrDNS con supporto IPv6. Integrando questa logica in un sistema di monitoraggio, è possibile rilevare proattivamente eventuali problemi di configurazione DNS prima che impattino sulla deliverability dei messaggi o sulla reputazione del dominio. Una buona pratica è quella di pianificare verifiche periodiche su tutti gli IP utilizzati dai propri servizi, archiviando i risultati per analisi storiche e per individuare regressioni dovute a modifiche infrastrutturali.