Verificare la configurazione del record PTR di un dominio con Python

Il record PTR (Pointer Record) è uno dei componenti più importanti dell'infrastruttura DNS, specialmente quando si gestiscono server di posta elettronica o servizi che richiedono una corretta autenticazione basata sull'indirizzo IP. A differenza dei record DNS tradizionali che mappano un nome di dominio a un indirizzo IP, il record PTR esegue l'operazione inversa: associa un indirizzo IP a un nome di dominio. Questa operazione è nota come reverse DNS lookup.

In questo articolo vedremo come verificare in modo programmatico la corretta configurazione del record PTR di un dominio utilizzando Python. Costruiremo uno strumento completo che esegue il lookup diretto, il lookup inverso e verifica la coerenza tra i due, una pratica fondamentale per garantire la consegna delle email e prevenire problemi di reputazione del server.

Cos'è un record PTR e perché è importante

Un record PTR è un tipo speciale di record DNS che vive in una zona DNS chiamata in-addr.arpa per IPv4 e ip6.arpa per IPv6. Il suo scopo è permettere ai sistemi remoti di verificare l'identità di un host a partire dal suo indirizzo IP. Per esempio, se un server di posta elettronica si presenta con un certo dominio, il server ricevente può eseguire un reverse DNS lookup sull'IP per verificare che corrisponda effettivamente a quel dominio.

La configurazione corretta del record PTR è cruciale per diversi motivi:

  • I server di posta elettronica come Gmail, Outlook e altri provider rifiutano o marcano come spam le email provenienti da server senza un PTR valido.
  • I sistemi di logging e analisi del traffico utilizzano il reverse DNS per identificare la provenienza delle connessioni.
  • I sistemi di sicurezza valutano la reputazione di un server anche in base alla coerenza tra DNS diretto e inverso (FCrDNS - Forward-confirmed reverse DNS).

Preparazione dell'ambiente

Per implementare la nostra soluzione utilizzeremo la libreria dnspython, lo standard de facto in Python per le operazioni DNS avanzate. Questa libreria offre un'API completa e ben documentata che ci permette di interrogare qualsiasi tipo di record DNS, gestire timeout e specificare server DNS personalizzati.

L'installazione avviene tramite pip:

# Installiamo la libreria dnspython
pip install dnspython

La libreria standard di Python include il modulo socket che offre alcune funzionalità di base per il reverse DNS, ma per un controllo più granulare e per la possibilità di interrogare server DNS specifici, dnspython è la scelta migliore.

Risoluzione DNS diretta

Il primo passo per verificare la configurazione PTR consiste nell'ottenere l'indirizzo IP del dominio attraverso una risoluzione DNS diretta (record A per IPv4 o AAAA per IPv6). Questo IP sarà poi utilizzato per il lookup inverso.

import dns.resolver
import dns.reversename
import dns.exception
from typing import Optional, List


def resolve_domain_to_ip(domain: str, record_type: str = "A") -> List[str]:
    """
    Risolve un dominio nei suoi indirizzi IP.
    Restituisce una lista di indirizzi IP associati al dominio.
    """
    resolver = dns.resolver.Resolver()
    # Impostiamo un timeout ragionevole per evitare attese eccessive
    resolver.timeout = 5
    resolver.lifetime = 10
    
    ip_addresses = []
    
    try:
        answers = resolver.resolve(domain, record_type)
        for rdata in answers:
            ip_addresses.append(str(rdata))
    except dns.resolver.NXDOMAIN:
        print(f"Il dominio {domain} non esiste")
    except dns.resolver.NoAnswer:
        print(f"Nessun record {record_type} trovato per {domain}")
    except dns.exception.Timeout:
        print(f"Timeout durante la risoluzione di {domain}")
    
    return ip_addresses

La funzione resolve_domain_to_ip accetta un nome di dominio e un tipo di record (di default A per IPv4) e restituisce una lista di indirizzi IP. Un dominio può infatti avere più record A associati per scopi di load balancing o ridondanza, quindi è importante gestire tutti i risultati.

Risoluzione DNS inversa

Una volta ottenuto l'indirizzo IP, possiamo procedere con il lookup inverso. La libreria dnspython offre il modulo dns.reversename che facilita la conversione di un indirizzo IP nel corrispondente nome nella zona in-addr.arpa o ip6.arpa.

def reverse_dns_lookup(ip_address: str) -> Optional[str]:
    """
    Esegue il reverse DNS lookup di un indirizzo IP.
    Restituisce il nome di dominio associato o None se non disponibile.
    """
    resolver = dns.resolver.Resolver()
    resolver.timeout = 5
    resolver.lifetime = 10
    
    try:
        # Convertiamo l'IP nel formato di reverse name
        reverse_name = dns.reversename.from_address(ip_address)
        # Eseguiamo la query PTR
        answers = resolver.resolve(reverse_name, "PTR")
        
        # Restituiamo il primo risultato rimuovendo il punto finale
        if answers:
            return str(answers[0]).rstrip(".")
    except dns.resolver.NXDOMAIN:
        print(f"Nessun record PTR configurato per {ip_address}")
    except dns.resolver.NoAnswer:
        print(f"Nessuna risposta PTR per {ip_address}")
    except dns.exception.Timeout:
        print(f"Timeout durante il lookup inverso di {ip_address}")
    except Exception as error:
        print(f"Errore durante il lookup inverso: {error}")
    
    return None

