github.com/darmach/terratest@v0.34.8-0.20210517103231-80931f95e3ff/modules/dns-helper/dns_local_server.go (about) 1 package dns_helper 2 3 import ( 4 "fmt" 5 "log" 6 "net" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/miekg/dns" 12 ) 13 14 var testDomain = "gruntwork.io" 15 16 // dnsDatabase stores a collection of DNSQuery with their respective DNSAnswers, to be used by a local dnsTestServer 17 type dnsDatabase map[DNSQuery]DNSAnswers 18 19 // dnsTestServer helper for testing this package using local DNS nameservers with test records 20 type dnsTestServer struct { 21 Server *dns.Server 22 DNSDatabase dnsDatabase 23 DNSDatabaseRetry dnsDatabase 24 } 25 26 // newDNSTestServer returns a new instance of dnsTestServer 27 func newDNSTestServer(server *dns.Server) *dnsTestServer { 28 return &dnsTestServer{Server: server, DNSDatabase: make(dnsDatabase), DNSDatabaseRetry: make(dnsDatabase)} 29 } 30 31 // Address returns the host:port string of the server listener 32 func (s *dnsTestServer) Address() string { 33 return s.Server.PacketConn.LocalAddr().String() 34 } 35 36 // AddEntryToDNSDatabase adds DNSAnswers to the DNSQuery in the database of the server 37 func (s *dnsTestServer) AddEntryToDNSDatabase(q DNSQuery, a DNSAnswers) { 38 s.DNSDatabase[q] = append(s.DNSDatabase[q], a...) 39 } 40 41 // AddEntryToDNSDatabaseRetry adds DNSAnswers to the DNSQuery in the database used when retrying 42 func (s *dnsTestServer) AddEntryToDNSDatabaseRetry(q DNSQuery, a DNSAnswers) { 43 s.DNSDatabaseRetry[q] = append(s.DNSDatabaseRetry[q], a...) 44 } 45 46 // setupTestDNSServers runs and returns 2x local dnsTestServer, initialized with NS records for the testDomain pointing to themselves 47 // it uses a handler that will send replies stored in their internal DNSDatabase 48 func setupTestDNSServers(t *testing.T) (s1, s2 *dnsTestServer) { 49 s1 = runTestDNSServer(t, "0") 50 s2 = runTestDNSServer(t, "0") 51 52 q := DNSQuery{"NS", testDomain} 53 a := DNSAnswers{{"NS", s1.Address() + "."}, {"NS", s2.Address() + "."}} 54 s1.AddEntryToDNSDatabase(q, a) 55 s2.AddEntryToDNSDatabase(q, a) 56 57 s1.Server.Handler.(*dns.ServeMux).HandleFunc(testDomain+".", func(w dns.ResponseWriter, r *dns.Msg) { 58 stdDNSHandler(t, w, r, s1, false) 59 }) 60 s2.Server.Handler.(*dns.ServeMux).HandleFunc(testDomain+".", func(w dns.ResponseWriter, r *dns.Msg) { 61 stdDNSHandler(t, w, r, s2, true) 62 }) 63 64 return s1, s2 65 } 66 67 // setupTestDNSServersRetry runs and returns 2x local dnsTestServer, initialized with NS records for the testDomain pointing to themselves 68 // it uses a handler that will send replies stored in their internal DNSDatabase, and then switch to their DNSDatabaseRetry after some time 69 func setupTestDNSServersRetry(t *testing.T) (s1, s2 *dnsTestServer) { 70 s1 = runTestDNSServer(t, "0") 71 s2 = runTestDNSServer(t, "0") 72 73 q := DNSQuery{"NS", testDomain} 74 a := DNSAnswers{{"NS", s1.Address() + "."}, {"NS", s2.Address() + "."}} 75 s1.AddEntryToDNSDatabase(q, a) 76 s2.AddEntryToDNSDatabase(q, a) 77 s1.AddEntryToDNSDatabaseRetry(q, a) 78 s2.AddEntryToDNSDatabaseRetry(q, a) 79 80 s1.Server.Handler.(*dns.ServeMux).HandleFunc(testDomain+".", func(w dns.ResponseWriter, r *dns.Msg) { 81 retryDNSHandler(t, w, r, s1, false) 82 }) 83 s2.Server.Handler.(*dns.ServeMux).HandleFunc(testDomain+".", func(w dns.ResponseWriter, r *dns.Msg) { 84 retryDNSHandler(t, w, r, s2, true) 85 }) 86 87 return s1, s2 88 } 89 90 // runTestDNSServer starts and returns a new dnsTestServer listening in localhost and the given UDP port 91 func runTestDNSServer(t *testing.T, port string) *dnsTestServer { 92 listener, err := net.ListenPacket("udp", "127.0.0.1:"+port) 93 94 if err != nil { 95 t.Fatal(err) 96 } 97 98 mux := dns.NewServeMux() 99 server := &dns.Server{PacketConn: listener, Net: "udp", Handler: mux} 100 101 go func() { 102 if err := server.ActivateAndServe(); err != nil { 103 log.Printf("Error in local DNS server: %s", err) 104 } 105 }() 106 107 return newDNSTestServer(server) 108 } 109 110 // doDNSAnswer sends replies to the DNS question from client, using the dnsDatabase to lookup the answers to the query 111 // when invertAnswers is true, reverses the order of the answers from the dnsDatabase, useful to simulate realistic nameservers behaviours 112 func doDNSAnswer(t *testing.T, w dns.ResponseWriter, r *dns.Msg, d dnsDatabase, invertAnswers bool) { 113 m := new(dns.Msg) 114 m.SetReply(r) 115 m.Authoritative = true 116 q := m.Question[0] 117 qtype := dns.TypeToString[q.Qtype] 118 answers := d[DNSQuery{qtype, strings.TrimSuffix(q.Name, ".")}] 119 120 var seen = make(map[DNSAnswer]bool) 121 122 for _, r := range answers { 123 if seen[r] { 124 continue 125 } 126 seen[r] = true 127 128 rr, err := dns.NewRR(fmt.Sprintf("%s %s", q.Name, r.String())) 129 130 if err != nil { 131 t.Fatalf("err: %s", err) 132 } 133 134 m.Answer = append(m.Answer, rr) 135 } 136 137 if invertAnswers { 138 for i, j := 0, len(m.Answer)-1; i < j; i, j = i+1, j-1 { 139 m.Answer[i], m.Answer[j] = m.Answer[j], m.Answer[i] 140 } 141 } 142 143 w.WriteMsg(m) 144 } 145 146 // stdDNSHandler uses the internal DNSDatabase to send answers to DNS queries 147 func stdDNSHandler(t *testing.T, w dns.ResponseWriter, r *dns.Msg, s *dnsTestServer, invertAnswers bool) { 148 doDNSAnswer(t, w, r, s.DNSDatabase, invertAnswers) 149 } 150 151 var startTime = time.Now() 152 153 // retryDNSHandler uses the internal DNSDatabase to send answers to DNS queries, and switches 154 // to using the internal DNSDatabaseRetry after 3 seconds from startup 155 func retryDNSHandler(t *testing.T, w dns.ResponseWriter, r *dns.Msg, s *dnsTestServer, invertAnswers bool) { 156 if time.Now().Sub(startTime).Seconds() > 3 { 157 doDNSAnswer(t, w, r, s.DNSDatabaseRetry, invertAnswers) 158 } else { 159 doDNSAnswer(t, w, r, s.DNSDatabase, invertAnswers) 160 } 161 }