mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 00:13:47 +03:00
implement only monitoring dns blocklists, without using them for incoming deliveries
so you can still know when someone has put you on their blocklist (which may affect delivery), without using them. also query dnsbls for our ips more often when we do more outgoing connections for delivery: once every 100 messages, but at least 5 mins and at most 3 hours since the previous check.
This commit is contained in:
parent
e0c36edb8f
commit
15e450df61
13 changed files with 305 additions and 83 deletions
|
@ -111,8 +111,10 @@ type Dynamic struct {
|
|||
WebDomainRedirects map[string]string `sconf:"optional" sconf-doc:"Redirect all requests from domain (key) to domain (value). Always redirects to HTTPS. For plain HTTP redirects, use a WebHandler with a WebRedirect."`
|
||||
WebHandlers []WebHandler `sconf:"optional" sconf-doc:"Handle webserver requests by serving static files, redirecting or reverse-proxying HTTP(s). The first matching WebHandler will handle the request. Built-in handlers, e.g. for account, admin, autoconfig and mta-sts always run first. If no handler matches, the response status code is file not found (404). If functionality you need is missng, simply forward the requests to an application that can provide the needed functionality."`
|
||||
Routes []Route `sconf:"optional" sconf-doc:"Routes for delivering outgoing messages through the queue. Each delivery attempt evaluates account routes, domain routes and finally these global routes. The transport of the first matching route is used in the delivery attempt. If no routes match, which is the default with no configured routes, messages are delivered directly from the queue."`
|
||||
MonitorDNSBLs []string `sconf:"optional" sconf-doc:"DNS blocklists to periodically check with if IPs we send from are present, without using them for checking incoming deliveries.. Also see DNSBLs in SMTP listeners in mox.conf, which specifies DNSBLs to use both for incoming deliveries and for checking our IPs against. Example DNSBLs: sbl.spamhaus.org, bl.spamcop.net."`
|
||||
|
||||
WebDNSDomainRedirects map[dns.Domain]dns.Domain `sconf:"-" json:"-"`
|
||||
MonitorDNSBLZones []dns.Domain `sconf:"-"`
|
||||
}
|
||||
|
||||
type ACME struct {
|
||||
|
@ -150,7 +152,7 @@ type Listener struct {
|
|||
// Reoriginated messages (such as messages sent to mailing list subscribers) should
|
||||
// keep REQUIRETLS. ../rfc/8689:412
|
||||
|
||||
DNSBLs []string `sconf:"optional" sconf-doc:"Addresses of DNS block lists for incoming messages. Block lists are only consulted for connections/messages without enough reputation to make an accept/reject decision. This prevents sending IPs of all communications to the block list provider. If any of the listed DNSBLs contains a requested IP address, the message is rejected as spam. The DNSBLs are checked for healthiness before use, at most once per 4 hours. Example DNSBLs: sbl.spamhaus.org, bl.spamcop.net. See https://www.spamhaus.org/sbl/ and https://www.spamcop.net/ for more information and terms of use."`
|
||||
DNSBLs []string `sconf:"optional" sconf-doc:"Addresses of DNS block lists for incoming messages. Block lists are only consulted for connections/messages without enough reputation to make an accept/reject decision. This prevents sending IPs of all communications to the block list provider. If any of the listed DNSBLs contains a requested IP address, the message is rejected as spam. The DNSBLs are checked for healthiness before use, at most once per 4 hours. IPs we can send from are periodically checked for being in the configured DNSBLs. See MonitorDNSBLs in domains.conf to only monitor IPs we send from, without using those DNSBLs for incoming messages. Example DNSBLs: sbl.spamhaus.org, bl.spamcop.net. See https://www.spamhaus.org/sbl/ and https://www.spamcop.net/ for more information and terms of use."`
|
||||
|
||||
FirstTimeSenderDelay *time.Duration `sconf:"optional" sconf-doc:"Delay before accepting a message from a first-time sender for the destination account. Default: 15s."`
|
||||
|
||||
|
|
|
@ -239,9 +239,12 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
|
|||
# accept/reject decision. This prevents sending IPs of all communications to the
|
||||
# block list provider. If any of the listed DNSBLs contains a requested IP
|
||||
# address, the message is rejected as spam. The DNSBLs are checked for healthiness
|
||||
# before use, at most once per 4 hours. Example DNSBLs: sbl.spamhaus.org,
|
||||
# bl.spamcop.net. See https://www.spamhaus.org/sbl/ and https://www.spamcop.net/
|
||||
# for more information and terms of use. (optional)
|
||||
# before use, at most once per 4 hours. IPs we can send from are periodically
|
||||
# checked for being in the configured DNSBLs. See MonitorDNSBLs in domains.conf to
|
||||
# only monitor IPs we send from, without using those DNSBLs for incoming messages.
|
||||
# Example DNSBLs: sbl.spamhaus.org, bl.spamcop.net. See
|
||||
# https://www.spamhaus.org/sbl/ and https://www.spamcop.net/ for more information
|
||||
# and terms of use. (optional)
|
||||
DNSBLs:
|
||||
-
|
||||
|
||||
|
@ -1198,6 +1201,14 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
|
|||
MinimumAttempts: 0
|
||||
Transport:
|
||||
|
||||
# DNS blocklists to periodically check with if IPs we send from are present,
|
||||
# without using them for checking incoming deliveries.. Also see DNSBLs in SMTP
|
||||
# listeners in mox.conf, which specifies DNSBLs to use both for incoming
|
||||
# deliveries and for checking our IPs against. Example DNSBLs: sbl.spamhaus.org,
|
||||
# bl.spamcop.net. (optional)
|
||||
MonitorDNSBLs:
|
||||
-
|
||||
|
||||
# Examples
|
||||
|
||||
Mox includes configuration files to illustrate common setups. You can see these
|
||||
|
|
|
@ -1069,6 +1069,35 @@ func AccountLimitsSave(ctx context.Context, account string, maxOutgoingMessagesP
|
|||
return nil
|
||||
}
|
||||
|
||||
func MonitorDNSBLsSave(ctx context.Context, zones []dns.Domain) (rerr error) {
|
||||
log := pkglog.WithContext(ctx)
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
log.Errorx("saving monitor dnsbl zones", rerr)
|
||||
}
|
||||
}()
|
||||
|
||||
Conf.dynamicMutex.Lock()
|
||||
defer Conf.dynamicMutex.Unlock()
|
||||
|
||||
c := Conf.Dynamic
|
||||
|
||||
// Compose new config without modifying existing data structures. If we fail, we
|
||||
// leave no trace.
|
||||
nc := c
|
||||
nc.MonitorDNSBLs = make([]string, len(zones))
|
||||
nc.MonitorDNSBLZones = nil
|
||||
for i, z := range zones {
|
||||
nc.MonitorDNSBLs[i] = z.Name()
|
||||
}
|
||||
|
||||
if err := writeDynamic(ctx, log, nc); err != nil {
|
||||
return fmt.Errorf("writing domains.conf: %v", err)
|
||||
}
|
||||
log.Info("monitor dnsbl zones saved")
|
||||
return nil
|
||||
}
|
||||
|
||||
type TLSMode uint8
|
||||
|
||||
const (
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"os/user"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -242,6 +243,13 @@ func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, d
|
|||
return
|
||||
}
|
||||
|
||||
func (c *Config) MonitorDNSBLs() (zones []dns.Domain) {
|
||||
c.withDynamicLock(func() {
|
||||
zones = c.Dynamic.MonitorDNSBLZones
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
|
||||
for _, l := range c.Static.Listeners {
|
||||
if l.TLS == nil || l.TLS.ACME == "" {
|
||||
|
@ -1609,6 +1617,19 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
|||
}
|
||||
}
|
||||
|
||||
for _, s := range c.MonitorDNSBLs {
|
||||
d, err := dns.ParseDomain(s)
|
||||
if err != nil {
|
||||
addErrorf("invalid monitor dnsbl zone %s: %v", s, err)
|
||||
continue
|
||||
}
|
||||
if slices.Contains(c.MonitorDNSBLZones, d) {
|
||||
addErrorf("duplicate zone %s in monitor dnsbl zones", d)
|
||||
continue
|
||||
}
|
||||
c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
@ -30,6 +31,10 @@ import (
|
|||
"github.com/mjl-/mox/tlsrpt"
|
||||
)
|
||||
|
||||
// Increased each time an outgoing connection is made for direct delivery. Used by
|
||||
// dnsbl monitoring to pace querying.
|
||||
var connectionCounter atomic.Int64
|
||||
|
||||
var (
|
||||
metricDestinations = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
|
@ -88,6 +93,10 @@ var (
|
|||
)
|
||||
)
|
||||
|
||||
func ConnectionCounter() int64 {
|
||||
return connectionCounter.Load()
|
||||
}
|
||||
|
||||
// todo: rename function, perhaps put some of the params in a delivery struct so we don't pass all the params all the time?
|
||||
func fail(ctx context.Context, qlog mlog.Log, m Msg, backoff time.Duration, permanent bool, remoteMTA dsn.NameIP, secodeOpt, errmsg, firstLine string, moreLines []string) {
|
||||
// todo future: when we implement relaying, we should be able to send DSNs to non-local users. and possibly specify a null mailfrom. ../rfc/5321:1503
|
||||
|
@ -534,6 +543,7 @@ func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
|
|||
if m.DialedIPs == nil {
|
||||
m.DialedIPs = map[string][]net.IP{}
|
||||
}
|
||||
connectionCounter.Add(1)
|
||||
conn, remoteIP, err = smtpclient.Dial(ctx, log.Logger, dialer, host, ips, 25, m.DialedIPs, mox.Conf.Static.SpecifiedSMTPListenIPs)
|
||||
}
|
||||
cancel()
|
||||
|
|
|
@ -708,6 +708,11 @@ and check the admin page for the needed DNS records.`)
|
|||
public.SMTP.DNSBLs = append(public.SMTP.DNSBLs, zone.Name())
|
||||
}
|
||||
|
||||
// Monitor DNSBLs by default, without using them for incoming deliveries.
|
||||
for _, zone := range zones {
|
||||
dc.MonitorDNSBLs = append(dc.MonitorDNSBLs, zone.Name())
|
||||
}
|
||||
|
||||
internal := config.Listener{
|
||||
IPs: privateListenerIPs,
|
||||
Hostname: "localhost",
|
||||
|
|
111
serve_unix.go
111
serve_unix.go
|
@ -14,8 +14,8 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
|
@ -29,10 +29,22 @@ import (
|
|||
"github.com/mjl-/mox/mlog"
|
||||
"github.com/mjl-/mox/mox-"
|
||||
"github.com/mjl-/mox/moxvar"
|
||||
"github.com/mjl-/mox/queue"
|
||||
"github.com/mjl-/mox/store"
|
||||
"github.com/mjl-/mox/updates"
|
||||
)
|
||||
|
||||
var metricDNSBL = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "mox_dnsbl_ips_success",
|
||||
Help: "DNSBL lookups to configured DNSBLs of our IPs.",
|
||||
},
|
||||
[]string{
|
||||
"zone",
|
||||
"ip",
|
||||
},
|
||||
)
|
||||
|
||||
func monitorDNSBL(log mlog.Log) {
|
||||
defer func() {
|
||||
// On error, don't bring down the entire server.
|
||||
|
@ -44,48 +56,71 @@ func monitorDNSBL(log mlog.Log) {
|
|||
}
|
||||
}()
|
||||
|
||||
l, ok := mox.Conf.Static.Listeners["public"]
|
||||
if !ok {
|
||||
log.Info("no listener named public, not monitoring our ips at dnsbls")
|
||||
return
|
||||
}
|
||||
|
||||
var zones []dns.Domain
|
||||
for _, zone := range l.SMTP.DNSBLs {
|
||||
d, err := dns.ParseDomain(zone)
|
||||
if err != nil {
|
||||
log.Fatalx("parsing dnsbls zone", err, slog.Any("zone", zone))
|
||||
}
|
||||
zones = append(zones, d)
|
||||
}
|
||||
if len(zones) == 0 {
|
||||
return
|
||||
}
|
||||
publicListener := mox.Conf.Static.Listeners["public"]
|
||||
|
||||
// We keep track of the previous metric values, so we can delete those we no longer
|
||||
// monitor.
|
||||
type key struct {
|
||||
zone dns.Domain
|
||||
ip string
|
||||
}
|
||||
metrics := map[key]prometheus.GaugeFunc{}
|
||||
var statusMutex sync.Mutex
|
||||
statuses := map[key]bool{}
|
||||
prevResults := map[key]struct{}{}
|
||||
|
||||
// Last time we checked, and how many outgoing delivery connections were made at that time.
|
||||
var last time.Time
|
||||
var lastConns int64
|
||||
|
||||
resolver := dns.StrictResolver{Pkg: "dnsblmonitor"}
|
||||
var sleep time.Duration // No sleep on first iteration.
|
||||
for {
|
||||
time.Sleep(sleep)
|
||||
sleep = 3 * time.Hour
|
||||
// We check more often when we send more. Every 100 messages, and between 5 mins
|
||||
// and 3 hours.
|
||||
conns := queue.ConnectionCounter()
|
||||
if sleep > 0 && conns < lastConns+100 && time.Since(last) < 3*time.Hour {
|
||||
continue
|
||||
}
|
||||
sleep = 5 * time.Minute
|
||||
lastConns = conns
|
||||
last = time.Now()
|
||||
|
||||
// Gather zones.
|
||||
zones := append([]dns.Domain{}, publicListener.SMTP.DNSBLZones...)
|
||||
for _, zone := range mox.Conf.MonitorDNSBLs() {
|
||||
if !slices.Contains(zones, zone) {
|
||||
zones = append(zones, zone)
|
||||
}
|
||||
}
|
||||
// And gather IPs.
|
||||
ips, err := mox.IPs(mox.Context, false)
|
||||
if err != nil {
|
||||
log.Errorx("listing ips for dnsbl monitor", err)
|
||||
// Mark checks as broken.
|
||||
for k := range prevResults {
|
||||
metricDNSBL.WithLabelValues(k.zone.Name(), k.ip).Set(-1)
|
||||
}
|
||||
continue
|
||||
}
|
||||
var publicIPs []net.IP
|
||||
var publicIPstrs []string
|
||||
for _, ip := range ips {
|
||||
if ip.IsLoopback() || ip.IsPrivate() {
|
||||
continue
|
||||
}
|
||||
publicIPs = append(publicIPs, ip)
|
||||
publicIPstrs = append(publicIPstrs, ip.String())
|
||||
}
|
||||
|
||||
// Remove labels that no longer exist from metric.
|
||||
for k := range prevResults {
|
||||
if !slices.Contains(zones, k.zone) || !slices.Contains(publicIPstrs, k.ip) {
|
||||
metricDNSBL.DeleteLabelValues(k.zone.Name(), k.ip)
|
||||
delete(prevResults, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Do DNSBL checks and update metric.
|
||||
for _, ip := range publicIPs {
|
||||
for _, zone := range zones {
|
||||
status, expl, err := dnsbl.Lookup(mox.Context, log.Logger, resolver, zone, ip)
|
||||
if err != nil {
|
||||
|
@ -95,32 +130,14 @@ func monitorDNSBL(log mlog.Log) {
|
|||
slog.String("expl", expl),
|
||||
slog.Any("status", status))
|
||||
}
|
||||
k := key{zone, ip.String()}
|
||||
|
||||
statusMutex.Lock()
|
||||
statuses[k] = status == dnsbl.StatusPass
|
||||
statusMutex.Unlock()
|
||||
|
||||
if _, ok := metrics[k]; !ok {
|
||||
metrics[k] = promauto.NewGaugeFunc(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "mox_dnsbl_ips_success",
|
||||
Help: "DNSBL lookups to configured DNSBLs of our IPs.",
|
||||
ConstLabels: prometheus.Labels{
|
||||
"zone": zone.LogString(),
|
||||
"ip": k.ip,
|
||||
},
|
||||
},
|
||||
func() float64 {
|
||||
statusMutex.Lock()
|
||||
defer statusMutex.Unlock()
|
||||
if statuses[k] {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
},
|
||||
)
|
||||
var v float64
|
||||
if status == dnsbl.StatusPass {
|
||||
v = 1
|
||||
}
|
||||
metricDNSBL.WithLabelValues(zone.Name(), ip.String()).Set(v)
|
||||
k := key{zone, ip.String()}
|
||||
prevResults[k] = struct{}{}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -212,6 +213,12 @@ func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
|
|||
panic(&sherpa.Error{Code: "user:error", Message: errmsg})
|
||||
}
|
||||
|
||||
func xusererrorf(ctx context.Context, format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
pkglog.WithContext(ctx).Error(msg)
|
||||
panic(&sherpa.Error{Code: "user:error", Message: msg})
|
||||
}
|
||||
|
||||
// LoginPrep returns a login token, and also sets it as cookie. Both must be
|
||||
// present in the call to Login.
|
||||
func (w Admin) LoginPrep(ctx context.Context) string {
|
||||
|
@ -1765,20 +1772,20 @@ func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
|
|||
//
|
||||
// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
|
||||
// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
|
||||
func (Admin) DNSBLStatus(ctx context.Context) map[string]map[string]string {
|
||||
func (Admin) DNSBLStatus(ctx context.Context) (results map[string]map[string]string, using, monitoring []dns.Domain) {
|
||||
log := mlog.New("webadmin", nil).WithContext(ctx)
|
||||
resolver := dns.StrictResolver{Pkg: "check", Log: log.Logger}
|
||||
return dnsblsStatus(ctx, log, resolver)
|
||||
}
|
||||
|
||||
func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) map[string]map[string]string {
|
||||
func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) (results map[string]map[string]string, using, monitoring []dns.Domain) {
|
||||
// todo: check health before using dnsbl?
|
||||
var dnsbls []dns.Domain
|
||||
if l, ok := mox.Conf.Static.Listeners["public"]; ok {
|
||||
for _, dnsbl := range l.SMTP.DNSBLs {
|
||||
zone, err := dns.ParseDomain(dnsbl)
|
||||
xcheckf(ctx, err, "parse dnsbl zone")
|
||||
dnsbls = append(dnsbls, zone)
|
||||
using = mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
|
||||
zones := append([]dns.Domain{}, using...)
|
||||
for _, zone := range mox.Conf.MonitorDNSBLs() {
|
||||
if !slices.Contains(zones, zone) {
|
||||
zones = append(zones, zone)
|
||||
monitoring = append(monitoring, zone)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1789,7 +1796,7 @@ func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) map[
|
|||
}
|
||||
ipstr := ip.String()
|
||||
r[ipstr] = map[string]string{}
|
||||
for _, zone := range dnsbls {
|
||||
for _, zone := range zones {
|
||||
status, expl, err := dnsbl.Lookup(ctx, log.Logger, resolver, zone, ip)
|
||||
result := string(status)
|
||||
if err != nil {
|
||||
|
@ -1801,7 +1808,29 @@ func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) map[
|
|||
r[ipstr][zone.LogString()] = result
|
||||
}
|
||||
}
|
||||
return r
|
||||
return r, using, monitoring
|
||||
}
|
||||
|
||||
func (Admin) MonitorDNSBLsSave(ctx context.Context, text string) {
|
||||
var zones []dns.Domain
|
||||
publicZones := mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
d, err := dns.ParseDomain(line)
|
||||
xcheckuserf(ctx, err, "parsing dnsbl zone %s", line)
|
||||
if slices.Contains(zones, d) {
|
||||
xusererrorf(ctx, "duplicate dnsbl zone %s", line)
|
||||
}
|
||||
if slices.Contains(publicZones, d) {
|
||||
xusererrorf(ctx, "dnsbl zone %s already present in public listener", line)
|
||||
}
|
||||
zones = append(zones, d)
|
||||
}
|
||||
err := mox.MonitorDNSBLsSave(ctx, zones)
|
||||
xcheckf(ctx, err, "saving monitoring dnsbl zones")
|
||||
}
|
||||
|
||||
// DomainRecords returns lines describing DNS records that should exist for the
|
||||
|
@ -1894,7 +1923,7 @@ func (Admin) AddressRemove(ctx context.Context, address string) {
|
|||
func (Admin) SetPassword(ctx context.Context, accountName, password string) {
|
||||
log := pkglog.WithContext(ctx)
|
||||
if len(password) < 8 {
|
||||
panic(&sherpa.Error{Code: "user:error", Message: "password must be at least 8 characters"})
|
||||
xusererrorf(ctx, "message must be at least 8 characters")
|
||||
}
|
||||
acc, err := store.OpenAccount(log, accountName)
|
||||
xcheckf(ctx, err, "open account")
|
||||
|
|
|
@ -714,10 +714,17 @@ var api;
|
|||
async DNSBLStatus() {
|
||||
const fn = "DNSBLStatus";
|
||||
const paramTypes = [];
|
||||
const returnTypes = [["{}", "{}", "string"]];
|
||||
const returnTypes = [["{}", "{}", "string"], ["[]", "Domain"], ["[]", "Domain"]];
|
||||
const params = [];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async MonitorDNSBLsSave(text) {
|
||||
const fn = "MonitorDNSBLsSave";
|
||||
const paramTypes = [["string"]];
|
||||
const returnTypes = [];
|
||||
const params = [text];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// DomainRecords returns lines describing DNS records that should exist for the
|
||||
// configured domain.
|
||||
async DomainRecords(domain) {
|
||||
|
@ -1624,7 +1631,7 @@ const index = async () => {
|
|||
fieldset.disabled = false;
|
||||
}
|
||||
window.location.hash = '#domains/' + domain.value;
|
||||
}, fieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), 'Domain', dom.br(), domain = dom.input(attr.required(''))), ' ', dom.label(style({ display: 'inline-block' }), 'Postmaster/reporting account', dom.br(), account = dom.input(attr.required(''))), ' ', dom.label(style({ display: 'inline-block' }), dom.span('Localpart (optional)', attr.title('Must be set if and only if account does not yet exist. The localpart for the user of this domain. E.g. postmaster.')), dom.br(), localpart = dom.input()), ' ', dom.submitbutton('Add domain', attr.title('Domain will be added and the config reloaded. You should add the required DNS records after adding the domain.')))), dom.br(), dom.h2('Reports'), dom.div(dom.a('DMARC', attr.href('#dmarc/reports'))), dom.div(dom.a('TLS', attr.href('#tlsrpt/reports'))), dom.br(), dom.h2('Operations'), dom.div(dom.a('MTA-STS policies', attr.href('#mtasts'))), dom.div(dom.a('DMARC evaluations', attr.href('#dmarc/evaluations'))), dom.div(dom.a('TLS connection results', attr.href('#tlsrpt/results'))), dom.div(style({ marginTop: '.5ex' }), dom.form(async function submit(e) {
|
||||
}, fieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), 'Domain', dom.br(), domain = dom.input(attr.required(''))), ' ', dom.label(style({ display: 'inline-block' }), 'Postmaster/reporting account', dom.br(), account = dom.input(attr.required(''))), ' ', dom.label(style({ display: 'inline-block' }), dom.span('Localpart (optional)', attr.title('Must be set if and only if account does not yet exist. The localpart for the user of this domain. E.g. postmaster.')), dom.br(), localpart = dom.input()), ' ', dom.submitbutton('Add domain', attr.title('Domain will be added and the config reloaded. You should add the required DNS records after adding the domain.')))), dom.br(), dom.h2('Reports'), dom.div(dom.a('DMARC', attr.href('#dmarc/reports'))), dom.div(dom.a('TLS', attr.href('#tlsrpt/reports'))), dom.br(), dom.h2('Operations'), dom.div(dom.a('MTA-STS policies', attr.href('#mtasts'))), dom.div(dom.a('DMARC evaluations', attr.href('#dmarc/evaluations'))), dom.div(dom.a('TLS connection results', attr.href('#tlsrpt/results'))), dom.div(dom.a('DNSBL', attr.href('#dnsbl'))), dom.div(style({ marginTop: '.5ex' }), dom.form(async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
|
@ -1642,7 +1649,7 @@ const index = async () => {
|
|||
}
|
||||
}, recvIDFieldset = dom.fieldset(dom.label('Received ID', attr.title('The ID in the Received header that was added during incoming delivery.')), ' ', recvID = dom.input(attr.required('')), ' ', dom.submitbutton('Lookup cid', attr.title('Logging about an incoming message includes an attribute "cid", a counter identifying the transaction related to delivery of the message. The ID in the received header is an encrypted cid, which this form decrypts, after which you can look it up in the logging.')), ' ', cidElem = dom.span()))),
|
||||
// todo: routing, globally, per domain and per account
|
||||
dom.br(), dom.h2('DNS blocklist status'), dom.div(dom.a('DNSBL status', attr.href('#dnsbl'))), dom.br(), dom.h2('Configuration'), dom.div(dom.a('Webserver', attr.href('#webserver'))), dom.div(dom.a('Files', attr.href('#config'))), dom.div(dom.a('Log levels', attr.href('#loglevels'))), footer);
|
||||
dom.br(), dom.h2('Configuration'), dom.div(dom.a('Webserver', attr.href('#webserver'))), dom.div(dom.a('Files', attr.href('#config'))), dom.div(dom.a('Log levels', attr.href('#loglevels'))), footer);
|
||||
};
|
||||
const config = async () => {
|
||||
const [staticPath, dynamicPath, staticText, dynamicText] = await client.ConfigFiles();
|
||||
|
@ -2653,14 +2660,31 @@ const makeMTASTSTable = (items) => {
|
|||
].map(v => dom.td(v === null ? [] : (v instanceof HTMLElement ? v : '' + v)))))));
|
||||
};
|
||||
const dnsbl = async () => {
|
||||
const ipZoneResults = await client.DNSBLStatus();
|
||||
const url = (ip) => {
|
||||
return 'https://multirbl.valli.org/lookup/' + encodeURIComponent(ip) + '.html';
|
||||
};
|
||||
const [ipZoneResults, usingZones, monitorZones] = await client.DNSBLStatus();
|
||||
const url = (ip) => 'https://multirbl.valli.org/lookup/' + encodeURIComponent(ip) + '.html';
|
||||
let fieldset;
|
||||
let monitorTextarea;
|
||||
dom._kids(page, crumbs(crumblink('Mox Admin', '#'), 'DNS blocklist status for IPs'), dom.p('Follow the external links to a third party DNSBL checker to see if the IP is on one of the many blocklist.'), dom.ul(Object.entries(ipZoneResults).sort().map(ipZones => {
|
||||
const [ip, zoneResults] = ipZones;
|
||||
return dom.li(link(url(ip), ip), !ipZones.length ? [] : dom.ul(Object.entries(zoneResults).sort().map(zoneResult => dom.li(zoneResult[0] + ': ', zoneResult[1] === 'pass' ? 'pass' : box(red, zoneResult[1])))));
|
||||
})), !Object.entries(ipZoneResults).length ? box(red, 'No IPs found.') : []);
|
||||
})), !Object.entries(ipZoneResults).length ? box(red, 'No IPs found.') : [], dom.br(), dom.h2('DNSBL zones checked due to being used for incoming deliveries'), (usingZones || []).length === 0 ?
|
||||
dom.div('None') :
|
||||
dom.ul((usingZones || []).map(zone => dom.li(domainString(zone)))), dom.br(), dom.h2('DNSBL zones to monitor only'), dom.form(async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
fieldset.disabled = true;
|
||||
try {
|
||||
await client.MonitorDNSBLsSave(monitorTextarea.value);
|
||||
dnsbl(); // Render page again.
|
||||
}
|
||||
catch (err) {
|
||||
console.log({ err });
|
||||
window.alert('Error: ' + errmsg(err));
|
||||
}
|
||||
finally {
|
||||
fieldset.disabled = false;
|
||||
}
|
||||
}, fieldset = dom.fieldset(dom.div('One per line'), dom.div(style({ marginBottom: '.5ex' }), monitorTextarea = dom.textarea(style({ width: '20rem' }), attr.rows('' + Math.max(5, 1 + (monitorZones || []).length)), new String((monitorZones || []).map(zone => domainName(zone)).join('\n'))), dom.div('Examples: sbl.spamhaus.org or bl.spamcop.net')), dom.div(dom.submitbutton('Save')))));
|
||||
};
|
||||
const queueList = async () => {
|
||||
const [msgs, transports] = await Promise.all([
|
||||
|
|
|
@ -333,6 +333,7 @@ const index = async () => {
|
|||
dom.div(dom.a('MTA-STS policies', attr.href('#mtasts'))),
|
||||
dom.div(dom.a('DMARC evaluations', attr.href('#dmarc/evaluations'))),
|
||||
dom.div(dom.a('TLS connection results', attr.href('#tlsrpt/results'))),
|
||||
dom.div(dom.a('DNSBL', attr.href('#dnsbl'))),
|
||||
dom.div(
|
||||
style({marginTop: '.5ex'}),
|
||||
dom.form(
|
||||
|
@ -361,9 +362,6 @@ const index = async () => {
|
|||
),
|
||||
// todo: routing, globally, per domain and per account
|
||||
dom.br(),
|
||||
dom.h2('DNS blocklist status'),
|
||||
dom.div(dom.a('DNSBL status', attr.href('#dnsbl'))),
|
||||
dom.br(),
|
||||
dom.h2('Configuration'),
|
||||
dom.div(dom.a('Webserver', attr.href('#webserver'))),
|
||||
dom.div(dom.a('Files', attr.href('#config'))),
|
||||
|
@ -2219,11 +2217,12 @@ const makeMTASTSTable = (items: api.PolicyRecord[]) => {
|
|||
}
|
||||
|
||||
const dnsbl = async () => {
|
||||
const ipZoneResults = await client.DNSBLStatus()
|
||||
const [ipZoneResults, usingZones, monitorZones] = await client.DNSBLStatus()
|
||||
|
||||
const url = (ip: string) => {
|
||||
return 'https://multirbl.valli.org/lookup/' + encodeURIComponent(ip) + '.html'
|
||||
}
|
||||
const url = (ip: string) => 'https://multirbl.valli.org/lookup/' + encodeURIComponent(ip) + '.html'
|
||||
|
||||
let fieldset: HTMLFieldSetElement
|
||||
let monitorTextarea: HTMLTextAreaElement
|
||||
|
||||
dom._kids(page,
|
||||
crumbs(
|
||||
|
@ -2248,6 +2247,43 @@ const dnsbl = async () => {
|
|||
})
|
||||
),
|
||||
!Object.entries(ipZoneResults).length ? box(red, 'No IPs found.') : [],
|
||||
dom.br(),
|
||||
dom.h2('DNSBL zones checked due to being used for incoming deliveries'),
|
||||
(usingZones || []).length === 0 ?
|
||||
dom.div('None') :
|
||||
dom.ul((usingZones || []).map(zone => dom.li(domainString(zone)))),
|
||||
dom.br(),
|
||||
dom.h2('DNSBL zones to monitor only'),
|
||||
dom.form(
|
||||
async function submit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
fieldset.disabled = true
|
||||
try {
|
||||
await client.MonitorDNSBLsSave(monitorTextarea.value)
|
||||
dnsbl() // Render page again.
|
||||
} catch (err) {
|
||||
console.log({err})
|
||||
window.alert('Error: ' + errmsg(err))
|
||||
} finally {
|
||||
fieldset.disabled = false
|
||||
}
|
||||
},
|
||||
fieldset=dom.fieldset(
|
||||
dom.div('One per line'),
|
||||
dom.div(
|
||||
style({marginBottom: '.5ex'}),
|
||||
monitorTextarea=dom.textarea(
|
||||
style({width: '20rem'}),
|
||||
attr.rows('' + Math.max(5, 1+(monitorZones || []).length)),
|
||||
new String((monitorZones || []).map(zone => domainName(zone)).join('\n')),
|
||||
),
|
||||
dom.div('Examples: sbl.spamhaus.org or bl.spamcop.net'),
|
||||
),
|
||||
dom.div(dom.submitbutton('Save')),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -432,15 +432,42 @@
|
|||
"Params": [],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "r0",
|
||||
"Name": "results",
|
||||
"Typewords": [
|
||||
"{}",
|
||||
"{}",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "using",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"Domain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "monitoring",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"Domain"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "MonitorDNSBLsSave",
|
||||
"Docs": "",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "text",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "DomainRecords",
|
||||
"Docs": "DomainRecords returns lines describing DNS records that should exist for the\nconfigured domain.",
|
||||
|
|
|
@ -1181,12 +1181,20 @@ export class Client {
|
|||
//
|
||||
// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
|
||||
// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
|
||||
async DNSBLStatus(): Promise<{ [key: string]: { [key: string]: string } }> {
|
||||
async DNSBLStatus(): Promise<[{ [key: string]: { [key: string]: string } }, Domain[] | null, Domain[] | null]> {
|
||||
const fn: string = "DNSBLStatus"
|
||||
const paramTypes: string[][] = []
|
||||
const returnTypes: string[][] = [["{}","{}","string"]]
|
||||
const returnTypes: string[][] = [["{}","{}","string"],["[]","Domain"],["[]","Domain"]]
|
||||
const params: any[] = []
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as { [key: string]: { [key: string]: string } }
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as [{ [key: string]: { [key: string]: string } }, Domain[] | null, Domain[] | null]
|
||||
}
|
||||
|
||||
async MonitorDNSBLsSave(text: string): Promise<void> {
|
||||
const fn: string = "MonitorDNSBLsSave"
|
||||
const paramTypes: string[][] = [["string"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [text]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
|
||||
// DomainRecords returns lines describing DNS records that should exist for the
|
||||
|
|
|
@ -241,9 +241,9 @@ another account can accept messages from the same sender.
|
|||
|
||||
### DNSBL
|
||||
|
||||
Mox can be configured to use an IP-based DNS blocklist (DNSBL). These are
|
||||
Mox can be configured to use an IP-based DNS blocklist (DNSBL). In other software, these are
|
||||
typically employed early in the SMTP session, to see if the remote IP is a
|
||||
known spammer. If so, the delivery attempt is stopped early. Mox doesn't use
|
||||
known spammer. If so, the delivery attempt is stopped immediately. Mox doesn't use
|
||||
DNSBLs in its default installation. But if it is configured to use a DNSBL, it
|
||||
is only invoked when the other reputation-based checks are not conclusive. For
|
||||
these reasons:
|
||||
|
@ -256,6 +256,9 @@ these reasons:
|
|||
3. No leaking of IP addresses of mail servers a mox instance is communicating
|
||||
with to the DNSBL operator.
|
||||
|
||||
Mox can also monitor DNSBLs for its own IPs only, without using those
|
||||
blocklists to analyze incoming deliveries. The status is exported in metrics.
|
||||
|
||||
### Greylisting
|
||||
|
||||
Greylisting is a commonly implemented mechanism whereby the first delivery
|
||||
|
|
Loading…
Reference in a new issue