rubydns 0.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +6 -14
  2. data/.gitignore +23 -14
  3. data/.rspec +4 -0
  4. data/.simplecov +15 -0
  5. data/.travis.yml +9 -5
  6. data/.yardopts +1 -0
  7. data/Gemfile +6 -2
  8. data/README.md +82 -92
  9. data/Rakefile +2 -5
  10. data/bin/rubydns-check +374 -0
  11. data/examples/Gemfile +7 -0
  12. data/examples/README.md +137 -0
  13. data/examples/basic-dns.rb +24 -0
  14. data/examples/cname.rb +25 -0
  15. data/{test/examples/dropping-dns.rb → examples/flakey-dns.rb} +28 -31
  16. data/{test/examples → examples}/fortune-dns.rb +42 -46
  17. data/examples/geoip-dns.rb +115 -0
  18. data/examples/simple.rb +25 -0
  19. data/{test/examples → examples}/soa-dns.rb +27 -27
  20. data/{test/examples → examples}/test-dns-1.rb +26 -20
  21. data/{test/examples → examples}/test-dns-2.rb +17 -19
  22. data/examples/wikipedia-dns.rb +107 -0
  23. data/lib/rubydns/rule_based_server.rb +180 -0
  24. data/lib/rubydns/version.rb +1 -1
  25. data/lib/rubydns.rb +13 -63
  26. data/rubydns.gemspec +29 -23
  27. data/spec/rubydns/daemon_spec.rb +114 -0
  28. data/{test/test_system.rb → spec/rubydns/injected_supervisor_spec.rb} +32 -25
  29. data/spec/rubydns/passthrough_spec.rb +85 -0
  30. data/spec/rubydns/rules_spec.rb +74 -0
  31. data/spec/spec_helper.rb +30 -0
  32. metadata +101 -78
  33. data/bin/rd-dns-check +0 -374
  34. data/bin/rd-resolve-test +0 -160
  35. data/lib/rubydns/chunked.rb +0 -34
  36. data/lib/rubydns/extensions/hexdump.rb +0 -38
  37. data/lib/rubydns/extensions/logger.rb +0 -30
  38. data/lib/rubydns/extensions/resolv.rb +0 -53
  39. data/lib/rubydns/extensions/string-1.8.rb +0 -35
  40. data/lib/rubydns/extensions/string-1.9.2.rb +0 -29
  41. data/lib/rubydns/extensions/string-1.9.3.rb +0 -31
  42. data/lib/rubydns/extensions/string.rb +0 -27
  43. data/lib/rubydns/handler.rb +0 -140
  44. data/lib/rubydns/message.rb +0 -41
  45. data/lib/rubydns/resolver.rb +0 -239
  46. data/lib/rubydns/server.rb +0 -241
  47. data/lib/rubydns/system.rb +0 -146
  48. data/lib/rubydns/transaction.rb +0 -250
  49. data/test/examples/geoip-dns.rb +0 -86
  50. data/test/helper.rb +0 -9
  51. data/test/test_daemon.rb +0 -100
  52. data/test/test_domains.txt +0 -185
  53. data/test/test_passthrough.rb +0 -80
  54. data/test/test_resolver.rb +0 -105
  55. data/test/test_rules.rb +0 -74
  56. data/test/test_slow_server.rb +0 -98
  57. data/test/test_truncation.rb +0 -78
  58. /data/{test → spec/rubydns}/hosts.txt +0 -0
