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 "net"26 "runtime/trace"27 "sort"28 "time"2930 "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/module"34 "github.com/foxcpp/maddy/internal/smtpconn"35)3637type mxConn struct {38 *smtpconn.C3940 // Domain this MX belongs to.41 domain string42 dnssecOk bool4344 // Errors occurred previously on this connection.45 errored bool4647 reuseLimit int4849 // Amount of times connection was used for an SMTP transaction.50 transactions int51 lastUseAt time.Time5253 // MX/TLS security level established for this connection.54 mxLevel module.MXLevel55 tlsLevel module.TLSLevel56}5758func (c *mxConn) Usable() bool {59 if c.C == nil || c.transactions > c.reuseLimit || c.C.Client() == nil || c.errored {60 return false61 }62 return c.C.Client().Reset() == nil63}6465func (c *mxConn) LastUseAt() time.Time {66 return c.lastUseAt67}6869func (c *mxConn) Close() error {70 return c.C.Close()71}7273func isVerifyError(err error) bool {74 var e *tls.CertificateVerificationError75 return errors.As(err, &e)76}7778// connect attempts to connect to the MX, first trying STARTTLS with X.50979// verification but falling back to unauthenticated TLS or plaintext as80// necessary.81//82// Return values:83// - tlsLevel TLS security level that was estabilished.84// - tlsErr Error that prevented TLS from working if tlsLevel != TLSAuthenticated85func (rd *remoteDelivery) connect(ctx context.Context, conn mxConn, host string, tlsCfg *tls.Config) (tlsLevel module.TLSLevel, tlsErr, err error) {86 tlsLevel = module.TLSAuthenticated87 if rd.rt.tlsConfig != nil {88 tlsCfg = rd.rt.tlsConfig.Clone()89 tlsCfg.ServerName = host90 }9192 rd.Log.DebugMsg("trying", "remote_server", host, "domain", conn.domain)9394retry:95 // smtpconn.C default TLS behavior is not useful for us, we want to handle96 // TLS errors separately hence starttls=false.97 _, err = conn.Connect(ctx, config.Endpoint{98 Host: host,99 Port: smtpPort,100 }, false, nil)101 if err != nil {102 return module.TLSNone, nil, err103 }104105 starttlsOk, _ := conn.Client().Extension("STARTTLS")106 if starttlsOk && tlsCfg != nil {107 if err := conn.Client().StartTLS(tlsCfg); err != nil {108 // Here we just issue STARTTLS command. If it fails for some109 // reason - this is either a connection problem or server actively110 // rejecting STARTTLS (despite advertising STARTTLS).111 // We err on the caution side here and do not perform any fallbacks.112 conn.DirectClose()113 return module.TLSNone, nil, err114 }115116 // TLS handshake is deferred to here, this is where we check errors and allow fallback.117 if err := conn.Client().Hello(rd.rt.hostname); err != nil {118 tlsErr = err119120 // Attempt TLS without authentication. It is still better than121 // plaintext and we might be able to actually authenticate the122 // server using DANE-EE/DANE-TA later.123 //124 // Check tlsLevel is to avoid looping forever if the same verify125 // error happens with InsecureSkipVerify too (e.g. certificate is126 // *too* broken).127 if isVerifyError(err) && tlsLevel == module.TLSAuthenticated {128 rd.Log.Error("TLS verify error, trying without authentication", err, "remote_server", host, "domain", conn.domain)129 tlsCfg.InsecureSkipVerify = true130 tlsLevel = module.TLSEncrypted131132 // TODO: Check go-smtp code to make TLS verification errors133 // non-sticky so we can properly send QUIT in this case.134 conn.DirectClose()135136 goto retry137 }138139 rd.Log.Error("TLS error, trying plaintext", err, "remote_server", host, "domain", conn.domain)140 tlsCfg = nil141 tlsLevel = module.TLSNone142 conn.DirectClose()143144 goto retry145 }146 } else {147 tlsLevel = module.TLSNone148 }149150 return tlsLevel, tlsErr, nil151}152153func (rd *remoteDelivery) attemptMX(ctx context.Context, conn *mxConn, record *net.MX) error {154 mxLevel := module.MXNone155156 connCtx, cancel := context.WithCancel(ctx)157 // Cancel async policy lookups if rd.connect fails.158 defer cancel()159160 for _, p := range rd.policies {161 policyLevel, err := p.CheckMX(connCtx, mxLevel, conn.domain, record.Host, conn.dnssecOk)162 if err != nil {163 return err164 }165 if policyLevel > mxLevel {166 mxLevel = policyLevel167 }168169 p.PrepareConn(ctx, record.Host)170 }171172 tlsLevel, tlsErr, err := rd.connect(connCtx, *conn, record.Host, rd.rt.tlsConfig)173 if err != nil {174 return err175 }176177 // Make decision based on the policy and connection state.178 //179 // Note: All policy errors are marked as temporary to give the local admin180 // chance to troubleshoot them without losing messages.181182 tlsState, _ := conn.Client().TLSConnectionState()183 for _, p := range rd.policies {184 policyLevel, err := p.CheckConn(connCtx, mxLevel, tlsLevel, conn.domain, record.Host, tlsState)185 if err != nil {186 conn.Close()187 return exterrors.WithFields(err, map[string]interface{}{"tls_err": tlsErr})188 }189 if policyLevel > tlsLevel {190 tlsLevel = policyLevel191 }192 }193194 conn.mxLevel = mxLevel195 conn.tlsLevel = tlsLevel196197 mxLevelCnt.WithLabelValues(rd.rt.Name(), mxLevel.String()).Inc()198 tlsLevelCnt.WithLabelValues(rd.rt.Name(), tlsLevel.String()).Inc()199200 return nil201}202203func (rd *remoteDelivery) connectionForDomain(ctx context.Context, domain string) (*mxConn, error) {204 if c, ok := rd.connections[domain]; ok {205 return c, nil206 }207208 pooledConn, err := rd.rt.pool.Get(ctx, domain)209 if err != nil {210 return nil, err211 }212213 var conn *mxConn214 // Ignore pool for connections with REQUIRETLS to avoid "pool poisoning"215 // where attacker can make messages indeliverable by forcing reuse of old216 // connection with weaker security.217 if pooledConn != nil && !rd.msgMeta.SMTPOpts.RequireTLS {218 conn = pooledConn.(*mxConn)219 rd.Log.Msg("reusing cached connection", "domain", domain, "transactions_counter", conn.transactions,220 "local_addr", conn.LocalAddr(), "remote_addr", conn.RemoteAddr())221 } else {222 rd.Log.DebugMsg("opening new connection", "domain", domain, "cache_ignored", pooledConn != nil)223 conn, err = rd.newConn(ctx, domain)224 if err != nil {225 return nil, err226 }227 }228229 if rd.msgMeta.SMTPOpts.RequireTLS {230 if conn.tlsLevel < module.TLSAuthenticated {231 conn.Close()232 return nil, &exterrors.SMTPError{233 Code: 550,234 EnhancedCode: exterrors.EnhancedCode{5, 7, 30},235 Message: "TLS it not available or unauthenticated but required (REQUIRETLS)",236 Misc: map[string]interface{}{237 "tls_level": conn.tlsLevel,238 },239 }240 }241 if conn.mxLevel < module.MX_MTASTS {242 conn.Close()243 return nil, &exterrors.SMTPError{244 Code: 550,245 EnhancedCode: exterrors.EnhancedCode{5, 7, 30},246 Message: "Failed to establish the MX record authenticity (REQUIRETLS)",247 Misc: map[string]interface{}{248 "mx_level": conn.mxLevel,249 },250 }251 }252 }253254 region := trace.StartRegion(ctx, "remote/limits.TakeDest")255 if err := rd.rt.limits.TakeDest(ctx, domain); err != nil {256 region.End()257 conn.Close()258 return nil, err259 }260 region.End()261262 // Relaxed REQUIRETLS mode is not conforming to the specification strictly263 // but allows to start deploying client support for REQUIRETLS without the264 // requirement for servers in the whole world to support it. The assumption265 // behind it is that MX for the recipient domain is the final destination266 // and all other forwarders behind it already have secure connection to267 // each other. Therefore it is enough to enforce strict security only on268 // the path to the MX even if it does not support the REQUIRETLS to propagate269 // this requirement further.270 if ok, _ := conn.Client().Extension("REQUIRETLS"); rd.rt.relaxedREQUIRETLS && !ok {271 rd.msgMeta.SMTPOpts.RequireTLS = false272 }273274 if err := conn.Mail(ctx, rd.mailFrom, rd.msgMeta.SMTPOpts); err != nil {275 conn.Close()276 return nil, err277 }278 conn.lastUseAt = time.Now()279280 rd.connections[domain] = conn281 return conn, nil282}283284func (rd *remoteDelivery) newConn(ctx context.Context, domain string) (*mxConn, error) {285 conn := mxConn{286 reuseLimit: rd.rt.connReuseLimit,287 C: smtpconn.New(),288 domain: domain,289 lastUseAt: time.Now(),290 }291292 conn.Dialer = rd.rt.dialer293 conn.Log = rd.Log294 conn.Hostname = rd.rt.hostname295 conn.AddrInSMTPMsg = true296 if rd.rt.connectTimeout != 0 {297 conn.ConnectTimeout = rd.rt.connectTimeout298 }299 if rd.rt.commandTimeout != 0 {300 conn.CommandTimeout = rd.rt.commandTimeout301 }302 if rd.rt.submissionTimeout != 0 {303 conn.SubmissionTimeout = rd.rt.submissionTimeout304 }305306 for _, p := range rd.policies {307 p.PrepareDomain(ctx, domain)308 }309310 region := trace.StartRegion(ctx, "remote/LookupMX")311 dnssecOk, records, err := rd.lookupMX(ctx, domain)312 region.End()313 if err != nil {314 return nil, err315 }316 conn.dnssecOk = dnssecOk317318 var lastErr error319 region = trace.StartRegion(ctx, "remote/Connect+TLS")320 for _, record := range records {321 if record.Host == "." {322 return nil, &exterrors.SMTPError{323 Code: 556,324 EnhancedCode: exterrors.EnhancedCode{5, 1, 10},325 Message: "Domain does not accept email (null MX)",326 }327 }328329 if err := rd.attemptMX(ctx, &conn, record); err != nil {330 if len(records) != 0 {331 rd.Log.Error("cannot use MX", err, "remote_server", record.Host, "domain", domain)332 }333 lastErr = err334 continue335 }336 break337 }338 region.End()339340 // Still not connected? Bail out.341 if conn.Client() == nil {342 return nil, &exterrors.SMTPError{343 Code: exterrors.SMTPCode(lastErr, 451, 550),344 EnhancedCode: exterrors.SMTPEnchCode(lastErr, exterrors.EnhancedCode{0, 4, 0}),345 Message: "No usable MXs, last err: " + lastErr.Error(),346 TargetName: "remote",347 Err: lastErr,348 Misc: map[string]interface{}{349 "domain": domain,350 },351 }352 }353354 return &conn, nil355}356357func (rd *remoteDelivery) lookupMX(ctx context.Context, domain string) (dnssecOk bool, records []*net.MX, err error) {358 if rd.rt.extResolver != nil {359 dnssecOk, records, err = rd.rt.extResolver.AuthLookupMX(context.Background(), domain)360 } else {361 records, err = rd.rt.resolver.LookupMX(ctx, dns.FQDN(domain))362 }363 if err != nil {364 reason, misc := exterrors.UnwrapDNSErr(err)365 return false, nil, &exterrors.SMTPError{366 Code: exterrors.SMTPCode(err, 451, 554),367 EnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 4, 4}),368 Message: "MX lookup error",369 TargetName: "remote",370 Reason: reason,371 Err: err,372 Misc: misc,373 }374 }375376 sort.Slice(records, func(i, j int) bool {377 return records[i].Pref < records[j].Pref378 })379380 // Fallback to A/AAA RR when no MX records are present as381 // required by RFC 5321 Section 5.1.382 if len(records) == 0 {383 records = append(records, &net.MX{384 Host: domain,385 Pref: 0,386 })387 }388389 return dnssecOk, records, err390}