celluloid-dns 0.0.1 → 0.17.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +0 -2
- data/.simplecov +15 -0
- data/.travis.yml +13 -7
- data/Gemfile +5 -6
- data/README.md +118 -41
- data/Rakefile +8 -3
- data/celluloid-dns.gemspec +29 -18
- data/lib/celluloid/dns.rb +30 -7
- data/lib/celluloid/dns/chunked.rb +34 -0
- data/lib/celluloid/dns/extensions/resolv.rb +136 -0
- data/lib/celluloid/dns/extensions/string.rb +28 -0
- data/lib/celluloid/dns/handler.rb +198 -0
- data/lib/celluloid/dns/logger.rb +31 -0
- data/lib/celluloid/dns/message.rb +76 -0
- data/lib/celluloid/dns/replace.rb +54 -0
- data/lib/celluloid/dns/resolver.rb +288 -0
- data/lib/celluloid/dns/server.rb +151 -27
- data/lib/celluloid/dns/system.rb +146 -0
- data/lib/celluloid/dns/transaction.rb +202 -0
- data/lib/celluloid/dns/transport.rb +75 -0
- data/lib/celluloid/dns/version.rb +23 -3
- data/spec/celluloid/dns/celluloid_bug_spec.rb +92 -0
- data/spec/celluloid/dns/hosts.txt +2 -0
- data/spec/celluloid/dns/ipv6_spec.rb +70 -0
- data/spec/celluloid/dns/message_spec.rb +56 -0
- data/spec/celluloid/dns/origin_spec.rb +106 -0
- data/spec/celluloid/dns/replace_spec.rb +42 -0
- data/spec/celluloid/dns/resolver_performance_spec.rb +110 -0
- data/spec/celluloid/dns/resolver_spec.rb +152 -0
- data/spec/celluloid/dns/server/bind9/generate-local.rb +25 -0
- data/spec/celluloid/dns/server/bind9/local.zone +5014 -0
- data/spec/celluloid/dns/server/bind9/named.conf +14 -0
- data/spec/celluloid/dns/server/bind9/named.run +0 -0
- data/spec/celluloid/dns/server/million.rb +85 -0
- data/spec/celluloid/dns/server_performance_spec.rb +139 -0
- data/spec/celluloid/dns/slow_server_spec.rb +91 -0
- data/spec/celluloid/dns/socket_spec.rb +71 -0
- data/spec/celluloid/dns/system_spec.rb +60 -0
- data/spec/celluloid/dns/transaction_spec.rb +138 -0
- data/spec/celluloid/dns/truncation_spec.rb +62 -0
- metadata +124 -56
- data/.coveralls.yml +0 -1
- data/.gitignore +0 -17
- data/CHANGES.md +0 -3
- data/LICENSE.txt +0 -22
- data/lib/celluloid/dns/request.rb +0 -46
- data/spec/celluloid/dns/server_spec.rb +0 -26
- data/spec/spec_helper.rb +0 -5
- data/tasks/rspec.task +0 -7
@@ -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
|
+
Celluloid::DNS::chunked(self)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,198 @@
|
|
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 Celluloid::DNS
|
24
|
+
class GenericHandler
|
25
|
+
include Celluloid::IO
|
26
|
+
|
27
|
+
def initialize(server)
|
28
|
+
@server = server
|
29
|
+
@logger = @server.logger || Celluloid.logger
|
30
|
+
|
31
|
+
@connections = Celluloid::Condition.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def stop
|
35
|
+
shutdown
|
36
|
+
|
37
|
+
@connections.wait
|
38
|
+
end
|
39
|
+
|
40
|
+
finalizer def stop
|
41
|
+
# Celluloid.logger.debug(self.class.name) {"-> Shutdown..."}
|
42
|
+
|
43
|
+
@socket.close if @socket
|
44
|
+
@socket = nil
|
45
|
+
|
46
|
+
# Celluloid.logger.debug(self.class.name) {"<- Shutdown..."}
|
47
|
+
end
|
48
|
+
|
49
|
+
def error_response(query = nil, code = Resolv::DNS::RCode::ServFail)
|
50
|
+
# Encoding may fail, so we need to handle this particular case:
|
51
|
+
server_failure = Resolv::DNS::Message::new(query ? query.id : 0)
|
52
|
+
|
53
|
+
server_failure.qr = 1
|
54
|
+
server_failure.opcode = query ? query.opcode : 0
|
55
|
+
server_failure.aa = 1
|
56
|
+
server_failure.rd = 0
|
57
|
+
server_failure.ra = 0
|
58
|
+
|
59
|
+
server_failure.rcode = code
|
60
|
+
|
61
|
+
# We can't do anything at this point...
|
62
|
+
return server_failure
|
63
|
+
end
|
64
|
+
|
65
|
+
def process_query(data, options)
|
66
|
+
@logger.debug "<> Receiving incoming query (#{data.bytesize} bytes) to #{self.class.name}..."
|
67
|
+
query = nil
|
68
|
+
|
69
|
+
begin
|
70
|
+
query = Celluloid::DNS::decode_message(data)
|
71
|
+
|
72
|
+
return @server.process_query(query, options)
|
73
|
+
rescue StandardError => error
|
74
|
+
@logger.error "<> Error processing request: #{error.inspect}!"
|
75
|
+
Celluloid::DNS::log_exception(@logger, error)
|
76
|
+
|
77
|
+
return error_response(query)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Handling incoming UDP requests, which are single data packets, and pass them on to the given server.
|
83
|
+
class UDPSocketHandler < GenericHandler
|
84
|
+
def initialize(server, socket)
|
85
|
+
super(server)
|
86
|
+
|
87
|
+
@socket = socket
|
88
|
+
|
89
|
+
async.run
|
90
|
+
end
|
91
|
+
|
92
|
+
def run
|
93
|
+
Celluloid.logger.debug(self.class.name) {"-> Run..."}
|
94
|
+
|
95
|
+
handle_connection while @socket
|
96
|
+
|
97
|
+
Celluloid.logger.debug(self.class.name) {"<- Run..."}
|
98
|
+
end
|
99
|
+
|
100
|
+
def respond(input_data, remote_host, remote_port)
|
101
|
+
options = {peer: remote_host, port: remote_port, proto: :udp}
|
102
|
+
|
103
|
+
response = process_query(input_data, options)
|
104
|
+
|
105
|
+
output_data = response.encode
|
106
|
+
|
107
|
+
@logger.debug "<#{response.id}> Writing #{output_data.bytesize} bytes response to client via UDP..."
|
108
|
+
|
109
|
+
if output_data.bytesize > UDP_TRUNCATION_SIZE
|
110
|
+
@logger.warn "<#{response.id}>Response via UDP was larger than #{UDP_TRUNCATION_SIZE}!"
|
111
|
+
|
112
|
+
# Reencode data with truncation flag marked as true:
|
113
|
+
truncation_error = Resolv::DNS::Message.new(response.id)
|
114
|
+
truncation_error.tc = 1
|
115
|
+
|
116
|
+
output_data = truncation_error.encode
|
117
|
+
end
|
118
|
+
|
119
|
+
@socket.send(output_data, 0, remote_host, remote_port)
|
120
|
+
rescue IOError => error
|
121
|
+
@logger.warn "<> UDP response failed: #{error.inspect}!"
|
122
|
+
rescue EOFError => error
|
123
|
+
@logger.warn "<> UDP session ended prematurely: #{error.inspect}!"
|
124
|
+
rescue DecodeError
|
125
|
+
@logger.warn "<> Could not decode incoming UDP data!"
|
126
|
+
end
|
127
|
+
|
128
|
+
def handle_connection
|
129
|
+
# @logger.debug "Waiting for incoming UDP packet #{@socket.inspect}..."
|
130
|
+
|
131
|
+
input_data, (_, remote_port, remote_host) = @socket.recvfrom(UDP_TRUNCATION_SIZE)
|
132
|
+
|
133
|
+
async.respond(input_data, remote_host, remote_port)
|
134
|
+
rescue IOError => error
|
135
|
+
@logger.warn "<> UDP connection failed: #{error.inspect}!"
|
136
|
+
rescue EOFError => error
|
137
|
+
@logger.warn "<> UDP session ended prematurely!"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class UDPHandler < UDPSocketHandler
|
142
|
+
def initialize(server, host, port)
|
143
|
+
family = Celluloid::DNS::address_family(host)
|
144
|
+
socket = UDPSocket.new(family)
|
145
|
+
|
146
|
+
socket.bind(host, port)
|
147
|
+
|
148
|
+
super(server, socket)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
class TCPSocketHandler < GenericHandler
|
153
|
+
def initialize(server, socket)
|
154
|
+
super(server)
|
155
|
+
|
156
|
+
@socket = socket
|
157
|
+
|
158
|
+
async.run
|
159
|
+
end
|
160
|
+
|
161
|
+
def run
|
162
|
+
Celluloid.logger.debug(self.class.name) {"-> Run..."}
|
163
|
+
|
164
|
+
async.handle_connection(@socket.accept) while @socket
|
165
|
+
|
166
|
+
Celluloid.logger.debug(self.class.name) {"<- Run..."}
|
167
|
+
end
|
168
|
+
|
169
|
+
def handle_connection(socket)
|
170
|
+
_, remote_port, remote_host = socket.peeraddr
|
171
|
+
options = {peer: remote_host, port: remote_port, proto: :tcp}
|
172
|
+
|
173
|
+
input_data = StreamTransport.read_chunk(socket)
|
174
|
+
|
175
|
+
response = process_query(input_data, options)
|
176
|
+
|
177
|
+
length = StreamTransport.write_message(socket, response)
|
178
|
+
|
179
|
+
@logger.debug "<#{response.id}> Wrote #{length} bytes via TCP..."
|
180
|
+
rescue EOFError => error
|
181
|
+
@logger.warn "<> TCP session ended prematurely!"
|
182
|
+
rescue Errno::ECONNRESET => error
|
183
|
+
@logger.warn "<> TCP connection reset by peer!"
|
184
|
+
rescue DecodeError
|
185
|
+
@logger.warn "<> Could not decode incoming TCP data!"
|
186
|
+
ensure
|
187
|
+
socket.close
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class TCPHandler < TCPSocketHandler
|
192
|
+
def initialize(server, host, port)
|
193
|
+
socket = TCPServer.new(host, port)
|
194
|
+
|
195
|
+
super(server, socket)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
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 Celluloid::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,76 @@
|
|
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 Celluloid::DNS
|
30
|
+
UDP_TRUNCATION_SIZE = 512
|
31
|
+
|
32
|
+
# The DNS message container.
|
33
|
+
Message = Resolv::DNS::Message
|
34
|
+
|
35
|
+
DecodeError = Resolv::DNS::DecodeError
|
36
|
+
|
37
|
+
@@dump_bad_message = nil
|
38
|
+
|
39
|
+
# 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.
|
40
|
+
def self.log_bad_messages!(log_path)
|
41
|
+
bad_messages_log = Logger.new(log_path, 10, 1024*100)
|
42
|
+
bad_messages_log.level = Logger::DEBUG
|
43
|
+
|
44
|
+
@dump_bad_message = lambda do |error, data|
|
45
|
+
bad_messages_log.debug("Bad message: #{Base64.encode64(data)}")
|
46
|
+
Celluloid::DNS.log_exception(bad_messages_log, error)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Decodes binary data into a {Message}.
|
51
|
+
def self.decode_message(data)
|
52
|
+
# Otherwise the decode process might fail with non-binary data.
|
53
|
+
if data.respond_to? :force_encoding
|
54
|
+
data.force_encoding("BINARY")
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
return Message.decode(data)
|
59
|
+
rescue DecodeError
|
60
|
+
raise
|
61
|
+
rescue StandardError => error
|
62
|
+
new_error = DecodeError.new(error.message)
|
63
|
+
new_error.set_backtrace(error.backtrace)
|
64
|
+
|
65
|
+
raise new_error
|
66
|
+
end
|
67
|
+
|
68
|
+
rescue => error
|
69
|
+
# Log the bad messsage if required:
|
70
|
+
if @dump_bad_message
|
71
|
+
@dump_bad_message.call(error, data)
|
72
|
+
end
|
73
|
+
|
74
|
+
raise
|
75
|
+
end
|
76
|
+
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 Celluloid::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,288 @@
|
|
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 'celluloid/io'
|
25
|
+
|
26
|
+
module Celluloid::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
|
+
include Celluloid::IO
|
47
|
+
|
48
|
+
# Servers are specified in the same manor as options[:listen], e.g.
|
49
|
+
# [:tcp/:udp, address, port]
|
50
|
+
# In the case of multiple servers, they will be checked in sequence.
|
51
|
+
def initialize(servers, options = {})
|
52
|
+
@servers = servers
|
53
|
+
|
54
|
+
@options = options
|
55
|
+
|
56
|
+
@origin = options[:origin] || nil
|
57
|
+
|
58
|
+
@logger = options[:logger] || Celluloid.logger
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_accessor :origin
|
62
|
+
|
63
|
+
def fully_qualified_name(name)
|
64
|
+
# If we are passed an existing deconstructed name:
|
65
|
+
if Resolv::DNS::Name === name
|
66
|
+
if name.absolute?
|
67
|
+
return name
|
68
|
+
else
|
69
|
+
return name.with_origin(@origin)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# ..else if we have a string, we need to do some basic processing:
|
74
|
+
if name.end_with? '.'
|
75
|
+
return Resolv::DNS::Name.create(name)
|
76
|
+
else
|
77
|
+
return Resolv::DNS::Name.create(name).with_origin(@origin)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Provides the next sequence identification number which is used to keep track of DNS messages.
|
82
|
+
def next_id!
|
83
|
+
# 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.
|
84
|
+
SecureRandom.random_number(2**16)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Look up a named resource of the given resource_class.
|
88
|
+
def query(name, resource_class = Resolv::DNS::Resource::IN::A)
|
89
|
+
message = Resolv::DNS::Message.new(next_id!)
|
90
|
+
message.rd = 1
|
91
|
+
message.add_question fully_qualified_name(name), resource_class
|
92
|
+
|
93
|
+
dispatch_request(message)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Yields a list of `Resolv::IPv4` and `Resolv::IPv6` addresses for the given `name` and `resource_class`. Raises a ResolutionFailure if no severs respond.
|
97
|
+
def addresses_for(name, resource_class = Resolv::DNS::Resource::IN::A, options = {})
|
98
|
+
name = fully_qualified_name(name)
|
99
|
+
|
100
|
+
cache = options.fetch(:cache, {})
|
101
|
+
retries = options.fetch(:retries, DEFAULT_RETRIES)
|
102
|
+
delay = options.fetch(:delay, DEFAULT_DELAY)
|
103
|
+
|
104
|
+
records = lookup(name, resource_class, cache) do |name, resource_class|
|
105
|
+
response = nil
|
106
|
+
|
107
|
+
retries.times do |i|
|
108
|
+
# Wait 10ms before trying again:
|
109
|
+
sleep delay if delay and i > 0
|
110
|
+
|
111
|
+
response = query(name, resource_class)
|
112
|
+
|
113
|
+
break if response
|
114
|
+
end
|
115
|
+
|
116
|
+
response or abort ResolutionFailure.new("Could not resolve #{name} after #{retries} attempt(s).")
|
117
|
+
end
|
118
|
+
|
119
|
+
addresses = []
|
120
|
+
|
121
|
+
if records
|
122
|
+
records.each do |record|
|
123
|
+
if record.respond_to? :address
|
124
|
+
addresses << record.address
|
125
|
+
else
|
126
|
+
# 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.
|
127
|
+
addresses += addresses_for(record.name, record.class, options.merge(cache: cache))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
if addresses.size > 0
|
133
|
+
return addresses
|
134
|
+
else
|
135
|
+
abort ResolutionFailure.new("Could not find any addresses for #{name}.")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def request_timeout
|
140
|
+
@options[:timeout] || DEFAULT_TIMEOUT
|
141
|
+
end
|
142
|
+
|
143
|
+
# 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.
|
144
|
+
def dispatch_request(message)
|
145
|
+
request = Request.new(message, @servers)
|
146
|
+
|
147
|
+
request.each do |server|
|
148
|
+
@logger.debug "[#{message.id}] Sending request #{message.question.inspect} to server #{server.inspect}" if @logger
|
149
|
+
|
150
|
+
begin
|
151
|
+
response = nil
|
152
|
+
|
153
|
+
# This may be causing a problem, perhaps try:
|
154
|
+
# after(timeout) { socket.close }
|
155
|
+
# https://github.com/celluloid/celluloid-io/issues/121
|
156
|
+
timeout(request_timeout) do
|
157
|
+
response = try_server(request, server)
|
158
|
+
end
|
159
|
+
|
160
|
+
if valid_response(message, response)
|
161
|
+
return response
|
162
|
+
end
|
163
|
+
rescue TaskTimeout
|
164
|
+
@logger.debug "[#{message.id}] Request timed out!" if @logger
|
165
|
+
rescue InvalidResponseError
|
166
|
+
@logger.warn "[#{message.id}] Invalid response from network: #{$!}!" if @logger
|
167
|
+
rescue DecodeError
|
168
|
+
@logger.warn "[#{message.id}] Error while decoding data from network: #{$!}!" if @logger
|
169
|
+
rescue IOError
|
170
|
+
@logger.warn "[#{message.id}] Error while reading from network: #{$!}!" if @logger
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
return nil
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
# Lookup a name/resource_class record but use the records cache if possible reather than making a new request if possible.
|
180
|
+
def lookup(name, resource_class = Resolv::DNS::Resource::IN::A, records = {})
|
181
|
+
records.fetch(name) do
|
182
|
+
response = yield(name, resource_class)
|
183
|
+
|
184
|
+
if response
|
185
|
+
response.answer.each do |name, ttl, record|
|
186
|
+
(records[name] ||= []) << record
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
records[name]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def try_server(request, server)
|
195
|
+
case server[0]
|
196
|
+
when :udp
|
197
|
+
try_udp_server(request, server[1], server[2])
|
198
|
+
when :tcp
|
199
|
+
try_tcp_server(request, server[1], server[2])
|
200
|
+
else
|
201
|
+
raise InvalidProtocolError.new(server)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def valid_response(message, response)
|
206
|
+
if response.tc != 0
|
207
|
+
@logger.warn "[#{message.id}] Received truncated response!" if @logger
|
208
|
+
elsif response.id != message.id
|
209
|
+
@logger.warn "[#{message.id}] Received response with incorrect message id: #{response.id}!" if @logger
|
210
|
+
else
|
211
|
+
@logger.debug "[#{message.id}] Received valid response with #{response.answer.count} answer(s)." if @logger
|
212
|
+
|
213
|
+
return true
|
214
|
+
end
|
215
|
+
|
216
|
+
return false
|
217
|
+
end
|
218
|
+
|
219
|
+
def try_udp_server(request, host, port)
|
220
|
+
family = Celluloid::DNS::address_family(host)
|
221
|
+
socket = UDPSocket.new(family)
|
222
|
+
|
223
|
+
socket.send(request.packet, 0, host, port)
|
224
|
+
|
225
|
+
data, (_, remote_port) = socket.recvfrom(UDP_TRUNCATION_SIZE)
|
226
|
+
# Need to check host, otherwise security issue.
|
227
|
+
|
228
|
+
# May indicate some kind of spoofing attack:
|
229
|
+
if port != remote_port
|
230
|
+
raise InvalidResponseError.new("Data was not received from correct remote port (#{port} != #{remote_port})")
|
231
|
+
end
|
232
|
+
|
233
|
+
message = Celluloid::DNS::decode_message(data)
|
234
|
+
ensure
|
235
|
+
socket.close if socket
|
236
|
+
end
|
237
|
+
|
238
|
+
def try_tcp_server(request, host, port)
|
239
|
+
socket = TCPSocket.new(host, port)
|
240
|
+
|
241
|
+
StreamTransport.write_chunk(socket, request.packet)
|
242
|
+
|
243
|
+
input_data = StreamTransport.read_chunk(socket)
|
244
|
+
|
245
|
+
message = Celluloid::DNS::decode_message(input_data)
|
246
|
+
rescue Errno::ECONNREFUSED => error
|
247
|
+
raise IOError.new(error.message)
|
248
|
+
rescue Errno::EPIPE => error
|
249
|
+
raise IOError.new(error.message)
|
250
|
+
rescue Errno::ECONNRESET => error
|
251
|
+
raise IOError.new(error.message)
|
252
|
+
ensure
|
253
|
+
socket.close if socket
|
254
|
+
end
|
255
|
+
|
256
|
+
# Manages a single DNS question message across one or more servers.
|
257
|
+
class Request
|
258
|
+
def initialize(message, servers)
|
259
|
+
@message = message
|
260
|
+
@packet = message.encode
|
261
|
+
|
262
|
+
@servers = servers.dup
|
263
|
+
|
264
|
+
# We select the protocol based on the size of the data:
|
265
|
+
if @packet.bytesize > UDP_TRUNCATION_SIZE
|
266
|
+
@servers.delete_if{|server| server[0] == :udp}
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
attr :message
|
271
|
+
attr :packet
|
272
|
+
attr :logger
|
273
|
+
|
274
|
+
def each(&block)
|
275
|
+
@servers.each do |server|
|
276
|
+
next if @packet.bytesize > UDP_TRUNCATION_SIZE
|
277
|
+
|
278
|
+
yield server
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def update_id!(id)
|
283
|
+
@message.id = id
|
284
|
+
@packet = @message.encode
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|