diff --git a/framework/module/mxauth.go b/framework/module/mxauth.go new file mode 100644 index 0000000..5dff1f8 --- /dev/null +++ b/framework/module/mxauth.go @@ -0,0 +1,136 @@ +package module + +import ( + "context" + "crypto/tls" +) + +const ( + AuthDisabled = "off" + AuthMTASTS = "mtasts" + AuthDNSSEC = "dnssec" + AuthCommonDomain = "common_domain" +) + +type ( + TLSLevel int + MXLevel int +) + +const ( + TLSNone TLSLevel = iota + TLSEncrypted + TLSAuthenticated + + MXNone MXLevel = iota + MX_MTASTS + MX_DNSSEC +) + +func (l TLSLevel) String() string { + switch l { + case TLSNone: + return "none" + case TLSEncrypted: + return "encrypted" + case TLSAuthenticated: + return "authenticated" + } + return "???" +} + +func (l MXLevel) String() string { + switch l { + case MXNone: + return "none" + case MX_MTASTS: + return "mtasts" + case MX_DNSSEC: + return "dnssec" + } + return "???" +} + +type ( + // MXAuthPolicy is an object that provides security check for outbound connections. + // It can do one of the following: + // + // - Check effective TLS level or MX level against some configured or + // discovered value. + // E.g. local policy. + // + // - Raise the security level if certain condition about used MX or + // connection is met. + // E.g. DANE MXAuthPolicy raises TLS level to Authenticated if a matching + // TLSA record is discovered. + // + // - Reject the connection if certain condition about used MX or + // connection is _not_ met. + // E.g. An enforced MTA-STS MXAuthPolicy rejects MX records not matching it. + // + // It is not recommended to mix different types of behavior described above + // in the same implementation. + // Specifically, the first type is used mostly for local policies and is not + // really practical. + // + // Modules implementing this interface should be registered with "mx_auth." + // prefix in name. + MXAuthPolicy interface { + Start(*MsgMetadata) DeliveryMXAuthPolicy + + // Weight is an integer in range 0-1000 that represents relative + // ordering of policy application. + Weight() int + } + + // DeliveryMXAuthPolicy is an interface of per-delivery object that estabilishes + // and verifies required and effective security for MX records and TLS + // connections. + DeliveryMXAuthPolicy interface { + // PrepareDomain is called before DNS MX lookup and may asynchronously + // start additional lookups necessary for policy application in CheckMX + // or CheckConn. + // + // If there any errors - they should be deferred to the CheckMX or + // CheckConn call. + PrepareDomain(ctx context.Context, domain string) + + // PrepareDomain is called before connection and may asynchronously + // start additional lookups necessary for policy application in + // CheckConn. + // + // If there any errors - they should be deferred to the CheckConn + // call. + PrepareConn(ctx context.Context, mx string) + + // CheckMX is called to check whether the policy permits to use a MX. + // + // mxLevel contains the MX security level estabilished by checks + // executed before. + // + // domain is passed to the CheckMX to allow simpler implementation + // of stateless policy objects. + // + // dnssec is true if the MX lookup was performed using DNSSEC-enabled + // resolver and the zone is signed and its signature is valid. + CheckMX(ctx context.Context, mxLevel MXLevel, domain, mx string, dnssec bool) (MXLevel, error) + + // CheckConn is called to check whether the policy permits to use this + // connection. + // + // tlsLevel and mxLevel contain the TLS security level estabilished by + // checks executed before. + // + // domain is passed to the CheckConn to allow simpler implementation + // of stateless policy objects. + // + // If tlsState.HandshakeCompleted is false, TLS is not used. If + // tlsState.VerifiedChains is nil, InsecureSkipVerify was used (no + // ServerName or PKI check was done). + CheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel TLSLevel, domain, mx string, tlsState tls.ConnectionState) (TLSLevel, error) + + // Reset cleans the internal object state for use with another message. + // newMsg may be nil if object is not needed anymore. + Reset(newMsg *MsgMetadata) + } +) diff --git a/internal/target/remote/connect.go b/internal/target/remote/connect.go index 4efabab..5b74a7a 100644 --- a/internal/target/remote/connect.go +++ b/internal/target/remote/connect.go @@ -10,6 +10,7 @@ import ( "github.com/foxcpp/maddy/framework/config" "github.com/foxcpp/maddy/framework/exterrors" + "github.com/foxcpp/maddy/framework/module" "github.com/foxcpp/maddy/internal/smtpconn" ) @@ -29,8 +30,8 @@ type mxConn struct { transactions int // MX/TLS security level established for this connection. - mxLevel MXLevel - tlsLevel TLSLevel + mxLevel module.MXLevel + tlsLevel module.TLSLevel } func (c *mxConn) Usable() bool { @@ -68,8 +69,8 @@ func isVerifyError(err error) bool { // Return values: // - tlsLevel TLS security level that was estabilished. // - tlsErr Error that prevented TLS from working if tlsLevel != TLSAuthenticated -func (rd *remoteDelivery) connect(ctx context.Context, conn mxConn, host string, tlsCfg *tls.Config) (tlsLevel TLSLevel, tlsErr error, err error) { - tlsLevel = TLSAuthenticated +func (rd *remoteDelivery) connect(ctx context.Context, conn mxConn, host string, tlsCfg *tls.Config) (tlsLevel module.TLSLevel, tlsErr error, err error) { + tlsLevel = module.TLSAuthenticated if rd.rt.tlsConfig != nil { tlsCfg = rd.rt.tlsConfig.Clone() tlsCfg.ServerName = host @@ -85,7 +86,7 @@ retry: Port: smtpPort, }, false, nil) if err != nil { - return TLSNone, nil, err + return module.TLSNone, nil, err } starttlsOk, _ := conn.Client().Extension("STARTTLS") @@ -100,10 +101,10 @@ retry: // Check tlsLevel is to avoid looping forever if the same verify // error happens with InsecureSkipVerify too (e.g. certificate is // *too* broken). - if isVerifyError(err) && tlsLevel == TLSAuthenticated { + if isVerifyError(err) && tlsLevel == module.TLSAuthenticated { rd.Log.Error("TLS verify error, trying without authentication", err, "remote_server", host, "domain", conn.domain) tlsCfg.InsecureSkipVerify = true - tlsLevel = TLSEncrypted + tlsLevel = module.TLSEncrypted // TODO: Check go-smtp code to make TLS verification errors // non-sticky so we can properly send QUIT in this case. @@ -114,20 +115,20 @@ retry: rd.Log.Error("TLS error, trying plaintext", err, "remote_server", host, "domain", conn.domain) tlsCfg = nil - tlsLevel = TLSNone + tlsLevel = module.TLSNone conn.DirectClose() goto retry } } else { - tlsLevel = TLSNone + tlsLevel = module.TLSNone } return tlsLevel, tlsErr, nil } func (rd *remoteDelivery) attemptMX(ctx context.Context, conn *mxConn, record *net.MX) error { - mxLevel := MXNone + mxLevel := module.MXNone connCtx, cancel := context.WithCancel(ctx) // Cancel async policy lookups if rd.connect fails. @@ -202,7 +203,7 @@ func (rd *remoteDelivery) connectionForDomain(ctx context.Context, domain string } if rd.msgMeta.SMTPOpts.RequireTLS { - if conn.tlsLevel < TLSAuthenticated { + if conn.tlsLevel < module.TLSAuthenticated { conn.Close() return nil, &exterrors.SMTPError{ Code: 550, @@ -213,7 +214,7 @@ func (rd *remoteDelivery) connectionForDomain(ctx context.Context, domain string }, } } - if conn.mxLevel < MX_MTASTS { + if conn.mxLevel < module.MX_MTASTS { conn.Close() return nil, &exterrors.SMTPError{ Code: 550, diff --git a/internal/target/remote/dane_test.go b/internal/target/remote/dane_test.go index 0f50f58..6b51da3 100644 --- a/internal/target/remote/dane_test.go +++ b/internal/target/remote/dane_test.go @@ -8,6 +8,7 @@ import ( "github.com/foxcpp/go-mockdns" "github.com/foxcpp/maddy/framework/dns" + "github.com/foxcpp/maddy/framework/module" "github.com/foxcpp/maddy/internal/testutils" miekgdns "github.com/miekg/dns" ) @@ -30,7 +31,7 @@ func targetWithExtResolver(t *testing.T, zones map[string]mockdns.Zone) (*mockdn extResolver.Cfg.Servers = []string{addr.IP.String()} extResolver.Cfg.Port = strconv.Itoa(addr.Port) - tgt := testTarget(t, zones, extResolver, []Policy{ + tgt := testTarget(t, zones, extResolver, []module.MXAuthPolicy{ testDANEPolicy(t, extResolver), }) return dnsSrv, tgt diff --git a/internal/target/remote/mxauth_test.go b/internal/target/remote/mxauth_test.go index ffc6450..f95727a 100644 --- a/internal/target/remote/mxauth_test.go +++ b/internal/target/remote/mxauth_test.go @@ -44,7 +44,7 @@ func TestRemoteDelivery_AuthMX_MTASTS(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), }) tgt.tlsConfig = clientCfg @@ -80,7 +80,7 @@ func TestRemoteDelivery_AuthMX_STSPreload(t *testing.T) { }, }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPreload(t, download), }) tgt.tlsConfig = clientCfg @@ -116,9 +116,9 @@ func TestRemoteDelivery_AuthMX_STSPreload_Fail(t *testing.T) { }, }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPreload(t, download), - &localPolicy{minMXLevel: MX_MTASTS}, + &localPolicy{minMXLevel: module.MX_MTASTS}, }) tgt.tlsConfig = clientCfg defer tgt.Close() @@ -159,9 +159,9 @@ func TestRemoteDelivery_AuthMX_STSPreload_NoTLS(t *testing.T) { }, }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPreload(t, download), - &localPolicy{minMXLevel: MX_MTASTS}, + &localPolicy{minMXLevel: module.MX_MTASTS}, }) defer tgt.Close() @@ -202,9 +202,9 @@ func TestRemoteDelivery_AuthMX_STSPreload_RequirePKIX(t *testing.T) { }, }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPreload(t, download), - &localPolicy{minMXLevel: MX_MTASTS}, + &localPolicy{minMXLevel: module.MX_MTASTS}, }) defer tgt.Close() @@ -255,10 +255,10 @@ func TestRemoteDelivery_AuthMX_STSPreload_MTASTS(t *testing.T) { }, }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), testSTSPreload(t, download), - &localPolicy{minMXLevel: MX_MTASTS}, + &localPolicy{minMXLevel: module.MX_MTASTS}, }) tgt.tlsConfig = clientCfg defer tgt.Close() @@ -301,9 +301,9 @@ func TestRemoteDelivery_MTASTS_SkipNonMatching(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), - &localPolicy{minMXLevel: MX_MTASTS}, + &localPolicy{minMXLevel: module.MX_MTASTS}, }) tgt.tlsConfig = clientCfg defer tgt.Close() @@ -341,9 +341,9 @@ func TestRemoteDelivery_AuthMX_MTASTS_Fail(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), - &localPolicy{minMXLevel: MX_MTASTS}, + &localPolicy{minMXLevel: module.MX_MTASTS}, }) tgt.tlsConfig = clientCfg defer tgt.Close() @@ -383,9 +383,9 @@ func TestRemoteDelivery_AuthMX_MTASTS_NoTLS(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), - &localPolicy{minMXLevel: MX_MTASTS}, + &localPolicy{minMXLevel: module.MX_MTASTS}, }) defer tgt.Close() @@ -424,9 +424,9 @@ func TestRemoteDelivery_AuthMX_MTASTS_RequirePKIX(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), - &localPolicy{minMXLevel: MX_MTASTS}, + &localPolicy{minMXLevel: module.MX_MTASTS}, }) defer tgt.Close() @@ -474,9 +474,9 @@ func TestRemoteDelivery_AuthMX_MTASTS_NoPolicy(t *testing.T) { return nil, mtasts.ErrNoPolicy } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), - &localPolicy{minMXLevel: MX_MTASTS}, + &localPolicy{minMXLevel: module.MX_MTASTS}, }) defer tgt.Close() @@ -558,8 +558,8 @@ func TestRemoteDelivery_AuthMX_DNSSEC_Fail(t *testing.T) { extResolver.Cfg.Servers = []string{addr.IP.String()} extResolver.Cfg.Port = strconv.Itoa(addr.Port) - tgt := testTarget(t, zones, extResolver, []Policy{ - &localPolicy{minMXLevel: MX_DNSSEC}, + tgt := testTarget(t, zones, extResolver, []module.MXAuthPolicy{ + &localPolicy{minMXLevel: module.MX_DNSSEC}, }) defer tgt.Close() @@ -599,7 +599,7 @@ func TestRemoteDelivery_REQUIRETLS(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), }) tgt.tlsConfig = clientCfg @@ -640,7 +640,7 @@ func TestRemoteDelivery_REQUIRETLS_Fail(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), }) tgt.tlsConfig = clientCfg @@ -685,7 +685,7 @@ func TestRemoteDelivery_REQUIRETLS_Relaxed(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), }) tgt.relaxedREQUIRETLS = true @@ -722,7 +722,7 @@ func TestRemoteDelivery_REQUIRETLS_Relaxed_NoMXAuth(t *testing.T) { return nil, mtasts.ErrNoPolicy } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), }) tgt.relaxedREQUIRETLS = true @@ -768,7 +768,7 @@ func TestRemoteDelivery_REQUIRETLS_Relaxed_NoTLS(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), }) tgt.relaxedREQUIRETLS = true @@ -814,7 +814,7 @@ func TestRemoteDelivery_REQUIRETLS_Relaxed_TLSFail(t *testing.T) { }, nil } - tgt := testTarget(t, zones, nil, []Policy{ + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ testSTSPolicy(t, zones, mtastsGet), }) tgt.relaxedREQUIRETLS = true diff --git a/internal/target/remote/policy_group.go b/internal/target/remote/policy_group.go index 90d2f3a..a6d27af 100644 --- a/internal/target/remote/policy_group.go +++ b/internal/target/remote/policy_group.go @@ -2,6 +2,7 @@ package remote import ( "github.com/foxcpp/maddy/framework/config" + modconfig "github.com/foxcpp/maddy/framework/config/module" "github.com/foxcpp/maddy/framework/module" ) @@ -15,9 +16,9 @@ import ( // implement any standard module interfaces besides module.Module and is // specific to the remote target. type PolicyGroup struct { - L []Policy + L []module.MXAuthPolicy instName string - pols map[string]Policy + pols map[string]module.MXAuthPolicy } func (pg *PolicyGroup) Init(cfg *config.Map) error { @@ -38,7 +39,8 @@ func (pg *PolicyGroup) Init(cfg *config.Map) error { return config.NodeErr(block, "duplicate policy block: %v", block.Name) } - policy, err := policyFromNode(debugLog, cfg, block) + var policy module.MXAuthPolicy + err := modconfig.ModuleFromNode("mx_auth", append([]string{block.Name}, block.Args...), block, cfg.Globals, &policy) if err != nil { return err } @@ -79,7 +81,7 @@ func init() { module.Register("mx_auth", func(_, instName string, _, _ []string) (module.Module, error) { return &PolicyGroup{ instName: instName, - pols: map[string]Policy{}, + pols: map[string]module.MXAuthPolicy{}, }, nil }) } diff --git a/internal/target/remote/remote.go b/internal/target/remote/remote.go index 0ec581c..f911ac5 100644 --- a/internal/target/remote/remote.go +++ b/internal/target/remote/remote.go @@ -30,52 +30,6 @@ import ( "golang.org/x/net/idna" ) -const ( - AuthDisabled = "off" - AuthMTASTS = "mtasts" - AuthDNSSEC = "dnssec" - AuthCommonDomain = "common_domain" -) - -type ( - TLSLevel int - MXLevel int -) - -const ( - TLSNone TLSLevel = iota - TLSEncrypted - TLSAuthenticated - - MXNone MXLevel = iota - MX_MTASTS - MX_DNSSEC -) - -func (l TLSLevel) String() string { - switch l { - case TLSNone: - return "none" - case TLSEncrypted: - return "encrypted" - case TLSAuthenticated: - return "authenticated" - } - return "???" -} - -func (l MXLevel) String() string { - switch l { - case MXNone: - return "none" - case MX_MTASTS: - return "mtasts" - case MX_DNSSEC: - return "dnssec" - } - return "???" -} - var smtpPort = "25" func moduleError(err error) error { @@ -94,7 +48,7 @@ type Target struct { dialer func(ctx context.Context, network, addr string) (net.Conn, error) extResolver *dns.ExtResolver - policies []Policy + policies []module.MXAuthPolicy limits *limits.Group allowSecOverride bool relaxedREQUIRETLS bool @@ -192,10 +146,6 @@ func (rt *Target) Init(cfg *config.Map) error { } func (rt *Target) Close() error { - for _, p := range rt.policies { - p.Close() - } - rt.pool.Close() return nil @@ -218,11 +168,11 @@ type remoteDelivery struct { recipients []string connections map[string]*mxConn - policies []DeliveryPolicy + policies []module.DeliveryMXAuthPolicy } func (rt *Target) Start(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) { - policies := make([]DeliveryPolicy, 0, len(rt.policies)) + policies := make([]module.DeliveryMXAuthPolicy, 0, len(rt.policies)) if !(msgMeta.TLSRequireOverride && rt.allowSecOverride) { for _, p := range rt.policies { policies = append(policies, p.Start(msgMeta)) diff --git a/internal/target/remote/remote_test.go b/internal/target/remote/remote_test.go index d6fc17a..a4ebe87 100644 --- a/internal/target/remote/remote_test.go +++ b/internal/target/remote/remote_test.go @@ -6,7 +6,6 @@ import ( "flag" "math/rand" "net" - "net/http" "os" "strconv" "testing" @@ -31,7 +30,7 @@ import ( // any useful data that can lead to outgoing connections being made. func testTarget(t *testing.T, zones map[string]mockdns.Zone, extResolver *dns.ExtResolver, - extraPolicies []Policy) *Target { + extraPolicies []module.MXAuthPolicy) *Target { resolver := &mockdns.Resolver{Zones: zones} tgt := Target{ @@ -56,7 +55,12 @@ func testTarget(t *testing.T, zones map[string]mockdns.Zone, extResolver *dns.Ex } func testSTSPolicy(t *testing.T, zones map[string]mockdns.Zone, mtastsGet func(context.Context, string) (*mtasts.Policy, error)) *mtastsPolicy { - p, err := NewMTASTSPolicy(&mockdns.Resolver{Zones: zones}, false, config.NewMap(nil, config.Node{ + m, err := NewMTASTSPolicy("mx_auth.mtasts", "test", nil, nil) + if err != nil { + t.Fatal(err) + } + p := m.(*mtastsPolicy) + err = p.Init(config.NewMap(nil, config.Node{ Children: []config.Node{ { Name: "cache", @@ -67,6 +71,7 @@ func testSTSPolicy(t *testing.T, zones map[string]mockdns.Zone, mtastsGet func(c if err != nil { t.Fatal(err) } + p.mtastsGet = mtastsGet p.log = testutils.Logger(t, "remote/mtasts") p.StartUpdater() @@ -75,7 +80,13 @@ func testSTSPolicy(t *testing.T, zones map[string]mockdns.Zone, mtastsGet func(c } func testSTSPreload(t *testing.T, download FuncPreloadList) *stsPreloadPolicy { - p, err := NewSTSPreloadPolicy(false, http.DefaultClient, download, config.NewMap(nil, config.Node{ + m, err := NewSTSPreload("mx_auth.mtasts", "test", nil, nil) + if err != nil { + t.Fatal(err) + } + p := m.(*stsPreloadPolicy) + p.listDownload = download + err = p.Init(config.NewMap(nil, config.Node{ Children: []config.Node{ { Name: "source", @@ -86,6 +97,7 @@ func testSTSPreload(t *testing.T, download FuncPreloadList) *stsPreloadPolicy { if err != nil { t.Fatal(err) } + p.log = testutils.Logger(t, "remote/preload") p.StartUpdater() @@ -93,7 +105,18 @@ func testSTSPreload(t *testing.T, download FuncPreloadList) *stsPreloadPolicy { } func testDANEPolicy(t *testing.T, extR *dns.ExtResolver) *danePolicy { - p := NewDANEPolicy(false) + m, err := NewDANEPolicy("mx_auth.dane", "test", nil, nil) + if err != nil { + t.Fatal(err) + } + p := m.(*danePolicy) + err = p.Init(config.NewMap(nil, config.Node{ + Children: nil, + })) + if err != nil { + t.Fatal(err) + } + p.extResolver = extR p.log = testutils.Logger(t, "remote/dane") return p @@ -850,8 +873,8 @@ func TestRemoteDelivery_RequireTLS_Missing(t *testing.T) { }, } - tgt := testTarget(t, zones, nil, []Policy{ - &localPolicy{minTLSLevel: TLSEncrypted}, + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ + &localPolicy{minTLSLevel: module.TLSEncrypted}, }) defer tgt.Close() @@ -874,8 +897,8 @@ func TestRemoteDelivery_RequireTLS_Present(t *testing.T) { }, } - tgt := testTarget(t, zones, nil, []Policy{ - &localPolicy{minTLSLevel: TLSEncrypted}, + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ + &localPolicy{minTLSLevel: module.TLSEncrypted}, }) tgt.tlsConfig = clientCfg defer tgt.Close() @@ -903,8 +926,8 @@ func TestRemoteDelivery_RequireTLS_NoErrFallback(t *testing.T) { srv.TLSConfig.MinVersion = tls.VersionTLS11 srv.TLSConfig.MaxVersion = tls.VersionTLS11 - tgt := testTarget(t, zones, nil, []Policy{ - &localPolicy{minTLSLevel: TLSEncrypted}, + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ + &localPolicy{minTLSLevel: module.TLSEncrypted}, }) tgt.tlsConfig = clientCfg defer tgt.Close() @@ -929,8 +952,8 @@ func TestRemoteDelivery_TLS_FallbackNoVerify(t *testing.T) { } // tlsConfig is not configured to trust server cert. - tgt := testTarget(t, zones, nil, []Policy{ - &localPolicy{minTLSLevel: TLSEncrypted}, + tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{ + &localPolicy{minTLSLevel: module.TLSEncrypted}, }) defer tgt.Close() diff --git a/internal/target/remote/security.go b/internal/target/remote/security.go index f643f30..98ceae7 100644 --- a/internal/target/remote/security.go +++ b/internal/target/remote/security.go @@ -5,7 +5,6 @@ import ( "crypto/tls" "errors" "fmt" - "net" "net/http" "os" "strings" @@ -23,90 +22,13 @@ import ( "github.com/foxcpp/maddy/internal/target" ) -type ( - // Policy is an object that provides security check for outbound connections. - // It can do one of the following: - // - // - Check effective TLS level or MX level against some configured or - // discovered value. - // E.g. local policy. - // - // - Raise the security level if certain condition about used MX or - // connection is met. - // E.g. DANE Policy raises TLS level to Authenticated is a matching - // TLSA record is discovered. - // - // - Reject the connection if certain condition about used MX or - // connection is _not_ met. - // E.g. An enforced MTA-STS Policy rejects MX records not matching it. - // - // It is not recommended to mix different types of behavior described above - // in the same implementation. - // Specifically, the first type is used mostly for local policies and not - // really practical. - Policy interface { - Start(*module.MsgMetadata) DeliveryPolicy - Close() error - } - - // DeliveryPolicy is an interface of per-delivery object that estabilishes - // and verifies required and effective security for MX records and TLS - // connections. - DeliveryPolicy interface { - // PrepareDomain is called before DNS MX lookup and may asynchronously - // start additional lookups necessary for policy application in CheckMX - // or CheckConn. - // - // If there any errors - they should be deferred to the CheckMX or - // CheckConn call. - PrepareDomain(ctx context.Context, domain string) - - // PrepareDomain is called before connection and may asynchronously - // start additional lookups necessary for policy application in - // CheckConn. - // - // If there any errors - they should be deferred to the CheckConn - // call. - PrepareConn(ctx context.Context, mx string) - - // CheckMX is called to check whether the policy permits to use a MX. - // - // mxLevel contains the MX security level estabilished by checks - // executed before. - // - // domain is passed to the CheckMX to allow simpler implementation - // of stateless policy objects. - // - // dnssec is true if the MX lookup was performed using DNSSEC-enabled - // resolver and the zone is signed and its signature is valid. - CheckMX(ctx context.Context, mxLevel MXLevel, domain, mx string, dnssec bool) (MXLevel, error) - - // CheckConn is called to check whether the policy permits to use this - // connection. - // - // tlsLevel and mxLevel contain the TLS security level estabilished by - // checks executed before. - // - // domain is passed to the CheckConn to allow simpler implementation - // of stateless policy objects. - // - // If tlsState.HandshakeCompleted is false, TLS is not used. If - // tlsState.VerifiedChains is nil, InsecureSkipVerify was used (no - // ServerName or PKI check was done). - CheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel TLSLevel, domain, mx string, tlsState tls.ConnectionState) (TLSLevel, error) - - // Reset cleans the internal object state for use with another message. - // newMsg may be nil if object is not needed anymore. - Reset(newMsg *module.MsgMetadata) - } -) - type ( mtastsPolicy struct { cache *mtasts.Cache mtastsGet func(context.Context, string) (*mtasts.Policy, error) updaterStop chan struct{} log log.Logger + instName string } mtastsDelivery struct { c *mtastsPolicy @@ -116,11 +38,26 @@ type ( } ) -func NewMTASTSPolicy(r dns.Resolver, debug bool, cfg *config.Map) (*mtastsPolicy, error) { - c := &mtastsPolicy{ - log: log.Logger{Name: "remote/mtasts", Debug: debug}, - } +func NewMTASTSPolicy(_, instName string, _, _ []string) (module.Module, error) { + return &mtastsPolicy{ + instName: instName, + log: log.Logger{Name: "mx_auth.mtasts", Debug: log.DefaultLogger.Debug}, + }, nil +} +func (c *mtastsPolicy) Name() string { + return c.log.Name +} + +func (c *mtastsPolicy) InstanceName() string { + return c.instName +} + +func (c *mtastsPolicy) Weight() int { + return 10 +} + +func (c *mtastsPolicy) Init(cfg *config.Map) error { var ( storeType string storeDir string @@ -128,13 +65,13 @@ func NewMTASTSPolicy(r dns.Resolver, debug bool, cfg *config.Map) (*mtastsPolicy cfg.Enum("cache", false, false, []string{"ram", "fs"}, "fs", &storeType) cfg.String("fs_dir", false, false, "mtasts_cache", &storeDir) if _, err := cfg.Process(); err != nil { - return nil, err + return err } switch storeType { case "fs": if err := os.MkdirAll(storeDir, os.ModePerm); err != nil { - return nil, err + return err } c.cache = mtasts.NewFSCache(storeDir) case "ram": @@ -145,7 +82,7 @@ func NewMTASTSPolicy(r dns.Resolver, debug bool, cfg *config.Map) (*mtastsPolicy c.cache.Resolver = dns.DefaultResolver() c.mtastsGet = c.cache.Get - return c, nil + return nil } // StartUpdater starts a goroutine to update MTA-STS cache periodically until @@ -182,7 +119,7 @@ func (c *mtastsPolicy) updater() { } } -func (c *mtastsPolicy) Start(msgMeta *module.MsgMetadata) DeliveryPolicy { +func (c *mtastsPolicy) Start(msgMeta *module.MsgMetadata) module.DeliveryMXAuthPolicy { return &mtastsDelivery{ c: c, log: target.DeliveryLogger(c.log, msgMeta), @@ -207,42 +144,42 @@ func (c *mtastsDelivery) PrepareDomain(ctx context.Context, domain string) { func (c *mtastsDelivery) PrepareConn(ctx context.Context, mx string) {} -func (c *mtastsDelivery) CheckMX(ctx context.Context, mxLevel MXLevel, domain, mx string, dnssec bool) (MXLevel, error) { +func (c *mtastsDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) { policyI, err := c.policyFut.GetContext(ctx) if err != nil { c.log.DebugMsg("MTA-STS error", "err", err) - return MXNone, nil + return module.MXNone, nil } policy := policyI.(*mtasts.Policy) if !policy.Match(mx) { if policy.Mode == mtasts.ModeEnforce { - return MXNone, &exterrors.SMTPError{ + return module.MXNone, &exterrors.SMTPError{ Code: 550, EnhancedCode: exterrors.EnhancedCode{5, 7, 0}, - Message: "Failed to estabilish the MX record authenticity (MTA-STS)", + Message: "Failed to estabilish the module.MX record authenticity (MTA-STS)", } } c.log.Msg("MX does not match published non-enforced MTA-STS policy", "mx", mx, "domain", c.domain) - return MXNone, nil + return module.MXNone, nil } - return MX_MTASTS, nil + return module.MX_MTASTS, nil } -func (c *mtastsDelivery) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel TLSLevel, domain, mx string, tlsState tls.ConnectionState) (TLSLevel, error) { +func (c *mtastsDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) { policyI, err := c.policyFut.GetContext(ctx) if err != nil { c.c.log.DebugMsg("MTA-STS error", "err", err) - return TLSNone, nil + return module.TLSNone, nil } policy := policyI.(*mtasts.Policy) if policy.Mode != mtasts.ModeEnforce { - return TLSNone, nil + return module.TLSNone, nil } if !tlsState.HandshakeComplete { - return TLSNone, &exterrors.SMTPError{ + return module.TLSNone, &exterrors.SMTPError{ Code: 451, EnhancedCode: exterrors.EnhancedCode{4, 7, 1}, Message: "TLS is required but unavailable or failed (MTA-STS)", @@ -250,10 +187,10 @@ func (c *mtastsDelivery) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLeve } if tlsState.VerifiedChains == nil { - return TLSNone, &exterrors.SMTPError{ + return module.TLSNone, &exterrors.SMTPError{ Code: 451, EnhancedCode: exterrors.EnhancedCode{4, 7, 1}, - Message: "Recipient server TLS certificate is not trusted but " + + Message: "Recipient server module.TLS certificate is not trusted but " + "authentication is required by MTA-STS", Misc: map[string]interface{}{ "tls_level": tlsLevel, @@ -261,7 +198,7 @@ func (c *mtastsDelivery) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLeve } } - return TLSNone, nil + return module.TLSNone, nil } func (c *mtastsDelivery) Reset(msgMeta *module.MsgMetadata) { @@ -295,32 +232,49 @@ type ( client *http.Client listDownload FuncPreloadList enforceTesting bool + + instName string } ) -func NewSTSPreloadPolicy(debug bool, client *http.Client, listDownload FuncPreloadList, cfg *config.Map) (*stsPreloadPolicy, error) { - p := &stsPreloadPolicy{ - log: log.Logger{Name: "remote/preload", Debug: debug}, - client: client, +func NewSTSPreload(_, instName string, _, _ []string) (module.Module, error) { + return &stsPreloadPolicy{ + instName: instName, + client: http.DefaultClient, listDownload: preload.Download, - } + log: log.Logger{Name: "mx_auth.sts_preload", Debug: log.DefaultLogger.Debug}, + }, nil +} +func (c *stsPreloadPolicy) Name() string { + return c.log.Name +} + +func (c *stsPreloadPolicy) InstanceName() string { + return c.instName +} + +func (c *stsPreloadPolicy) Weight() int { + return 30 // after MTA-STS +} + +func (c *stsPreloadPolicy) Init(cfg *config.Map) error { var sourcePath string cfg.String("source", false, false, "eff", &sourcePath) - cfg.Bool("enforce_testing", false, true, &p.enforceTesting) + cfg.Bool("enforce_testing", false, true, &c.enforceTesting) if _, err := cfg.Process(); err != nil { - return nil, err + return err } var err error - p.l, err = p.load(client, listDownload, sourcePath) + c.l, err = c.load(c.client, c.listDownload, sourcePath) if err != nil { - return nil, err + return err } - p.sourcePath = sourcePath + c.sourcePath = sourcePath - return p, nil + return nil } func (p *stsPreloadPolicy) load(client *http.Client, listDownload FuncPreloadList, sourcePath string) (*preload.List, error) { @@ -445,19 +399,19 @@ type preloadDelivery struct { mtastsPresent bool } -func (p *stsPreloadPolicy) Start(*module.MsgMetadata) DeliveryPolicy { +func (p *stsPreloadPolicy) Start(*module.MsgMetadata) module.DeliveryMXAuthPolicy { return &preloadDelivery{stsPreloadPolicy: p} } func (p *preloadDelivery) Reset(*module.MsgMetadata) {} func (p *preloadDelivery) PrepareDomain(ctx context.Context, domain string) {} func (p *preloadDelivery) PrepareConn(ctx context.Context, mx string) {} -func (p *preloadDelivery) CheckMX(ctx context.Context, mxLevel MXLevel, domain, mx string, dnssec bool) (MXLevel, error) { +func (p *preloadDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) { // MTA-STS policy was discovered and took effect already. Do not use // preload list. - if mxLevel == MX_MTASTS { + if mxLevel == module.MX_MTASTS { p.mtastsPresent = true - return MXNone, nil + return module.MXNone, nil } p.lLock.RLock() @@ -465,36 +419,36 @@ func (p *preloadDelivery) CheckMX(ctx context.Context, mxLevel MXLevel, domain, if p.l.Expired() { p.log.Msg("STARTTLS Everywhere list is expired, ignoring") - return MXNone, nil + return module.MXNone, nil } ent, ok := p.l.Lookup(domain) if !ok { p.log.DebugMsg("no entry", "domain", domain) - return MXNone, nil + return module.MXNone, nil } sts := ent.STS(p.l) if !sts.Match(mx) { if sts.Mode == mtasts.ModeEnforce || p.enforceTesting { - return MXNone, &exterrors.SMTPError{ + return module.MXNone, &exterrors.SMTPError{ Code: 550, EnhancedCode: exterrors.EnhancedCode{5, 7, 0}, - Message: "Failed to estabilish the MX record authenticity (STARTTLS Everywhere)", + Message: "Failed to estabilish the module.MX record authenticity (STARTTLS Everywhere)", } } p.log.Msg("MX does not match published non-enforced STARTLS Everywhere entry", "mx", mx, "domain", domain) - return MXNone, nil + return module.MXNone, nil } p.log.DebugMsg("MX OK", "domain", domain) - return MX_MTASTS, nil + return module.MX_MTASTS, nil } -func (p *preloadDelivery) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel TLSLevel, domain, mx string, tlsState tls.ConnectionState) (TLSLevel, error) { +func (p *preloadDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) { // MTA-STS policy was discovered and took effect already. Do not use - // preload list. We cannot check level for MX_MTASTS because we can set + // preload list. We cannot check level for module.MX_MTASTS because we can set // it too in CheckMX. if p.mtastsPresent { - return TLSNone, nil + return module.TLSNone, nil } p.lLock.RLock() @@ -502,20 +456,20 @@ func (p *preloadDelivery) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLev if p.l.Expired() { p.log.Msg("STARTTLS Everywhere list is expired, ignoring") - return TLSNone, nil + return module.TLSNone, nil } ent, ok := p.l.Lookup(domain) if !ok { p.log.DebugMsg("no entry", "domain", domain) - return TLSNone, nil + return module.TLSNone, nil } if ent.Mode != mtasts.ModeEnforce && !p.enforceTesting { - return TLSNone, nil + return module.TLSNone, nil } if !tlsState.HandshakeComplete { - return TLSNone, &exterrors.SMTPError{ + return module.TLSNone, &exterrors.SMTPError{ Code: 451, EnhancedCode: exterrors.EnhancedCode{4, 7, 1}, Message: "TLS is required but unavailable or failed (STARTTLS Everywhere)", @@ -523,10 +477,10 @@ func (p *preloadDelivery) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLev } if tlsState.VerifiedChains == nil { - return TLSNone, &exterrors.SMTPError{ + return module.TLSNone, &exterrors.SMTPError{ Code: 451, EnhancedCode: exterrors.EnhancedCode{4, 7, 1}, - Message: "Recipient server TLS certificate is not trusted but " + + Message: "Recipient server module.TLS certificate is not trusted but " + "authentication is required by STARTTLS Everywhere list", Misc: map[string]interface{}{ "tls_level": tlsLevel, @@ -536,7 +490,7 @@ func (p *preloadDelivery) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLev p.log.DebugMsg("TLS OK", "domain", domain) - return TLSNone, nil + return module.TLSNone, nil } func (p *stsPreloadPolicy) Close() error { @@ -548,9 +502,34 @@ func (p *stsPreloadPolicy) Close() error { return nil } -type dnssecPolicy struct{} +type dnssecPolicy struct { + instName string +} -func (dnssecPolicy) Start(*module.MsgMetadata) DeliveryPolicy { +func NewDNSSECPolicy(_, instName string, _, _ []string) (module.Module, error) { + return &dnssecPolicy{ + instName: instName, + }, nil +} + +func (c *dnssecPolicy) Name() string { + return "mx_auth.dnssec" +} + +func (c *dnssecPolicy) InstanceName() string { + return c.instName +} + +func (c *dnssecPolicy) Weight() int { + return 1 +} + +func (c *dnssecPolicy) Init(cfg *config.Map) error { + _, err := cfg.Process() // will fail if there is any directive + return err +} + +func (dnssecPolicy) Start(*module.MsgMetadata) module.DeliveryMXAuthPolicy { return dnssecPolicy{} } @@ -562,21 +541,22 @@ func (dnssecPolicy) Reset(*module.MsgMetadata) {} func (dnssecPolicy) PrepareDomain(ctx context.Context, domain string) {} func (dnssecPolicy) PrepareConn(ctx context.Context, mx string) {} -func (dnssecPolicy) CheckMX(ctx context.Context, mxLevel MXLevel, domain, mx string, dnssec bool) (MXLevel, error) { +func (dnssecPolicy) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) { if dnssec { - return MX_DNSSEC, nil + return module.MX_DNSSEC, nil } - return MXNone, nil + return module.MXNone, nil } -func (dnssecPolicy) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel TLSLevel, domain, mx string, tlsState tls.ConnectionState) (TLSLevel, error) { - return TLSNone, nil +func (dnssecPolicy) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) { + return module.TLSNone, nil } type ( danePolicy struct { extResolver *dns.ExtResolver log log.Logger + instName string } daneDelivery struct { c *danePolicy @@ -584,19 +564,37 @@ type ( } ) -func NewDANEPolicy(debug bool) *danePolicy { - extR, err := dns.NewExtResolver() - if err != nil { - log.DefaultLogger.Error("DANE support is no-op: unable to init EDNS resolver", err) - } - +func NewDANEPolicy(_, instName string, _, _ []string) (module.Module, error) { return &danePolicy{ - log: log.Logger{Name: "remote/dane", Debug: debug}, - extResolver: extR, - } + instName: instName, + log: log.Logger{Name: "remote/dane", Debug: log.DefaultLogger.Debug}, + }, nil } -func (c *danePolicy) Start(*module.MsgMetadata) DeliveryPolicy { +func (c *danePolicy) Name() string { + return "mx_auth.dane" +} + +func (c *danePolicy) InstanceName() string { + return c.instName +} + +func (c *danePolicy) Weight() int { + return 10 +} + +func (c *danePolicy) Init(cfg *config.Map) error { + var err error + c.extResolver, err = dns.NewExtResolver() + if err != nil { + c.log.Error("DANE support is no-op: unable to init EDNS resolver", err) + } + + _, err = cfg.Process() // will fail if there is any directive + return err +} + +func (c *danePolicy) Start(*module.MsgMetadata) module.DeliveryMXAuthPolicy { return &daneDelivery{c: c} } @@ -635,21 +633,21 @@ func (c *daneDelivery) PrepareConn(ctx context.Context, mx string) { }() } -func (c *daneDelivery) CheckMX(ctx context.Context, mxLevel MXLevel, domain, mx string, dnssec bool) (MXLevel, error) { - return MXNone, nil +func (c *daneDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) { + return module.MXNone, nil } -func (c *daneDelivery) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel TLSLevel, domain, mx string, tlsState tls.ConnectionState) (TLSLevel, error) { +func (c *daneDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) { // No DNSSEC support. if c.c.extResolver == nil { - return TLSNone, nil + return module.TLSNone, nil } recsI, err := c.tlsaFut.GetContext(ctx) if err != nil { // No records. if dns.IsNotFound(err) { - return TLSNone, nil + return module.TLSNone, nil } // Lookup error here indicates a resolution failure or may also @@ -659,32 +657,49 @@ func (c *daneDelivery) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel // We assume DANE failure in both cases as a safety measure. // However, there is a possibility of a temporary error condition, // so we mark it as such. - return TLSNone, exterrors.WithTemporary(err, true) + return module.TLSNone, exterrors.WithTemporary(err, true) } recs := recsI.([]dns.TLSA) overridePKIX, err := verifyDANE(recs, mx, tlsState) if err != nil { - return TLSNone, err + return module.TLSNone, err } if overridePKIX { - return TLSAuthenticated, nil + return module.TLSAuthenticated, nil } - return TLSNone, nil + return module.TLSNone, nil } func (c *daneDelivery) Reset(*module.MsgMetadata) {} type ( localPolicy struct { - minTLSLevel TLSLevel - minMXLevel MXLevel + instName string + minTLSLevel module.TLSLevel + minMXLevel module.MXLevel } ) -func NewLocalPolicy(cfg *config.Map) (localPolicy, error) { - l := localPolicy{} +func NewLocalPolicy(_, instName string, _, _ []string) (module.Module, error) { + return &localPolicy{ + instName: instName, + }, nil +} +func (c *localPolicy) Name() string { + return "mx_auth.local_policy" +} + +func (c *localPolicy) InstanceName() string { + return c.instName +} + +func (c *localPolicy) Weight() int { + return 1000 +} + +func (c *localPolicy) Init(cfg *config.Map) error { var ( minTLSLevel string minMXLevel string @@ -695,31 +710,31 @@ func NewLocalPolicy(cfg *config.Map) (localPolicy, error) { cfg.Enum("min_mx_level", false, false, []string{"none", "mtasts", "dnssec"}, "none", &minMXLevel) if _, err := cfg.Process(); err != nil { - return localPolicy{}, err + return err } // Enum checks the value against allowed list, no 'default' necessary. switch minTLSLevel { case "none": - l.minTLSLevel = TLSNone + c.minTLSLevel = module.TLSNone case "encrypted": - l.minTLSLevel = TLSEncrypted + c.minTLSLevel = module.TLSEncrypted case "authenticated": - l.minTLSLevel = TLSAuthenticated + c.minTLSLevel = module.TLSAuthenticated } switch minMXLevel { case "none": - l.minMXLevel = MXNone + c.minMXLevel = module.MXNone case "mtasts": - l.minMXLevel = MX_MTASTS + c.minMXLevel = module.MX_MTASTS case "dnssec": - l.minMXLevel = MX_DNSSEC + c.minMXLevel = module.MX_DNSSEC } - return l, nil + return nil } -func (l localPolicy) Start(msgMeta *module.MsgMetadata) DeliveryPolicy { +func (l localPolicy) Start(msgMeta *module.MsgMetadata) module.DeliveryMXAuthPolicy { return l } @@ -731,25 +746,25 @@ func (l localPolicy) Reset(*module.MsgMetadata) {} func (l localPolicy) PrepareDomain(ctx context.Context, domain string) {} func (l localPolicy) PrepareConn(ctx context.Context, mx string) {} -func (l localPolicy) CheckMX(ctx context.Context, mxLevel MXLevel, domain, mx string, dnssec bool) (MXLevel, error) { +func (l localPolicy) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) { if mxLevel < l.minMXLevel { - return MXNone, &exterrors.SMTPError{ + return module.MXNone, &exterrors.SMTPError{ // Err on the side of caution if policy evaluation was messed up by // a temporary error (we can't know with the current design). Code: 451, EnhancedCode: exterrors.EnhancedCode{4, 7, 0}, - Message: "Failed to estabilish the MX record authenticity", + Message: "Failed to estabilish the module.MX record authenticity", Misc: map[string]interface{}{ "mx_level": mxLevel, }, } } - return MXNone, nil + return module.MXNone, nil } -func (l localPolicy) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel TLSLevel, domain, mx string, tlsState tls.ConnectionState) (TLSLevel, error) { +func (l localPolicy) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) { if tlsLevel < l.minTLSLevel { - return TLSNone, &exterrors.SMTPError{ + return module.TLSNone, &exterrors.SMTPError{ Code: 451, EnhancedCode: exterrors.EnhancedCode{4, 7, 1}, Message: "TLS it not available or unauthenticated but required", @@ -758,42 +773,13 @@ func (l localPolicy) CheckConn(ctx context.Context, mxLevel MXLevel, tlsLevel TL }, } } - return TLSNone, nil + return module.TLSNone, nil } -func policyFromNode(debugLog bool, m *config.Map, node config.Node) (Policy, error) { - if len(node.Args) != 0 { - return nil, config.NodeErr(node, "no arguments allowed") - } - - var ( - policy Policy - err error - ) - switch node.Name { - case "mtasts": - policy, err = NewMTASTSPolicy(net.DefaultResolver, log.DefaultLogger.Debug, config.NewMap(m.Globals, node)) - case "dane": - if node.Children != nil { - return nil, config.NodeErr(node, "policy offers no additional configuration") - } - policy = NewDANEPolicy(debugLog) - case "dnssec": - if node.Children != nil { - return nil, config.NodeErr(node, "policy offers no additional configuration") - } - policy = &dnssecPolicy{} - case "sts_preload": - policy, err = NewSTSPreloadPolicy(log.DefaultLogger.Debug, http.DefaultClient, preload.Download, - config.NewMap(m.Globals, node)) - case "local_policy": - policy, err = NewLocalPolicy(config.NewMap(m.Globals, node)) - default: - return nil, config.NodeErr(node, "unknown policy module: %v", node.Name) - } - if err != nil { - return nil, err - } - - return policy, nil +func init() { + module.Register("mx_auth.mtasts", NewMTASTSPolicy) + module.Register("mx_auth.sts_preload", NewSTSPreload) + module.Register("mx_auth.dnssec", NewDNSSECPolicy) + module.Register("mx_auth.dane", NewDANEPolicy) + module.Register("mx_auth.local_policy", NewLocalPolicy) }