@@ -1,239 +0,0 @@
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 'rubydns/message'
22
-
23
- module RubyDNS
24
- class InvalidProtocolError < StandardError
25
- end
26
-
27
- class ResolutionFailure < StandardError
28
- end
29
-
30
- class Resolver
31
- # Servers are specified in the same manor as options[:listen], e.g.
32
- # [:tcp/:udp, address, port]
33
- # In the case of multiple servers, they will be checked in sequence.
34
- def initialize(servers, options = {})
35
- @servers = servers
36
- @sequence = 0
37
-
38
- @options = options
39
- end
40
-
41
- # Provides the next sequence identification number which is used to keep track of DNS messages.
42
- def next_id!
43
- return (@sequence += 1)
44
- end
45
-
46
- # Look up a named resource of the given resource_class.
47
- def query(name, resource_class = Resolv::DNS::Resource::IN::A, &block)
48
- message = Resolv::DNS::Message.new(next_id!)
49
- message.rd = 1
50
- message.add_question name, resource_class
51
-
52
- Request.fetch(message, @servers, @options, &block)
53
- end
54
-
55
- # Yields a list of `Resolv::IPv4` and `Resolv::IPv6` addresses for the given `name` and `resource_class`.
56
- def addresses_for(name, resource_class = Resolv::DNS::Resource::IN::A, &block)
57
- query(name, resource_class) do |response|
58
- # Resolv::DNS::Name doesn't retain the trailing dot.
59
- name = name.sub(/\.$/, '')
60
-
61
- case response
62
- when Message
63
- yield response.answer.select{|record| record[0].to_s == name}.collect{|record| record[2].address}
64
- else
65
- yield []
66
- end
67
- end
68
- end
69
-
70
- # Manages a single DNS question message across one or more servers.
71
- class Request
72
- include EventMachine::Deferrable
73
-
74
- def self.fetch(*args)
75
- request = self.new(*args)
76
-
77
- request.callback do |message|
78
- yield message
79
- end
80
-
81
- request.errback do |error|
82
- yield error
83
- end
84
-
85
- request.run!
86
- end
87
-
88
- def initialize(message, servers, options = {}, &block)
89
- @message = message
90
- @packet = message.encode
91
-
92
- @servers = servers.dup
93
-
94
- # We select the protocol based on the size of the data:
95
- if @packet.bytesize > UDP_TRUNCATION_SIZE
96
- @servers.delete_if{|server| server[0] == :udp}
97
- end
98
-
99
- # Measured in seconds:
100
- @timeout = options[:timeout] || 5
101
-
102
- @logger = options[:logger]
103
- end
104
-
105
- attr :message
106
- attr :packet
107
- attr :logger
108
-
109
- def run!
110
- try_next_server!
111
- end
112
-
113
- def process_response!(response)
114
- if Exception === response
115
- @logger.warn "[#{@message.id}] Failure while processing response #{exception}!" if @logger
116
- RubyDNS.log_exception(@logger, response) if @logger
117
-
118
- try_next_server!
119
- elsif response.tc != 0
120
- @logger.warn "[#{@message.id}] Received truncated response!" if @logger
121
-
122
- try_next_server!
123
- elsif response.id != @message.id
124
- @logger.warn "[#{@message.id}] Received response with incorrect message id: #{response.id}" if @logger
125
-
126
- try_next_server!
127
- else
128
- @logger.warn "[#{@message.id}] Received valid response #{response.inspect}" if @logger
129
-
130
- succeed response
131
- end
132
- end
133
-
134
- private
135
-
136
- def try_next_server!
137
- if @request
138
- @request.close_connection
139
- @request = nil
140
- end
141
-
142
- if @servers.size > 0
143
- @server = @servers.shift
144
-
145
- @logger.debug "[#{@message.id}] Sending request to server #{@server.inspect}" if @logger
146
-
147
- # We make requests one at a time to the given server, naturally the servers are ordered in terms of priority.
148
- case @server[0]
149
- when :udp
150
- @request = UDPRequestHandler.open(@server[1], @server[2], self)
151
- when :tcp
152
- @request = TCPRequestHandler.open(@server[1], @server[2], self)
153
- else
154
- raise InvalidProtocolError.new(@server)
155
- end
156
-
157
- # Setting up the timeout...
158
- EventMachine::Timer.new(@timeout) do
159
- @logger.debug "[#{@message.id}] Request timed out!" if @logger
160
-
161
- try_next_server!
162
- end
163
- else
164
- fail ResolutionFailure.new("No available servers responded to the request.")
165
- end
166
- end
167
-
168
- module UDPRequestHandler
169
- def self.open(host, port, request)
170
- # Open a datagram socket... EventMachine doesn't support connected datagram sockets, so we have to cheat a bit:
171
- EventMachine::open_datagram_socket('', 0, self, request, host, port)
172
- end
173
-
174
- def initialize(request, host, port)
175
- @request = request
176
- @host = host
177
- @port = port
178
- end
179
-
180
- def post_init
181
- # Sending question to remote DNS server...
182
- send_datagram(@request.packet, @host, @port)
183
- end
184
-
185
- def receive_data(data)
186
- # Receiving response from remote DNS server...
187
- message = RubyDNS::decode_message(data)
188
-
189
- # The message id must match, and it can't be truncated:
190
- @request.process_response!(message)
191
- rescue Resolv::DNS::DecodeError => error
192
- @request.process_response!(error)
193
- end
194
- end
195
-
196
- module TCPRequestHandler
197
- def self.open(host, port, request)
198
- EventMachine::connect(host, port, TCPRequestHandler, request)
199
- end
200
-
201
- def initialize(request)
202
- @request = request
203
- @buffer = nil
204
- @length = nil
205
- end
206
-
207
- def post_init
208
- data = @request.packet
209
-
210
- send_data([data.bytesize].pack('n'))
211
- send_data data
212
- end
213
-
214
- def receive_data(data)
215
- # We buffer data until we've received the entire packet:
216
- @buffer ||= BinaryStringIO.new
217
- @buffer.write(data)
218
-
219
- if @length == nil
220
- if @buffer.size > 2
221
- @length = @buffer.string.byteslice(0, 2).unpack('n')[0]
222
- end
223
- end
224
-
225
- # If we have received more data than expected, should this be an error?
226
- if @buffer.size >= (@length + 2)
227
- data = @buffer.string.byteslice(2, @length)
228
-
229
- message = RubyDNS::decode_message(data)
230
-
231
- @request.process_response!(message)
232
- end
233
- rescue Resolv::DNS::DecodeError => error
234
- @request.process_response!(error)
235
- end
236
- end
237
- end
238
- end
239
- end
@@ -1,241 +0,0 @@
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 'rubydns/transaction'
22
- require 'rubydns/extensions/logger'
23
-
24
- module RubyDNS
25
-
26
- # This class provides the core of the DSL. It contains a list of rules which
27
- # are used to match against incoming DNS questions. These rules are used to
28
- # generate responses which are either DNS resource records or failures.
29
- class Server
30
- class Rule
31
- def initialize(pattern, callback)
32
- @pattern = pattern
33
- @callback = callback
34
- end
35
-
36
- def match(name, resource_class)
37
- # If the pattern doesn't specify any resource classes, we implicitly pass this test:
38
- return true if @pattern.size < 2
39
-
40
- # Otherwise, we try to match against some specific resource classes:
41
- if Class === @pattern[1]
42
- @pattern[1] == resource_class
43
- else
44
- @pattern[1].include?(resource_class) rescue false
45
- end
46
- end
47
-
48
- def call(server, name, resource_class, *args)
49
- unless match(name, resource_class)
50
- server.logger.debug "Resource class #{resource_class} failed to match #{@pattern[1].inspect}!"
51
-
52
- return false
53
- end
54
-
55
- # Match succeeded against name?
56
- case @pattern[0]
57
- when Regexp
58
- match_data = @pattern[0].match(name)
59
- if match_data
60
- server.logger.debug "Regexp pattern matched with #{match_data.inspect}."
61
- return @callback[*args, match_data]
62
- end
63
- when String
64
- if @pattern[0] == name
65
- server.logger.debug "String pattern matched."
66
- return @callback[*args]
67
- end
68
- else
69
- if (@pattern[0].call(name, resource_class) rescue false)
70
- server.logger.debug "Callable pattern matched."
71
- return @callback[*args]
72
- end
73
- end
74
-
75
- server.logger.debug "No pattern matched."
76
- # We failed to match the pattern.
77
- return false
78
- end
79
-
80
- def to_s
81
- @pattern.inspect
82
- end
83
- end
84
-
85
- # Instantiate a server with a block
86
- #
87
- # server = Server.new do
88
- # match(/server.mydomain.com/, IN::A) do |transaction|
89
- # transaction.respond!("1.2.3.4")
90
- # end
91
- # end
92
- #
93
- def initialize(&block)
94
- @events = {}
95
- @rules = []
96
- @otherwise = nil
97
-
98
- @logger = Logger.new($stderr)
99
-
100
- if block_given?
101
- instance_eval &block
102
- end
103
- end
104
-
105
- attr :logger, true
106
-
107
- # This function connects a pattern with a block. A pattern is either
108
- # a String or a Regex instance. Optionally, a second argument can be
109
- # provided which is either a String, Symbol or Array of resource record
110
- # types which the rule matches against.
111
- #
112
- # match("www.google.com")
113
- # match("gmail.com", IN::MX)
114
- # match(/g?mail.(com|org|net)/, [IN::MX, IN::A])
115
- #
116
- def match(*pattern, &block)
117
- @rules << Rule.new(pattern, block)
118
- end
119
-
120
- # Register a named event which may be invoked later using #fire
121
- # on(:start) do |server|
122
- # RExec.change_user(RUN_AS)
123
- # end
124
- def on(event_name, &block)
125
- @events[event_name] = block
126
- end
127
-
128
- # Fire the named event, which must have been registered using on.
129
- def fire(event_name)
130
- callback = @events[event_name]
131
-
132
- if callback
133
- callback.call(self)
134
- end
135
- end
136
-
137
- # Specify a default block to execute if all other rules fail to match.
138
- # This block is typially used to pass the request on to another server
139
- # (i.e. recursive request).
140
- #
141
- # otherwise do |transaction|
142
- # transaction.passthrough!($R)
143
- # end
144
- #
145
- def otherwise(&block)
146
- @otherwise = block
147
- end
148
-
149
- def next!
150
- throw :next
151
- end
152
-
153
- # Give a name and a record type, try to match a rule and use it for
154
- # processing the given arguments.
155
- #
156
- # If a rule returns false, it is considered that the rule failed and
157
- # futher matching is carried out.
158
- def process(name, resource_class, *args)
159
- @logger.debug "Searching for #{name} #{resource_class.name}"
160
-
161
- @rules.each do |rule|
162
- @logger.debug "Checking rule #{rule}..."
163
-
164
- catch (:next) do
165
- # If the rule returns true, we assume that it was successful and no further rules need to be evaluated.
166
- return true if rule.call(self, name, resource_class, *args)
167
- end
168
- end
169
-
170
- if @otherwise
171
- @otherwise.call(*args)
172
- else
173
- @logger.warn "Failed to handle #{name} #{resource_class.name}!"
174
- end
175
- end
176
-
177
- # Process an incoming DNS message. Returns a serialized message to be
178
- # sent back to the client.
179
- def process_query(query, options = {}, &block)
180
- # Setup answer
181
- answer = Resolv::DNS::Message::new(query.id)
182
- answer.qr = 1 # 0 = Query, 1 = Response
183
- answer.opcode = query.opcode # Type of Query; copy from query
184
- answer.aa = 1 # Is this an authoritative response: 0 = No, 1 = Yes
185
- answer.rd = query.rd # Is Recursion Desired, copied from query
186
- answer.ra = 0 # Does name server support recursion: 0 = No, 1 = Yes
187
- answer.rcode = 0 # Response code: 0 = No errors
188
-
189
- # 1/ This chain contains a reverse list of question lambdas.
190
- chain = []
191
-
192
- # 4/ Finally, the answer is given back to the calling block:
193
- chain << lambda do
194
- @logger.debug "Passing answer back to caller..."
195
- yield answer
196
- end
197
-
198
- # There may be multiple questions per query
199
- query.question.reverse.each do |question, resource_class|
200
- next_link = chain.last
201
-
202
- chain << lambda do
203
- @logger.debug "Processing question #{question} #{resource_class}..."
204
-
205
- transaction = Transaction.new(self, query, question, resource_class, answer, options)
206
-
207
- # Call the next link in the chain:
208
- transaction.callback do
209
- # 3/ ... which calls the previous item in the chain, i.e. the next question to be answered:
210
- next_link.call
211
- end
212
-
213
- # If there was an error, log it and fail:
214
- transaction.errback do |response|
215
- if Exception === response
216
- @logger.error "Exception thrown while processing #{transaction}!"
217
- RubyDNS.log_exception(@logger, response)
218
- else
219
- @logger.error "Failure while processing #{transaction}!"
220
- @logger.error "#{response.inspect}"
221
- end
222
-
223
- answer.rcode = Resolv::DNS::RCode::ServFail
224
-
225
- chain.first.call
226
- end
227
-
228
- begin
229
- # Transaction.process will call succeed if it wasn't deferred:
230
- transaction.process
231
- rescue
232
- transaction.fail($!)
233
- end
234
- end
235
- end
236
-
237
- # 2/ We call the last lambda...
238
- chain.last.call
239
- end
240
- end
241
- end
@@ -1,146 +0,0 @@
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
- begin
22
- require 'win32/resolv'
23
- rescue LoadError
24
- # Ignore this - we aren't running on windows.
25
- end
26
-
27
- module RubyDNS
28
- # This module encapsulates system dependent name lookup functionality.
29
- module System
30
- RESOLV_CONF = "/etc/resolv.conf"
31
- HOSTS = "/etc/hosts"
32
-
33
- def self.hosts_path
34
- if RUBY_PLATFORM =~ /mswin32|mingw|bccwin/
35
- Win32::Resolv.get_hosts_path
36
- else
37
- HOSTS
38
- end
39
- end
40
-
41
- # This code is very experimental
42
- class Hosts
43
- def initialize
44
- @addresses = {}
45
- @names = {}
46
- end
47
-
48
- attr :addresses
49
- attr :names
50
-
51
- # This is used to match names against the list of known hosts:
52
- def call(name)
53
- @names.include?(name)
54
- end
55
-
56
- def lookup(name)
57
- addresses = @names[name]
58
-
59
- if addresses
60
- addresses.last
61
- else
62
- nil
63
- end
64
- end
65
-
66
- alias [] lookup
67
-
68
- def add(address, names)
69
- @addresses[address] ||= []
70
- @addresses[address] += names
71
-
72
- names.each do |name|
73
- @names[name] ||= []
74
- @names[name] << address
75
- end
76
- end
77
-
78
- def parse_hosts(io)
79
- io.each do |line|
80
- line.sub!(/#.*/, '')
81
- address, hostname, *aliases = line.split(/\s+/)
82
-
83
- add(address, [hostname] + aliases)
84
- end
85
- end
86
-
87
- def self.local
88
- hosts = self.new
89
-
90
- path = System::hosts_path
91
-
92
- if path and File.exist?(path)
93
- File.open(path) do |file|
94
- hosts.parse_hosts(file)
95
- end
96
- end
97
-
98
- return hosts
99
- end
100
- end
101
-
102
- def self.parse_resolv_configuration(path)
103
- nameservers = []
104
- File.open(path) do |file|
105
- file.each do |line|
106
- # Remove any comments:
107
- line.sub!(/[#;].*/, '')
108
-
109
- # Extract resolv.conf command:
110
- keyword, *args = line.split(/\s+/)
111
-
112
- case keyword
113
- when 'nameserver'
114
- nameservers += args
115
- end
116
- end
117
- end
118
-
119
- return nameservers
120
- end
121
-
122
- def self.standard_connections(nameservers)
123
- connections = []
124
-
125
- nameservers.each do |host|
126
- connections << [:udp, host, 53]
127
- connections << [:tcp, host, 53]
128
- end
129
-
130
- return connections
131
- end
132
-
133
- # Get a list of standard nameserver connections which can be used for querying any standard servers that the system has been configured with. There is no equivalent facility to use the `hosts` file at present.
134
- def self.nameservers
135
- nameservers = []
136
-
137
- if File.exist? RESOLV_CONF
138
- nameservers = parse_resolv_configuration(RESOLV_CONF)
139
- elsif defined?(Win32::Resolv) and RUBY_PLATFORM =~ /mswin32|cygwin|mingw|bccwin/
140
- search, nameservers = Win32::Resolv.get_resolv_info
141
- end
142
-
143
- return standard_connections(nameservers)
144
- end
145
- end
146
- end