github.com/thanos-io/thanos@v0.32.5/pkg/discovery/dns/miekgdns/lookup.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package miekgdns 5 6 import ( 7 "bytes" 8 "net" 9 10 "github.com/miekg/dns" 11 "github.com/pkg/errors" 12 ) 13 14 var ErrNoSuchHost = errors.New("no such host") 15 16 // Copied and slightly adjusted from Prometheus DNS SD: 17 // https://github.com/prometheus/prometheus/blob/be3c082539d85908ce03b6d280f83343e7c930eb/discovery/dns/dns.go#L212 18 19 // lookupWithSearchPath tries to get an answer for various permutations of 20 // the given name, appending the system-configured search path as necessary. 21 // 22 // There are three possible outcomes: 23 // 24 // 1. One of the permutations of the given name is recognized as 25 // "valid" by the DNS, in which case we consider ourselves "done" 26 // and that answer is returned. Note that, due to the way the DNS 27 // handles "name has resource records, but none of the specified type", 28 // the answer received may have an empty set of results. 29 // 30 // 2. All of the permutations of the given name are responded to by one of 31 // the servers in the "nameservers" list with the answer "that name does 32 // not exist" (NXDOMAIN). In that case, it can be considered 33 // pseudo-authoritative that there are no records for that name. 34 // 35 // 3. One or more of the names was responded to by all servers with some 36 // sort of error indication. In that case, we can't know if, in fact, 37 // there are records for the name or not, so whatever state the 38 // configuration is in, we should keep it that way until we know for 39 // sure (by, presumably, all the names getting answers in the future). 40 // 41 // Outcomes 1 and 2 are indicated by a valid response message (possibly an 42 // empty one) and no error. Outcome 3 is indicated by an error return. The 43 // error will be generic-looking, because trying to return all the errors 44 // returned by the combination of all name permutations and servers is a 45 // nightmare. 46 func (r *Resolver) lookupWithSearchPath(name string, qtype dns.Type) (*dns.Msg, error) { 47 conf, err := dns.ClientConfigFromFile(r.ResolvConf) 48 if err != nil { 49 return nil, errors.Wrapf(err, "could not load resolv.conf: %s", err) 50 } 51 52 var errs []error 53 for _, lname := range conf.NameList(name) { 54 response, err := lookupFromAnyServer(lname, qtype, conf) 55 if err != nil { 56 // We can't go home yet, because a later name 57 // may give us a valid, successful answer. However 58 // we can no longer say "this name definitely doesn't 59 // exist", because we did not get that answer for 60 // at least one name. 61 errs = append(errs, err) 62 continue 63 } 64 65 if response.Rcode == dns.RcodeSuccess { 66 // Outcome 1: GOLD! 67 return response, nil 68 } 69 } 70 71 if len(errs) == 0 { 72 // Outcome 2: everyone says NXDOMAIN. 73 return &dns.Msg{}, ErrNoSuchHost 74 } 75 // Outcome 3: boned. 76 return nil, errors.Errorf("could not resolve %q: all servers responded with errors to at least one search domain. Errs %s", name, fmtErrs(errs)) 77 } 78 79 // lookupFromAnyServer uses all configured servers to try and resolve a specific 80 // name. If a viable answer is received from a server, then it is 81 // immediately returned, otherwise the other servers in the config are 82 // tried, and if none of them return a viable answer, an error is returned. 83 // 84 // A "viable answer" is one which indicates either: 85 // 86 // 1. "yes, I know that name, and here are its records of the requested type" 87 // (RCODE==SUCCESS, ANCOUNT > 0); 88 // 2. "yes, I know that name, but it has no records of the requested type" 89 // (RCODE==SUCCESS, ANCOUNT==0); or 90 // 3. "I know that name doesn't exist" (RCODE==NXDOMAIN). 91 // 92 // A non-viable answer is "anything else", which encompasses both various 93 // system-level problems (like network timeouts) and also 94 // valid-but-unexpected DNS responses (SERVFAIL, REFUSED, etc). 95 func lookupFromAnyServer(name string, qtype dns.Type, conf *dns.ClientConfig) (*dns.Msg, error) { 96 client := &dns.Client{} 97 98 var errs []error 99 100 // TODO(bwplotka): Worth to do fanout and grab fastest as golang native lib? 101 for _, server := range conf.Servers { 102 servAddr := net.JoinHostPort(server, conf.Port) 103 msg, err := askServerForName(name, qtype, client, servAddr, true) 104 if err != nil { 105 errs = append(errs, errors.Wrapf(err, "resolution against server %s for %s", server, name)) 106 continue 107 } 108 109 if msg.Rcode == dns.RcodeSuccess || msg.Rcode == dns.RcodeNameError { 110 return msg, nil 111 } 112 } 113 114 return nil, errors.Errorf("could not resolve %s: no servers returned a viable answer. Errs %v", name, fmtErrs(errs)) 115 } 116 117 func fmtErrs(errs []error) string { 118 b := bytes.Buffer{} 119 for _, err := range errs { 120 b.WriteString(";") 121 b.WriteString(err.Error()) 122 } 123 return b.String() 124 } 125 126 // askServerForName makes a request to a specific DNS server for a specific 127 // name (and qtype). Retries with TCP in the event of response truncation, 128 // but otherwise just sends back whatever the server gave, whether that be a 129 // valid-looking response, or an error. 130 func askServerForName(name string, qType dns.Type, client *dns.Client, servAddr string, edns bool) (*dns.Msg, error) { 131 msg := &dns.Msg{} 132 133 msg.SetQuestion(dns.Fqdn(name), uint16(qType)) 134 if edns { 135 msg.SetEdns0(dns.DefaultMsgSize, false) 136 } 137 138 response, _, err := client.Exchange(msg, servAddr) 139 if err != nil { 140 return nil, errors.Wrapf(err, "exchange") 141 } 142 143 if response.Truncated { 144 if client.Net == "tcp" { 145 return nil, errors.New("got truncated message on TCP (64kiB limit exceeded?)") 146 } 147 148 // TCP fallback. 149 client.Net = "tcp" 150 return askServerForName(name, qType, client, servAddr, false) 151 } 152 153 return response, nil 154 }