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 "crypto/tls"23 "net"24 "strconv"25 "testing"2627 "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)3334func 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 }3940 dialer := net.Dialer{}41 dialer.Resolver = &net.Resolver{}42 dnsSrv.PatchNet(dialer.Resolver)43 addr := dnsSrv.LocalAddr().(*net.UDPAddr)4445 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)5152 tgt := testTarget(t, zones, extResolver, []module.MXAuthPolicy{53 testDANEPolicy(t, extResolver),54 })55 return dnsSrv, tgt56}5758func 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}7677func 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)8182 // 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 }9899 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 )106107 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})108 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})109}110111func 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)115116 // 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 }135136 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 )143144 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})145 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})146}147148func 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)152153 // 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 }173174 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 )181182 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})183 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})184}185186func 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)190191 // 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 cert203 // => 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 succeed211 // 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 }217218 dnsSrv, tgt := targetWithExtResolver(t, zones)219 defer dnsSrv.Close()220 tgt.tlsConfig = clientCfg221222 _, 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}230231func 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)235236 // 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 }250251 dnsSrv, tgt := targetWithExtResolver(t, zones)252 defer dnsSrv.Close()253254 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})255 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})256}257258func 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)262263 // 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 }281282 dnsSrv, tgt := targetWithExtResolver(t, zones)283 defer dnsSrv.Close()284285 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})286 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})287}288289func 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)293294 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 }308309 dnsSrv, tgt := targetWithExtResolver(t, zones)310 defer dnsSrv.Close()311 tgt.tlsConfig = clientCfg312313 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})314 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})315}316317func 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)321322 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 }337338 dnsSrv, tgt := targetWithExtResolver(t, zones)339 defer dnsSrv.Close()340 tgt.tlsConfig = clientCfg341342 _, 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}350351func 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)355356 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 }365366 dnsSrv, tgt := targetWithExtResolver(t, zones)367 defer dnsSrv.Close()368 tgt.tlsConfig = clientCfg369370 testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"})371 be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"})372}373374func 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)378379 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 }391392 dnsSrv, tgt := targetWithExtResolver(t, zones)393 defer dnsSrv.Close()394 tgt.tlsConfig = clientCfg395396 _, 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}404405func 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)409410 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()427428 _, 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}436437func 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)441442 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()460461 // Cause failure through version incompatibility.462 tgt.tlsConfig = &tls.Config{463 MaxVersion: tls.VersionTLS12,464 MinVersion: tls.VersionTLS12,465 }466 srv.TLSConfig.MinVersion = tls.VersionTLS11467 srv.TLSConfig.MaxVersion = tls.VersionTLS11468469 // 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}