class Resolver
Resolve names to addresses using the DNS protocol.
Nested
Definitions
def self.default(**options)
The default resolver for the system.
Implementation
def self.default(**options)
System.resolver(**options)
end
def initialize(endpoint, timeout: nil, ndots: 1, search: nil, origin: nil, cache: Cache.new, **options)
Servers are specified in the same manor as options[:listen], e.g. [:tcp/:udp, address, port] In the case of multiple servers, they will be checked in sequence.
Implementation
def initialize(endpoint, timeout: nil, ndots: 1, search: nil, origin: nil, cache: Cache.new, **options)
@endpoint = endpoint
# Legacy support for multiple endpoints:
if @endpoint.is_a?(Array)
warn "Using Array specifications for endpoint is deprecated. Please use IO::Endpoint::CompositeEndpoint instead.", uplevel: 1
endpoints = @endpoint.map do |specification|
::IO::Endpoint.public_send(specification[0], *specification[1..-1])
end
@endpoint = ::IO::Endpoint.composite(*endpoints)
end
if timeout
@endpoint = @endpoint.with(timeout: timeout)
end
@ndots = ndots
if search
@search = search
else
@search = [nil]
end
if origin
@search = [origin] + @search
end
@cache = cache
@options = options
@count = 0
end
attr :search
The search domains, which are used to generate fully qualified names if required.
def fully_qualified_names(name)
Generates a fully qualified name from a given name.
Signature
-
parameter
name
String | Resolv::DNS::Name
The name to fully qualify.
Implementation
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
def next_id!
Provides the next sequence identification number which is used to keep track of DNS messages.
Implementation
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
def query(name, resource_class = Resolv::DNS::Resource::IN::A)
Query a named resource and return the response.
Bypasses the cache and always makes a new request.
Signature
-
returns
Resolv::DNS::Message
The response from the server.
Implementation
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
def records_for(name, resource_classes)
Look up a named resource of the given resource_class.
Implementation
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
def addresses_for(name, resource_classes = ADDRESS_RESOURCE_CLASSES)
Yields a list of Resolv::IPv4
and Resolv::IPv6
addresses for the given name
and resource_class
. Raises a ResolutionFailure if no severs respond.
Implementation
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
def dispatch_query(name, resource_class)
In general, DNS servers are only able to handle a single question at a time. This method is used to dispatch a single query to the server and wait for a response.
Implementation
def dispatch_query(name, resource_class)
message = Resolv::DNS::Message.new(self.next_id!)
message.rd = 1
message.add_question(name, resource_class)
return dispatch_request(message)
end
def dispatch_request(message)
Send the message to available servers. If no servers respond correctly, nil is returned. This result indicates a failure of the resolver to correctly contact any server and get a valid response.
Implementation
def dispatch_request(message)
request = Request.new(message, @endpoint)
error = nil
request.each do |endpoint|
Console.debug "[#{message.id}] Sending request #{message.question.inspect} to address #{endpoint.inspect}"
begin
response = try_server(request, endpoint)
if valid_response(message, response)
return response
end
rescue => error
# Try the next server.
end
end
if error
raise error
end
return nil
end