1// Package iprev checks if an IP has a reverse DNS name configured and that the
2// reverse DNS name resolves back to the IP (RFC 8601, Section 3).
3package iprev
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "net"
10 "time"
11
12 "golang.org/x/exp/slog"
13
14 "github.com/mjl-/mox/dns"
15 "github.com/mjl-/mox/mlog"
16 "github.com/mjl-/mox/stub"
17)
18
19var xlog = mlog.New("iprev", nil)
20
21var (
22 MetricIPRev stub.HistogramVec = stub.HistogramVecIgnore{}
23)
24
25// Lookup errors.
26var (
27 ErrNoRecord = errors.New("iprev: no reverse dns record")
28 ErrDNS = errors.New("iprev: dns lookup") // Temporary error.
29)
30
31// ../rfc/8601:1082
32
33// Status is the result of a lookup.
34type Status string
35
36const (
37 StatusPass Status = "pass" // Reverse and forward lookup results were in agreement.
38 StatusFail Status = "fail" // Reverse and forward lookup results were not in agreement, but at least the reverse name does exist.
39 StatusTemperror Status = "temperror" // Temporary error, e.g. DNS timeout.
40 StatusPermerror Status = "permerror" // Permanent error and later retry is unlikely to succeed. E.g. no PTR record.
41)
42
43// Lookup checks whether an IP has a proper reverse & forward
44// DNS configuration. I.e. that it is explicitly associated with its domain name.
45//
46// A PTR lookup is done on the IP, resulting in zero or more names. These names are
47// forward resolved (A or AAAA) until the original IP address is found. The first
48// matching name is returned as "name". All names, matching or not, are returned as
49// "names".
50//
51// If a temporary error occurred, rerr is set.
52func Lookup(ctx context.Context, resolver dns.Resolver, ip net.IP) (rstatus Status, name string, names []string, authentic bool, rerr error) {
53 log := xlog.WithContext(ctx)
54 start := time.Now()
55 defer func() {
56 MetricIPRev.ObserveLabels(float64(time.Since(start))/float64(time.Second), string(rstatus))
57 log.Debugx("iprev lookup result", rerr,
58 slog.Any("ip", ip),
59 slog.Any("status", rstatus),
60 slog.Duration("duration", time.Since(start)))
61 }()
62
63 revNames, result, revErr := dns.WithPackage(resolver, "iprev").LookupAddr(ctx, ip.String())
64 if dns.IsNotFound(revErr) {
65 return StatusPermerror, "", nil, result.Authentic, ErrNoRecord
66 } else if revErr != nil {
67 return StatusTemperror, "", nil, result.Authentic, fmt.Errorf("%w: %s", ErrDNS, revErr)
68 }
69
70 var lastErr error
71 authentic = result.Authentic
72 for _, rname := range revNames {
73 ips, result, err := dns.WithPackage(resolver, "iprev").LookupIP(ctx, "ip", rname)
74 authentic = authentic && result.Authentic
75 for _, fwdIP := range ips {
76 if ip.Equal(fwdIP) {
77 return StatusPass, rname, revNames, authentic, nil
78 }
79 }
80 if err != nil && !dns.IsNotFound(err) {
81 lastErr = err
82 }
83 }
84 if lastErr != nil {
85 return StatusTemperror, "", revNames, authentic, fmt.Errorf("%w: %s", ErrDNS, lastErr)
86 }
87 return StatusFail, "", revNames, authentic, nil
88}
89