Add reverse lookup and forward confirmation

This commit is contained in:
nemunaire 2025-10-23 15:59:57 +07:00
commit 84a504d668
8 changed files with 324 additions and 5 deletions

View file

@ -73,6 +73,22 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.Authentic
spfDomain = *results.RpDomain
}
// Store sender IP for later use in scoring
var senderIP string
if headersResults.ReceivedChain != nil && len(*headersResults.ReceivedChain) > 0 {
firstHop := (*headersResults.ReceivedChain)[0]
if firstHop.Ip != nil && *firstHop.Ip != "" {
senderIP = *firstHop.Ip
ptrRecords, forwardRecords := d.checkPTRAndForward(senderIP)
if len(ptrRecords) > 0 {
results.PtrRecords = &ptrRecords
}
if len(forwardRecords) > 0 {
results.PtrForwardRecords = &forwardRecords
}
}
}
// Check MX records for From domain (where replies would go)
results.FromMxRecords = d.checkMXRecords(fromDomain)
@ -613,16 +629,78 @@ func (d *DNSAnalyzer) validateBIMI(record string) bool {
return true
}
// checkPTRAndForward performs reverse DNS lookup (PTR) and forward confirmation (A/AAAA)
// Returns PTR hostnames and their corresponding forward-resolved IPs
func (d *DNSAnalyzer) checkPTRAndForward(ip string) ([]string, []string) {
ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
defer cancel()
// Perform reverse DNS lookup (PTR)
ptrNames, err := d.resolver.LookupAddr(ctx, ip)
if err != nil || len(ptrNames) == 0 {
return nil, nil
}
var forwardIPs []string
seenIPs := make(map[string]bool)
// For each PTR record, perform forward DNS lookup (A/AAAA)
for _, ptrName := range ptrNames {
// Look up A records
ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
aRecords, err := d.resolver.LookupHost(ctx, ptrName)
cancel()
if err == nil {
for _, forwardIP := range aRecords {
if !seenIPs[forwardIP] {
forwardIPs = append(forwardIPs, forwardIP)
seenIPs[forwardIP] = true
}
}
}
}
return ptrNames, forwardIPs
}
// CalculateDNSScore calculates the DNS score from records results
// Returns a score from 0-100 where higher is better
func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults) (int, string) {
// senderIP is the original sender IP address used for FCrDNS verification
func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults, senderIP string) (int, string) {
if results == nil {
return 0, ""
}
score := 0
// TODO: 20 points for correct PTR and A/AAAA
// PTR and Forward DNS: 20 points
// Proper reverse DNS (PTR) and forward-confirmed reverse DNS (FCrDNS) is important for deliverability
if results.PtrRecords != nil && len(*results.PtrRecords) > 0 {
// 10 points for having PTR records
score += 10
if len(*results.PtrRecords) > 1 {
// Penalty has it's bad to have multiple PTR records
score -= 3
}
// Additional 10 points for forward-confirmed reverse DNS (FCrDNS)
// This means the PTR hostname resolves back to IPs that include the original sender IP
if results.PtrForwardRecords != nil && len(*results.PtrForwardRecords) > 0 && senderIP != "" {
// Verify that the sender IP is in the list of forward-resolved IPs
fcrDnsValid := false
for _, forwardIP := range *results.PtrForwardRecords {
if forwardIP == senderIP {
fcrDnsValid = true
break
}
}
if fcrDnsValid {
score += 10
}
}
}
// MX Records: 20 points (10 for From domain, 10 for Return-Path domain)
// Having valid MX records is critical for email deliverability

View file

@ -98,7 +98,15 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
dnsScore := 0
var dnsGrade string
if results.DNS != nil {
dnsScore, dnsGrade = r.dnsAnalyzer.CalculateDNSScore(results.DNS)
// Extract sender IP from received chain for FCrDNS verification
var senderIP string
if results.Headers != nil && results.Headers.ReceivedChain != nil && len(*results.Headers.ReceivedChain) > 0 {
firstHop := (*results.Headers.ReceivedChain)[0]
if firstHop.Ip != nil {
senderIP = *firstHop.Ip
}
}
dnsScore, dnsGrade = r.dnsAnalyzer.CalculateDNSScore(results.DNS, senderIP)
}
authScore := 0
@ -178,6 +186,7 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
// Calculate overall score as mean of all category scores
categoryScores := []int{
report.Summary.DnsScore,
report.Summary.AuthenticationScore,
report.Summary.BlacklistScore,
report.Summary.ContentScore,