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	"crypto/tls"
 23	"net"
 24	"strconv"
 25	"testing"
 26
 27	"github.com/foxcpp/go-mockdns"
 28	"github.com/foxcpp/maddy/framework/dns"
 29	"github.com/foxcpp/maddy/framework/module"
 30	"github.com/foxcpp/maddy/internal/testutils"
 31	miekgdns "github.com/miekg/dns"
 32)
 33
 34func targetWithExtResolver(t *testing.T, zones map[string]mockdns.Zone) (*mockdns.Server, *Target) {
 35	dnsSrv, err := mockdns.NewServerWithLogger(zones, testutils.Logger(t, "mockdns"), false)
 36	if err != nil {
 37		t.Fatal(err)
 38	}
 39
 40	dialer := net.Dialer{}
 41	dialer.Resolver = &net.Resolver{}
 42	dnsSrv.PatchNet(dialer.Resolver)
 43	addr := dnsSrv.LocalAddr().(*net.UDPAddr)
 44
 45	extResolver, err := dns.NewExtResolver()
 46	if err != nil {
 47		t.Fatal(err)
 48	}
 49	extResolver.Cfg.Servers = []string{addr.IP.String()}
 50	extResolver.Cfg.Port = strconv.Itoa(addr.Port)
 51
 52	tgt := testTarget(t, zones, extResolver, []module.MXAuthPolicy{
 53		testDANEPolicy(t, extResolver),
 54	})
 55	return dnsSrv, tgt
 56}
 57
 58func tlsaRecord(name string, usage, matchType, selector uint8, cert string) map[miekgdns.Type][]miekgdns.RR {
 59	return map[miekgdns.Type][]miekgdns.RR{
 60		miekgdns.Type(miekgdns.TypeTLSA): {
 61			&miekgdns.TLSA{
 62				Hdr: miekgdns.RR_Header{
 63					Name:   name,
 64					Class:  miekgdns.ClassINET,
 65					Rrtype: miekgdns.TypeTLSA,
 66					Ttl:    9999,
 67				},
 68				Usage:        usage,
 69				MatchingType: matchType,
 70				Selector:     selector,
 71				Certificate:  cert,
 72			},
 73		},
 74	}
 75}
 76
 77func TestRemoteDelivery_DANE_Ok(t *testing.T) {
 78	_, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)
 79	defer srv.Close()
 80	defer testutils.CheckSMTPConnLeak(t, srv)
 81
 82	// RFC 7672, Section 2.2.2. "Non-CNAME" case.
 83	zones := map[string]mockdns.Zone{
 84		"example.invalid.": {
 85			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
 86		},
 87		"mx.example.invalid.": {
 88			AD: true,
 89			A:  []string{"127.0.0.1"},
 90		},
 91		"_25._tcp.mx.example.invalid.": {
 92			AD: true,
 93			Misc: tlsaRecord(
 94				"_25._tcp.mx.example.invalid.",
 95				3, 1, 1, "a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf"),
 96		},
 97	}
 98
 99	dnsSrv, tgt := targetWithExtResolver(t, zones)
