github.com/darmach/terratest@v0.34.8-0.20210517103231-80931f95e3ff/modules/dns-helper/dns_helper.go (about) 1 // Package dns_helper contains helpers to interact with the Domain Name System. 2 package dns_helper 3 4 import ( 5 "fmt" 6 "net" 7 "reflect" 8 "sort" 9 "strings" 10 "time" 11 12 "github.com/gruntwork-io/terratest/modules/logger" 13 "github.com/gruntwork-io/terratest/modules/retry" 14 "github.com/gruntwork-io/terratest/modules/testing" 15 "github.com/miekg/dns" 16 "github.com/stretchr/testify/require" 17 ) 18 19 // DNSFindNameservers tries to find the NS record for the given FQDN, iterating down the domain hierarchy 20 // until it founds the NS records and returns it. Fails if there's any error or no NS record is found up to the apex domain. 21 func DNSFindNameservers(t testing.TestingT, fqdn string, resolvers []string) []string { 22 nameservers, err := DNSFindNameserversE(t, fqdn, resolvers) 23 require.NoError(t, err) 24 return nameservers 25 } 26 27 // DNSFindNameserversE tries to find the NS record for the given FQDN, iterating down the domain hierarchy 28 // until it founds the NS records and returns it. Returns the last error if the apex domain is reached with no result. 29 func DNSFindNameserversE(t testing.TestingT, fqdn string, resolvers []string) ([]string, error) { 30 var lookupFunc func(domain string) ([]string, error) 31 32 if resolvers == nil { 33 lookupFunc = func(domain string) ([]string, error) { 34 var nameservers []string 35 res, err := net.LookupNS(domain) 36 for _, ns := range res { 37 nameservers = append(nameservers, ns.Host) 38 } 39 return nameservers, err 40 } 41 } else { 42 lookupFunc = func(domain string) ([]string, error) { 43 var nameservers []string 44 res, err := DNSLookupE(t, DNSQuery{"NS", domain}, resolvers) 45 for _, r := range res { 46 if r.Type == "NS" { 47 nameservers = append(nameservers, r.Value) 48 } 49 } 50 return nameservers, err 51 } 52 } 53 54 parts := strings.Split(fqdn, ".") 55 56 var domain string 57 for i := range parts[:len(parts)-1] { 58 domain = strings.Join(parts[i:], ".") 59 res, err := lookupFunc(domain) 60 61 if len(res) > 0 { 62 var nameservers []string 63 64 for _, ns := range res { 65 nameservers = append(nameservers, strings.TrimSuffix(ns, ".")) 66 } 67 68 logger.Logf(t, "FQDN %s belongs to domain %s, found NS record: %s", fqdn, domain, nameservers) 69 return nameservers, nil 70 } 71 72 if err != nil { 73 logger.Logf(t, err.Error()) 74 } 75 } 76 77 err := &NSNotFoundError{fqdn, domain} 78 return nil, err 79 } 80 81 // DNSLookupAuthoritative gets authoritative answers for the specified record and type. 82 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 83 // Fails on any error from DNSLookupAuthoritativeE. 84 func DNSLookupAuthoritative(t testing.TestingT, query DNSQuery, resolvers []string) DNSAnswers { 85 res, err := DNSLookupAuthoritativeE(t, query, resolvers) 86 require.NoError(t, err) 87 return res 88 } 89 90 // DNSLookupAuthoritativeE gets authoritative answers for the specified record and type. 91 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 92 // Returns NotFoundError when no answer found in any authoritative nameserver. 93 // Returns any underlying error from individual lookups. 94 func DNSLookupAuthoritativeE(t testing.TestingT, query DNSQuery, resolvers []string) (DNSAnswers, error) { 95 nameservers, err := DNSFindNameserversE(t, query.Name, resolvers) 96 97 if err != nil { 98 return nil, err 99 } 100 101 return DNSLookupE(t, query, nameservers) 102 } 103 104 // DNSLookupAuthoritativeWithRetry repeatedly gets authoritative answers for the specified record and type 105 // until ANY of the authoritative nameservers found replies with non-empty answer matching the expectedAnswers, 106 // or until max retries has been exceeded. 107 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 108 // Fails on any error from DNSLookupAuthoritativeWithRetryE. 109 func DNSLookupAuthoritativeWithRetry(t testing.TestingT, query DNSQuery, resolvers []string, maxRetries int, sleepBetweenRetries time.Duration) DNSAnswers { 110 res, err := DNSLookupAuthoritativeWithRetryE(t, query, resolvers, maxRetries, sleepBetweenRetries) 111 require.NoError(t, err) 112 return res 113 } 114 115 // DNSLookupAuthoritativeWithRetryE repeatedly gets authoritative answers for the specified record and type 116 // until ANY of the authoritative nameservers found replies with non-empty answer matching the expectedAnswers, 117 // or until max retries has been exceeded. 118 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 119 func DNSLookupAuthoritativeWithRetryE(t testing.TestingT, query DNSQuery, resolvers []string, maxRetries int, sleepBetweenRetries time.Duration) (DNSAnswers, error) { 120 res, err := retry.DoWithRetryInterfaceE( 121 t, fmt.Sprintf("DNSLookupAuthoritativeE %s record for %s using authoritative nameservers", query.Type, query.Name), 122 maxRetries, sleepBetweenRetries, 123 func() (interface{}, error) { 124 return DNSLookupAuthoritativeE(t, query, resolvers) 125 }) 126 127 return res.(DNSAnswers), err 128 } 129 130 // DNSLookupAuthoritativeAll gets authoritative answers for the specified record and type. 131 // All the authoritative nameservers found must give the same answers. 132 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 133 // Fails on any error from DNSLookupAuthoritativeAllE. 134 func DNSLookupAuthoritativeAll(t testing.TestingT, query DNSQuery, resolvers []string) DNSAnswers { 135 res, err := DNSLookupAuthoritativeAllE(t, query, resolvers) 136 require.NoError(t, err) 137 return res 138 } 139 140 // DNSLookupAuthoritativeAllE gets authoritative answers for the specified record and type. 141 // All the authoritative nameservers found must give the same answers. 142 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 143 // Returns InconsistentAuthoritativeError when any authoritative nameserver gives a different answer. 144 // Returns any underlying error. 145 func DNSLookupAuthoritativeAllE(t testing.TestingT, query DNSQuery, resolvers []string) (DNSAnswers, error) { 146 nameservers, err := DNSFindNameserversE(t, query.Name, resolvers) 147 148 if err != nil { 149 return nil, err 150 } 151 152 var answers DNSAnswers 153 154 for _, ns := range nameservers { 155 res, err := DNSLookupE(t, query, []string{ns}) 156 157 if err != nil { 158 return nil, err 159 } 160 161 if len(answers) > 0 { 162 if !reflect.DeepEqual(answers, res) { 163 err := &InconsistentAuthoritativeError{Query: query, Answers: res, Nameserver: ns, PreviousAnswers: answers} 164 return nil, err 165 } 166 } else { 167 answers = res 168 } 169 } 170 171 return answers, nil 172 } 173 174 // DNSLookupAuthoritativeAllWithRetry repeatedly sends DNS requests for the specified record and type, 175 // until ALL authoritative nameservers reply with the exact same non-empty answers or until max retries has been exceeded. 176 // If defined, uses the given resolvers instead of the default system ones to find the authoritative nameservers. 177 // Fails when max retries has been exceeded. 178 func DNSLookupAuthoritativeAllWithRetry(t testing.TestingT, query DNSQuery, resolvers []string, maxRetries int, sleepBetweenRetries time.Duration) { 179 _, err := DNSLookupAuthoritativeAllWithRetryE(t, query, resolvers, maxRetries, sleepBetweenRetries) 180 require.NoError(t, err) 181 } 182 183 // DNSLookupAuthoritativeAllWithRetryE repeatedly sends DNS requests for the specified record and type, 184 // until ALL authoritative nameservers reply with the exact same non-empty answers or until max retries has been exceeded. 185 // If defined, uses the given resolvers instead of the default system ones to find the authoritative nameservers. 186 func DNSLookupAuthoritativeAllWithRetryE(t testing.TestingT, query DNSQuery, resolvers []string, maxRetries int, sleepBetweenRetries time.Duration) (DNSAnswers, error) { 187 res, err := retry.DoWithRetryInterfaceE( 188 t, fmt.Sprintf("DNSLookupAuthoritativeAllE %s record for %s using authoritative nameservers", query.Type, query.Name), 189 maxRetries, sleepBetweenRetries, 190 func() (interface{}, error) { 191 return DNSLookupAuthoritativeAllE(t, query, resolvers) 192 }) 193 194 return res.(DNSAnswers), err 195 } 196 197 // DNSLookupAuthoritativeAllWithValidation gets authoritative answers for the specified record and type. 198 // All the authoritative nameservers found must give the same answers and match the expectedAnswers. 199 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 200 // Fails on any underlying error from DNSLookupAuthoritativeAllWithValidationE. 201 func DNSLookupAuthoritativeAllWithValidation(t testing.TestingT, query DNSQuery, resolvers []string, expectedAnswers DNSAnswers) { 202 err := DNSLookupAuthoritativeAllWithValidationE(t, query, resolvers, expectedAnswers) 203 require.NoError(t, err) 204 } 205 206 // DNSLookupAuthoritativeAllWithValidationE gets authoritative answers for the specified record and type. 207 // All the authoritative nameservers found must give the same answers and match the expectedAnswers. 208 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 209 // Returns ValidationError when expectedAnswers differ from the obtained ones. 210 // Returns any underlying error from DNSLookupAuthoritativeAllE. 211 func DNSLookupAuthoritativeAllWithValidationE(t testing.TestingT, query DNSQuery, resolvers []string, expectedAnswers DNSAnswers) error { 212 expectedAnswers.Sort() 213 214 answers, err := DNSLookupAuthoritativeAllE(t, query, resolvers) 215 216 if err != nil { 217 return err 218 } 219 220 if !reflect.DeepEqual(answers, expectedAnswers) { 221 err := &ValidationError{Query: query, Answers: answers, ExpectedAnswers: expectedAnswers} 222 return err 223 } 224 225 return nil 226 } 227 228 // DNSLookupAuthoritativeAllWithValidationRetry repeatedly gets authoritative answers for the specified record and type 229 // until ALL the authoritative nameservers found give the same answers and match the expectedAnswers, 230 // or until max retries has been exceeded. 231 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 232 // Fails when max retries has been exceeded. 233 func DNSLookupAuthoritativeAllWithValidationRetry(t testing.TestingT, query DNSQuery, resolvers []string, expectedAnswers DNSAnswers, maxRetries int, sleepBetweenRetries time.Duration) { 234 err := DNSLookupAuthoritativeAllWithValidationRetryE(t, query, resolvers, expectedAnswers, maxRetries, sleepBetweenRetries) 235 require.NoError(t, err) 236 } 237 238 // DNSLookupAuthoritativeAllWithValidationRetryE repeatedly gets authoritative answers for the specified record and type 239 // until ALL the authoritative nameservers found give the same answers and match the expectedAnswers, 240 // or until max retries has been exceeded. 241 // If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers. 242 func DNSLookupAuthoritativeAllWithValidationRetryE(t testing.TestingT, query DNSQuery, resolvers []string, expectedAnswers DNSAnswers, maxRetries int, sleepBetweenRetries time.Duration) error { 243 _, err := retry.DoWithRetryInterfaceE( 244 t, fmt.Sprintf("DNSLookupAuthoritativeAllWithValidationRetryE %s record for %s using authoritative nameservers", query.Type, query.Name), 245 maxRetries, sleepBetweenRetries, 246 func() (interface{}, error) { 247 return nil, DNSLookupAuthoritativeAllWithValidationE(t, query, resolvers, expectedAnswers) 248 }) 249 250 return err 251 } 252 253 // DNSLookup sends a DNS query for the specified record and type using the given resolvers. 254 // Fails on any error. 255 // Supported record types: A, AAAA, CNAME, MX, NS, TXT 256 func DNSLookup(t testing.TestingT, query DNSQuery, resolvers []string) DNSAnswers { 257 res, err := DNSLookupE(t, query, resolvers) 258 require.NoError(t, err) 259 return res 260 } 261 262 // DNSLookupE sends a DNS query for the specified record and type using the given resolvers. 263 // Returns QueryTypeError when record type is not supported. 264 // Returns any underlying error. 265 // Supported record types: A, AAAA, CNAME, MX, NS, TXT 266 func DNSLookupE(t testing.TestingT, query DNSQuery, resolvers []string) (DNSAnswers, error) { 267 if len(resolvers) == 0 { 268 err := &NoResolversError{} 269 return nil, err 270 } 271 272 var dnsAnswers DNSAnswers 273 var err error 274 for _, resolver := range resolvers { 275 dnsAnswers, err = dnsLookup(t, query, resolver) 276 277 if err == nil { 278 return dnsAnswers, nil 279 } 280 } 281 282 return nil, err 283 } 284 285 // dnsLookup sends a DNS query for the specified record and type using the given resolver. 286 // Returns DNSAnswers to the DNSQuery. 287 // If no records found, returns NotFoundError. 288 func dnsLookup(t testing.TestingT, query DNSQuery, resolver string) (DNSAnswers, error) { 289 switch query.Type { 290 case "A", "AAAA", "CNAME", "MX", "NS", "TXT": 291 default: 292 err := &QueryTypeError{query.Type} 293 return nil, err 294 } 295 296 qType, ok := dns.StringToType[strings.ToUpper(query.Type)] 297 if !ok { 298 err := &QueryTypeError{query.Type} 299 return nil, err 300 } 301 302 if strings.LastIndex(resolver, ":") <= strings.LastIndex(resolver, "]") { 303 resolver += ":53" 304 } 305 306 c := new(dns.Client) 307 m := new(dns.Msg) 308 m.SetQuestion(dns.Fqdn(query.Name), qType) 309 310 in, _, err := c.Exchange(m, resolver) 311 if err != nil { 312 logger.Logf(t, "Error sending DNS query %s: %s", query, err) 313 return nil, err 314 } 315 316 if len(in.Answer) == 0 { 317 err := &NotFoundError{query, resolver} 318 return nil, err 319 } 320 321 var dnsAnswers DNSAnswers 322 323 for _, a := range in.Answer { 324 switch at := a.(type) { 325 case *dns.A: 326 dnsAnswers = append(dnsAnswers, DNSAnswer{"A", at.A.String()}) 327 case *dns.AAAA: 328 dnsAnswers = append(dnsAnswers, DNSAnswer{"AAAA", at.AAAA.String()}) 329 case *dns.CNAME: 330 dnsAnswers = append(dnsAnswers, DNSAnswer{"CNAME", at.Target}) 331 case *dns.NS: 332 dnsAnswers = append(dnsAnswers, DNSAnswer{"NS", at.Ns}) 333 case *dns.MX: 334 dnsAnswers = append(dnsAnswers, DNSAnswer{"MX", fmt.Sprintf("%d %s", at.Preference, at.Mx)}) 335 case *dns.TXT: 336 for _, txt := range at.Txt { 337 dnsAnswers = append(dnsAnswers, DNSAnswer{"TXT", fmt.Sprintf(`"%s"`, txt)}) 338 } 339 } 340 } 341 342 dnsAnswers.Sort() 343 344 return dnsAnswers, nil 345 } 346 347 // DNSQuery type 348 type DNSQuery struct { 349 Type, Name string 350 } 351 352 // DNSAnswer type 353 type DNSAnswer struct { 354 Type, Value string 355 } 356 357 func (a DNSAnswer) String() string { 358 return fmt.Sprintf("%s %s", a.Type, a.Value) 359 } 360 361 // DNSAnswers type 362 type DNSAnswers []DNSAnswer 363 364 // Sort sorts the answers by type and value 365 func (a DNSAnswers) Sort() { 366 sort.Slice(a, func(i, j int) bool { 367 return a[i].Type < a[j].Type || a[i].Value < a[j].Value 368 }) 369 }