12 "github.com/mjl-/adns"
14 "github.com/mjl-/mox/dns"
15 "github.com/mjl-/mox/mlog"
18func domain(s string) dns.Domain {
19 d, err := dns.ParseDomain(s)
21 panic("parse domain: " + err.Error())
26func ipdomain(s string) dns.IPDomain {
29 return dns.IPDomain{IP: ip}
31 d, err := dns.ParseDomain(s)
33 panic(fmt.Sprintf("parse domain %q: %v", s, err))
35 return dns.IPDomain{Domain: d}
38func ipdomains(s ...string) (l []dns.IPDomain) {
40 l = append(l, ipdomain(e))
45// Test basic MX lookup case, but also following CNAME, detecting CNAME loops and
46// having a CNAME limit, connecting directly to a host, and domain that does not
47// exist or has temporary error.
48func TestGatherDestinations(t *testing.T) {
49 ctxbg := context.Background()
50 log := mlog.New("smtpclient", nil)
52 resolver := dns.MockResolver{
53 MX: map[string][]*net.MX{
54 "basic.example.": {{Host: "mail.basic.example.", Pref: 10}},
55 "multimx.example.": {{Host: "mail1.multimx.example.", Pref: 10}, {Host: "mail2.multimx.example.", Pref: 10}},
56 "nullmx.example.": {{Host: ".", Pref: 10}},
57 "temperror-mx.example.": {{Host: "absent.example.", Pref: 10}},
59 A: map[string][]string{
60 "mail.basic.example": {"10.0.0.1"},
61 "justhost.example.": {"10.0.0.1"}, // No MX record for domain, only an A record.
62 "temperror-a.example.": {"10.0.0.1"},
64 AAAA: map[string][]string{
65 "justhost6.example.": {"2001:db8::1"}, // No MX record for domain, only an AAAA record.
67 CNAME: map[string]string{
68 "cname.example.": "basic.example.",
69 "cname-to-inauthentic.example.": "cnameinauthentic.example.",
70 "cnameinauthentic.example.": "basic.example.",
71 "cnameloop.example.": "cnameloop2.example.",
72 "cnameloop2.example.": "cnameloop.example.",
73 "danglingcname.example.": "absent.example.", // Points to missing name.
74 "temperror-cname.example.": "absent.example.",
77 "mx temperror-mx.example.",
78 "host temperror-a.example.",
79 "cname temperror-cname.example.",
81 Inauthentic: []string{"cname cnameinauthentic.example."},
83 for i := 0; i <= 16; i++ {
84 s := fmt.Sprintf("cnamelimit%d.example.", i)
85 next := fmt.Sprintf("cnamelimit%d.example.", i+1)
86 resolver.CNAME[s] = next
89 test := func(ipd dns.IPDomain, expHosts []dns.IPDomain, expDomain dns.Domain, expPerm, expAuthic, expExpAuthic bool, expErr error) {
92 _, authic, authicExp, ed, hosts, perm, err := GatherDestinations(ctxbg, log.Logger, resolver, ipd)
93 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
94 // todo: could also check the individual errors? code currently does not have structured errors.
95 t.Fatalf("gather hosts: %v, expected %v", err, expErr)
100 if !reflect.DeepEqual(hosts, expHosts) || ed != expDomain || perm != expPerm || authic != expAuthic || authicExp != expExpAuthic {
101 t.Fatalf("got hosts %#v, effectiveDomain %#v, permanent %#v, authic %v %v, expected %#v %#v %#v %v %v", hosts, ed, perm, authic, authicExp, expHosts, expDomain, expPerm, expAuthic, expExpAuthic)
105 var zerodom dns.Domain
107 for i := 0; i < 2; i++ {
109 resolver.AllAuthentic = authic
110 // Basic with simple MX.
111 test(ipdomain("basic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, authic, authic, nil)
112 test(ipdomain("multimx.example"), ipdomains("mail1.multimx.example", "mail2.multimx.example"), domain("multimx.example"), false, authic, authic, nil)
114 test(ipdomain("justhost.example"), ipdomains("justhost.example"), domain("justhost.example"), false, authic, authic, nil)
115 // Only an AAAA record.
116 test(ipdomain("justhost6.example"), ipdomains("justhost6.example"), domain("justhost6.example"), false, authic, authic, nil)
118 test(ipdomain("cname.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, authic, authic, nil)
119 // No MX/CNAME, non-existence of host will be found out later.
120 test(ipdomain("absent.example"), ipdomains("absent.example"), domain("absent.example"), false, authic, authic, nil)
121 // Followed CNAME, has no MX, non-existence of host will be found out later.
122 test(ipdomain("danglingcname.example"), ipdomains("absent.example"), domain("absent.example"), false, authic, authic, nil)
123 test(ipdomain("cnamelimit1.example"), nil, zerodom, true, authic, authic, errCNAMELimit)
124 test(ipdomain("cnameloop.example"), nil, zerodom, true, authic, authic, errCNAMELoop)
125 test(ipdomain("nullmx.example"), nil, zerodom, true, authic, authic, errNoMail)
126 test(ipdomain("temperror-mx.example"), nil, zerodom, false, authic, authic, errDNS)
127 test(ipdomain("temperror-cname.example"), nil, zerodom, false, authic, authic, errDNS)
130 test(ipdomain("10.0.0.1"), ipdomains("10.0.0.1"), zerodom, false, false, false, nil)
131 test(ipdomain("cnameinauthentic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, false, false, nil)
132 test(ipdomain("cname-to-inauthentic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, true, false, nil)
135func TestGatherIPs(t *testing.T) {
136 ctxbg := context.Background()
137 log := mlog.New("smtpclient", nil)
139 resolver := dns.MockResolver{
140 A: map[string][]string{
141 "host1.example.": {"10.0.0.1"},
142 "host2.example.": {"10.0.0.2"},
143 "temperror-a.example.": {"10.0.0.3"},
145 AAAA: map[string][]string{
146 "host2.example.": {"2001:db8::1"},
148 CNAME: map[string]string{
149 "cname1.example.": "host1.example.",
150 "cname-to-inauthentic.example.": "cnameinauthentic.example.",
151 "cnameinauthentic.example.": "host1.example.",
152 "cnameloop.example.": "cnameloop2.example.",
153 "cnameloop2.example.": "cnameloop.example.",
154 "danglingcname.example.": "absent.example.", // Points to missing name.
155 "temperror-cname.example.": "absent.example.",
158 "host temperror-a.example.",
159 "cname temperror-cname.example.",
161 Inauthentic: []string{"cname cnameinauthentic.example."},
164 test := func(host dns.IPDomain, expAuthic, expAuthicExp bool, expHostExp dns.Domain, expIPs []net.IP, expErr any) {
167 authic, authicExp, hostExp, ips, _, err := GatherIPs(ctxbg, log.Logger, resolver, host, nil)
168 if (err == nil) != (expErr == nil) || err != nil && !(errors.Is(err, expErr.(error)) || errors.As(err, &expErr)) {
169 // todo: could also check the individual errors?
170 t.Fatalf("gather hosts: %v, expected %v", err, expErr)
175 if expHostExp == zerohost {
176 expHostExp = host.Domain
178 if authic != expAuthic || authicExp != expAuthicExp || hostExp != expHostExp || !reflect.DeepEqual(ips, expIPs) {
179 t.Fatalf("got authic %v %v, host %v, ips %v, expected %v %v %v %v", authic, authicExp, hostExp, ips, expAuthic, expAuthicExp, expHostExp, expIPs)
183 ips := func(l ...string) (r []net.IP) {
184 for _, s := range l {
185 r = append(r, net.ParseIP(s))
190 for i := 0; i < 2; i++ {
192 resolver.AllAuthentic = authic
194 test(ipdomain("host1.example"), authic, authic, zerohost, ips("10.0.0.1"), nil)
195 test(ipdomain("host2.example"), authic, authic, zerohost, ips("10.0.0.2", "2001:db8::1"), nil)
196 test(ipdomain("cname-to-inauthentic.example"), authic, false, domain("host1.example"), ips("10.0.0.1"), nil)
197 test(ipdomain("cnameloop.example"), authic, authic, zerohost, nil, errCNAMELimit)
198 test(ipdomain("bogus.example"), authic, authic, zerohost, nil, &adns.DNSError{})
199 test(ipdomain("danglingcname.example"), authic, authic, zerohost, nil, &adns.DNSError{})
200 test(ipdomain("temperror-a.example"), authic, authic, zerohost, nil, &adns.DNSError{})
201 test(ipdomain("temperror-cname.example"), authic, authic, zerohost, nil, &adns.DNSError{})
204 test(ipdomain("cnameinauthentic.example"), false, false, domain("host1.example"), ips("10.0.0.1"), nil)
205 test(ipdomain("cname-to-inauthentic.example"), true, false, domain("host1.example"), ips("10.0.0.1"), nil)
208func TestGatherTLSA(t *testing.T) {
209 ctxbg := context.Background()
210 log := mlog.New("smtpclient", nil)
212 record := func(usage, selector, matchType uint8) adns.TLSA {
214 Usage: adns.TLSAUsage(usage),
215 Selector: adns.TLSASelector(selector),
216 MatchType: adns.TLSAMatchType(matchType),
217 CertAssoc: make([]byte, sha256.Size), // Assume sha256.
220 records := func(l ...adns.TLSA) []adns.TLSA {
224 record0 := record(3, 1, 1)
225 list0 := records(record0)
226 record1 := record(3, 0, 1)
227 list1 := records(record1)
229 resolver := dns.MockResolver{
230 TLSA: map[string][]adns.TLSA{
231 "_25._tcp.host0.example.": list0,
232 "_25._tcp.host1.example.": list1,
233 "_25._tcp.inauthentic.example.": list1,
234 "_25._tcp.temperror-cname.example.": list1,
236 CNAME: map[string]string{
237 "_25._tcp.cname.example.": "_25._tcp.host1.example.",
238 "_25._tcp.cnameloop.example.": "_25._tcp.cnameloop2.example.",
239 "_25._tcp.cnameloop2.example.": "_25._tcp.cnameloop.example.",
240 "_25._tcp.cname-to-inauthentic.example.": "_25._tcp.cnameinauthentic.example.",
241 "_25._tcp.cnameinauthentic.example.": "_25._tcp.host1.example.",
242 "_25._tcp.danglingcname.example.": "_25._tcp.absent.example.", // Points to missing name.
245 "cname _25._tcp.temperror-cname.example.",
247 Inauthentic: []string{
248 "cname _25._tcp.cnameinauthentic.example.",
249 "tlsa _25._tcp.inauthentic.example.",
253 test := func(host dns.Domain, expandedAuthentic bool, expandedHost dns.Domain, expDANERequired bool, expRecords []adns.TLSA, expBaseDom dns.Domain, expErr any) {
256 daneReq, records, baseDom, err := GatherTLSA(ctxbg, log.Logger, resolver, host, expandedAuthentic, expandedHost)
257 if (err == nil) != (expErr == nil) || err != nil && !(errors.Is(err, expErr.(error)) || errors.As(err, &expErr)) {
258 // todo: could also check the individual errors?
259 t.Fatalf("gather tlsa: %v, expected %v", err, expErr)
261 if daneReq != expDANERequired {
262 t.Fatalf("got daneRequired %v, expected %v", daneReq, expDANERequired)
267 if !reflect.DeepEqual(records, expRecords) || baseDom != expBaseDom {
268 t.Fatalf("got records, baseDomain %v %v, expected %v %v", records, baseDom, expRecords, expBaseDom)
272 resolver.AllAuthentic = true
273 test(domain("host1.example"), false, domain("host1.example"), true, list1, domain("host1.example"), nil)
274 test(domain("host1.example"), true, domain("host1.example"), true, list1, domain("host1.example"), nil)
275 test(domain("host0.example"), true, domain("host1.example"), true, list1, domain("host1.example"), nil)
276 test(domain("host0.example"), false, domain("host1.example"), true, list0, domain("host0.example"), nil)
278 // CNAME for TLSA at cname.example should be followed.
279 test(domain("host0.example"), true, domain("cname.example"), true, list1, domain("cname.example"), nil)
280 // TLSA records at original domain should be followed.
281 test(domain("host0.example"), false, domain("cname.example"), true, list0, domain("host0.example"), nil)
283 test(domain("cnameloop.example"), false, domain("cnameloop.example"), true, nil, zerohost, errCNAMELimit)
285 test(domain("host0.example"), false, domain("inauthentic.example"), true, list0, domain("host0.example"), nil)
286 test(domain("inauthentic.example"), false, domain("inauthentic.example"), false, nil, domain("inauthentic.example"), nil)
287 test(domain("temperror-cname.example"), false, domain("temperror-cname.example"), true, nil, domain("temperror-cname.example"), &adns.DNSError{})
289 test(domain("host1.example"), true, domain("cname-to-inauthentic.example"), true, list1, domain("host1.example"), nil)
290 test(domain("host1.example"), true, domain("danglingcname.example"), true, list1, domain("host1.example"), nil)
291 test(domain("danglingcname.example"), true, domain("danglingcname.example"), false, nil, domain("danglingcname.example"), nil)
294func TestGatherTLSANames(t *testing.T) {
295 a, b, c, d := domain("nexthop.example"), domain("nexthopexpanded.example"), domain("base.example"), domain("baseexpanded.example")
296 test := func(haveMX, nexthopExpAuth, tlsabaseExpAuth bool, expDoms ...dns.Domain) {
298 doms := GatherTLSANames(haveMX, nexthopExpAuth, tlsabaseExpAuth, a, b, c, d)
299 if !reflect.DeepEqual(doms, expDoms) {
300 t.Fatalf("got domains %v, expected %v", doms, expDoms)
304 test(false, false, false, c)
305 test(false, false, true, d, c)
306 test(true, true, true, d, c, a, b)
307 test(true, true, false, c, a, b)
308 test(true, false, false, a)