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  }