mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
e0c36edb8f
embarrassingly, we didn't accept all reports we generated. after the changed handling of reports about mx/mail host vs recipient domains, would send reports to mail hosts about multiple recipient domains + the mail host. and we included a policy domain of the mail host when sending to a recipient domain. we were still being strict in what we accepted: only a single domain in total in the entire report, and we still enforced that a report sent to the mx host tlsrpt address only contained the mx host as policy domain. and likewise for recipient domains and their tls reporting addresses. those checks would reject reports generated by a mox instance. this probably only happens with dane configured, probably most users haven't seen it because of that. somewhat related to issue #125
175 lines
5.2 KiB
Go
175 lines
5.2 KiB
Go
// Package tlsrptdb stores reports from "SMTP TLS Reporting" in its database.
|
|
package tlsrptdb
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
|
|
"github.com/mjl-/bstore"
|
|
|
|
"github.com/mjl-/mox/dns"
|
|
"github.com/mjl-/mox/mlog"
|
|
"github.com/mjl-/mox/mox-"
|
|
"github.com/mjl-/mox/tlsrpt"
|
|
)
|
|
|
|
var (
|
|
metricSession = promauto.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "mox_tlsrptdb_session_total",
|
|
Help: "Number of sessions, both success and known result types.",
|
|
},
|
|
[]string{"type"}, // Known result types, and "success"
|
|
)
|
|
|
|
knownResultTypes = map[tlsrpt.ResultType]struct{}{
|
|
tlsrpt.ResultSTARTTLSNotSupported: {},
|
|
tlsrpt.ResultCertificateHostMismatch: {},
|
|
tlsrpt.ResultCertificateExpired: {},
|
|
tlsrpt.ResultTLSAInvalid: {},
|
|
tlsrpt.ResultDNSSECInvalid: {},
|
|
tlsrpt.ResultDANERequired: {},
|
|
tlsrpt.ResultCertificateNotTrusted: {},
|
|
tlsrpt.ResultSTSPolicyInvalid: {},
|
|
tlsrpt.ResultSTSWebPKIInvalid: {},
|
|
tlsrpt.ResultValidationFailure: {},
|
|
tlsrpt.ResultSTSPolicyFetch: {},
|
|
}
|
|
)
|
|
|
|
// TLSReportRecord is a TLS report as a database record, including information
|
|
// about the sender.
|
|
//
|
|
// todo: should be named just Record, but it would cause a sherpa type name conflict.
|
|
type TLSReportRecord struct {
|
|
ID int64 `bstore:"typename Record"`
|
|
Domain string `bstore:"index"` // Policy domain to which the TLS report applies. Unicode.
|
|
FromDomain string
|
|
MailFrom string
|
|
HostReport bool // Report for host TLSRPT record, as opposed to domain TLSRPT record.
|
|
Report tlsrpt.Report
|
|
}
|
|
|
|
func reportDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
if ReportDB == nil {
|
|
p := mox.DataDirPath("tlsrpt.db")
|
|
os.MkdirAll(filepath.Dir(p), 0770)
|
|
db, err := bstore.Open(ctx, p, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, ReportDBTypes...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ReportDB = db
|
|
}
|
|
return ReportDB, nil
|
|
}
|
|
|
|
// AddReport adds a TLS report to the database.
|
|
//
|
|
// The report should have come in over SMTP, with a DKIM-validated
|
|
// verifiedFromDomain. Using HTTPS for reports is not recommended as there is no
|
|
// authentication on the reports origin.
|
|
//
|
|
// Only reports for known domains are added to the database. Unknown domains are
|
|
// ignored without causing an error, unless no known domain was found in the report
|
|
// at all.
|
|
//
|
|
// Prometheus metrics are updated only for configured domains.
|
|
func AddReport(ctx context.Context, log mlog.Log, verifiedFromDomain dns.Domain, mailFrom string, hostReport bool, r *tlsrpt.Report) error {
|
|
db, err := reportDB(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(r.Policies) == 0 {
|
|
return fmt.Errorf("no policies in report")
|
|
}
|
|
|
|
var inserted int
|
|
return db.Write(ctx, func(tx *bstore.Tx) error {
|
|
for _, p := range r.Policies {
|
|
pp := p.Policy
|
|
|
|
d, err := dns.ParseDomain(pp.Domain)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid domain %v in tls report: %v", d, err)
|
|
}
|
|
|
|
if _, ok := mox.Conf.Domain(d); !ok && d != mox.Conf.Static.HostnameDomain {
|
|
log.Info("unknown host/recipient policy domain in tls report, not storing", slog.Any("domain", d), slog.String("mailfrom", mailFrom))
|
|
continue
|
|
}
|
|
|
|
metricSession.WithLabelValues("success").Add(float64(p.Summary.TotalSuccessfulSessionCount))
|
|
for _, f := range p.FailureDetails {
|
|
var result string
|
|
if _, ok := knownResultTypes[f.ResultType]; ok {
|
|
result = string(f.ResultType)
|
|
} else {
|
|
result = "other"
|
|
}
|
|
metricSession.WithLabelValues(result).Add(float64(f.FailedSessionCount))
|
|
}
|
|
|
|
record := TLSReportRecord{0, d.Name(), verifiedFromDomain.Name(), mailFrom, d == mox.Conf.Static.HostnameDomain, *r}
|
|
if err := tx.Insert(&record); err != nil {
|
|
return fmt.Errorf("inserting report for domain: %w", err)
|
|
}
|
|
inserted++
|
|
}
|
|
if inserted == 0 {
|
|
return fmt.Errorf("no domains in report recognized")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Records returns all TLS reports in the database.
|
|
func Records(ctx context.Context) ([]TLSReportRecord, error) {
|
|
db, err := reportDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bstore.QueryDB[TLSReportRecord](ctx, db).List()
|
|
}
|
|
|
|
// RecordID returns the report for the ID.
|
|
func RecordID(ctx context.Context, id int64) (TLSReportRecord, error) {
|
|
db, err := reportDB(ctx)
|
|
if err != nil {
|
|
return TLSReportRecord{}, err
|
|
}
|
|
|
|
e := TLSReportRecord{ID: id}
|
|
err = db.Get(ctx, &e)
|
|
return e, err
|
|
}
|
|
|
|
// RecordsPeriodPolicyDomain returns the reports overlapping start and end, for the
|
|
// given policy domain. If policy domain is empty, records for all domains are
|
|
// returned.
|
|
func RecordsPeriodDomain(ctx context.Context, start, end time.Time, policyDomain dns.Domain) ([]TLSReportRecord, error) {
|
|
db, err := reportDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q := bstore.QueryDB[TLSReportRecord](ctx, db)
|
|
var zerodom dns.Domain
|
|
if policyDomain != zerodom {
|
|
q.FilterNonzero(TLSReportRecord{Domain: policyDomain.Name()})
|
|
}
|
|
q.FilterFn(func(r TLSReportRecord) bool {
|
|
dr := r.Report.DateRange
|
|
return !dr.Start.Before(start) && dr.Start.Before(end) || dr.End.After(start) && !dr.End.After(end)
|
|
})
|
|
return q.List()
|
|
}
|