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 }