Il metodo dns.reversename.from_address gestisce automaticamente sia IPv4 che IPv6, costruendo il nome di dominio inverso corretto. Per esempio, l'indirizzo 192.0.2.1 diventa 1.2.0.192.in-addr.arpa. Questa è la forma in cui il record PTR viene effettivamente memorizzato nei server DNS.

Verifica della coerenza FCrDNS

Il vero test di una configurazione PTR corretta non si limita alla sua esistenza, ma richiede la verifica della coerenza tra DNS diretto e inverso. Questo controllo, chiamato Forward-confirmed reverse DNS (FCrDNS), garantisce che il dominio restituito dal lookup inverso, quando risolto a sua volta, restituisca lo stesso IP di partenza.

def verify_fcrdns(domain: str) -> dict:
    """
    Verifica la configurazione FCrDNS completa di un dominio.
    Restituisce un dizionario con i risultati della verifica.
    """
    result = {
        "domain": domain,
        "forward_lookup": [],
        "reverse_lookup": {},
        "fcrdns_valid": False,
        "details": []
    }
    
    # Step 1: risoluzione del dominio in IP
    ip_addresses = resolve_domain_to_ip(domain)
    result["forward_lookup"] = ip_addresses
    
    if not ip_addresses:
        result["details"].append("Impossibile risolvere il dominio")
        return result
    
    # Step 2: per ogni IP eseguiamo il reverse lookup
    matches = []
    for ip in ip_addresses:
        ptr_record = reverse_dns_lookup(ip)
        result["reverse_lookup"][ip] = ptr_record
        
        if ptr_record:
            # Step 3: verifichiamo che il PTR risolva allo stesso IP
            ptr_ips = resolve_domain_to_ip(ptr_record)
            if ip in ptr_ips:
                matches.append(ip)
                result["details"].append(
                    f"FCrDNS valido per {ip}: PTR -> {ptr_record}"
                )
            else:
                result["details"].append(
                    f"Mismatch FCrDNS per {ip}: il PTR {ptr_record} "
                    f"risolve a {ptr_ips}"
                )
        else:
            result["details"].append(f"Nessun record PTR per {ip}")
    
    # La verifica è valida se almeno un IP ha un FCrDNS coerente
    result["fcrdns_valid"] = len(matches) > 0
    
    return result

Questa funzione implementa la procedura completa di verifica FCrDNS in tre passaggi: risoluzione diretta, risoluzione inversa per ogni IP ottenuto, e infine verifica che il nome restituito dal PTR risolva effettivamente all'IP originale. È importante notare che un dominio può avere più IP, e ognuno deve essere verificato indipendentemente.

Utilizzo di un server DNS personalizzato

In alcuni scenari può essere utile interrogare un server DNS specifico, per esempio per testare la configurazione prima della propagazione globale o per evitare risposte memorizzate nella cache locale. La libreria dnspython permette di specificare i nameserver da utilizzare:

def query_with_custom_nameserver(
    domain: str, 
    nameservers: List[str],
    record_type: str = "A"
) -> List[str]:
    """
    Esegue una query DNS utilizzando nameserver specifici.
    Utile per bypassare la cache o testare server autoritativi.
    """
    resolver = dns.resolver.Resolver(configure=False)
    resolver.nameservers = nameservers
    resolver.timeout = 5
    resolver.lifetime = 10
    
    results = []
    
    try:
        answers = resolver.resolve(domain, record_type)
        for rdata in answers:
            results.append(str(rdata))
    except dns.exception.DNSException as error:
        print(f"Errore nella query: {error}")
    
    return results


# Esempio di utilizzo con i DNS pubblici di Cloudflare e Google
public_dns = ["1.1.1.1", "8.8.8.8"]
ips = query_with_custom_nameserver("example.com", public_dns)

Specificando configure=False nel costruttore del resolver, evitiamo che vengano caricate le configurazioni DNS del sistema operativo, garantendo che vengano utilizzati esclusivamente i nameserver da noi indicati.

Gestione di IPv6

Con la diffusione di IPv6, è fondamentale che lo strumento di verifica supporti anche questo protocollo. La buona notizia è che dnspython gestisce IPv6 in modo trasparente, ma dobbiamo richiedere esplicitamente i record AAAA invece dei record A:

