Verificare la configurazione del record PTR di un dominio con Go
Il record PTR (Pointer Record) è uno degli elementi più importanti e spesso più trascurati della configurazione DNS di un dominio. Mentre i record A e AAAA mappano un nome di dominio a un indirizzo IP, il record PTR svolge l'operazione inversa: associa un indirizzo IP a un nome di dominio. Questa operazione, nota come reverse DNS lookup, è fondamentale per molte applicazioni, in particolare per i server di posta elettronica, dove la presenza e la corretta configurazione del record PTR è spesso un requisito per evitare che i messaggi vengano contrassegnati come spam.
In questo articolo vedremo come implementare in Go uno strumento completo per verificare la configurazione del record PTR di un dominio, analizzando le librerie standard del linguaggio, le tecniche di risoluzione DNS e le best practice per la gestione degli errori.
Cos'è un record PTR e perché è importante
Il record PTR è definito nella zona DNS speciale in-addr.arpa per gli indirizzi IPv4 e ip6.arpa per gli indirizzi IPv6. Quando si effettua una query PTR su un indirizzo IP come 192.0.2.1, la query viene effettivamente inoltrata al dominio 1.2.0.192.in-addr.arpa, ottenendo come risposta un FQDN (Fully Qualified Domain Name) come ad esempio mail.example.com.
L'importanza del record PTR risiede principalmente in tre aspetti: la verifica dell'autenticità del mittente nelle comunicazioni email (FCrDNS, Forward-Confirmed Reverse DNS), il logging e l'analisi del traffico di rete, e la conformità con le politiche di sicurezza di molti servizi online che richiedono una corrispondenza tra IP e dominio.
Il package net di Go per le risoluzioni DNS
Il package net della libreria standard di Go offre tutte le funzionalità necessarie per effettuare query DNS, inclusa la risoluzione inversa. Le funzioni principali che utilizzeremo sono net.LookupAddr per ottenere i nomi associati a un indirizzo IP e net.LookupHost per la risoluzione diretta, utile per la verifica FCrDNS.
Iniziamo con un esempio base che mostra come effettuare una semplice query PTR:
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Utilizzo: ptr-check <indirizzo-ip>")
os.Exit(1)
}
ipAddress := os.Args[1]
// Esecuzione della query PTR sull'indirizzo IP fornito
names, err := net.LookupAddr(ipAddress)
if err != nil {
fmt.Printf("Errore durante la risoluzione PTR: %v\n", err)
os.Exit(1)
}
fmt.Printf("Record PTR trovati per %s:\n", ipAddress)
for _, name := range names {
fmt.Printf(" - %s\n", name)
}
}
Questo programma accetta un indirizzo IP come argomento da riga di comando ed esegue una query PTR. La funzione net.LookupAddr restituisce uno slice di stringhe, poiché un indirizzo IP può teoricamente avere più record PTR associati, anche se nella pratica è raro e generalmente sconsigliato.
Risoluzione del dominio e verifica del PTR
Per verificare la configurazione completa del record PTR di un dominio, dobbiamo prima risolvere il dominio nel suo indirizzo IP, quindi effettuare la query PTR su quell'indirizzo e infine verificare che il nome restituito corrisponda al dominio originale. Questo processo è noto come Forward-Confirmed Reverse DNS.
package main
import (
"fmt"
"net"
"strings"
)
// PTRResult contiene il risultato della verifica del record PTR
type PTRResult struct {
Domain string
IPAddress string
PTRRecords []string
IsValid bool
Error error
}
func checkPTRRecord(domain string) *PTRResult {
result := &PTRResult{
Domain: domain,
}
// Risoluzione del dominio in indirizzo IP
ips, err := net.LookupIP(domain)
if err != nil {
result.Error = fmt.Errorf("impossibile risolvere il dominio: %w", err)
return result
}
if len(ips) == 0 {
result.Error = fmt.Errorf("nessun indirizzo IP trovato per il dominio")
return result
}
// Utilizzo del primo indirizzo IP disponibile
result.IPAddress = ips[0].String()
// Esecuzione della query PTR
names, err := net.LookupAddr(result.IPAddress)
if err != nil {
result.Error = fmt.Errorf("impossibile risolvere il record PTR: %w", err)
return result
}
result.PTRRecords = names
// Verifica della corrispondenza tra dominio e PTR
for _, name := range names {
// Rimozione del punto finale dal FQDN
cleanName := strings.TrimSuffix(name, ".")
if strings.EqualFold(cleanName, domain) {
result.IsValid = true
break
}
}
return result
}
func main() {
domain := "example.com"
result := checkPTRRecord(domain)
if result.Error != nil {
fmt.Printf("Errore: %v\n", result.Error)
return
}
fmt.Printf("Dominio: %s\n", result.Domain)
fmt.Printf("Indirizzo IP: %s\n", result.IPAddress)
fmt.Printf("Record PTR: %v\n", result.PTRRecords)
fmt.Printf("PTR valido: %v\n", result.IsValid)
}
In questo esempio abbiamo creato una struttura PTRResult che incapsula tutte le informazioni rilevanti sulla verifica. La funzione checkPTRRecord esegue le tre operazioni fondamentali: risoluzione del dominio, query PTR e verifica della corrispondenza. L'utilizzo di strings.EqualFold garantisce un confronto case-insensitive, dato che i nomi di dominio sono insensibili al caso.
Gestione di indirizzi IPv4 e IPv6
Una verifica completa del record PTR deve gestire correttamente sia indirizzi IPv4 che IPv6. Il package net di Go gestisce in modo trasparente entrambi i tipi di indirizzi, ma è utile distinguerli per fornire un report più dettagliato all'utente.
package main
import (
"fmt"
"net"
)
// IPInfo contiene informazioni su un indirizzo IP
type IPInfo struct {
Address string
Version string
PTR []string
HasError bool
ErrorMsg string
}
func analyzeDomain(domain string) []IPInfo {
var results []IPInfo
// Recupero di tutti gli indirizzi IP associati al dominio
ips, err := net.LookupIP(domain)
if err != nil {
return results
}
for _, ip := range ips {
info := IPInfo{
Address: ip.String(),
}
// Determinazione della versione del protocollo IP
if ip.To4() != nil {
info.Version = "IPv4"
} else {
info.Version = "IPv6"
}
// Esecuzione della query PTR per l'indirizzo
names, err := net.LookupAddr(ip.String())
if err != nil {
info.HasError = true
info.ErrorMsg = err.Error()
} else {
info.PTR = names
}
results = append(results, info)
}
return results
}
func main() {
domain := "google.com"
results := analyzeDomain(domain)
fmt.Printf("Analisi PTR per %s:\n\n", domain)
for _, info := range results {
fmt.Printf("Indirizzo: %s (%s)\n", info.Address, info.Version)
if info.HasError {
fmt.Printf(" Errore: %s\n", info.ErrorMsg)
} else {
for _, ptr := range info.PTR {
fmt.Printf(" PTR: %s\n", ptr)
}
}
fmt.Println()
}
}
Il metodo To4() di net.IP restituisce un valore non nil solo se l'indirizzo è effettivamente un IPv4, permettendoci di distinguere facilmente tra le due versioni del protocollo. Questo è particolarmente utile perché molti domini moderni hanno sia record A che record AAAA, e ciascuno richiede una verifica PTR separata.
Utilizzo di un resolver DNS personalizzato
In alcuni scenari può essere necessario utilizzare un server DNS specifico invece di quello configurato a livello di sistema operativo. Questo è particolarmente utile per il debugging o per verificare la propagazione DNS attraverso diversi resolver. Go permette di creare un net.Resolver personalizzato con un dialer custom.
package main
import (
"context"
"fmt"
"net"
"time"
)
func createCustomResolver(dnsServer string) *net.Resolver {
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
dialer := net.Dialer{
Timeout: 5 * time.Second,
}
// Utilizzo del server DNS specificato
return dialer.DialContext(ctx, network, dnsServer)
},
}
}
func lookupPTRWithResolver(ipAddress, dnsServer string) ([]string, error) {
resolver := createCustomResolver(dnsServer)
// Creazione di un contesto con timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return resolver.LookupAddr(ctx, ipAddress)
}
func main() {
// Utilizzo del DNS pubblico di Cloudflare
dnsServer := "1.1.1.1:53"
ipAddress := "8.8.8.8"
names, err := lookupPTRWithResolver(ipAddress, dnsServer)
if err != nil {
fmt.Printf("Errore: %v\n", err)
return
}
fmt.Printf("Record PTR per %s tramite %s:\n", ipAddress, dnsServer)
for _, name := range names {
fmt.Printf(" - %s\n", name)
}
}
L'opzione PreferGo: true indica al resolver di utilizzare l'implementazione Go pura invece delle chiamate cgo al sistema, garantendo un comportamento consistente su tutte le piattaforme. Il timeout configurato nel dialer e nel context impedisce che il programma si blocchi indefinitamente in caso di server DNS non raggiungibili.
Implementazione di una verifica completa
Mettiamo ora insieme tutti i concetti visti finora per creare uno strumento completo che esegue una verifica approfondita della configurazione PTR di un dominio. Lo strumento gestirà più indirizzi IP, sia IPv4 che IPv6, eseguirà la verifica FCrDNS e fornirà un report dettagliato.
package main
import (
"context"
"fmt"
"net"
"os"
"strings"
"time"
)
// PTRCheck rappresenta il risultato della verifica per un singolo IP
type PTRCheck struct {
IPAddress string
IPVersion string
PTRRecords []string
ForwardIPs []string
FCrDNSValid bool
DomainMatch bool
Error error
}
// DomainReport contiene il report completo per un dominio
type DomainReport struct {
Domain string
Checks []PTRCheck
Summary string
}
func performPTRCheck(domain string, ip net.IP, timeout time.Duration) PTRCheck {
check := PTRCheck{
IPAddress: ip.String(),
}
if ip.To4() != nil {
check.IPVersion = "IPv4"
} else {
check.IPVersion = "IPv6"
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resolver := net.DefaultResolver
// Risoluzione PTR
names, err := resolver.LookupAddr(ctx, ip.String())
if err != nil {
check.Error = fmt.Errorf("query PTR fallita: %w", err)
return check
}
check.PTRRecords = names
// Verifica corrispondenza con il dominio
for _, name := range names {
cleanName := strings.TrimSuffix(name, ".")
if strings.EqualFold(cleanName, domain) {
check.DomainMatch = true
break
}
}
// Verifica FCrDNS: il PTR deve risolvere all'IP originale
if len(names) > 0 {
ptrName := strings.TrimSuffix(names[0], ".")
forwardIPs, err := resolver.LookupHost(ctx, ptrName)
if err == nil {
check.ForwardIPs = forwardIPs
for _, forwardIP := range forwardIPs {
if forwardIP == ip.String() {
check.FCrDNSValid = true
break
}
}
}
}
return check
}
func generateReport(domain string) *DomainReport {
report := &DomainReport{
Domain: domain,
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", domain)
if err != nil {
report.Summary = fmt.Sprintf("Impossibile risolvere il dominio: %v", err)
return report
}
// Esecuzione delle verifiche per ogni indirizzo IP
for _, ip := range ips {
check := performPTRCheck(domain, ip, 5*time.Second)
report.Checks = append(report.Checks, check)
}
// Generazione del sommario
validCount := 0
for _, check := range report.Checks {
if check.FCrDNSValid && check.DomainMatch {
validCount++
}
}
report.Summary = fmt.Sprintf("%d/%d indirizzi con configurazione PTR valida",
validCount, len(report.Checks))
return report
}
func printReport(report *DomainReport) {
fmt.Printf("=== Report PTR per %s ===\n\n", report.Domain)
for i, check := range report.Checks {
fmt.Printf("[%d] Indirizzo: %s (%s)\n", i+1, check.IPAddress, check.IPVersion)
if check.Error != nil {
fmt.Printf(" Errore: %v\n", check.Error)
continue
}
fmt.Printf(" Record PTR:\n")
for _, ptr := range check.PTRRecords {
fmt.Printf(" - %s\n", ptr)
}
fmt.Printf(" Corrispondenza dominio: %v\n", check.DomainMatch)
fmt.Printf(" FCrDNS valido: %v\n", check.FCrDNSValid)
if len(check.ForwardIPs) > 0 {
fmt.Printf(" IP da risoluzione diretta: %v\n", check.ForwardIPs)
}
fmt.Println()
}
fmt.Printf("Riepilogo: %s\n", report.Summary)
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Utilizzo: ptr-checker <dominio>")
os.Exit(1)
}
domain := os.Args[1]
report := generateReport(domain)
printReport(report)
}
Questo strumento completo esegue una serie di verifiche per ogni indirizzo IP associato al dominio. La verifica FCrDNS è particolarmente importante perché molti servizi, in particolare i provider di posta elettronica, richiedono che il record PTR risolva a un nome che a sua volta risolve allo stesso indirizzo IP originale. Senza questa corrispondenza bidirezionale, le email inviate dal server potrebbero essere rifiutate o contrassegnate come spam.
Gestione degli errori e dei timeout
Le query DNS possono fallire per molteplici ragioni: timeout di rete, server DNS non raggiungibili, record inesistenti, o errori di configurazione. Una gestione robusta degli errori è essenziale per qualsiasi strumento di verifica DNS. Go offre il pattern dei wrapped errors tramite fmt.Errorf con il verbo %w, che permette di mantenere la catena degli errori per analisi successive.
package main
import (
"context"
"errors"
"fmt"
"net"
"time"
)
// Errori personalizzati per la verifica PTR
var (
ErrDomainNotFound = errors.New("dominio non trovato")
ErrPTRNotFound = errors.New("record PTR non trovato")
ErrTimeout = errors.New("timeout durante la query")
ErrInvalidPTR = errors.New("configurazione PTR non valida")
)
func robustPTRLookup(domain string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Prima fase: risoluzione del dominio
ips, err := net.DefaultResolver.LookupIP(ctx, "ip4", domain)
if err != nil {
// Verifica del tipo di errore specifico
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
if dnsErr.IsNotFound {
return nil, fmt.Errorf("%w: %s", ErrDomainNotFound, domain)
}
if dnsErr.IsTimeout {
return nil, fmt.Errorf("%w: %s", ErrTimeout, domain)
}
}
return nil, fmt.Errorf("errore di risoluzione: %w", err)
}
if len(ips) == 0 {
return nil, ErrDomainNotFound
}
// Seconda fase: query PTR
names, err := net.DefaultResolver.LookupAddr(ctx, ips[0].String())
if err != nil {
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
return nil, fmt.Errorf("%w per %s", ErrPTRNotFound, ips[0])
}
return nil, fmt.Errorf("errore PTR: %w", err)
}
return names, nil
}
func main() {
domains := []string{
"google.com",
"example.invalid",
"github.com",
}
for _, domain := range domains {
names, err := robustPTRLookup(domain)
if err != nil {
// Gestione differenziata degli errori
switch {
case errors.Is(err, ErrDomainNotFound):
fmt.Printf("[%s] Dominio non esistente\n", domain)
case errors.Is(err, ErrPTRNotFound):
fmt.Printf("[%s] Record PTR mancante\n", domain)
case errors.Is(err, ErrTimeout):
fmt.Printf("[%s] Timeout della query\n", domain)
default:
fmt.Printf("[%s] Errore generico: %v\n", domain, err)
}
continue
}
fmt.Printf("[%s] PTR: %v\n", domain, names)
}
}
L'utilizzo di net.DNSError tramite errors.As permette di accedere ai campi specifici dell'errore DNS, come IsNotFound e IsTimeout, per fornire messaggi di errore più precisi e informativi. La definizione di errori personalizzati (sentinel errors) facilita inoltre la gestione differenziata degli errori da parte del codice chiamante.
Considerazioni finali
La verifica del record PTR è un'operazione apparentemente semplice ma che nasconde diverse sottigliezze tecniche. Go, con la sua libreria standard ricca e ben progettata, offre tutti gli strumenti necessari per implementare verifiche DNS complete e robuste. Il package net gestisce in modo trasparente la complessità della risoluzione DNS, permettendoci di concentrarci sulla logica applicativa.
Quando si implementa uno strumento di verifica PTR per ambienti di produzione, è importante considerare aspetti come la concorrenza (utilizzando goroutine per verificare più domini in parallelo), il caching dei risultati per ridurre il carico sui server DNS, e l'integrazione con sistemi di monitoraggio per ricevere alert in caso di anomalie nella configurazione. La verifica FCrDNS, in particolare, dovrebbe essere parte integrante dei controlli di salute di qualsiasi server di posta elettronica o servizio che richieda autenticazione basata sull'indirizzo IP.
Il codice presentato in questo articolo costituisce una base solida che può essere estesa con funzionalità aggiuntive come l'analisi dei record SPF e DMARC, la verifica della consistenza tra più resolver DNS, o l'integrazione con API di terze parti per la verifica della reputazione degli indirizzi IP.