100	defer dnsSrv.Close()
101	tgt.policies = append(tgt.policies,
102		&localPolicy{
103			minTLSLevel: module.TLSAuthenticated, // Established via DANE instead of PKIX.
104		},
105	)
106
107	testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})
108	be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})
109}
110
111func TestRemoteDelivery_DANE_CNAMEd_1(t *testing.T) {
112	_, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)
113	defer srv.Close()
114	defer testutils.CheckSMTPConnLeak(t, srv)
115
116	// RFC 7672, Section 2.2.2. "Secure CNAME" case - TLSA at CNAME matches.
117	zones := map[string]mockdns.Zone{
118		"example.invalid.": {
119			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
120		},
121		"mx.example.invalid.": {
122			AD:    true,
123			CNAME: "mx.cname.invalid.",
124		},
125		"mx.cname.invalid.": {
126			A: []string{"127.0.0.1"},
127		},
128		"_25._tcp.mx.cname.invalid.": {
129			AD: true,
130			Misc: tlsaRecord(
131				"_25._tcp.mx.cname.invalid.",
132				3, 1, 1, "a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf"),
133		},
134	}
135
136	dnsSrv, tgt := targetWithExtResolver(t, zones)
137	defer dnsSrv.Close()
138	tgt.policies = append(tgt.policies,
139		&localPolicy{
140			minTLSLevel: module.TLSAuthenticated, // Established via DANE instead of PKIX.
141		},
142	)
143
144	testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})
145	be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})
146}
147
148func TestRemoteDelivery_DANE_CNAMEd_2(t *testing.T) {
149	_, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)
150	defer srv.Close()
151	defer testutils.CheckSMTPConnLeak(t, srv)
152
153	// RFC 7672, Section 2.2.2. "Secure CNAME" case - TLSA at initial name matches.
154	zones := map[string]mockdns.Zone{
155		"example.invalid.": {
156			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
157		},
158		"mx.example.invalid.": {
159			AD:    true,
160			CNAME: "mx.cname.invalid.",
161		},
162		"_25._tcp.mx.example.invalid.": {
163			AD: true,
164			Misc: tlsaRecord(
165				"_25._tcp.mx.cname.invalid.",
166				3, 1, 1, "a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf"),
167		},
168		"mx.cname.invalid.": {
169			AD: true,
170			A:  []string{"127.0.0.1"},
171		},
172	}
173
174	dnsSrv, tgt := targetWithExtResolver(t, zones)
175	defer dnsSrv.Close()
176	tgt.policies = append(tgt.policies,
177		&localPolicy{
178			minTLSLevel: module.TLSAuthenticated, // Established via DANE instead of PKIX.
179		},
180	)
181
182	testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})
183	be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})
184}
185
186func TestRemoteDelivery_DANE_InsecureCNAMEDest(t *testing.T) {
187	clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)
188	defer srv.Close()
189	defer testutils.CheckSMTPConnLeak(t, srv)
190
191	// RFC 7672, Section 2.2.2. "Insecure CNAME" case - initial name is secure.
192	zones := map[string]mockdns.Zone{
193		"example.invalid.": {
194			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
195		},
196		"mx.example.invalid.": {
197			AD:    true,
198			CNAME: "mx.cname.invalid.",
199		},
200		"_25._tcp.mx.example.invalid.": {
201			AD: true,
202			// This is the record that activates DANE but does not match the cert
203			// => delivery is failed.
204			Misc: tlsaRecord(
205				"_25._tcp.mx.example.invalid.",
206				3, 1, 1, "a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cb"),
207		},
208		"_25._tcp.mx.cname.invalid.": {
209			AD: false,
210			// This is the record that matches the cert and would make delivery succeed
211			// but it should not be considered since AD=false.
212			Misc: tlsaRecord(
213				"_25._tcp.mx.cname.invalid.",
214				3, 1, 1, "a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf"),
215		},
216	}
217
218	dnsSrv, tgt := targetWithExtResolver(t, zones)
219	defer dnsSrv.Close()
220	tgt.tlsConfig = clientCfg
221
222	_, err := testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})
223	if err == nil {
224		t.Error("Expected an error, got none")
225	}
226	if be.MailFromCounter != 0 {
227		t.Fatal("MAIL FROM issued but should not")
228	}
229}
230
231func TestRemoteDelivery_DANE_NonAD_TLSA_Ignore(t *testing.T) {
232	be, srv := testutils.SMTPServer(t, "127.0.0.1:"+smtpPort)
233	defer srv.Close()
234	defer testutils.CheckSMTPConnLeak(t, srv)
235
236	// RFC 7672, Section 2.2.2. "Non-CNAME" case - initial name is insecure.
237	zones := map[string]mockdns.Zone{
238		"example.invalid.": {
239			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
240		},
241		"mx.example.invalid.": {
242			A: []string{"127.0.0.1"},
243		},
244		"_25._tcp.mx.example.invalid.": {
245			Misc: tlsaRecord(
246				"_25._tcp.mx.example.invalid.",
247				3, 1, 1, "a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cb"),
248		},
249	}
250
251	dnsSrv, tgt := targetWithExtResolver(t, zones)
252	defer dnsSrv.Close()
253
254	testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})
255	be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})
256}
257
258func TestRemoteDelivery_DANE_NonADIgnore_CNAME(t *testing.T) {
259	be, srv := testutils.SMTPServer(t, "127.0.0.1:"+smtpPort)
260	defer srv.Close()
261	defer testutils.CheckSMTPConnLeak(t, srv)
262
263	// RFC 7672, Section 2.2.2. "Insecure CNAME" case - initial name is insecure.
264	zones := map[string]mockdns.Zone{
265		"example.invalid.": {
266			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
267		},
268		"mx.example.invalid.": {
269			CNAME: "mx.cname.invalid.",
270		},
271		"mx.cname.invalid.": {
272			A: []string{"127.0.0.1"},
273		},
274		"_25._tcp.mx.cname.invalid.": {
275			AD: true,
276			Misc: tlsaRecord(
277				"_25._tcp.mx.example.invalid.",
278				3, 1, 1, "a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cb"),
279		},
280	}
281
282	dnsSrv, tgt := targetWithExtResolver(t, zones)
283	defer dnsSrv.Close()
284
285	testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})
286	be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})
287}
288
289func TestRemoteDelivery_DANE_SkipAUnauth(t *testing.T) {
290	clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)
291	defer srv.Close()
292	defer testutils.CheckSMTPConnLeak(t, srv)
293
294	zones := map[string]mockdns.Zone{
295		"example.invalid.": {
296			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
297		},
298		"mx.example.invalid.": {
299			A: []string{"127.0.0.1"},
300		},
301		"_25._tcp.mx.example.invalid.": {
302			AD: false,
303			Misc: tlsaRecord(
304				"_25._tcp.mx.example.invalid.",
305				3, 1, 1, "invalid hex will cause serialization error and no response will be sent"),
306		},
307	}
308
309	dnsSrv, tgt := targetWithExtResolver(t, zones)
310	defer dnsSrv.Close()
311	tgt.tlsConfig = clientCfg
312
313	testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})
314	be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})
315}
316
317func TestRemoteDelivery_DANE_Mismatch(t *testing.T) {
318	clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)
319	defer srv.Close()
320	defer testutils.CheckSMTPConnLeak(t, srv)
321
322	zones := map[string]mockdns.Zone{
323		"example.invalid.": {
324			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
325		},
326		"mx.example.invalid.": {
327			AD: true,
328			A:  []string{"127.0.0.1"},
329		},
330		"_25._tcp.mx.example.invalid.": {
331			AD: true,
332			Misc: tlsaRecord(
333				"_25._tcp.mx.example.invalid.",
334				3, 1, 1, "ffb5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf"),
335		},
336	}
337
338	dnsSrv, tgt := targetWithExtResolver(t, zones)
339	defer dnsSrv.Close()
340	tgt.tlsConfig = clientCfg
341
342	_, err := testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})
343	if err == nil {
344		t.Error("Expected an error, got none")
345	}
346	if be.MailFromCounter != 0 {
347		t.Fatal("MAIL FROM issued but should not")
348	}
349}
350
351func TestRemoteDelivery_DANE_NoRecord(t *testing.T) {
352	clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)
353	defer srv.Close()
354	defer testutils.CheckSMTPConnLeak(t, srv)
355
356	zones := map[string]mockdns.Zone{
357		"example.invalid.": {
358			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
359		},
360		"mx.example.invalid.": {
361			AD: true,
362			A:  []string{"127.0.0.1"},
363		},
364	}
365
366	dnsSrv, tgt := targetWithExtResolver(t, zones)
367	defer dnsSrv.Close()
368	tgt.tlsConfig = clientCfg
369
370	testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})
371	be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})
372}
373
374func TestRemoteDelivery_DANE_LookupErr(t *testing.T) {
375	clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)
376	defer srv.Close()
377	defer testutils.CheckSMTPConnLeak(t, srv)
378
379	zones := map[string]mockdns.Zone{
380		"example.invalid.": {
381			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
382		},
383		"mx.example.invalid.": {
384			AD: true,
385			A:  []string{"127.0.0.1"},
386		},
387		"_25._tcp.mx.example.invalid.": {
388			Err: &net.DNSError{},
389		},
390	}
391
392	dnsSrv, tgt := targetWithExtResolver(t, zones)
393	defer dnsSrv.Close()
394	tgt.tlsConfig = clientCfg
395
396	_, err := testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})
397	if err == nil {
398		t.Error("Expected an error, got none")
399	}
400	if be.MailFromCounter != 0 {
401		t.Fatal("MAIL FROM issued but should not")
402	}
403}
404
405func TestRemoteDelivery_DANE_NoTLS(t *testing.T) {
406	be, srv := testutils.SMTPServer(t, "127.0.0.1:"+smtpPort)
407	defer srv.Close()
408	defer testutils.CheckSMTPConnLeak(t, srv)
409
410	zones := map[string]mockdns.Zone{
411		"example.invalid.": {
412			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
413		},
414		"mx.example.invalid.": {
415			AD: true,
416			A:  []string{"127.0.0.1"},
417		},
418		"_25._tcp.mx.example.invalid.": {
419			AD: true,
420			Misc: tlsaRecord(
421				"_25._tcp.mx.example.invalid.",
422				3, 1, 1, "a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf"),
423		},
424	}
425	dnsSrv, tgt := targetWithExtResolver(t, zones)
426	defer dnsSrv.Close()
427
428	_, err := testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})
429	if err == nil {
430		t.Error("Expected an error, got none")
431	}
432	if be.MailFromCounter != 0 {
433		t.Fatal("MAIL FROM issued but should not")
434	}
435}
436
437func TestRemoteDelivery_DANE_TLSError(t *testing.T) {
438	_, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)
439	defer srv.Close()
440	defer testutils.CheckSMTPConnLeak(t, srv)
441
442	zones := map[string]mockdns.Zone{
443		"example.invalid.": {
444			AD: true,
445			MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},
446		},
447		"mx.example.invalid.": {
448			AD: true,
449			A:  []string{"127.0.0.1"},
450		},
451		"_25._tcp.mx.example.invalid.": {
452			AD: true,
453			Misc: tlsaRecord(
454				"_25._tcp.mx.example.invalid.",
455				3, 1, 1, "a9b5cb4d02f996f6385debe9a8952f1af1f4aec7eae0f37c2cd6d0d8ee8391cf"),
456		},
457	}
458	dnsSrv, tgt := targetWithExtResolver(t, zones)
459	defer dnsSrv.Close()
460
461	// Cause failure through version incompatibility.
462	tgt.tlsConfig = &tls.Config{
463		MaxVersion: tls.VersionTLS12,
464		MinVersion: tls.VersionTLS12,
465	}
466	srv.TLSConfig.MinVersion = tls.VersionTLS11
467	srv.TLSConfig.MaxVersion = tls.VersionTLS11
468
469	// DANE should prevent the fallback to plaintext.
470	_, err := testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})
471	if err == nil {
472		t.Error("Expected an error, got none")
473	}
474	if be.MailFromCounter != 0 {
475		t.Fatal("MAIL FROM issued but should not")
476	}
477}