mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 18:24:35 +03:00
mox!
This commit is contained in:
90
iprev/iprev.go
Normal file
90
iprev/iprev.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Package iprev checks if an IP has a reverse DNS name configured and that the
|
||||
// reverse DNS name resolves back to the IP (RFC 8601, Section 3).
|
||||
package iprev
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/mjl-/mox/dns"
|
||||
"github.com/mjl-/mox/mlog"
|
||||
)
|
||||
|
||||
var xlog = mlog.New("iprev")
|
||||
|
||||
var (
|
||||
metricIPRev = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "mox_iprev_lookup_total",
|
||||
Help: "Number of iprev lookups.",
|
||||
Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30},
|
||||
},
|
||||
[]string{"status"},
|
||||
)
|
||||
)
|
||||
|
||||
// Lookup errors.
|
||||
var (
|
||||
ErrNoRecord = errors.New("iprev: no reverse dns record")
|
||||
ErrDNS = errors.New("iprev: dns lookup")
|
||||
)
|
||||
|
||||
// ../rfc/8601:1082
|
||||
|
||||
// Status is the result of a lookup.
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusPass Status = "pass" // Reverse and forward lookup results were in agreement.
|
||||
StatusFail Status = "fail" // Reverse and forward lookup results were not in agreement, but at least the reverse name does exist.
|
||||
StatusTemperror Status = "temperror" // Temporary error, e.g. DNS timeout.
|
||||
StatusPermerror Status = "permerror" // Permanent error and later retry is unlikely to succeed. E.g. no PTR record.
|
||||
)
|
||||
|
||||
// Lookup checks whether an IP has a proper reverse & forward
|
||||
// DNS configuration. I.e. that it is explicitly associated with its domain name.
|
||||
//
|
||||
// A PTR lookup is done on the IP, resulting in zero or more names. These names are
|
||||
// forward resolved (A or AAAA) until the original IP address is found. The first
|
||||
// matching name is returned as "name". All names, matching or not, are returned as
|
||||
// "names".
|
||||
//
|
||||
// If a temporary error occurred, rerr is set.
|
||||
func Lookup(ctx context.Context, resolver dns.Resolver, ip net.IP) (rstatus Status, name string, names []string, rerr error) {
|
||||
log := xlog.WithContext(ctx)
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricIPRev.WithLabelValues(string(rstatus)).Observe(float64(time.Since(start)) / float64(time.Second))
|
||||
log.Debugx("iprev lookup result", rerr, mlog.Field("ip", ip), mlog.Field("status", rstatus), mlog.Field("duration", time.Since(start)))
|
||||
}()
|
||||
|
||||
revNames, revErr := dns.WithPackage(resolver, "iprev").LookupAddr(ctx, ip.String())
|
||||
if dns.IsNotFound(revErr) {
|
||||
return StatusPermerror, "", nil, ErrNoRecord
|
||||
} else if revErr != nil {
|
||||
return StatusTemperror, "", nil, fmt.Errorf("%w: %s", ErrDNS, revErr)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, rname := range revNames {
|
||||
ips, err := dns.WithPackage(resolver, "iprev").LookupIP(ctx, "ip", rname)
|
||||
for _, fwdIP := range ips {
|
||||
if ip.Equal(fwdIP) {
|
||||
return StatusPass, rname, revNames, nil
|
||||
}
|
||||
}
|
||||
if err != nil && !dns.IsNotFound(err) {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
if lastErr != nil {
|
||||
return StatusTemperror, "", revNames, fmt.Errorf("%w: %s", ErrDNS, lastErr)
|
||||
}
|
||||
return StatusFail, "", revNames, nil
|
||||
}
|
68
iprev/iprev_test.go
Normal file
68
iprev/iprev_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package iprev
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mjl-/mox/dns"
|
||||
)
|
||||
|
||||
func TestIPRev(t *testing.T) {
|
||||
resolver := dns.MockResolver{
|
||||
PTR: map[string][]string{
|
||||
"10.0.0.1": {"basic.example."},
|
||||
"10.0.0.4": {"absent.example.", "b.example."},
|
||||
"10.0.0.5": {"other.example.", "c.example."},
|
||||
"10.0.0.6": {"temperror.example.", "d.example."},
|
||||
"10.0.0.7": {"temperror.example.", "temperror2.example."},
|
||||
"10.0.0.8": {"other.example."},
|
||||
"2001:db8::1": {"basic6.example."},
|
||||
},
|
||||
A: map[string][]string{
|
||||
"basic.example.": {"10.0.0.1"},
|
||||
"b.example.": {"10.0.0.4"},
|
||||
"c.example.": {"10.0.0.5"},
|
||||
"d.example.": {"10.0.0.6"},
|
||||
"other.example.": {"10.9.9.9"},
|
||||
"temperror.example.": {"10.0.0.99"},
|
||||
"temperror2.example.": {"10.0.0.99"},
|
||||
},
|
||||
AAAA: map[string][]string{
|
||||
"basic6.example.": {"2001:db8::1"},
|
||||
},
|
||||
Fail: map[dns.Mockreq]struct{}{
|
||||
{Type: "ptr", Name: "10.0.0.3"}: {},
|
||||
{Type: "ptr", Name: "2001:db8::3"}: {},
|
||||
{Type: "ip", Name: "temperror.example."}: {},
|
||||
{Type: "ip", Name: "temperror2.example."}: {},
|
||||
},
|
||||
}
|
||||
|
||||
test := func(ip string, expStatus Status, expName string, expNames string, expErr error) {
|
||||
t.Helper()
|
||||
|
||||
status, name, names, err := Lookup(context.Background(), resolver, net.ParseIP(ip))
|
||||
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
|
||||
t.Fatalf("got err %v, expected err %v", err, expErr)
|
||||
} else if err != nil {
|
||||
return
|
||||
} else if status != expStatus || name != expName || strings.Join(names, ",") != expNames {
|
||||
t.Fatalf("got status %q, name %q, expNames %v, expected %q %q %v", status, name, names, expStatus, expName, expNames)
|
||||
}
|
||||
}
|
||||
|
||||
test("10.0.0.1", StatusPass, "basic.example.", "basic.example.", nil)
|
||||
test("10.0.0.2", StatusPermerror, "", "", ErrNoRecord)
|
||||
test("10.0.0.3", StatusTemperror, "", "", ErrDNS)
|
||||
test("10.0.0.4", StatusPass, "b.example.", "absent.example.,b.example.", nil)
|
||||
test("10.0.0.5", StatusPass, "c.example.", "other.example.,c.example.", nil)
|
||||
test("10.0.0.6", StatusPass, "d.example.", "temperror.example.,d.example.", nil)
|
||||
test("10.0.0.7", StatusTemperror, "", "temperror.example.,temperror2.example.", ErrDNS)
|
||||
test("10.0.0.8", StatusFail, "", "other.example.", nil)
|
||||
test("2001:db8::1", StatusPass, "basic6.example.", "basic6.example.", nil)
|
||||
test("2001:db8::2", StatusPermerror, "", "", ErrNoRecord)
|
||||
test("2001:db8::3", StatusTemperror, "", "", ErrDNS)
|
||||
}
|
Reference in New Issue
Block a user