// Copyright 2015 ISRG. All rights reserved // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package core import ( "fmt" "net" "os" "strings" "testing" "time" "github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns" ) const dnsLoopbackAddr = "127.0.0.1:4053" func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) { defer w.Close() m := new(dns.Msg) m.SetReply(r) m.Compress = false appendAnswer := func(rr dns.RR) { m.Answer = append(m.Answer, rr) } for _, q := range r.Question { q.Name = strings.ToLower(q.Name) if q.Name == "servfail.com." { m.Rcode = dns.RcodeServerFailure break } switch q.Qtype { case dns.TypeSOA: record := new(dns.SOA) record.Hdr = dns.RR_Header{Name: "letsencrypt.org.", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 0} record.Ns = "ns.letsencrypt.org." record.Mbox = "master.letsencrypt.org." record.Serial = 1 record.Refresh = 1 record.Retry = 1 record.Expire = 1 record.Minttl = 1 appendAnswer(record) case dns.TypeAAAA: if q.Name == "v6.letsencrypt.org." { record := new(dns.AAAA) record.Hdr = dns.RR_Header{Name: "v6.letsencrypt.org.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 0} record.AAAA = net.ParseIP("::1") appendAnswer(record) } case dns.TypeA: if q.Name == "cps.letsencrypt.org." { record := new(dns.A) record.Hdr = dns.RR_Header{Name: "cps.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0} record.A = net.ParseIP("127.0.0.1") appendAnswer(record) } case dns.TypeCNAME: if q.Name == "cname.letsencrypt.org." { record := new(dns.CNAME) record.Hdr = dns.RR_Header{Name: "cname.letsencrypt.org.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 30} record.Target = "cps.letsencrypt.org." appendAnswer(record) } if q.Name == "cname.example.com." { record := new(dns.CNAME) record.Hdr = dns.RR_Header{Name: "cname.example.com.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 30} record.Target = "CAA.example.com." appendAnswer(record) } case dns.TypeDNAME: if q.Name == "dname.letsencrypt.org." { record := new(dns.DNAME) record.Hdr = dns.RR_Header{Name: "dname.letsencrypt.org.", Rrtype: dns.TypeDNAME, Class: dns.ClassINET, Ttl: 30} record.Target = "cps.letsencrypt.org." appendAnswer(record) } case dns.TypeCAA: if q.Name == "bracewel.net." || q.Name == "caa.example.com." { record := new(dns.CAA) record.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeCAA, Class: dns.ClassINET, Ttl: 0} record.Tag = "issue" record.Value = "letsencrypt.org" record.Flag = 1 appendAnswer(record) } if q.Name == "cname.example.com." { record := new(dns.CAA) record.Hdr = dns.RR_Header{Name: "caa.example.com.", Rrtype: dns.TypeCAA, Class: dns.ClassINET, Ttl: 0} record.Tag = "issue" record.Value = "letsencrypt.org" record.Flag = 1 appendAnswer(record) } case dns.TypeTXT: if q.Name == "split-txt.letsencrypt.org." { record := new(dns.TXT) record.Hdr = dns.RR_Header{Name: "split-txt.letsencrypt.org.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0} record.Txt = []string{"a", "b", "c"} appendAnswer(record) } } } w.WriteMsg(m) return } func serveLoopResolver(stopChan chan bool) chan bool { dns.HandleFunc(".", mockDNSQuery) server := &dns.Server{Addr: dnsLoopbackAddr, Net: "udp", ReadTimeout: time.Millisecond, WriteTimeout: time.Millisecond} waitChan := make(chan bool, 1) go func() { waitChan <- true err := server.ListenAndServe() if err != nil { fmt.Println(err) return } }() go func() { <-stopChan err := server.Shutdown() if err != nil { fmt.Println(err) } }() return waitChan } func TestMain(m *testing.M) { stop := make(chan bool, 1) wait := serveLoopResolver(stop) <-wait ret := m.Run() stop <- true os.Exit(ret) } func TestDNSNoServers(t *testing.T) { obj := NewTestDNSResolverImpl(time.Hour, []string{}) _, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeA) test.AssertError(t, err, "No servers") } func TestDNSOneServer(t *testing.T) { obj := NewTestDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr}) _, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeSOA) test.AssertNotError(t, err, "No message") } func TestDNSDuplicateServers(t *testing.T) { obj := NewTestDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr, dnsLoopbackAddr}) _, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeSOA) test.AssertNotError(t, err, "No message") } func TestDNSLookupsNoServer(t *testing.T) { obj := NewTestDNSResolverImpl(time.Second*10, []string{}) _, _, err := obj.LookupTXT("letsencrypt.org") test.AssertError(t, err, "No servers") _, _, err = obj.LookupHost("letsencrypt.org") test.AssertError(t, err, "No servers") _, _, err = obj.LookupCAA("letsencrypt.org") test.AssertError(t, err, "No servers") } func TestDNSServFail(t *testing.T) { obj := NewTestDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr}) bad := "servfail.com" _, _, err := obj.LookupTXT(bad) test.AssertError(t, err, "LookupTXT didn't return an error") _, _, err = obj.LookupHost(bad) test.AssertError(t, err, "LookupHost didn't return an error") // CAA lookup ignores validation failures from the resolver for now // and returns an empty list of CAA records. emptyCaa, _, err := obj.LookupCAA(bad) test.Assert(t, len(emptyCaa) == 0, "Query returned non-empty list of CAA records") test.AssertNotError(t, err, "LookupCAA returned an error") } func TestDNSLookupTXT(t *testing.T) { obj := NewTestDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr}) a, rtt, err := obj.LookupTXT("letsencrypt.org") t.Logf("A: %v RTT %s", a, rtt) test.AssertNotError(t, err, "No message") a, rtt, err = obj.LookupTXT("split-txt.letsencrypt.org") t.Logf("A: %v RTT %s", a, rtt) test.AssertNotError(t, err, "No message") test.AssertEquals(t, len(a), 1) test.AssertEquals(t, a[0], "abc") } func TestDNSLookupHost(t *testing.T) { obj := NewTestDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr}) ip, _, err := obj.LookupHost("servfail.com") t.Logf("servfail.com - IP: %s, Err: %s", ip, err) test.AssertError(t, err, "Server failure") test.Assert(t, len(ip) == 0, "Should not have IPs") ip, _, err = obj.LookupHost("nonexistent.letsencrypt.org") t.Logf("nonexistent.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to not exist") test.Assert(t, len(ip) == 0, "Should not have IPs") // Single IPv4 address ip, _, err = obj.LookupHost("cps.letsencrypt.org") t.Logf("cps.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to exist") test.Assert(t, len(ip) == 1, "Should have IP") ip, _, err = obj.LookupHost("cps.letsencrypt.org") t.Logf("cps.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to exist") test.Assert(t, len(ip) == 1, "Should have IP") // No IPv6 ip, _, err = obj.LookupHost("v6.letsencrypt.org") t.Logf("v6.letsencrypt.org - IP: %s, Err: %s", ip, err) test.AssertNotError(t, err, "Not an error to exist") test.Assert(t, len(ip) == 0, "Should not have IPs") } func TestDNSLookupCAA(t *testing.T) { obj := NewTestDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr}) caas, _, err := obj.LookupCAA("bracewel.net") test.AssertNotError(t, err, "CAA lookup failed") test.Assert(t, len(caas) > 0, "Should have CAA records") caas, _, err = obj.LookupCAA("nonexistent.letsencrypt.org") test.AssertNotError(t, err, "CAA lookup failed") test.Assert(t, len(caas) == 0, "Shouldn't have CAA records") caas, _, err = obj.LookupCAA("cname.example.com") test.AssertNotError(t, err, "CAA lookup failed") test.Assert(t, len(caas) > 0, "Should follow CNAME to find CAA") }