Async::DNS SourceAsyncDNSResolver

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