mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
72ac1fde29
- prometheus is now behind an interface, they aren't dependencies for the reusable components anymore. - some dependencies have been inverted: instead of packages importing a main package to get configuration, the main package now sets configuration in these packages. that means fewer internals are pulled in. - some functions now have new parameters for values that were retrieved from package "mox-".
123 lines
3.5 KiB
Go
123 lines
3.5 KiB
Go
// Package dnsbl implements DNS block lists (RFC 5782), for checking incoming messages from sources without reputation.
|
|
package dnsbl
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/exp/slog"
|
|
|
|
"github.com/mjl-/mox/dns"
|
|
"github.com/mjl-/mox/mlog"
|
|
"github.com/mjl-/mox/stub"
|
|
)
|
|
|
|
var (
|
|
MetricLookup stub.HistogramVec = stub.HistogramVecIgnore{}
|
|
)
|
|
|
|
var ErrDNS = errors.New("dnsbl: dns error")
|
|
|
|
// Status is the result of a DNSBL lookup.
|
|
type Status string
|
|
|
|
var (
|
|
StatusTemperr Status = "temperror" // Temporary failure.
|
|
StatusPass Status = "pass" // Not present in block list.
|
|
StatusFail Status = "fail" // Present in block list.
|
|
)
|
|
|
|
// Lookup checks if "ip" occurs in the DNS block list "zone" (e.g. dnsbl.example.org).
|
|
func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, zone dns.Domain, ip net.IP) (rstatus Status, rexplanation string, rerr error) {
|
|
log := mlog.New("dnsbl", elog)
|
|
start := time.Now()
|
|
defer func() {
|
|
MetricLookup.ObserveLabels(float64(time.Since(start))/float64(time.Second), zone.Name(), string(rstatus))
|
|
log.Debugx("dnsbl lookup result", rerr,
|
|
slog.Any("zone", zone),
|
|
slog.Any("ip", ip),
|
|
slog.Any("status", rstatus),
|
|
slog.String("explanation", rexplanation),
|
|
slog.Duration("duration", time.Since(start)))
|
|
}()
|
|
|
|
b := &strings.Builder{}
|
|
v4 := ip.To4()
|
|
if v4 != nil {
|
|
// ../rfc/5782:148
|
|
s := len(v4) - 1
|
|
for i := s; i >= 0; i-- {
|
|
if i < s {
|
|
b.WriteByte('.')
|
|
}
|
|
b.WriteString(strconv.Itoa(int(v4[i])))
|
|
}
|
|
} else {
|
|
// ../rfc/5782:270
|
|
s := len(ip) - 1
|
|
const chars = "0123456789abcdef"
|
|
for i := s; i >= 0; i-- {
|
|
if i < s {
|
|
b.WriteByte('.')
|
|
}
|
|
v := ip[i]
|
|
b.WriteByte(chars[v>>0&0xf])
|
|
b.WriteByte('.')
|
|
b.WriteByte(chars[v>>4&0xf])
|
|
}
|
|
}
|
|
b.WriteString("." + zone.ASCII + ".")
|
|
addr := b.String()
|
|
|
|
// ../rfc/5782:175
|
|
_, _, err := dns.WithPackage(resolver, "dnsbl").LookupIP(ctx, "ip4", addr)
|
|
if dns.IsNotFound(err) {
|
|
return StatusPass, "", nil
|
|
} else if err != nil {
|
|
return StatusTemperr, "", fmt.Errorf("%w: %s", ErrDNS, err)
|
|
}
|
|
|
|
txts, _, err := dns.WithPackage(resolver, "dnsbl").LookupTXT(ctx, addr)
|
|
if dns.IsNotFound(err) {
|
|
return StatusFail, "", nil
|
|
} else if err != nil {
|
|
log.Debugx("looking up txt record from dnsbl", err, slog.String("addr", addr))
|
|
return StatusFail, "", nil
|
|
}
|
|
return StatusFail, strings.Join(txts, "; "), nil
|
|
}
|
|
|
|
// CheckHealth checks whether the DNSBL "zone" is operating correctly by
|
|
// querying for 127.0.0.2 (must be present) and 127.0.0.1 (must not be present).
|
|
// Users of a DNSBL should periodically check if the DNSBL is still operating
|
|
// properly.
|
|
// For temporary errors, ErrDNS is returned.
|
|
func CheckHealth(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, zone dns.Domain) (rerr error) {
|
|
log := mlog.New("dnsbl", elog)
|
|
start := time.Now()
|
|
defer func() {
|
|
log.Debugx("dnsbl healthcheck result", rerr, slog.Any("zone", zone), slog.Duration("duration", time.Since(start)))
|
|
}()
|
|
|
|
// ../rfc/5782:355
|
|
status1, _, err1 := Lookup(ctx, log.Logger, resolver, zone, net.IPv4(127, 0, 0, 1))
|
|
status2, _, err2 := Lookup(ctx, log.Logger, resolver, zone, net.IPv4(127, 0, 0, 2))
|
|
if status1 == StatusPass && status2 == StatusFail {
|
|
return nil
|
|
} else if status1 == StatusFail {
|
|
return fmt.Errorf("dnsbl contains unwanted test address 127.0.0.1")
|
|
} else if status2 == StatusPass {
|
|
return fmt.Errorf("dnsbl does not contain required test address 127.0.0.2")
|
|
}
|
|
if err1 != nil {
|
|
return err1
|
|
} else if err2 != nil {
|
|
return err2
|
|
}
|
|
return ErrDNS
|
|
}
|