mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
ec967ef321
the stuttering was introduced to make the same type name declared in multiple packages, and used in the admin sherpa api, unique. with sherpadoc's new rename, we can make them unique when generating the api definition/docs, and the Go code can use nicer names.
173 lines
5 KiB
Go
173 lines
5 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: {},
|
|
}
|
|
)
|
|
|
|
// Record is a TLS report as a database record, including information
|
|
// about the sender.
|
|
type Record struct {
|
|
ID int64
|
|
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 := Record{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) ([]Record, error) {
|
|
db, err := reportDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bstore.QueryDB[Record](ctx, db).List()
|
|
}
|
|
|
|
// RecordID returns the report for the ID.
|
|
func RecordID(ctx context.Context, id int64) (Record, error) {
|
|
db, err := reportDB(ctx)
|
|
if err != nil {
|
|
return Record{}, err
|
|
}
|
|
|
|
e := Record{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) ([]Record, error) {
|
|
db, err := reportDB(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q := bstore.QueryDB[Record](ctx, db)
|
|
var zerodom dns.Domain
|
|
if policyDomain != zerodom {
|
|
q.FilterNonzero(Record{Domain: policyDomain.Name()})
|
|
}
|
|
q.FilterFn(func(r Record) 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()
|
|
}
|