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 "strconv"27 "testing"2829 "github.com/emersion/go-smtp"30 "github.com/foxcpp/go-mockdns"31 "github.com/foxcpp/go-mtasts"32 "github.com/foxcpp/maddy/framework/dns"33 "github.com/foxcpp/maddy/framework/module"34 "github.com/foxcpp/maddy/internal/testutils"35)3637func TestRemoteDelivery_AuthMX_MTASTS(t *testing.T) {38 clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)39 defer srv.Close()40 defer testutils.CheckSMTPConnLeak(t, srv)41 zones := map[string]mockdns.Zone{42 "example.invalid.": {43 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},44 },45 "mx.example.invalid.": {46 A: []string{"127.0.0.1"},47 },48 }4950 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {51 if domain != "example.invalid" {52 return nil, errors.New("Wrong domain in lookup")53 }5455 return &mtasts.Policy{56 // Testing policy is enough.57 Mode: mtasts.ModeTesting,58 MX: []string{"mx.example.invalid"},59 }, nil60 }6162 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{63 testSTSPolicy(t, zones, mtastsGet),64 })65 tgt.tlsConfig = clientCfg66 defer tgt.Close()6768 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})69 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})70}7172func TestRemoteDelivery_MTASTS_SkipNonMatching(t *testing.T) {73 _, be1, srv1 := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)74 defer srv1.Close()75 defer testutils.CheckSMTPConnLeak(t, srv1)7677 clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.2:"+smtpPort)78 defer srv.Close()79 defer testutils.CheckSMTPConnLeak(t, srv)80 zones := map[string]mockdns.Zone{81 "example.invalid.": {82 MX: []net.MX{83 {Host: "mx2.example.invalid.", Pref: 5},84 {Host: "mx1.example.invalid.", Pref: 10},85 },86 },87 "mx1.example.invalid.": {88 A: []string{"127.0.0.1"},89 },90 "mx2.example.invalid.": {91 A: []string{"127.0.0.2"},92 },93 }9495 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {96 if domain != "example.invalid" {97 return nil, errors.New("Wrong domain in lookup")98 }99100 return &mtasts.Policy{101 Mode: mtasts.ModeEnforce,102 MX: []string{"mx2.example.invalid"},103 }, nil104 }105106 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{107 testSTSPolicy(t, zones, mtastsGet),108 &localPolicy{minMXLevel: module.MX_MTASTS},109 })110 tgt.tlsConfig = clientCfg111 defer tgt.Close()112113 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})114 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})115116 if be1.MailFromCounter != 0 {117 t.Fatal("MAIL FROM issued for server failing authentication")118 }119}120121func TestRemoteDelivery_AuthMX_MTASTS_Fail(t *testing.T) {122 clientCfg, be1, srv1 := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)123 defer srv1.Close()124 defer testutils.CheckSMTPConnLeak(t, srv1)125126 zones := map[string]mockdns.Zone{127 "example.invalid.": {128 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},129 },130 "mx.example.invalid.": {131 A: []string{"127.0.0.1"},132 },133 }134135 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {136 if domain != "example.invalid" {137 return nil, errors.New("Wrong domain in lookup")138 }139140 return &mtasts.Policy{141 Mode: mtasts.ModeTesting,142 MX: []string{"mx4.example.invalid"}, // not mx.example.invalid!143 }, nil144 }145146 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{147 testSTSPolicy(t, zones, mtastsGet),148 &localPolicy{minMXLevel: module.MX_MTASTS},149 })150 tgt.tlsConfig = clientCfg151 defer tgt.Close()152153 _, err := testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})154 if err == nil {155 t.Fatal("Expected an error, got none")156 }157158 if be1.MailFromCounter != 0 {159 t.Fatal("MAIL FROM issued for server failing authentication")160 }161}162163func TestRemoteDelivery_AuthMX_MTASTS_NoTLS(t *testing.T) {164 be1, srv1 := testutils.SMTPServer(t, "127.0.0.1:"+smtpPort)165 defer srv1.Close()166 defer testutils.CheckSMTPConnLeak(t, srv1)167168 zones := map[string]mockdns.Zone{169 "example.invalid.": {170 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},171 },172 "mx.example.invalid.": {173 A: []string{"127.0.0.1"},174 },175 }176177 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {178 if domain != "example.invalid" {179 return nil, errors.New("Wrong domain in lookup")180 }181182 return &mtasts.Policy{183 Mode: mtasts.ModeEnforce,184 MX: []string{"mx.example.invalid"},185 }, nil186 }187188 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{189 testSTSPolicy(t, zones, mtastsGet),190 &localPolicy{minMXLevel: module.MX_MTASTS},191 })192 defer tgt.Close()193194 _, err := testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})195 if err == nil {196 t.Fatal("Expected an error, got none")197 }198199 if be1.MailFromCounter != 0 {200 t.Fatal("MAIL FROM issued for server failing authentication")201 }202}203204func TestRemoteDelivery_AuthMX_MTASTS_RequirePKIX(t *testing.T) {205 _, be1, srv1 := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)206 defer srv1.Close()207 defer testutils.CheckSMTPConnLeak(t, srv1)208209 zones := map[string]mockdns.Zone{210 "example.invalid.": {211 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},212 },213 "mx.example.invalid.": {214 A: []string{"127.0.0.1"},215 },216 }217218 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {219 if domain != "example.invalid" {220 return nil, errors.New("Wrong domain in lookup")221 }222223 return &mtasts.Policy{224 Mode: mtasts.ModeEnforce,225 MX: []string{"mx.example.invalid"},226 }, nil227 }228229 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{230 testSTSPolicy(t, zones, mtastsGet),231 &localPolicy{minMXLevel: module.MX_MTASTS},232 })233 defer tgt.Close()234235 _, err := testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})236 if err == nil {237 t.Fatal("Expected an error, got none")238 }239240 if be1.MailFromCounter != 0 {241 t.Fatal("MAIL FROM issued for server failing authentication")242 }243}244245func TestRemoteDelivery_AuthMX_MTASTS_NoPolicy(t *testing.T) {246 // At the moment, implementation ensures all MX policy checks are completed247 // before attempting to connect.248 // However, we cannot run complete go-smtp server to check whether it is249 // violated and the connection is actually estabilished since this causes250 // weird race conditions when test completes before go-smtp has the251 // chance to fully initialize itself (Serve is still at the conn.listeners252 // assignment when Close is called).253 //254 // The issue was resolved upstream by introducing locking around internal255 // listeners slice use. Uses of FailOnConn remain since they pretty much do256 // not hurt.257 //258 // https://builds.sr.ht/~emersion/job/147975259 tarpit := testutils.FailOnConn(t, "127.0.0.1:"+smtpPort)260 defer tarpit.Close()261262 zones := map[string]mockdns.Zone{263 "example.invalid.": {264 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},265 },266 "mx.example.invalid.": {267 A: []string{"127.0.0.1"},268 },269 }270271 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {272 if domain != "example.invalid" {273 return nil, errors.New("Wrong domain in lookup")274 }275276 return nil, mtasts.ErrNoPolicy277 }278279 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{280 testSTSPolicy(t, zones, mtastsGet),281 &localPolicy{minMXLevel: module.MX_MTASTS},282 })283 defer tgt.Close()284285 _, err := testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})286 if err == nil {287 t.Fatal("Expected an error, got none")288 }289}290291func TestRemoteDelivery_AuthMX_DNSSEC(t *testing.T) {292 be, srv := testutils.SMTPServer(t, "127.0.0.1:"+smtpPort)293 defer srv.Close()294 defer testutils.CheckSMTPConnLeak(t, srv)295296 zones := map[string]mockdns.Zone{297 "example.invalid.": {298 AD: true,299 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},300 },301 "mx.example.invalid.": {302 A: []string{"127.0.0.1"},303 },304 }305306 dnsSrv, err := mockdns.NewServerWithLogger(zones, testutils.Logger(t, "mockdns"), false)307 if err != nil {308 t.Fatal(err)309 }310 defer dnsSrv.Close()311312 dialer := net.Dialer{}313 dialer.Resolver = &net.Resolver{}314 dnsSrv.PatchNet(dialer.Resolver)315 addr := dnsSrv.LocalAddr().(*net.UDPAddr)316317 extResolver, err := dns.NewExtResolver()318 if err != nil {319 t.Fatal(err)320 }321 extResolver.Cfg.Servers = []string{addr.IP.String()}322 extResolver.Cfg.Port = strconv.Itoa(addr.Port)323324 tgt := testTarget(t, zones, extResolver, nil)325 defer tgt.Close()326327 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})328 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})329}330331func TestRemoteDelivery_AuthMX_DNSSEC_Fail(t *testing.T) {332 be, srv := testutils.SMTPServer(t, "127.0.0.1:"+smtpPort)333 defer srv.Close()334 defer testutils.CheckSMTPConnLeak(t, srv)335336 zones := map[string]mockdns.Zone{337 "example.invalid.": {338 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},339 },340 "mx.example.invalid.": {341 A: []string{"127.0.0.1"},342 },343 }344345 dnsSrv, err := mockdns.NewServerWithLogger(zones, testutils.Logger(t, "mockdns"), false)346 if err != nil {347 t.Fatal(err)348 }349 defer dnsSrv.Close()350351 dialer := net.Dialer{}352 dialer.Resolver = &net.Resolver{}353 dnsSrv.PatchNet(dialer.Resolver)354 addr := dnsSrv.LocalAddr().(*net.UDPAddr)355356 extResolver, err := dns.NewExtResolver()357 if err != nil {358 t.Fatal(err)359 }360 extResolver.Cfg.Servers = []string{addr.IP.String()}361 extResolver.Cfg.Port = strconv.Itoa(addr.Port)362363 tgt := testTarget(t, zones, extResolver, []module.MXAuthPolicy{364 &localPolicy{minMXLevel: module.MX_DNSSEC},365 })366 defer tgt.Close()367368 _, err = testutils.DoTestDeliveryErr(t, tgt, "test@example.com", []string{"test@example.invalid"})369 if err == nil {370 t.Fatal("Expected an error, got none")371 }372373 if be.MailFromCounter != 0 {374 t.Fatal("MAIL FROM issued for server failing authentication")375 }376}377378func TestRemoteDelivery_REQUIRETLS(t *testing.T) {379 clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)380 srv.EnableREQUIRETLS = true381 defer srv.Close()382 defer testutils.CheckSMTPConnLeak(t, srv)383 zones := map[string]mockdns.Zone{384 "example.invalid.": {385 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},386 },387 "mx.example.invalid.": {388 A: []string{"127.0.0.1"},389 },390 }391392 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {393 if domain != "example.invalid" {394 return nil, errors.New("Wrong domain in lookup")395 }396397 return &mtasts.Policy{398 // Testing policy is enough.399 Mode: mtasts.ModeTesting,400 MX: []string{"mx.example.invalid"},401 }, nil402 }403404 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{405 testSTSPolicy(t, zones, mtastsGet),406 })407 tgt.tlsConfig = clientCfg408 defer tgt.Close()409410 testutils.DoTestDeliveryMeta(t, tgt, "test@example.com", []string{"test@example.invalid"}, &module.MsgMetadata{411 OriginalFrom: "test@example.com",412 SMTPOpts: smtp.MailOptions{413 RequireTLS: true,414 },415 })416 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})417}418419func TestRemoteDelivery_REQUIRETLS_Fail(t *testing.T) {420 clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)421 srv.EnableREQUIRETLS = false /* no REQUIRETLS */422 defer srv.Close()423 defer testutils.CheckSMTPConnLeak(t, srv)424 zones := map[string]mockdns.Zone{425 "example.invalid.": {426 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},427 },428 "mx.example.invalid.": {429 A: []string{"127.0.0.1"},430 },431 }432433 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {434 if domain != "example.invalid" {435 return nil, errors.New("Wrong domain in lookup")436 }437438 return &mtasts.Policy{439 // Testing policy is enough.440 Mode: mtasts.ModeTesting,441 MX: []string{"mx.example.invalid"},442 }, nil443 }444445 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{446 testSTSPolicy(t, zones, mtastsGet),447 })448 tgt.tlsConfig = clientCfg449 defer tgt.Close()450451 if _, err := testutils.DoTestDeliveryErrMeta(t, tgt, "test@example.com", []string{"test@example.invalid"}, &module.MsgMetadata{452 OriginalFrom: "test@example.com",453 SMTPOpts: smtp.MailOptions{454 RequireTLS: true,455 },456 }); err == nil {457 t.Error("Expected an error, got none")458 }459 if be.MailFromCounter != 0 {460 t.Fatal("MAIL FROM issued for server failing authentication")461 }462}463464func TestRemoteDelivery_REQUIRETLS_Relaxed(t *testing.T) {465 clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)466 srv.EnableREQUIRETLS = false /* no REQUIRETLS */467 defer srv.Close()468 defer testutils.CheckSMTPConnLeak(t, srv)469 zones := map[string]mockdns.Zone{470 "example.invalid.": {471 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},472 },473 "mx.example.invalid.": {474 A: []string{"127.0.0.1"},475 },476 }477478 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {479 if domain != "example.invalid" {480 return nil, errors.New("Wrong domain in lookup")481 }482483 return &mtasts.Policy{484 // Testing policy is enough.485 Mode: mtasts.ModeTesting,486 MX: []string{"mx.example.invalid"},487 }, nil488 }489490 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{491 testSTSPolicy(t, zones, mtastsGet),492 })493 tgt.relaxedREQUIRETLS = true494 tgt.tlsConfig = clientCfg495 defer tgt.Close()496497 testutils.DoTestDeliveryMeta(t, tgt, "test@example.com", []string{"test@example.invalid"}, &module.MsgMetadata{498 OriginalFrom: "test@example.com",499 SMTPOpts: smtp.MailOptions{500 RequireTLS: true,501 },502 })503 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})504}505506func TestRemoteDelivery_REQUIRETLS_Relaxed_NoMXAuth(t *testing.T) {507 clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)508 srv.EnableREQUIRETLS = false /* no REQUIRETLS */509 defer srv.Close()510 defer testutils.CheckSMTPConnLeak(t, srv)511 zones := map[string]mockdns.Zone{512 "example.invalid.": {513 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},514 },515 "mx.example.invalid.": {516 A: []string{"127.0.0.1"},517 },518 }519520 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {521 if domain != "example.invalid" {522 return nil, errors.New("Wrong domain in lookup")523 }524 return nil, mtasts.ErrNoPolicy525 }526527 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{528 testSTSPolicy(t, zones, mtastsGet),529 })530 tgt.relaxedREQUIRETLS = true531 tgt.tlsConfig = clientCfg532 defer tgt.Close()533534 if _, err := testutils.DoTestDeliveryErrMeta(t, tgt, "test@example.com", []string{"test@example.invalid"}, &module.MsgMetadata{535 OriginalFrom: "test@example.com",536 SMTPOpts: smtp.MailOptions{537 RequireTLS: true,538 },539 }); err == nil {540 t.Error("Expected an error, got none")541 }542 if be.MailFromCounter != 0 {543 t.Fatal("MAIL FROM issued for server failing authentication")544 }545}546547func TestRemoteDelivery_REQUIRETLS_Relaxed_NoTLS(t *testing.T) {548 be, srv := testutils.SMTPServer(t, "127.0.0.1:"+smtpPort)549 srv.EnableREQUIRETLS = false /* no REQUIRETLS */550 defer srv.Close()551 defer testutils.CheckSMTPConnLeak(t, srv)552 zones := map[string]mockdns.Zone{553 "example.invalid.": {554 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},555 },556 "mx.example.invalid.": {557 A: []string{"127.0.0.1"},558 },559 }560561 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {562 if domain != "example.invalid" {563 return nil, errors.New("Wrong domain in lookup")564 }565566 return &mtasts.Policy{567 // Testing policy is enough.568 Mode: mtasts.ModeTesting,569 MX: []string{"mx.example.invalid"},570 }, nil571 }572573 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{574 testSTSPolicy(t, zones, mtastsGet),575 })576 tgt.relaxedREQUIRETLS = true577 tgt.tlsConfig = nil578 defer tgt.Close()579580 if _, err := testutils.DoTestDeliveryErrMeta(t, tgt, "test@example.com", []string{"test@example.invalid"}, &module.MsgMetadata{581 OriginalFrom: "test@example.com",582 SMTPOpts: smtp.MailOptions{583 RequireTLS: true,584 },585 }); err == nil {586 t.Error("Expected an error, got none")587 }588 if be.MailFromCounter != 0 {589 t.Fatal("MAIL FROM issued for server failing authentication")590 }591}592593func TestRemoteDelivery_REQUIRETLS_Relaxed_TLSFail(t *testing.T) {594 clientCfg, be, srv := testutils.SMTPServerSTARTTLS(t, "127.0.0.1:"+smtpPort)595 srv.EnableREQUIRETLS = false /* no REQUIRETLS */596 defer srv.Close()597 defer testutils.CheckSMTPConnLeak(t, srv)598 zones := map[string]mockdns.Zone{599 "example.invalid.": {600 MX: []net.MX{{Host: "mx.example.invalid.", Pref: 10}},601 },602 "mx.example.invalid.": {603 A: []string{"127.0.0.1"},604 },605 }606607 mtastsGet := func(_ context.Context, domain string) (*mtasts.Policy, error) {608 if domain != "example.invalid" {609 return nil, errors.New("Wrong domain in lookup")610 }611612 return &mtasts.Policy{613 // Testing policy is enough.614 Mode: mtasts.ModeTesting,615 MX: []string{"mx.example.invalid"},616 }, nil617 }618619 tgt := testTarget(t, zones, nil, []module.MXAuthPolicy{620 testSTSPolicy(t, zones, mtastsGet),621 })622 tgt.relaxedREQUIRETLS = true623 // Cause failure through version incompatibility.624 clientCfg.MaxVersion = tls.VersionTLS12625 clientCfg.MinVersion = tls.VersionTLS12626 srv.TLSConfig.MinVersion = tls.VersionTLS11627 srv.TLSConfig.MaxVersion = tls.VersionTLS11628 tgt.tlsConfig = clientCfg629 defer tgt.Close()630631 if _, err := testutils.DoTestDeliveryErrMeta(t, tgt, "test@example.com", []string{"test@example.invalid"}, &module.MsgMetadata{632 OriginalFrom: "test@example.com",633 SMTPOpts: smtp.MailOptions{634 RequireTLS: true,635 },636 }); err == nil {637 t.Error("Expected an error, got none")638 }639 if be.MailFromCounter != 0 {640 t.Fatal("MAIL FROM issued for server failing authentication")641 }642}