maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package remote
 20
 21import (
 22	"context"
 23	"crypto/tls"
 24	"errors"
 25	"net"
 26	"runtime/trace"
 27	"sort"
 28	"time"
 29
 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/module"
 34	"github.com/foxcpp/maddy/internal/smtpconn"
 35)
 36
 37type mxConn struct {
 38	*smtpconn.C
 39
 40	// Domain this MX belongs to.
 41	domain   string
 42	dnssecOk bool
 43
 44	// Errors occurred previously on this connection.
 45	errored bool
 46
 47	reuseLimit int
 48
 49	// Amount of times connection was used for an SMTP transaction.
 50	transactions int
 51	lastUseAt    time.Time
 52
 53	// MX/TLS security level established for this connection.
 54	mxLevel  module.MXLevel
 55	tlsLevel module.TLSLevel
 56}
 57
 58func (c *mxConn) Usable() bool {
 59	if c.C == nil || c.transactions > c.reuseLimit || c.C.Client() == nil || c.errored {
 60		return false
 61	}
 62	return c.C.Client().Reset() == nil
 63}
 64
 65func (c *mxConn) LastUseAt() time.Time {
 66	return c.lastUseAt
 67}
 68
 69func (c *mxConn) Close() error {
 70	return c.C.Close()
 71}
 72
 73func isVerifyError(err error) bool {
 74	var e *tls.CertificateVerificationError
 75	return errors.As(err, &e)
 76}
 77
 78// connect attempts to connect to the MX, first trying STARTTLS with X.509
 79// verification but falling back to unauthenticated TLS or plaintext as
 80// necessary.
 81//
 82// Return values:
 83// - tlsLevel    TLS security level that was estabilished.
 84// - tlsErr      Error that prevented TLS from working if tlsLevel != TLSAuthenticated
 85func (rd *remoteDelivery) connect(ctx context.Context, conn mxConn, host string, tlsCfg *tls.Config) (tlsLevel module.TLSLevel, tlsErr, err error) {
 86	tlsLevel = module.TLSAuthenticated
 87	if rd.rt.tlsConfig != nil {
 88		tlsCfg = rd.rt.tlsConfig.Clone()
 89		tlsCfg.ServerName = host
 90	}
 91
 92	rd.Log.DebugMsg("trying", "remote_server", host, "domain", conn.domain)
 93
 94retry:
 95	// smtpconn.C default TLS behavior is not useful for us, we want to handle
 96	// 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, err
103	}
104
105	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 some
109			// reason - this is either a connection problem or server actively
110			// 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, err
114		}
115
116		// 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 = err
119
120			// Attempt TLS without authentication. It is still better than
121			// plaintext and we might be able to actually authenticate the
122			// server using DANE-EE/DANE-TA later.
123			//
124			// Check tlsLevel is to avoid looping forever if the same verify
125			// error happens with InsecureSkipVerify too (e.g. certificate is
126			// *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 = true
130				tlsLevel = module.TLSEncrypted
131
132				// TODO: Check go-smtp code to make TLS verification errors
133				// non-sticky so we can properly send QUIT in this case.
134				conn.DirectClose()
135
136				goto retry
137			}
138
139			rd.Log.Error("TLS error, trying plaintext", err, "remote_server", host, "domain", conn.domain)
140			tlsCfg = nil
141			tlsLevel = module.TLSNone
142			conn.DirectClose()
143
144			goto retry
145		}
146	} else {
147		tlsLevel = module.TLSNone
148	}
149
150	return tlsLevel, tlsErr, nil
151}
152
153func (rd *remoteDelivery) attemptMX(ctx context.Context, conn *mxConn, record *net.MX) error {
154	mxLevel := module.MXNone
155
156	connCtx, cancel := context.WithCancel(ctx)
157	// Cancel async policy lookups if rd.connect fails.
158	defer cancel()
159
160	for _, p := range rd.policies {
161		policyLevel, err := p.CheckMX(connCtx, mxLevel, conn.domain, record.Host, conn.dnssecOk)
162		if err != nil {
163			return err
164		}
165		if policyLevel > mxLevel {
166			mxLevel = policyLevel
167		}
168
169		p.PrepareConn(ctx, record.Host)
170	}
171
172	tlsLevel, tlsErr, err := rd.connect(connCtx, *conn, record.Host, rd.rt.tlsConfig)
173	if err != nil {
174		return err
175	}
176
177	// Make decision based on the policy and connection state.
178	//
179	// Note: All policy errors are marked as temporary to give the local admin
180	// chance to troubleshoot them without losing messages.
181
182	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 = policyLevel
191		}
192	}
193
194	conn.mxLevel = mxLevel
195	conn.tlsLevel = tlsLevel
196
197	mxLevelCnt.WithLabelValues(rd.rt.Name(), mxLevel.String()).Inc()
198	tlsLevelCnt.WithLabelValues(rd.rt.Name(), tlsLevel.String()).Inc()
199
200	return nil
201}
202
203func (rd *remoteDelivery) connectionForDomain(ctx context.Context, domain string) (*mxConn, error) {
204	if c, ok := rd.connections[domain]; ok {
205		return c, nil
206	}
207
208	pooledConn, err := rd.rt.pool.Get(ctx, domain)
209	if err != nil {
210		return nil, err
211	}
212
213	var conn *mxConn
214	// Ignore pool for connections with REQUIRETLS to avoid "pool poisoning"
215	// where attacker can make messages indeliverable by forcing reuse of old
216	// 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, err
226		}
227	}
228
229	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	}
253
254	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, err
259	}
260	region.End()
261
262	// Relaxed REQUIRETLS mode is not conforming to the specification strictly
263	// but allows to start deploying client support for REQUIRETLS without the
264	// requirement for servers in the whole world to support it. The assumption
265	// behind it is that MX for the recipient domain is the final destination
266	// and all other forwarders behind it already have secure connection to
267	// each other. Therefore it is enough to enforce strict security only on
268	// the path to the MX even if it does not support the REQUIRETLS to propagate
269	// this requirement further.
270	if ok, _ := conn.Client().Extension("REQUIRETLS"); rd.rt.relaxedREQUIRETLS && !ok {
271		rd.msgMeta.SMTPOpts.RequireTLS = false
272	}
273
274	if err := conn.Mail(ctx, rd.mailFrom, rd.msgMeta.SMTPOpts); err != nil {
275		conn.Close()
276		return nil, err
277	}
278	conn.lastUseAt = time.Now()
279
280	rd.connections[domain] = conn
281	return conn, nil
282}
283
284func (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	}
291
292	conn.Dialer = rd.rt.dialer
293	conn.Log = rd.Log
294	conn.Hostname = rd.rt.hostname
295	conn.AddrInSMTPMsg = true
296	if rd.rt.connectTimeout != 0 {
297		conn.ConnectTimeout = rd.rt.connectTimeout
298	}
299	if rd.rt.commandTimeout != 0 {
300		conn.CommandTimeout = rd.rt.commandTimeout
301	}
302	if rd.rt.submissionTimeout != 0 {
303		conn.SubmissionTimeout = rd.rt.submissionTimeout
304	}
305
306	for _, p := range rd.policies {
307		p.PrepareDomain(ctx, domain)
308	}
309
310	region := trace.StartRegion(ctx, "remote/LookupMX")
311	dnssecOk, records, err := rd.lookupMX(ctx, domain)
312	region.End()
313	if err != nil {
314		return nil, err
315	}
316	conn.dnssecOk = dnssecOk
317
318	var lastErr error
319	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		}
328
329		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 = err
334			continue
335		}
336		break
337	}
338	region.End()
339
340	// 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	}
353
354	return &conn, nil
355}
356
357func (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	}
375
376	sort.Slice(records, func(i, j int) bool {
377		return records[i].Pref < records[j].Pref
378	})
379
380	// Fallback to A/AAA RR when no MX records are present as
381	// 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	}
388
389	return dnssecOk, records, err
390}