1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package remote2021import (22 "context"23 "crypto/tls"24 "errors"25 "os"26 "runtime/debug"27 "time"2829 "github.com/foxcpp/go-mtasts"30 "github.com/foxcpp/maddy/framework/config"31 "github.com/foxcpp/maddy/framework/dns"32 "github.com/foxcpp/maddy/framework/exterrors"33 "github.com/foxcpp/maddy/framework/future"34 "github.com/foxcpp/maddy/framework/log"35 "github.com/foxcpp/maddy/framework/module"36 "github.com/foxcpp/maddy/internal/target"37)3839type (40 mtastsPolicy struct {41 cache *mtasts.Cache42 mtastsGet func(context.Context, string) (*mtasts.Policy, error)43 updaterStop chan struct{}44 log log.Logger45 instName string46 }47 mtastsDelivery struct {48 c *mtastsPolicy49 domain string50 policyFut *future.Future51 log log.Logger52 }53)5455func NewMTASTSPolicy(_, instName string, _, _ []string) (module.Module, error) {56 return &mtastsPolicy{57 instName: instName,58 log: log.Logger{Name: "mx_auth.mtasts", Debug: log.DefaultLogger.Debug},59 }, nil60}6162func (c *mtastsPolicy) Name() string {63 return c.log.Name64}6566func (c *mtastsPolicy) InstanceName() string {67 return c.instName68}6970func (c *mtastsPolicy) Weight() int {71 return 1072}7374func (c *mtastsPolicy) Init(cfg *config.Map) error {75 var (76 storeType string77 storeDir string78 )79 cfg.Enum("cache", false, false, []string{"ram", "fs"}, "fs", &storeType)80 cfg.String("fs_dir", false, false, "mtasts_cache", &storeDir)81 if _, err := cfg.Process(); err != nil {82 return err83 }8485 switch storeType {86 case "fs":87 if err := os.MkdirAll(storeDir, os.ModePerm); err != nil {88 return err89 }90 c.cache = mtasts.NewFSCache(storeDir)91 case "ram":92 c.cache = mtasts.NewRAMCache()93 default:94 panic("mtasts policy init: unknown cache type")95 }96 c.cache.Resolver = dns.DefaultResolver()97 c.mtastsGet = c.cache.Get9899 return nil100}101102// StartUpdater starts a goroutine to update MTA-STS cache periodically until103// Close is called.104//105// It can be called only once per mtastsPolicy instance.106func (c *mtastsPolicy) StartUpdater() {107 c.updaterStop = make(chan struct{})108 go c.updater()109}110111func (c *mtastsPolicy) updater() {112 defer func() {113 if err := recover(); err != nil {114 stack := debug.Stack()115 log.Printf("panic during MTA-STS update: %v\n%s", err, stack)116 log.Printf("MTA-STS cache refresh disabled due to critical error")117 c.updaterStop = nil118 }119 }()120121 // Always update cache on start-up since we may have been down for some122 // time.123 c.log.Debugln("updating MTA-STS cache...")124 if err := c.cache.Refresh(); err != nil {125 c.log.Error("MTA-STS cache update error", err)126 }127 c.log.Debugln("updating MTA-STS cache... done!")128129 t := time.NewTicker(12 * time.Hour)130 for {131 select {132 case <-t.C:133 c.log.Debugln("updating MTA-STS cache...")134 if err := c.cache.Refresh(); err != nil {135 c.log.Error("MTA-STS cache opdate error", err)136 }137 c.log.Debugln("updating MTA-STS cache... done!")138 case <-c.updaterStop:139 c.updaterStop <- struct{}{}140 return141 }142 }143}144145func (c *mtastsPolicy) Start(msgMeta *module.MsgMetadata) module.DeliveryMXAuthPolicy {146 return &mtastsDelivery{147 c: c,148 log: target.DeliveryLogger(c.log, msgMeta),149 }150}151152func (c *mtastsPolicy) Close() error {153 if c.updaterStop != nil {154 c.updaterStop <- struct{}{}155 <-c.updaterStop156 c.updaterStop = nil157 }158 return nil159}160161func (c *mtastsDelivery) PrepareDomain(ctx context.Context, domain string) {162 c.policyFut = future.New()163 go func() {164 c.policyFut.Set(c.c.mtastsGet(ctx, domain))165 }()166}167168func (c *mtastsDelivery) PrepareConn(ctx context.Context, mx string) {}169170func (c *mtastsDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {171 policyI, err := c.policyFut.GetContext(ctx)172 if err != nil {173 c.log.DebugMsg("MTA-STS error", "err", err)174 return module.MXNone, nil175 }176 policy := policyI.(*mtasts.Policy)177178 if !policy.Match(mx) {179 if policy.Mode == mtasts.ModeEnforce {180 return module.MXNone, &exterrors.SMTPError{181 Code: 550,182 EnhancedCode: exterrors.EnhancedCode{5, 7, 0},183 Message: "Failed to establish the MX record authenticity (MTA-STS)",184 }185 }186 c.log.Msg("MX does not match published non-enforced MTA-STS policy", "mx", mx, "domain", c.domain)187 return module.MXNone, nil188 }189 return module.MX_MTASTS, nil190}191192func (c *mtastsDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {193 policyI, err := c.policyFut.GetContext(ctx)194 if err != nil {195 c.c.log.DebugMsg("MTA-STS error", "err", err)196 return module.TLSNone, nil197 }198 policy := policyI.(*mtasts.Policy)199200 if policy.Mode != mtasts.ModeEnforce {201 return module.TLSNone, nil202 }203204 if !tlsState.HandshakeComplete {205 return module.TLSNone, &exterrors.SMTPError{206 Code: 451,207 EnhancedCode: exterrors.EnhancedCode{4, 7, 1},208 Message: "TLS is required but unavailable or failed (MTA-STS)",209 }210 }211212 if tlsState.VerifiedChains == nil {213 return module.TLSNone, &exterrors.SMTPError{214 Code: 451,215 EnhancedCode: exterrors.EnhancedCode{4, 7, 1},216 Message: "Recipient server TLS certificate is not trusted but " +217 "authentication is required by MTA-STS",218 Misc: map[string]interface{}{219 "tls_level": tlsLevel,220 },221 }222 }223224 return module.TLSNone, nil225}226227func (c *mtastsDelivery) Reset(msgMeta *module.MsgMetadata) {228 c.policyFut = nil229 if msgMeta != nil {230 c.log = target.DeliveryLogger(c.c.log, msgMeta)231 }232}233234// Stub that will be removed in 0.5.235type stsPreloadPolicy struct {236 log log.Logger237 instName string238}239240func NewSTSPreload(_, instName string, _, _ []string) (module.Module, error) {241 return &stsPreloadPolicy{242 instName: instName,243 log: log.Logger{Name: "mx_auth.sts_preload", Debug: log.DefaultLogger.Debug},244 }, nil245}246247func (c *stsPreloadPolicy) Name() string {248 return c.log.Name249}250251func (c *stsPreloadPolicy) InstanceName() string {252 return c.instName253}254255func (c *stsPreloadPolicy) Weight() int {256 return 30 // after MTA-STS257}258259func (c *stsPreloadPolicy) Init(cfg *config.Map) error {260 c.log.Println("sts_preload module is deprecated and is no-op as the list is expired and unmaintained")261262 var (263 sourcePath string264 enforceTesting bool265 )266 cfg.String("source", false, false, "eff", &sourcePath)267 cfg.Bool("enforce_testing", false, true, &enforceTesting)268 if _, err := cfg.Process(); err != nil {269 return err270 }271272 return nil273}274275type preloadDelivery struct {276 *stsPreloadPolicy277}278279func (p *stsPreloadPolicy) Start(*module.MsgMetadata) module.DeliveryMXAuthPolicy {280 return &preloadDelivery{stsPreloadPolicy: p}281}282283func (p *preloadDelivery) Reset(*module.MsgMetadata) {}284func (p *preloadDelivery) PrepareDomain(ctx context.Context, domain string) {}285func (p *preloadDelivery) PrepareConn(ctx context.Context, mx string) {}286func (p *preloadDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {287 return mxLevel, nil288}289290func (p *preloadDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {291 return tlsLevel, nil292}293294func (p *stsPreloadPolicy) Close() error {295 return nil296}297298type dnssecPolicy struct {299 instName string300}301302func NewDNSSECPolicy(_, instName string, _, _ []string) (module.Module, error) {303 return &dnssecPolicy{304 instName: instName,305 }, nil306}307308func (c *dnssecPolicy) Name() string {309 return "mx_auth.dnssec"310}311312func (c *dnssecPolicy) InstanceName() string {313 return c.instName314}315316func (c *dnssecPolicy) Weight() int {317 return 1318}319320func (c *dnssecPolicy) Init(cfg *config.Map) error {321 _, err := cfg.Process() // will fail if there is any directive322 return err323}324325func (dnssecPolicy) Start(*module.MsgMetadata) module.DeliveryMXAuthPolicy {326 return dnssecPolicy{}327}328329func (dnssecPolicy) Close() error {330 return nil331}332333func (dnssecPolicy) Reset(*module.MsgMetadata) {}334func (dnssecPolicy) PrepareDomain(ctx context.Context, domain string) {}335func (dnssecPolicy) PrepareConn(ctx context.Context, mx string) {}336337func (dnssecPolicy) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {338 if dnssec {339 return module.MX_DNSSEC, nil340 }341 return module.MXNone, nil342}343344func (dnssecPolicy) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {345 return module.TLSNone, nil346}347348type (349 danePolicy struct {350 extResolver *dns.ExtResolver351 log log.Logger352 instName string353 }354 daneDelivery struct {355 c *danePolicy356 tlsaFut *future.Future357 }358)359360func NewDANEPolicy(_, instName string, _, _ []string) (module.Module, error) {361 return &danePolicy{362 instName: instName,363 log: log.Logger{Name: "remote/dane", Debug: log.DefaultLogger.Debug},364 }, nil365}366367func (c *danePolicy) Name() string {368 return "mx_auth.dane"369}370371func (c *danePolicy) InstanceName() string {372 return c.instName373}374375func (c *danePolicy) Weight() int {376 return 10377}378379func (c *danePolicy) Init(cfg *config.Map) error {380 var err error381 c.extResolver, err = dns.NewExtResolver()382 if err != nil {383 c.log.Error("DANE support is no-op: unable to init EDNS resolver", err)384 }385386 cfg.Bool("debug", true, log.DefaultLogger.Debug, &c.log.Debug)387388 _, err = cfg.Process()389 return err390}391392func (c *danePolicy) Start(*module.MsgMetadata) module.DeliveryMXAuthPolicy {393 return &daneDelivery{c: c}394}395396func (c *danePolicy) Close() error {397 return nil398}399400func (c *daneDelivery) PrepareDomain(ctx context.Context, domain string) {}401402func (c *daneDelivery) discoverTLSA(ctx context.Context, mx string) ([]dns.TLSA, error) {403 adA, rname, err := c.c.extResolver.CheckCNAMEAD(ctx, mx)404 if err != nil {405 // This may indicate a bogus DNSSEC signature or other lookup issue406 // (including non-existing domain).407 // Per RFC 7672, any I/O errors (including SERVFAIL) should408 // cause delivery to be delayed.409 return nil, err410 }411 if rname == "" {412 // No A/AAAA records, short-circuit discovery instead of doing useless413 // queries.414 return nil, errors.New("no address associated with the host")415 }416 if !adA {417 // If A lookup is not DNSSEC-authenticated we assume the server cannot418 // have TLSA record and skip trying to actually lookup TLSA419 // to avoid hitting weird errors like SERVFAIL, NOTIMP420 // e.g. see https://github.com/foxcpp/maddy/issues/287421 if rname == mx {422 c.c.log.Debugln("skipping DANE for", mx, "due to non-authenticated A records")423 return nil, nil424 }425426 // But if it is CNAME'd then we may not want to skip it and actually427 // consider initial name since it may be signed. To confirm the428 // initial name is signed, do CNAME lookup.429 cnameAD, _, err := c.c.extResolver.AuthLookupCNAME(ctx, mx)430 if err != nil {431 return nil, err432 }433 if !cnameAD {434 c.c.log.Debugln("skipping DANE for", mx, "due to non-authenticated CNAME record")435 return nil, nil436 }437 }438439 // If there was a CNAME - try it first.440 if rname != mx {441 ad, recs, err := c.c.extResolver.AuthLookupTLSA(ctx, "25", "tcp", rname)442 if err != nil && !dns.IsNotFound(err) {443 return nil, err444 }445 if ad && len(recs) != 0 {446 // recs may be empty or contain only unusable records - this is447 // okay per RFC 7672, no fallback to initial name is done.448 c.c.log.Debugln("using", len(recs), "DANE records at", rname, "to authenticate", mx)449 return recs, nil450 }451 // Per RFC 7672 Section 2.2 we interpret a non-authenticated RRset just452 // like an empty RRset and fallback to trying original name.453 c.c.log.Debugln("ignoring non-authenticated TLSA records for", rname)454 }455456 // If initial name is not a CNAME or final canonical name is not "secure"457 // - we consider TLSA under the initial name.458 ad, recs, err := c.c.extResolver.AuthLookupTLSA(ctx, "25", "tcp", mx)459 if err != nil && !dns.IsNotFound(err) {460 return nil, err461 }462 if !ad {463 c.c.log.Debugln("ignoring non-authenticated TLSA records for", mx)464 return nil, nil465 }466467 c.c.log.Debugln("using", len(recs), "DANE records at original name to authenticate", mx)468 return recs, nil469}470471func (c *daneDelivery) PrepareConn(ctx context.Context, mx string) {472 // No DNSSEC support.473 if c.c.extResolver == nil {474 return475 }476477 c.tlsaFut = future.New()478479 go func() {480 defer func() {481 if err := recover(); err != nil {482 stack := debug.Stack()483 log.Printf("panic during extended resolver lookup: %v\n%s", err, stack)484 }485 }()486487 c.tlsaFut.Set(c.discoverTLSA(ctx, dns.FQDN(mx)))488 }()489}490491func (c *daneDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {492 return module.MXNone, nil493}494495func (c *daneDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {496 // No DNSSEC support.497 if c.c.extResolver == nil {498 return module.TLSNone, nil499 }500501 recsI, err := c.tlsaFut.GetContext(ctx)502 if err != nil {503 // No records.504 if dns.IsNotFound(err) {505 return module.TLSNone, nil506 }507508 // Lookup error here indicates a resolution failure or may also509 // indicate a bogus DNSSEC signature.510 // There is a big problem with differentiating these two.511 //512 // We assume DANE failure in both cases as a safety measure.513 // However, there is a possibility of a temporary error condition,514 // so we mark it as such.515 return module.TLSNone, exterrors.WithTemporary(err, true)516 }517 recs := recsI.([]dns.TLSA)518519 overridePKIX, err := verifyDANE(recs, tlsState)520 if err != nil {521 return module.TLSNone, err522 }523 if overridePKIX {524 return module.TLSAuthenticated, nil525 }526 return module.TLSNone, nil527}528529func (c *daneDelivery) Reset(*module.MsgMetadata) {}530531type (532 localPolicy struct {533 instName string534 minTLSLevel module.TLSLevel535 minMXLevel module.MXLevel536 }537)538539func NewLocalPolicy(_, instName string, _, _ []string) (module.Module, error) {540 return &localPolicy{541 instName: instName,542 }, nil543}544545func (c *localPolicy) Name() string {546 return "mx_auth.local_policy"547}548549func (c *localPolicy) InstanceName() string {550 return c.instName551}552553func (c *localPolicy) Weight() int {554 return 1000555}556557func (c *localPolicy) Init(cfg *config.Map) error {558 var (559 minTLSLevel string560 minMXLevel string561 )562563 cfg.Enum("min_tls_level", false, false,564 []string{"none", "encrypted", "authenticated"}, "encrypted", &minTLSLevel)565 cfg.Enum("min_mx_level", false, false,566 []string{"none", "mtasts", "dnssec"}, "none", &minMXLevel)567 if _, err := cfg.Process(); err != nil {568 return err569 }570571 // Enum checks the value against allowed list, no 'default' necessary.572 switch minTLSLevel {573 case "none":574 c.minTLSLevel = module.TLSNone575 case "encrypted":576 c.minTLSLevel = module.TLSEncrypted577 case "authenticated":578 c.minTLSLevel = module.TLSAuthenticated579 }580 switch minMXLevel {581 case "none":582 c.minMXLevel = module.MXNone583 case "mtasts":584 c.minMXLevel = module.MX_MTASTS585 case "dnssec":586 c.minMXLevel = module.MX_DNSSEC587 }588589 return nil590}591592func (l localPolicy) Start(msgMeta *module.MsgMetadata) module.DeliveryMXAuthPolicy {593 return l594}595596func (l localPolicy) Close() error {597 return nil598}599600func (l localPolicy) Reset(*module.MsgMetadata) {}601func (l localPolicy) PrepareDomain(ctx context.Context, domain string) {}602func (l localPolicy) PrepareConn(ctx context.Context, mx string) {}603604func (l localPolicy) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {605 if mxLevel < l.minMXLevel {606 return module.MXNone, &exterrors.SMTPError{607 // Err on the side of caution if policy evaluation was messed up by608 // a temporary error (we can't know with the current design).609 Code: 451,610 EnhancedCode: exterrors.EnhancedCode{4, 7, 0},611 Message: "Failed to establish the MX record authenticity",612 Misc: map[string]interface{}{613 "mx_level": mxLevel,614 "required_mx_level": l.minMXLevel,615 },616 }617 }618 return module.MXNone, nil619}620621func (l localPolicy) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {622 if tlsLevel < l.minTLSLevel {623 return module.TLSNone, &exterrors.SMTPError{624 Code: 451,625 EnhancedCode: exterrors.EnhancedCode{4, 7, 1},626 Message: "TLS it not available or unauthenticated but required",627 Misc: map[string]interface{}{628 "tls_level": tlsLevel,629 "required_tls_level": l.minTLSLevel,630 },631 }632 }633 return module.TLSNone, nil634}635636func init() {637 module.Register("mx_auth.mtasts", NewMTASTSPolicy)638 module.Register("mx_auth.sts_preload", NewSTSPreload)639 module.Register("mx_auth.dnssec", NewDNSSECPolicy)640 module.Register("mx_auth.dane", NewDANEPolicy)641 module.Register("mx_auth.local_policy", NewLocalPolicy)642}