-
Notifications
You must be signed in to change notification settings - Fork 262
/
Copy pathconnect.go
390 lines (333 loc) · 11.2 KB
/
connect.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <[email protected]>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package remote
import (
"context"
"crypto/tls"
"errors"
"net"
"runtime/trace"
"sort"
"time"
"github.com/foxcpp/maddy/framework/config"
"github.com/foxcpp/maddy/framework/dns"
"github.com/foxcpp/maddy/framework/exterrors"
"github.com/foxcpp/maddy/framework/module"
"github.com/foxcpp/maddy/internal/smtpconn"
)
type mxConn struct {
*smtpconn.C
// Domain this MX belongs to.
domain string
dnssecOk bool
// Errors occurred previously on this connection.
errored bool
reuseLimit int
// Amount of times connection was used for an SMTP transaction.
transactions int
lastUseAt time.Time
// MX/TLS security level established for this connection.
mxLevel module.MXLevel
tlsLevel module.TLSLevel
}
func (c *mxConn) Usable() bool {
if c.C == nil || c.transactions > c.reuseLimit || c.C.Client() == nil || c.errored {
return false
}
return c.C.Client().Reset() == nil
}
func (c *mxConn) LastUseAt() time.Time {
return c.lastUseAt
}
func (c *mxConn) Close() error {
return c.C.Close()
}
func isVerifyError(err error) bool {
var e *tls.CertificateVerificationError
return errors.As(err, &e)
}
// connect attempts to connect to the MX, first trying STARTTLS with X.509
// verification but falling back to unauthenticated TLS or plaintext as
// necessary.
//
// 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 module.TLSLevel, tlsErr, err error) {
tlsLevel = module.TLSAuthenticated
if rd.rt.tlsConfig != nil {
tlsCfg = rd.rt.tlsConfig.Clone()
tlsCfg.ServerName = host
}
rd.Log.DebugMsg("trying", "remote_server", host, "domain", conn.domain)
retry:
// smtpconn.C default TLS behavior is not useful for us, we want to handle
// TLS errors separately hence starttls=false.
_, err = conn.Connect(ctx, config.Endpoint{
Host: host,
Port: smtpPort,
}, false, nil)
if err != nil {
return module.TLSNone, nil, err
}
starttlsOk, _ := conn.Client().Extension("STARTTLS")
if starttlsOk && tlsCfg != nil {
if err := conn.Client().StartTLS(tlsCfg); err != nil {
// Here we just issue STARTTLS command. If it fails for some
// reason - this is either a connection problem or server actively
// rejecting STARTTLS (despite advertising STARTTLS).
// We err on the caution side here and do not perform any fallbacks.
conn.DirectClose()
return module.TLSNone, nil, err
}
// TLS handshake is deferred to here, this is where we check errors and allow fallback.
if err := conn.Client().Hello(rd.rt.hostname); err != nil {
tlsErr = err
// Attempt TLS without authentication. It is still better than
// plaintext and we might be able to actually authenticate the
// server using DANE-EE/DANE-TA later.
//
// 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 == module.TLSAuthenticated {
rd.Log.Error("TLS verify error, trying without authentication", err, "remote_server", host, "domain", conn.domain)
tlsCfg.InsecureSkipVerify = true
tlsLevel = module.TLSEncrypted
// TODO: Check go-smtp code to make TLS verification errors
// non-sticky so we can properly send QUIT in this case.
conn.DirectClose()
goto retry
}
rd.Log.Error("TLS error, trying plaintext", err, "remote_server", host, "domain", conn.domain)
tlsCfg = nil
tlsLevel = module.TLSNone
conn.DirectClose()
goto retry
}
} else {
tlsLevel = module.TLSNone
}
return tlsLevel, tlsErr, nil
}
func (rd *remoteDelivery) attemptMX(ctx context.Context, conn *mxConn, record *net.MX) error {
mxLevel := module.MXNone
connCtx, cancel := context.WithCancel(ctx)
// Cancel async policy lookups if rd.connect fails.
defer cancel()
for _, p := range rd.policies {
policyLevel, err := p.CheckMX(connCtx, mxLevel, conn.domain, record.Host, conn.dnssecOk)
if err != nil {
return err
}
if policyLevel > mxLevel {
mxLevel = policyLevel
}
p.PrepareConn(ctx, record.Host)
}
tlsLevel, tlsErr, err := rd.connect(connCtx, *conn, record.Host, rd.rt.tlsConfig)
if err != nil {
return err
}
// Make decision based on the policy and connection state.
//
// Note: All policy errors are marked as temporary to give the local admin
// chance to troubleshoot them without losing messages.
tlsState, _ := conn.Client().TLSConnectionState()
for _, p := range rd.policies {
policyLevel, err := p.CheckConn(connCtx, mxLevel, tlsLevel, conn.domain, record.Host, tlsState)
if err != nil {
conn.Close()
return exterrors.WithFields(err, map[string]interface{}{"tls_err": tlsErr})
}
if policyLevel > tlsLevel {
tlsLevel = policyLevel
}
}
conn.mxLevel = mxLevel
conn.tlsLevel = tlsLevel
mxLevelCnt.WithLabelValues(rd.rt.Name(), mxLevel.String()).Inc()
tlsLevelCnt.WithLabelValues(rd.rt.Name(), tlsLevel.String()).Inc()
return nil
}
func (rd *remoteDelivery) connectionForDomain(ctx context.Context, domain string) (*mxConn, error) {
if c, ok := rd.connections[domain]; ok {
return c, nil
}
pooledConn, err := rd.rt.pool.Get(ctx, domain)
if err != nil {
return nil, err
}
var conn *mxConn
// Ignore pool for connections with REQUIRETLS to avoid "pool poisoning"
// where attacker can make messages indeliverable by forcing reuse of old
// connection with weaker security.
if pooledConn != nil && !rd.msgMeta.SMTPOpts.RequireTLS {
conn = pooledConn.(*mxConn)
rd.Log.Msg("reusing cached connection", "domain", domain, "transactions_counter", conn.transactions,
"local_addr", conn.LocalAddr(), "remote_addr", conn.RemoteAddr())
} else {
rd.Log.DebugMsg("opening new connection", "domain", domain, "cache_ignored", pooledConn != nil)
conn, err = rd.newConn(ctx, domain)
if err != nil {
return nil, err
}
}
if rd.msgMeta.SMTPOpts.RequireTLS {
if conn.tlsLevel < module.TLSAuthenticated {
conn.Close()
return nil, &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 7, 30},
Message: "TLS it not available or unauthenticated but required (REQUIRETLS)",
Misc: map[string]interface{}{
"tls_level": conn.tlsLevel,
},
}
}
if conn.mxLevel < module.MX_MTASTS {
conn.Close()
return nil, &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 7, 30},
Message: "Failed to establish the MX record authenticity (REQUIRETLS)",
Misc: map[string]interface{}{
"mx_level": conn.mxLevel,
},
}
}
}
region := trace.StartRegion(ctx, "remote/limits.TakeDest")
if err := rd.rt.limits.TakeDest(ctx, domain); err != nil {
region.End()
conn.Close()
return nil, err
}
region.End()
// Relaxed REQUIRETLS mode is not conforming to the specification strictly
// but allows to start deploying client support for REQUIRETLS without the
// requirement for servers in the whole world to support it. The assumption
// behind it is that MX for the recipient domain is the final destination
// and all other forwarders behind it already have secure connection to
// each other. Therefore it is enough to enforce strict security only on
// the path to the MX even if it does not support the REQUIRETLS to propagate
// this requirement further.
if ok, _ := conn.Client().Extension("REQUIRETLS"); rd.rt.relaxedREQUIRETLS && !ok {
rd.msgMeta.SMTPOpts.RequireTLS = false
}
if err := conn.Mail(ctx, rd.mailFrom, rd.msgMeta.SMTPOpts); err != nil {
conn.Close()
return nil, err
}
conn.lastUseAt = time.Now()
rd.connections[domain] = conn
return conn, nil
}
func (rd *remoteDelivery) newConn(ctx context.Context, domain string) (*mxConn, error) {
conn := mxConn{
reuseLimit: rd.rt.connReuseLimit,
C: smtpconn.New(),
domain: domain,
lastUseAt: time.Now(),
}
conn.Dialer = rd.rt.dialer
conn.Log = rd.Log
conn.Hostname = rd.rt.hostname
conn.AddrInSMTPMsg = true
if rd.rt.connectTimeout != 0 {
conn.ConnectTimeout = rd.rt.connectTimeout
}
if rd.rt.commandTimeout != 0 {
conn.CommandTimeout = rd.rt.commandTimeout
}
if rd.rt.submissionTimeout != 0 {
conn.SubmissionTimeout = rd.rt.submissionTimeout
}
for _, p := range rd.policies {
p.PrepareDomain(ctx, domain)
}
region := trace.StartRegion(ctx, "remote/LookupMX")
dnssecOk, records, err := rd.lookupMX(ctx, domain)
region.End()
if err != nil {
return nil, err
}
conn.dnssecOk = dnssecOk
var lastErr error
region = trace.StartRegion(ctx, "remote/Connect+TLS")
for _, record := range records {
if record.Host == "." {
return nil, &exterrors.SMTPError{
Code: 556,
EnhancedCode: exterrors.EnhancedCode{5, 1, 10},
Message: "Domain does not accept email (null MX)",
}
}
if err := rd.attemptMX(ctx, &conn, record); err != nil {
if len(records) != 0 {
rd.Log.Error("cannot use MX", err, "remote_server", record.Host, "domain", domain)
}
lastErr = err
continue
}
break
}
region.End()
// Still not connected? Bail out.
if conn.Client() == nil {
return nil, &exterrors.SMTPError{
Code: exterrors.SMTPCode(lastErr, 451, 550),
EnhancedCode: exterrors.SMTPEnchCode(lastErr, exterrors.EnhancedCode{0, 4, 0}),
Message: "No usable MXs, last err: " + lastErr.Error(),
TargetName: "remote",
Err: lastErr,
Misc: map[string]interface{}{
"domain": domain,
},
}
}
return &conn, nil
}
func (rd *remoteDelivery) lookupMX(ctx context.Context, domain string) (dnssecOk bool, records []*net.MX, err error) {
if rd.rt.extResolver != nil {
dnssecOk, records, err = rd.rt.extResolver.AuthLookupMX(context.Background(), domain)
} else {
records, err = rd.rt.resolver.LookupMX(ctx, dns.FQDN(domain))
}
if err != nil {
reason, misc := exterrors.UnwrapDNSErr(err)
return false, nil, &exterrors.SMTPError{
Code: exterrors.SMTPCode(err, 451, 554),
EnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 4, 4}),
Message: "MX lookup error",
TargetName: "remote",
Reason: reason,
Err: err,
Misc: misc,
}
}
sort.Slice(records, func(i, j int) bool {
return records[i].Pref < records[j].Pref
})
// Fallback to A/AAA RR when no MX records are present as
// required by RFC 5321 Section 5.1.
if len(records) == 0 {
records = append(records, &net.MX{
Host: domain,
Pref: 0,
})
}
return dnssecOk, records, err
}