async-dns 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.travis.yml +18 -0
  4. data/Gemfile +14 -0
  5. data/README.md +144 -0
  6. data/Rakefile +32 -0
  7. data/async-dns.gemspec +31 -0
  8. data/lib/async/dns.rb +36 -0
  9. data/lib/async/dns/chunked.rb +34 -0
  10. data/lib/async/dns/extensions/resolv.rb +136 -0
  11. data/lib/async/dns/extensions/string.rb +28 -0
  12. data/lib/async/dns/handler.rb +229 -0
  13. data/lib/async/dns/logger.rb +31 -0
  14. data/lib/async/dns/message.rb +75 -0
  15. data/lib/async/dns/replace.rb +54 -0
  16. data/lib/async/dns/resolver.rb +280 -0
  17. data/lib/async/dns/server.rb +154 -0
  18. data/lib/async/dns/system.rb +146 -0
  19. data/lib/async/dns/transaction.rb +202 -0
  20. data/lib/async/dns/transport.rb +78 -0
  21. data/lib/async/dns/version.rb +25 -0
  22. data/spec/async/dns/handler_spec.rb +58 -0
  23. data/spec/async/dns/hosts.txt +2 -0
  24. data/spec/async/dns/ipv6_spec.rb +78 -0
  25. data/spec/async/dns/message_spec.rb +56 -0
  26. data/spec/async/dns/origin_spec.rb +106 -0
  27. data/spec/async/dns/replace_spec.rb +44 -0
  28. data/spec/async/dns/resolver_performance_spec.rb +110 -0
  29. data/spec/async/dns/resolver_spec.rb +151 -0
  30. data/spec/async/dns/server/bind9/generate-local.rb +25 -0
  31. data/spec/async/dns/server/bind9/local.zone +5014 -0
  32. data/spec/async/dns/server/bind9/named.conf +14 -0
  33. data/spec/async/dns/server/bind9/named.run +0 -0
  34. data/spec/async/dns/server/million.rb +85 -0
  35. data/spec/async/dns/server_performance_spec.rb +138 -0
  36. data/spec/async/dns/slow_server_spec.rb +97 -0
  37. data/spec/async/dns/socket_spec.rb +70 -0
  38. data/spec/async/dns/system_spec.rb +57 -0
  39. data/spec/async/dns/transaction_spec.rb +140 -0
  40. data/spec/async/dns/truncation_spec.rb +61 -0
  41. data/spec/spec_helper.rb +60 -0
  42. metadata +175 -0
