Class: Async::DNS::Resolver

Inherits:
Object
  • Object
show all
Defined in:
lib/async/dns/resolver.rb

Overview

Resolve names to addresses using the DNS protocol.

Constant Summary collapse

ADDRESS_RESOURCE_CLASSES =
[Resolv::DNS::Resource::IN::A]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(endpoint, ndots: 1, search: nil, origin: nil, cache: Cache.new, **options) ⇒ Resolver

Servers are specified in the same manor as options, e.g.

[:tcp/:udp, address, port]

In the case of multiple servers, they will be checked in sequence.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/async/dns/resolver.rb', line 37

def initialize(endpoint, ndots: 1, search: nil, origin: nil, cache: Cache.new, **options) 
  @endpoint = endpoint
  
  @ndots = ndots
  if search
    @search = search
  else
    @search = [nil]
  end
  
  if origin
    @search = [origin] + @search
  end
  
  @cache = cache
  @options = options
end

Instance Attribute Details

#searchObject (readonly)

The search domains, which are used to generate fully qualified names if required.



56
57
58
# File 'lib/async/dns/resolver.rb', line 56

def search
  @search
end

Class Method Details

.default(**options) ⇒ Object

The default resolver for the system.



30
31
32
# File 'lib/async/dns/resolver.rb', line 30

def self.default(**options)
  System.resolver(**options)
end

Instance Method Details

#addresses_for(name, resource_classes = ADDRESS_RESOURCE_CLASSES) ⇒ Object

Yields a list of ‘Resolv::IPv4` and `Resolv::IPv6` addresses for the given `name` and `resource_class`. Raises a ResolutionFailure if no severs respond.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/async/dns/resolver.rb', line 131

def addresses_for(name, resource_classes = ADDRESS_RESOURCE_CLASSES)
  records = self.records_for(name, resource_classes)
  
  if records.empty?
    raise ResolutionFailure.new("Could not find any records for #{name.inspect}!")
  end
  
  addresses = []
  
  if records
    records.each do |record|
      if record.respond_to? :address
        addresses << record.address
      else
        # The most common case here is that record.class is IN::CNAME and we need to figure out the address. Usually the upstream DNS server would have replied with this too, and this will be loaded from the response if possible without requesting additional information:
        addresses += addresses_for(record.name, resource_classes)
      end
    end
  end
  
  if addresses.empty?
    raise ResolutionFailure.new("Could not find any addresses for #{name.inspect}!")
  end
  
  return addresses
end

#fully_qualified_names(name) ⇒ Object

Generates a fully qualified name from a given name.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/async/dns/resolver.rb', line 61

def fully_qualified_names(name)
  return to_enum(:fully_qualified_names, name) unless block_given?
  
  name = Resolv::DNS::Name.create(name)
  
  if name.absolute?
    yield name
  else
    if @ndots <= name.length - 1
      yield name
    end
    
    @search.each do |domain|
      yield name.with_origin(domain)
    end
  end
end

#next_id!Object

Provides the next sequence identification number which is used to keep track of DNS messages.



80
81
82
83
# File 'lib/async/dns/resolver.rb', line 80

def next_id!
  # Using sequential numbers for the query ID is generally a bad thing because over UDP they can be spoofed. 16-bits isn't hard to guess either, but over UDP we also use a random port, so this makes effectively 32-bits of entropy to guess per request.
  SecureRandom.random_number(2**16)
end

#query(name, resource_class = Resolv::DNS::Resource::IN::A) ⇒ Object

Query a named resource and return the response.

Bypasses the cache and always makes a new request.



90
91
92
93
94
95
96
97
98
99
100
# File 'lib/async/dns/resolver.rb', line 90

def query(name, resource_class = Resolv::DNS::Resource::IN::A)
  response = nil
  
  self.fully_qualified_names(name) do |fully_qualified_name|
    response = self.dispatch_query(fully_qualified_name, resource_class)
    
    break if response.rcode == Resolv::DNS::RCode::NoError
  end
  
  return response
end

#records_for(name, resource_classes) ⇒ Object

Look up a named resource of the given resource_class.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/async/dns/resolver.rb', line 103

def records_for(name, resource_classes)
  Console.debug(self) {"Looking up records for #{name.inspect} with #{resource_classes.inspect}."}
  resource_classes = Array(resource_classes)
  resources = nil
  
  self.fully_qualified_names(name) do |fully_qualified_name|
    resources = @cache.fetch(fully_qualified_name, resource_classes) do |name, resource_class|
      if response = self.dispatch_query(name, resource_class)
        response.answer.each do |name, ttl, record|
          Console.debug(self) {"Caching record for #{name.inspect} with #{record.class} and TTL #{ttl}."}
          @cache.store(name, resource_class, record)
        end
      end
    end
    
    break if resources.any?
  end
  
  return resources
end