Class: Async::DNS::Resolver

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

Defined Under Namespace

Classes: Request

Constant Summary collapse

DEFAULT_TIMEOUT =

Wait for up to 5 seconds for a response. Override with ‘options`

5.0
DEFAULT_DELAY =

10ms wait between making requests. Override with ‘options`

0.01
DEFAULT_RETRIES =

Try a given request 10 times before failing. Override with ‘options`.

10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(endpoints, origin: nil, logger: Console.logger, timeout: DEFAULT_TIMEOUT) ⇒ 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.



49
50
51
52
53
54
55
# File 'lib/async/dns/resolver.rb', line 49

def initialize(endpoints, origin: nil, logger: Console.logger, timeout: DEFAULT_TIMEOUT)
	@endpoints = endpoints
	
	@origin = origin
	@logger = logger
	@timeout = timeout
end

Instance Attribute Details

#originObject

Returns the value of attribute origin.



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

def origin
  @origin
end

Instance Method Details

#addresses_for(name, resource_class = Resolv::DNS::Resource::IN::A, options = {}) ⇒ 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.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/async/dns/resolver.rb', line 93

def addresses_for(name, resource_class = Resolv::DNS::Resource::IN::A, options = {})
	name = fully_qualified_name(name)
	
	cache = options.fetch(:cache, {})
	retries = options.fetch(:retries, DEFAULT_RETRIES)
	delay = options.fetch(:delay, DEFAULT_DELAY)
	
	records = lookup(name, resource_class, cache) do |lookup_name, lookup_resource_class|
		response = nil
		
		retries.times do |i|
			# Wait 10ms before trying again:
			sleep delay if delay and i > 0
			
			response = query(lookup_name, lookup_resource_class)
			
			break if response
		end
		
		response or raise ResolutionFailure.new("Could not resolve #{name} after #{retries} attempt(s).")
	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, record.class, options.merge(cache: cache))
			end
		end
	end
	
	if addresses.size > 0
		return addresses
	else
		raise ResolutionFailure.new("Could not find any addresses for #{name}.")
	end
end

#dispatch_request(message, task: Async::Task.current) ⇒ Object

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.



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/async/dns/resolver.rb', line 136

def dispatch_request(message, task: Async::Task.current)
	request = Request.new(message, @endpoints)
	
	request.each do |endpoint|
		@logger.debug "[#{message.id}] Sending request #{message.question.inspect} to address #{endpoint.inspect}" if @logger
		
		begin
			response = nil
			
			task.with_timeout(@timeout) do
				@logger.debug "[#{message.id}] -> Try address #{endpoint}" if @logger
				response = try_server(request, endpoint)
				@logger.debug "[#{message.id}] <- Try address #{endpoint} = #{response}" if @logger
			end
			
			if valid_response(message, response)
				return response
			end
		rescue Async::TimeoutError
			@logger.debug "[#{message.id}] Request timed out!" if @logger
		rescue InvalidResponseError
			@logger.warn "[#{message.id}] Invalid response from network: #{$!}!" if @logger
		rescue DecodeError
			@logger.warn "[#{message.id}] Error while decoding data from network: #{$!}!" if @logger
		rescue IOError, Errno::ECONNRESET
			@logger.warn "[#{message.id}] Error while reading from network: #{$!}!" if @logger
		rescue EOFError
			@logger.warn "[#{message.id}] Could not read complete response from network: #{$!}" if @logger
		end
	end
	
	return nil
end

#fully_qualified_name(name) ⇒ Object



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

def fully_qualified_name(name)
	# If we are passed an existing deconstructed name:
	if Resolv::DNS::Name === name
		if name.absolute?
			return name
		else
			return name.with_origin(@origin)
		end
	end
	
	# ..else if we have a string, we need to do some basic processing:
	if name.end_with? '.'
		return Resolv::DNS::Name.create(name)
	else
		return Resolv::DNS::Name.create(name).with_origin(@origin)
	end
end

#next_id!Object

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



78
79
80
81
# File 'lib/async/dns/resolver.rb', line 78

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

Look up a named resource of the given resource_class.



84
85
86
87
88
89
90
# File 'lib/async/dns/resolver.rb', line 84

def query(name, resource_class = Resolv::DNS::Resource::IN::A)
	message = Resolv::DNS::Message.new(next_id!)
	message.rd = 1
	message.add_question fully_qualified_name(name), resource_class
	
	dispatch_request(message)
end