@@ -0,0 +1,28 @@
1
+ # Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative '../chunked'
22
+
23
+ class String
24
+ # Chunk a string which is required for the TEXT `resource_class`.
25
+ def chunked(chunk_size = 255)
26
+ Async::DNS::chunked(self)
27
+ end
28
+ end
@@ -0,0 +1,229 @@
1
+ # Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'transport'
22
+
23
+ module Async::DNS
24
+ class GenericHandler
25
+ def initialize(server)
26
+ @server = server
27
+ @logger = @server.logger || Async.logger
28
+ end
29
+
30
+ def error_response(query = nil, code = Resolv::DNS::RCode::ServFail)
31
+ # Encoding may fail, so we need to handle this particular case:
32
+ server_failure = Resolv::DNS::Message::new(query ? query.id : 0)
33
+
34
+ server_failure.qr = 1
35
+ server_failure.opcode = query ? query.opcode : 0
36
+ server_failure.aa = 1
37
+ server_failure.rd = 0
38
+ server_failure.ra = 0
39
+
40
+ server_failure.rcode = code
41
+
42
+ # We can't do anything at this point...
43
+ return server_failure
44
+ end
45
+
46
+ def process_query(data, options)
47
+ @logger.debug "<> Receiving incoming query (#{data.bytesize} bytes) to #{self.class.name}..."
48
+ query = nil
49
+
50
+ begin
51
+ query = Async::DNS::decode_message(data)
52
+
53
+ return @server.process_query(query, options)
54
+ rescue StandardError => error
55
+ @logger.error "<> Error processing request: #{error.inspect}!"
56
+ Async::DNS::log_exception(@logger, error)
57
+
58
+ return error_response(query)
59
+ end
60
+ end
61
+ end
62
+
63
+ # Handling incoming UDP requests, which are single data packets, and pass them on to the given server.
64
+ class UDPHandler < GenericHandler
65
+ def run(socket, reactor:)
66
+ Async.logger.debug(self.class.name) {"-> Run on #{socket}..."}
67
+
68
+ while true
69
+ Async.logger.debug(self.class.name) {"-> socket.recvfrom"}
70
+ input_data, (_, remote_port, remote_host) = socket.recvfrom(UDP_TRUNCATION_SIZE)
71
+ Async.logger.debug(self.class.name) {"<- socket.recvfrom"}
72
+
73
+ reactor.async do
74
+ respond(socket, input_data, remote_host, remote_port)
75
+ end
76
+ end
77
+ ensure
78
+ Async.logger.debug(self.class.name) {"<- Run ensure... #{$!}"}
79
+ end
80
+
81
+ def respond(socket, input_data, remote_host, remote_port)
82
+ options = {peer: remote_host, port: remote_port, proto: :udp}
83
+
84
+ response = process_query(input_data, options)
85
+
86
+ output_data = response.encode
87
+
88
+ @logger.debug "<#{response.id}> Writing #{output_data.bytesize} bytes response to client via UDP..."
89
+
90
+ if output_data.bytesize > UDP_TRUNCATION_SIZE
91
+ @logger.warn "<#{response.id}>Response via UDP was larger than #{UDP_TRUNCATION_SIZE}!"
92
+
93
+ # Reencode data with truncation flag marked as true:
94
+ truncation_error = Resolv::DNS::Message.new(response.id)
95
+ truncation_error.tc = 1
96
+
97
+ output_data = truncation_error.encode
98
+ end
99
+
100
+ socket.send(output_data, 0, remote_host, remote_port)
101
+ rescue IOError => error
102
+ @logger.warn "<> UDP response failed: #{error.inspect}!"
103
+ rescue EOFError => error
104
+ @logger.warn "<> UDP session ended prematurely: #{error.inspect}!"
105
+ rescue DecodeError
106
+ @logger.warn "<> Could not decode incoming UDP data!"
107
+ end
108
+ end
109
+
110
+ class UDPSocketHandler < UDPHandler
111
+ def initialize(server, socket)
112
+ @socket = socket
113
+
114
+ super(server)
115
+ end
116
+
117
+ attr :socket
118
+
119
+ def run(reactor: Async::Task.current.reactor)
120
+ reactor.async(self.socket) do |socket|
121
+ super(socket, reactor: reactor)
122
+ end
123
+ end
124
+ end
125
+
126
+ class UDPServerHandler < UDPHandler
127
+ def initialize(server, host, port)
128
+ @host = host
129
+ @port = port
130
+
131
+ super(server)
132
+ end
133
+
134
+ attr :host
135
+ attr :port
136
+
137
+ def run(reactor: Async::Task.current.reactor)
138
+ reactor.with(make_socket) do |socket|
139
+ super(socket, reactor: reactor)
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def make_socket
146
+ family ||= Async::DNS::address_family(@host)
147
+
148
+ socket = ::UDPSocket.new(family)
149
+ socket.bind(@host, @port)
150
+
151
+ return socket
152
+ end
153
+ end
154
+
155
+ class TCPHandler < GenericHandler
156
+ def run(socket, reactor:)
157
+ Async.logger.debug(self.class.name) {"-> Run on #{socket}..."}
158
+
159
+ reactor.with(socket.accept) do |client|
160
+ handle_connection(client)
161
+ end while true
162
+ ensure
163
+ Async.logger.debug(self.class.name) {"<- Run ensure... #{$!}"}
164
+ end
165
+
166
+ def handle_connection(socket)
167
+ context = Async::Task.current
168
+
169
+ _, remote_port, remote_host = socket.io.peeraddr
170
+ options = {peer: remote_host, port: remote_port, proto: :tcp}
171
+
172
+ input_data = StreamTransport.read_chunk(socket)
173
+
174
+ response = process_query(input_data, options)
175
+
176
+ length = StreamTransport.write_message(socket, response)
177
+
178
+ @logger.debug "<#{response.id}> Wrote #{length} bytes via TCP..."
179
+ rescue EOFError => error
180
+ @logger.warn "<> Error: TCP session ended prematurely!"
181
+ rescue Errno::ECONNRESET => error
182
+ @logger.warn "<> Error: TCP connection reset by peer!"
183
+ rescue Errno::EPIPE
184
+ @logger.warn "<> Error: TCP session failed due to broken pipe!"
185
+ rescue DecodeError
186
+ @logger.warn "<> Error: Could not decode incoming TCP data!"
187
+ end
188
+ end
189
+
190
+ class TCPSocketHandler < TCPHandler
191
+ def initialize(server, socket)
192
+ @socket = socket
193
+
194
+ super(server)
195
+ end
196
+
197
+ attr :socket
198
+
199
+ def run(reactor: Async::Task.current.reactor)
200
+ reactor.async(@socket) do |socket|
201
+ super(socket, reactor: reactor)
202
+ end
203
+ end
204
+ end
205
+
206
+ class TCPServerHandler < TCPHandler
207
+ def initialize(server, host, port)
208
+ @host = host
209
+ @port = port
210
+
211
+ super(server)
212
+ end
213
+
214
+ attr :host
215
+ attr :port
216
+
217
+ def run(reactor: Async::Task.current.reactor)
218
+ reactor.with(make_socket) do |socket|
219
+ super(socket, reactor: reactor)
220
+ end
221
+ end
222
+
223
+ private
224
+
225
+ def make_socket
226
+ ::TCPServer.new(@host, @port)
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'logger'
22
+
23
+ module Async::DNS
24
+ # Logs an exception nicely to a standard `Logger`.
25
+ def self.log_exception(logger, exception)
26
+ logger.error "#{exception.class}: #{exception.message}"
27
+ if exception.backtrace
28
+ Array(exception.backtrace).each { |at| logger.error at }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,75 @@
1
+ # Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'stringio'
22
+ require 'resolv'
23
+
24
+ require 'base64'
25
+
26
+ require_relative 'logger'
27
+ require_relative 'extensions/resolv'
28
+
29
+ module Async::DNS
30
+ UDP_TRUNCATION_SIZE = 512
31
+
32
+ # The DNS message container.
33
+ Message = ::Resolv::DNS::Message
34
+ DecodeError = ::Resolv::DNS::DecodeError
35
+
36
+ @@dump_bad_message = nil
37
+
38
+ # Call this function with a path where bad messages will be saved. Any message that causes an exception to be thrown while decoding the binary will be saved in base64 for later inspection. The log file could grow quickly so be careful - not designed for long term use.
39
+ def self.log_bad_messages!(log_path)
40
+ bad_messages_log = Logger.new(log_path, 10, 1024*100)
41
+ bad_messages_log.level = Logger::DEBUG
42
+
43
+ @dump_bad_message = lambda do |error, data|
44
+ bad_messages_log.debug("Bad message: #{Base64.encode64(data)}")
45
+ Async::DNS.log_exception(bad_messages_log, error)
46
+ end
47
+ end
48
+
49
+ # Decodes binary data into a {Message}.
50
+ def self.decode_message(data)
51
+ # Otherwise the decode process might fail with non-binary data.
52
+ if data.respond_to? :force_encoding
53
+ data.force_encoding("BINARY")
54
+ end
55
+
56
+ begin
57
+ return Message.decode(data)
58
+ rescue DecodeError
59
+ raise
60
+ rescue StandardError => error
61
+ new_error = DecodeError.new(error.message)
62
+ new_error.set_backtrace(error.backtrace)
63
+
64
+ raise new_error
65
+ end
66
+
67
+ rescue => error
68
+ # Log the bad messsage if required:
69
+ if @dump_bad_message
70
+ @dump_bad_message.call(error, data)
71
+ end
72
+
73
+ raise
74
+ end
75
+ end
@@ -0,0 +1,54 @@
1
+ # Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'resolv'
22
+ require 'resolv-replace'
23
+
24
+ module Async::DNS
25
+ module Replace
26
+ class << self
27
+ attr :resolver, true
28
+
29
+ def resolver?
30
+ resolver != nil
31
+ end
32
+
33
+ def get_address(host)
34
+ begin
35
+ resolver.addresses_for(host).sample.to_s
36
+ rescue ResolutionFailure
37
+ raise SocketError, "Hostname not known: #{host}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class << IPSocket
44
+ @@resolver = nil
45
+
46
+ def getaddress(host)
47
+ if Replace.resolver?
48
+ Replace.get_address(host)
49
+ else
50
+ original_resolv_getaddress(host)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,280 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'handler'
22
+
23
+ require 'securerandom'
24
+ require 'async'
25
+
26
+ module Async::DNS
27
+ class InvalidProtocolError < StandardError
28
+ end
29
+
30
+ class InvalidResponseError < StandardError
31
+ end
32
+
33
+ class ResolutionFailure < StandardError
34
+ end
35
+
36
+ class Resolver
37
+ # Wait for up to 5 seconds for a response. Override with `options[:timeout]`
38
+ DEFAULT_TIMEOUT = 5.0
39
+
40
+ # 10ms wait between making requests. Override with `options[:delay]`
41
+ DEFAULT_DELAY = 0.01
42
+
43
+ # Try a given request 10 times before failing. Override with `options[:retries]`.
44
+ DEFAULT_RETRIES = 10
45
+
46
+ # Servers are specified in the same manor as options[:listen], e.g.
47
+ # [:tcp/:udp, address, port]
48
+ # In the case of multiple servers, they will be checked in sequence.
49
+ def initialize(servers, origin: nil, logger: Async.logger, timeout: DEFAULT_TIMEOUT)
50
+ @servers = servers
51
+
52
+ @origin = origin
53
+ @logger = logger
54
+ @timeout = timeout
55
+ end
56
+
57
+ attr_accessor :origin
58
+
59
+ def fully_qualified_name(name)
60
+ # If we are passed an existing deconstructed name:
61
+ if Resolv::DNS::Name === name
62
+ if name.absolute?
63
+ return name
64
+ else
65
+ return name.with_origin(@origin)
66
+ end
67
+ end
68
+
69
+ # ..else if we have a string, we need to do some basic processing:
70
+ if name.end_with? '.'
71
+ return Resolv::DNS::Name.create(name)
72
+ else
73
+ return Resolv::DNS::Name.create(name).with_origin(@origin)
74
+ end
75
+ end
76
+
77
+ # Provides the next sequence identification number which is used to keep track of DNS messages.
78
+ def next_id!
79
+ # 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.
80
+ SecureRandom.random_number(2**16)
81
+ end
82
+
83
+ # Look up a named resource of the given resource_class.
84
+ def query(name, resource_class = Resolv::DNS::Resource::IN::A)
85
+ message = Resolv::DNS::Message.new(next_id!)
86
+ message.rd = 1
87
+ message.add_question fully_qualified_name(name), resource_class
88
+
89
+ dispatch_request(message)
90
+ end
91
+
92
+ # 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
+ def addresses_for(name, resource_class = Resolv::DNS::Resource::IN::A, options = {})
94
+ name = fully_qualified_name(name)
95
+
96
+ cache = options.fetch(:cache, {})
97
+ retries = options.fetch(:retries, DEFAULT_RETRIES)
98
+ delay = options.fetch(:delay, DEFAULT_DELAY)
99
+
100
+ records = lookup(name, resource_class, cache) do |name, resource_class|
101
+ response = nil
102
+
103
+ retries.times do |i|
104
+ # Wait 10ms before trying again:
105
+ sleep delay if delay and i > 0
106
+
107
+ response = query(name, resource_class)
108
+
109
+ break if response
110
+ end
111
+
112
+ response or raise ResolutionFailure.new("Could not resolve #{name} after #{retries} attempt(s).")
113
+ end
114
+
115
+ addresses = []
116
+
117
+ if records
118
+ records.each do |record|
119
+ if record.respond_to? :address
120
+ addresses << record.address
121
+ else
122
+ # 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.
123
+ addresses += addresses_for(record.name, record.class, options.merge(cache: cache))
124
+ end
125
+ end
126
+ end
127
+
128
+ if addresses.size > 0
129
+ return addresses
130
+ else
131
+ raise ResolutionFailure.new("Could not find any addresses for #{name}.")
132
+ end
133
+ end
134
+
135
+ # 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
+ def dispatch_request(message)
137
+ request = Request.new(message, @servers)
138
+ context = Async::Task.current
139
+
140
+ request.each do |server|
141
+ @logger.debug "[#{message.id}] Sending request #{message.question.inspect} to server #{server.inspect}" if @logger
142
+
143
+ begin
144
+ response = nil
145
+
146
+ context.timeout(@timeout) do
147
+ @logger.debug "[#{message.id}] -> Try server #{server}" if @logger
148
+ response = try_server(request, server)
149
+ @logger.debug "[#{message.id}] <- Try server #{server} = #{response}" if @logger
150
+ end
151
+
152
+ if valid_response(message, response)
153
+ return response
154
+ end
155
+ rescue Async::TimeoutError
156
+ @logger.debug "[#{message.id}] Request timed out!" if @logger
157
+ rescue InvalidResponseError
158
+ @logger.warn "[#{message.id}] Invalid response from network: #{$!}!" if @logger
159
+ rescue DecodeError
160
+ @logger.warn "[#{message.id}] Error while decoding data from network: #{$!}!" if @logger
161
+ rescue IOError
162
+ @logger.warn "[#{message.id}] Error while reading from network: #{$!}!" if @logger
163
+ end
164
+ end
165
+
166
+ return nil
167
+ end
168
+
169
+ private
170
+
171
+ # Lookup a name/resource_class record but use the records cache if possible reather than making a new request if possible.
172
+ def lookup(name, resource_class = Resolv::DNS::Resource::IN::A, records = {})
173
+ records.fetch(name) do
174
+ response = yield(name, resource_class)
175
+
176
+ if response
177
+ response.answer.each do |name, ttl, record|
178
+ (records[name] ||= []) << record
179
+ end
180
+ end
181
+
182
+ records[name]
183
+ end
184
+ end
185
+
186
+ def try_server(request, server)
187
+ case server[0]
188
+ when :udp
189
+ try_udp_server(request, server[1], server[2])
190
+ when :tcp
191
+ try_tcp_server(request, server[1], server[2])
192
+ else
193
+ raise InvalidProtocolError.new(server)
194
+ end
195
+ end
196
+
197
+ def valid_response(message, response)
198
+ if response.tc != 0
199
+ @logger.warn "[#{message.id}] Received truncated response!" if @logger
200
+ elsif response.id != message.id
201
+ @logger.warn "[#{message.id}] Received response with incorrect message id: #{response.id}!" if @logger
202
+ else
203
+ @logger.debug "[#{message.id}] Received valid response with #{response.answer.count} answer(s)." if @logger
204
+
205
+ return true
206
+ end
207
+
208
+ return false
209
+ end
210
+
211
+ def udp_socket(family)
212
+ @udp_sockets[family] ||= UDPSocket.new(family)
213
+ end
214
+
215
+ def try_udp_server(request, host, port)
216
+ context = Async::Task.current
217
+
218
+ family = Async::DNS::address_family(host)
219
+
220
+ context.with(UDPSocket.new(family)) do |socket|
221
+ socket.send(request.packet, 0, host, port)
222
+
223
+ data, (_, remote_port) = socket.recvfrom(UDP_TRUNCATION_SIZE)
224
+ # Need to check host, otherwise security issue.
225
+
226
+ # May indicate some kind of spoofing attack:
227
+ if port != remote_port
228
+ raise InvalidResponseError.new("Data was not received from correct remote port (#{port} != #{remote_port})")
229
+ end
230
+
231
+ return Async::DNS::decode_message(data)
232
+ end
233
+ end
234
+
235
+ def try_tcp_server(request, host, port)
236
+ context = Async::Task.current
237
+
238
+ context.with(TCPSocket.new(host, port)) do |socket|
239
+ StreamTransport.write_chunk(socket, request.packet)
240
+
241
+ input_data = StreamTransport.read_chunk(socket)
242
+
243
+ return Async::DNS::decode_message(input_data)
244
+ end
245
+ end
246
+
247
+ # Manages a single DNS question message across one or more servers.
248
+ class Request
249
+ def initialize(message, servers)
250
+ @message = message
251
+ @packet = message.encode
252
+
253
+ @servers = servers.dup
254
+
255
+ # We select the protocol based on the size of the data:
256
+ if @packet.bytesize > UDP_TRUNCATION_SIZE
257
+ @servers.delete_if{|server| server[0] == :udp}
258
+ end
259
+ end
260
+
261
+ attr :message
262
+ attr :packet
263
+ attr :logger
264
+
265
+ def each(&block)
266
+ @servers.each do |server|
267
+ # TODO: This seems odd...
268
+ next if @packet.bytesize > UDP_TRUNCATION_SIZE
269
+
270
+ yield server
271
+ end
272
+ end
273
+
274
+ def update_id!(id)
275
+ @message.id = id
276
+ @packet = @message.encode
277
+ end
278
+ end
279
+ end
280
+ end