github.com/elfadel/cilium@v1.6.12/pkg/fqdn/lookup.go (about)

     1  // Copyright 2018 Authors of Cilium
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package fqdn
    16  
    17  import (
    18  	"fmt"
    19  	"math"
    20  	"math/rand"
    21  	"net"
    22  	"time"
    23  
    24  	"github.com/miekg/dns"
    25  )
    26  
    27  func init() {
    28  	rand.Seed(time.Now().UnixNano())
    29  }
    30  
    31  // DNSIPRecords mimics the RR data from an A or AAAA response.
    32  // My kingdom for a DNS IP RR type that isn't hidden in the stdlib or has a
    33  // million layers of type indirection.
    34  type DNSIPRecords struct {
    35  	// TTL is the time, in seconds, that these IPs are valid for
    36  	TTL int
    37  
    38  	// IPs are the IPs associated with a DNS Name
    39  	IPs []net.IP
    40  }
    41  
    42  var (
    43  	// dnsConfig is the general config. It must be set via SetDNSConfig otherwise
    44  	// no lookups will actually happen.
    45  	dnsConfig = &dns.ClientConfig{
    46  		Servers: nil,
    47  	}
    48  
    49  	// clientUDP and clientTCP can be reused, and will coalesce multiple queries
    50  	// for the same (Qname, Qtype, Qclass)
    51  	clientUDP, clientTCP *dns.Client
    52  )
    53  
    54  // ConfigFromResolvConf parses the configuration in /etc/resolv.conf and sets
    55  // the configuration for pkg/fqdn.
    56  // nameservers and opt timeout are supported.
    57  // search and ndots are NOT supported.
    58  // This call is not thread safe.
    59  func ConfigFromResolvConf() error {
    60  	dnsConf, err := dns.ClientConfigFromFile("/etc/resolv.conf")
    61  	if err != nil {
    62  		return err
    63  	}
    64  	SetDNSConfig(dnsConf)
    65  
    66  	return nil
    67  }
    68  
    69  // SetDNSConfig store conf in pkg/fqdn as the global config. It also creates
    70  // the global UDP and TCP clients used for DNS lookups in
    71  // DNSLookupDefaultResolver.
    72  // Only .Servers and .Timeout are utilized from conf.
    73  // This call is not thread safe.
    74  func SetDNSConfig(conf *dns.ClientConfig) {
    75  	dnsConfig = conf
    76  
    77  	clientUDP = &dns.Client{
    78  		Net:            "udp",
    79  		Timeout:        time.Duration(dnsConfig.Timeout) * time.Second,
    80  		SingleInflight: true,
    81  	}
    82  
    83  	clientTCP = &dns.Client{
    84  		Net:            "tcp",
    85  		Timeout:        time.Duration(dnsConfig.Timeout) * time.Second,
    86  		SingleInflight: true,
    87  	}
    88  }
    89  
    90  // DNSLookupDefaultResolver sequentially and synchronously runs a DNS lookup
    91  // for every name in dnsNames. It does not use net.DefaultResolver, but
    92  // dnsConfig from this package (populated from resolv.conf). The configured
    93  // servers are always queried in the same order, only moving to the next on
    94  // errors (such as timeouts). The names are queried in random order (as the map
    95  // iteration is random) but, for each name, A records and then AAAA records are
    96  // requested in that order.
    97  // It will return:
    98  // DNSIPs: a map of DNS names to their IPs and associated smallest TTL (only
    99  // contains successful lookups). CNAME records in the response are collapsed
   100  // into the IPs later in the response data. The CNAME TTL is considered when
   101  // finding the smallest TTL.
   102  // DNSErrors: a map of DNS names to lookup errors.
   103  //
   104  // DNSLookupDefaultResolver is used by DNSPoller when no alternative LookupDNSNames is provided.
   105  func DNSLookupDefaultResolver(dnsNames []string) (DNSIPs map[string]*DNSIPRecords, DNSErrors map[string]error) {
   106  	return doResolverLogic(lookup, dnsNames)
   107  }
   108  
   109  // doResolverLogic exists to allow testing the more complex logic around
   110  // collecting A and AAAA records, handling CNAMEs and trying different servers.
   111  func doResolverLogic(lookupFunc func(string, string, uint16) (*dns.Msg, error), dnsNames []string) (DNSIPs map[string]*DNSIPRecords, DNSErrors map[string]error) {
   112  	DNSIPs = make(map[string]*DNSIPRecords)
   113  	DNSErrors = make(map[string]error)
   114  
   115  	// This is the top-level list of names to query
   116  	for _, dnsName := range dnsNames {
   117  		responseData := &DNSIPRecords{TTL: math.MaxInt32}
   118  
   119  		// Query for A & AAAA for each dnsName
   120  	perTypeToQuery:
   121  		for _, dnsType := range []dns.Type{dns.Type(dns.TypeA), dns.Type(dns.TypeAAAA)} { // the dns library doesn't use dns.Type
   122  
   123  			// Try the servers in the order they were configured in resolv.conf
   124  		perServerToAttempt:
   125  			for _, server := range dnsConfig.Servers {
   126  				response, err := lookupFunc(server+":"+dnsConfig.Port, dnsName, uint16(dnsType))
   127  				// Move onto the next server when the response is bad
   128  				switch {
   129  				case err != nil:
   130  					DNSErrors[dnsName] = fmt.Errorf("error when querying %s: %s", server, err)
   131  					continue perServerToAttempt
   132  				case !response.Response:
   133  					continue perServerToAttempt
   134  				case response.Rcode != dns.RcodeSuccess: // e.g. NXDomain or Refused
   135  					// Not an error, but also no data we can use. Move on to the next
   136  					// type. We assume that the servers are not lying to us (i.e. they
   137  					// can all answer the query)
   138  					DNSErrors[dnsName] = fmt.Errorf("no data when querying %s", server)
   139  					continue perTypeToQuery
   140  				}
   141  
   142  				// To arrive here means:
   143  				//  - The server responded without a communication error
   144  				//  - response.Rcode == dns.RcodeSuccess
   145  				delete(DNSErrors, dnsName) // clear any errors we set for other servers
   146  
   147  				for _, answer := range response.Answer {
   148  					switch answer := answer.(type) {
   149  					case *dns.A:
   150  						DNSIPs[dnsName] = responseData // return only when we have an answer
   151  						responseData.IPs = append(responseData.IPs, answer.A)
   152  						responseData.TTL = ttlMin(responseData.TTL, int(answer.Hdr.Ttl))
   153  
   154  					case *dns.AAAA:
   155  						DNSIPs[dnsName] = responseData // return only when we have an answer
   156  						responseData.IPs = append(responseData.IPs, answer.AAAA)
   157  						responseData.TTL = ttlMin(responseData.TTL, int(answer.Hdr.Ttl))
   158  
   159  					case *dns.CNAME:
   160  						// Do we need to enforce any policy on this?
   161  						// Responses with CNAMEs from recursive resolvers will have IPs
   162  						// included, and we will return those as the IPs for dnsName.
   163  						// We still track the TTL because the lowest TTL in the chain
   164  						// determines the valid caching time for the whole response.
   165  						responseData.TTL = ttlMin(responseData.TTL, int(answer.Hdr.Ttl))
   166  
   167  					// Treat an inappropriate response like no response, and try another
   168  					// server
   169  					default:
   170  						DNSErrors[dnsName] = fmt.Errorf("unexpected DNS Resource Records(%T) in response from %s: %s", answer, server, err)
   171  						continue perServerToAttempt
   172  					}
   173  				}
   174  
   175  				// We have a valid response, stop trying queryNames or other servers.
   176  				continue perTypeToQuery
   177  			}
   178  		}
   179  	}
   180  
   181  	return DNSIPs, DNSErrors
   182  }
   183  
   184  // lookup sends a single DNS lookup to server for name/dnsType.
   185  // It uses the global clients and their configured timeouts and retries.
   186  func lookup(server string, name string, dnsType uint16) (response *dns.Msg, err error) {
   187  	name = dns.Fqdn(name)
   188  	m := &dns.Msg{}
   189  	m.SetQuestion(name, dnsType)
   190  	m.Rcode = dns.OpcodeQuery
   191  	m.RecursionDesired = true
   192  
   193  	response, _, err = clientUDP.Exchange(m, server)
   194  	if err == nil && m.Response && !m.Truncated {
   195  		return response, nil
   196  	}
   197  
   198  	// Try TCP on error, a truncated response, or some bogus non-response
   199  	response, _, err = clientTCP.Exchange(m, server)
   200  	return response, err
   201  }
   202  
   203  // ttlMin returns the lower of i and j, or the one that is not 0.
   204  func ttlMin(i, j int) int {
   205  	if i < j {
   206  		return i
   207  	}
   208  	return j
   209  }