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