def verify_dual_stack(domain: str) -> dict:
    """
    Verifica la configurazione PTR sia per IPv4 che per IPv6.
    Restituisce i risultati separati per entrambi i protocolli.
    """
    result = {
        "domain": domain,
        "ipv4": {},
        "ipv6": {}
    }
    
    # Verifica IPv4
    ipv4_addresses = resolve_domain_to_ip(domain, "A")
    for ip in ipv4_addresses:
        result["ipv4"][ip] = reverse_dns_lookup(ip)
    
    # Verifica IPv6
    ipv6_addresses = resolve_domain_to_ip(domain, "AAAA")
    for ip in ipv6_addresses:
        result["ipv6"][ip] = reverse_dns_lookup(ip)
    
    return result

Per i server di posta elettronica moderni è particolarmente importante avere il record PTR configurato correttamente sia per IPv4 che per IPv6, dato che molti provider stanno preferendo le connessioni IPv6 quando disponibili.

Uno strumento completo da riga di comando

Mettiamo insieme tutti i pezzi in uno script utilizzabile da riga di comando che accetta un dominio e produce un report completo della configurazione DNS:

import sys
import json


def generate_report(domain: str) -> dict:
    """
    Genera un report completo della configurazione DNS di un dominio.
    Include verifica PTR per IPv4, IPv6 e coerenza FCrDNS.
    """
    report = {
        "domain": domain,
        "dual_stack": verify_dual_stack(domain),
        "fcrdns": verify_fcrdns(domain)
    }
    
    return report


def print_report(report: dict) -> None:
    """
    Stampa un report leggibile dei risultati della verifica.
    """
    print(f"\n{'=' * 60}")
    print(f"Report DNS per: {report['domain']}")
    print(f"{'=' * 60}\n")
    
    # Sezione IPv4
    print("Configurazione IPv4:")
    if report["dual_stack"]["ipv4"]:
        for ip, ptr in report["dual_stack"]["ipv4"].items():
            status = ptr if ptr else "NESSUN PTR"
            print(f"  {ip} -> {status}")
    else:
        print("  Nessun record A trovato")
    
    # Sezione IPv6
    print("\nConfigurazione IPv6:")
    if report["dual_stack"]["ipv6"]:
        for ip, ptr in report["dual_stack"]["ipv6"].items():
            status = ptr if ptr else "NESSUN PTR"
            print(f"  {ip} -> {status}")
    else:
        print("  Nessun record AAAA trovato")
    
    # Sezione FCrDNS
    print("\nVerifica FCrDNS:")
    fcrdns = report["fcrdns"]
    print(f"  Valido: {'SI' if fcrdns['fcrdns_valid'] else 'NO'}")
    for detail in fcrdns["details"]:
        print(f"  - {detail}")


def main() -> None:
    """
    Punto di ingresso dello script.
    Accetta il dominio come argomento da riga di comando.
    """
    if len(sys.argv) < 2:
        print("Utilizzo: python ptr_check.py  [--json]")
        sys.exit(1)
    
    domain = sys.argv[1]
    output_json = "--json" in sys.argv
    
    report = generate_report(domain)
    
    if output_json:
        # Output in formato JSON per integrazione con altri tool
        print(json.dumps(report, indent=2, ensure_ascii=False))
    else:
        print_report(report)


if __name__ == "__main__":
    main()

Lo script accetta un dominio come argomento e opzionalmente il flag --json per produrre l'output in formato JSON, utile per l'integrazione con altri strumenti di monitoraggio. L'esecuzione tipica è:

# Verifica standard con output leggibile
python ptr_check.py example.com

# Output in formato JSON per integrazioni
python ptr_check.py example.com --json

Considerazioni sulle prestazioni

Quando si effettuano verifiche DNS su larga scala, per esempio monitorando centinaia di domini, è importante considerare alcuni aspetti prestazionali. Le query DNS sono operazioni di rete e possono essere lente, specialmente quando il server DNS deve effettuare lookup ricorsivi. L'utilizzo di operazioni asincrone con asyncio e la libreria aiodns può migliorare significativamente i tempi di esecuzione quando si verificano molti domini in parallelo.

Inoltre, è buona pratica implementare un sistema di caching dei risultati per evitare query ridondanti, specialmente se lo strumento viene eseguito ripetutamente. Il TTL (Time To Live) dei record DNS fornisce un'indicazione naturale di quanto a lungo un risultato può essere considerato valido.

Conclusioni

La verifica programmatica della configurazione del record PTR è uno strumento prezioso per amministratori di sistema, sviluppatori di servizi email e chiunque gestisca infrastrutture di rete. Python con la libreria dnspython offre tutti gli strumenti necessari per costruire soluzioni robuste e flessibili.

Lo script presentato in questo articolo può essere ulteriormente esteso integrando notifiche automatiche quando vengono rilevati problemi, salvando i risultati in un database per il monitoraggio storico, o esponendo le funzionalità tramite un'API REST per l'integrazione con dashboard di monitoraggio. La verifica FCrDNS dovrebbe essere parte integrante delle procedure di setup e di monitoraggio continuo di qualsiasi server di produzione, in particolare per i servizi che dipendono dalla reputazione dell'indirizzo IP come i mail server.