rubydns 0.5.0 → 0.5.1

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.
data/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  RubyDNS
2
2
  =======
3
3
 
4
- * Author: Samuel G. D. Williams (<http://www.oriontransfer.co.nz>)
5
- * Copyright (C) 2009, 2011 Samuel G. D. Williams.
6
4
  * Released under the MIT license.
5
+ * Copyright (C) 2009, 2011 [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams/).
7
6
  * [![Build Status](https://secure.travis-ci.org/ioquatix/rubydns.png)](http://travis-ci.org/ioquatix/rubydns)
8
7
 
9
8
  RubyDNS is a simple programmatic DSL (domain specific language) for configuring and running a DNS server. RubyDNS provides a daemon that runs a DNS server which can process DNS requests depending on specific policy. Rule selection is based on pattern matching, and results can be hard-coded, computed, fetched from a remote DNS server, fetched from a local cache, etc.
@@ -63,8 +62,6 @@ After starting this server you can test it using dig:
63
62
  Compatibility
64
63
  -------------
65
64
 
66
- From RubyDNS version `0.4.0`, the recommended minimum Ruby version is `1.9.3` for complete support. Some features may not work as expected on Ruby version `1.8.x` and it is not tested significantly.
67
-
68
65
  ### Migrating from RubyDNS 0.3.x to 0.4.x ###
69
66
 
70
67
  Due to changes in `resolv.rb`, superficial parts of RubyDNS have changed. Rather than using `:A` to specify A-records, one must now use the class name.
@@ -76,12 +73,68 @@ becomes
76
73
  IN = Resolv::DNS::Resource::IN
77
74
  match(..., IN::A)
78
75
 
76
+ ### Migrating from RubyDNS 0.4.x to 0.5.x ###
77
+
78
+ The system standard resolver was synchronous, and this could stall the server when making upstream requests to other DNS servers. A new resolver `RubyDNS::Resolver` now provides an asynchronous interface and the `Transaction::passthrough` makes exclusive use of this to provide high performance asynchonous resolution.
79
+
80
+ Here is a basic example of how to use the new resolver in full. It is important to provide both `:udp` and `:tcp` connection specifications, so that large requests will be handled correctly:
81
+
82
+ resolver = RubyDNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]])
83
+
84
+ EventMachine::run do
85
+ resolver.query('google.com', IN::A) do |response|
86
+ case response
87
+ when RubyDNS::Message
88
+ puts "Got response: #{response.answers.first}"
89
+ else
90
+ # Response is of class RubyDNS::ResolutionFailure
91
+ puts "Failed: #{response.message}"
92
+ end
93
+
94
+ EventMachine::stop
95
+ end
96
+ end
97
+
98
+ Existing code that uses `Resolv::DNS` as a resolver will need to be updated:
99
+
100
+ # 1/ Add this at the top of your file; Host specific system information:
101
+ require 'rubydns/system'
102
+
103
+ # 2/ Change from R = Resolv::DNS.new to:
104
+ R = RubyDNS::Resolver.new(RubyDNS::System::nameservers)
105
+
106
+ Everything else in the server can remain the same. You can see a complete example in `test/test_resolver.rb`.
107
+
108
+ #### Deferred Transactions ####
109
+
110
+ The implementation of the above depends on a new feature which was added in 0.5.0:
111
+
112
+ transaction.defer!
113
+
114
+ Once you call this, the transaction won't complete until you call either `transaction.succeed` or `transaction.fail`.
115
+
116
+ RubyDNS::run_server(:listen => SERVER_PORTS) do
117
+ match(/\.*.com/, IN::A) do |match, transaction|
118
+ transaction.defer!
119
+
120
+ # No domain exists, after 5 seconds:
121
+ EventMachine::Timer.new(5) do
122
+ transaction.failure!(:NXDomain)
123
+ end
124
+ end
125
+
126
+ otherwise do
127
+ transaction.failure!(:NXDomain)
128
+ end
129
+ end
130
+
131
+ You can see a complete example in `test/test_slow_server.rb`.
132
+
79
133
  Todo
80
134
  ----
81
135
 
82
- * Support for more features of DNS such as zone transfer
83
- * Support reverse records more easily
84
- * Better support for deferred requests/concurrency.
136
+ * Support for more features of DNS such as zone transfer.
137
+ * Support reverse records more easily.
85
138
 
86
139
  License
87
140
  -------
data/lib/rubydns.rb CHANGED
@@ -21,16 +21,14 @@
21
21
  require 'rubydns/version'
22
22
 
23
23
  if RUBY_VERSION < "1.9"
24
- require 'rubydns/extensions/resolv-1.8'
25
24
  require 'rubydns/extensions/string-1.8'
26
25
  elsif RUBY_VERSION < "1.9.3"
27
- require 'rubydns/extensions/resolv-1.9'
28
26
  require 'rubydns/extensions/string-1.9.2'
29
27
  else
30
- require 'rubydns/extensions/resolv-1.9'
31
28
  require 'rubydns/extensions/string-1.9.3'
32
29
  end
33
30
 
31
+ require 'rubydns/message'
34
32
  require 'rubydns/server'
35
33
  require 'rubydns/resolver'
36
34
  require 'rubydns/handler'
@@ -22,31 +22,6 @@ require 'resolv'
22
22
 
23
23
  class Resolv
24
24
  class DNS
25
- # Queries the given DNS server and returns its response in its entirety.
26
- # This allows such responses to be passed upstream with little or no
27
- # modification/reinterpretation.
28
- def query(name, typeclass)
29
- lazy_initialize
30
- requester = make_requester
31
- senders = {}
32
- begin
33
- @config.resolv(name) {|candidate, tout, nameserver|
34
- msg = Message.new
35
- msg.rd = 1
36
- msg.add_question(candidate, typeclass)
37
- unless sender = senders[[candidate, nameserver]]
38
- sender = senders[[candidate, nameserver]] =
39
- requester.sender(msg, candidate, nameserver)
40
- end
41
- reply, reply_name = requester.request(sender, tout)
42
-
43
- return reply, reply_name
44
- }
45
- ensure
46
- requester.close
47
- end
48
- end
49
-
50
25
  class Message
51
26
  # Merge the given message with this message. A number of heuristics are
52
27
  # applied in order to ensure that the result makes sense. For example,
@@ -21,19 +21,24 @@
21
21
  require 'rubydns/message'
22
22
 
23
23
  module RubyDNS
24
+
25
+ def self.get_peer_details(connection)
26
+ Socket.unpack_sockaddr_in(connection.get_peername)[1]
27
+ end
28
+
24
29
  module UDPHandler
25
30
  def initialize(server)
26
31
  @server = server
27
32
  end
28
33
 
29
- def self.process(server, data, &block)
34
+ def self.process(server, data, options = {}, &block)
30
35
  server.logger.debug "Receiving incoming query (#{data.bytesize} bytes)..."
31
36
  query = nil
32
37
 
33
38
  begin
34
39
  query = RubyDNS::decode_message(data)
35
40
 
36
- return server.process_query(query, &block)
41
+ return server.process_query(query, options, &block)
37
42
  rescue
38
43
  server.logger.error "Error processing request!"
39
44
  server.logger.error "#{$!.class}: #{$!.message}"
@@ -56,7 +61,9 @@ module RubyDNS
56
61
  end
57
62
 
58
63
  def receive_data(data)
59
- UDPHandler.process(@server, data) do |answer|
64
+ options = {:peer => RubyDNS::get_peer_details(self)}
65
+
66
+ UDPHandler.process(@server, data, options) do |answer|
60
67
  data = answer.encode
61
68
 
62
69
  @server.logger.debug "Writing response to client (#{data.bytesize} bytes) via UDP..."
@@ -107,7 +114,9 @@ module RubyDNS
107
114
  if (@buffer.size - @processed) >= @length
108
115
  data = @buffer.string.byteslice(@processed, @length)
109
116
 
110
- UDPHandler.process(@server, data) do |answer|
117
+ options = {:peer => RubyDNS::get_peer_details(self)}
118
+
119
+ UDPHandler.process(@server, data, options) do |answer|
111
120
  data = answer.encode
112
121
 
113
122
  @server.logger.debug "Writing response to client (#{data.bytesize} bytes) via TCP..."
@@ -22,6 +22,8 @@ require 'eventmachine'
22
22
  require 'stringio'
23
23
  require 'resolv'
24
24
 
25
+ require 'rubydns/extensions/resolv'
26
+
25
27
  module RubyDNS
26
28
  UDP_TRUNCATION_SIZE = 512
27
29
 
@@ -26,6 +26,61 @@ module RubyDNS
26
26
  # are used to match against incoming DNS questions. These rules are used to
27
27
  # generate responses which are either DNS resource records or failures.
28
28
  class Server
29
+ class Rule
30
+ def initialize(pattern, callback)
31
+ @pattern = pattern
32
+ @callback = callback
33
+ end
34
+
35
+ def match(name, resource_class)
36
+ # If the pattern doesn't specify any resource classes, we implicitly pass this test:
37
+ return true if @pattern.size < 2
38
+
39
+ # Otherwise, we try to match against some specific resource classes:
40
+ if Class === @pattern[1]
41
+ @pattern[1] == resource_class
42
+ else
43
+ @pattern[1].include?(resource_class) rescue false
44
+ end
45
+ end
46
+
47
+ def call(server, name, resource_class, *args)
48
+ unless match(name, resource_class)
49
+ server.logger.debug "Resource class #{resource_class} failed to match #{@pattern[1].inspect}!"
50
+
51
+ return false
52
+ end
53
+
54
+ # Match succeeded against name?
55
+ case @pattern[0]
56
+ when Regexp
57
+ match_data = @pattern[0].match(name)
58
+ if match_data
59
+ server.logger.debug "Regexp pattern matched with #{match_data.inspect}."
60
+ return @callback[match_data, *args]
61
+ end
62
+ when String
63
+ if @pattern[0] == name
64
+ server.logger.debug "String pattern matched."
65
+ return @callback[*args]
66
+ end
67
+ else
68
+ if (@pattern[0].call(name, resource_class) rescue false)
69
+ server.logger.debug "Callable pattern matched."
70
+ return @callback[*args]
71
+ end
72
+ end
73
+
74
+ server.logger.debug "No pattern matched."
75
+ # We failed to match the pattern.
76
+ return false
77
+ end
78
+
79
+ def to_s
80
+ @pattern.inspect
81
+ end
82
+ end
83
+
29
84
  # Instantiate a server with a block
30
85
  #
31
86
  # server = Server.new do
@@ -58,7 +113,7 @@ module RubyDNS
58
113
  # match(/g?mail.(com|org|net)/, [IN::MX, IN::A])
59
114
  #
60
115
  def match(*pattern, &block)
61
- @rules << [pattern, Proc.new(&block)]
116
+ @rules << Rule.new(pattern, block)
62
117
  end
63
118
 
64
119
  # Register a named event which may be invoked later using #fire
@@ -66,7 +121,7 @@ module RubyDNS
66
121
  # RExec.change_user(RUN_AS)
67
122
  # end
68
123
  def on(event_name, &block)
69
- @events[event_name] = Proc.new(&block)
124
+ @events[event_name] = block
70
125
  end
71
126
 
72
127
  # Fire the named event, which must have been registered using on.
@@ -87,7 +142,11 @@ module RubyDNS
87
142
  # end
88
143
  #
89
144
  def otherwise(&block)
90
- @otherwise = Proc.new(&block)
145
+ @otherwise = block
146
+ end
147
+
148
+ def next!
149
+ throw :next
91
150
  end
92
151
 
93
152
  # Give a name and a record type, try to match a rule and use it for
@@ -99,55 +158,11 @@ module RubyDNS
99
158
  @logger.debug "Searching for #{name} #{resource_class.name}"
100
159
 
101
160
  @rules.each do |rule|
102
- @logger.debug "Checking rule #{rule[0].inspect}..."
103
-
104
- pattern = rule[0]
105
-
106
- # Match failed against resource_class?
107
- case pattern[1]
108
- when Class
109
- next unless pattern[1] == resource_class
110
- @logger.debug "Resource class #{resource_class.name} matched"
111
- when Array
112
- next unless pattern[1].include?(resource_class)
113
- @logger.debug "Resource class #{resource_class} matched #{pattern[1].inspect}"
114
- end
161
+ @logger.debug "Checking rule #{rule}..."
115
162
 
116
- # Match succeeded against name?
117
- case pattern[0]
118
- when Regexp
119
- match_data = pattern[0].match(name)
120
- if match_data
121
- @logger.debug "Query #{name} matched #{pattern[0].to_s} with result #{match_data.inspect}"
122
- if rule[1].call(match_data, *args)
123
- @logger.debug "Rule returned successfully"
124
- return
125
- end
126
- else
127
- @logger.debug "Query #{name} failed to match against #{pattern[0].to_s}"
128
- end
129
- when String
130
- if pattern[0] == name
131
- @logger.debug "Query #{name} matched #{pattern[0]}"
132
- if rule[1].call(*args)
133
- @logger.debug "Rule returned successfully"
134
- return
135
- end
136
- else
137
- @logger.debug "Query #{name} failed to match against #{pattern[0]}"
138
- end
139
- else
140
- if pattern[0].respond_to? :call
141
- if pattern[0].call(name)
142
- @logger.debug "Query #{name} matched #{pattern[0]}"
143
- if rule[1].call(*args)
144
- @logger.debug "Rule returned successfully"
145
- return
146
- end
147
- else
148
- @logger.debug "Query #{name} failed to match against #{pattern[0]}"
149
- end
150
- end
163
+ catch (:next) do
164
+ # If the rule returns true, we assume that it was successful and no further rules need to be evaluated.
165
+ return true if rule.call(self, name, resource_class, *args)
151
166
  end
152
167
  end
153
168
 
@@ -160,7 +175,7 @@ module RubyDNS
160
175
 
161
176
  # Process an incoming DNS message. Returns a serialized message to be
162
177
  # sent back to the client.
163
- def process_query(query, &block)
178
+ def process_query(query, options = {}, &block)
164
179
  # Setup answer
165
180
  answer = Resolv::DNS::Message::new(query.id)
166
181
  answer.qr = 1 # 0 = Query, 1 = Response
@@ -186,7 +201,7 @@ module RubyDNS
186
201
  chain << lambda do
187
202
  @logger.debug "Processing question #{question} #{resource_class}..."
188
203
 
189
- transaction = Transaction.new(self, query, question, resource_class, answer)
204
+ transaction = Transaction.new(self, query, question, resource_class, answer, options)
190
205
 
191
206
  # Call the next link in the chain:
192
207
  transaction.callback do
@@ -196,9 +211,16 @@ module RubyDNS
196
211
 
197
212
  # If there was an error, log it and fail:
198
213
  transaction.errback do |response|
199
- @logger.error "Exception thrown while processing #{transaction}!"
200
- @logger.error "#{response.class}: #{response.message}"
201
- response.backtrace.each { |at| @logger.error at }
214
+ if Exception === response
215
+ @logger.error "Exception thrown while processing #{transaction}!"
216
+ @logger.error "#{response.class}: #{response.message}"
217
+ if response.backtrace
218
+ Array(response.backtrace).each { |at| @logger.error at }
219
+ end
220
+ else
221
+ @logger.error "Failure while processing #{transaction}!"
222
+ @logger.error "#{response.inspect}"
223
+ end
202
224
 
203
225
  answer.rcode = Resolv::DNS::RCode::ServFail
204
226
 
@@ -27,13 +27,15 @@ module RubyDNS
27
27
  class Transaction
28
28
  include EventMachine::Deferrable
29
29
 
30
- def initialize(server, query, question, resource_class, answer)
30
+ def initialize(server, query, question, resource_class, answer, options = {})
31
31
  @server = server
32
32
  @query = query
33
33
  @question = question
34
34
  @resource_class = resource_class
35
35
  @answer = answer
36
36
 
37
+ @options = options
38
+
37
39
  @deferred = false
38
40
  @question_appended = false
39
41
  end
@@ -51,6 +53,9 @@ module RubyDNS
51
53
  # The current full answer to the incoming query.
52
54
  attr :answer
53
55
 
56
+ # Any options or configuration associated with the given transaction.
57
+ attr :options
58
+
54
59
  # Return the name of the question, which is typically the requested hostname.
55
60
  def name
56
61
  @question.to_s
@@ -63,8 +68,8 @@ module RubyDNS
63
68
 
64
69
  # Run a new query through the rules with the given name and resource type. The
65
70
  # results of this query are appended to the current transactions <tt>answer</tt>.
66
- def append_query!(name, resource_class = nil)
67
- Transaction.new(@server, @query, name, resource_class || @resource_class, @answer).process
71
+ def append_query!(name, resource_class = nil, options = {})
72
+ Transaction.new(@server, @query, name, resource_class || @resource_class, @answer, options).process
68
73
  end
69
74
 
70
75
  def process(&finished)
@@ -93,6 +98,8 @@ module RubyDNS
93
98
  end
94
99
 
95
100
  @answer.merge!(response)
101
+
102
+ succeed if @deferred
96
103
  end
97
104
 
98
105
  true
@@ -117,8 +124,10 @@ module RubyDNS
117
124
  case response
118
125
  when RubyDNS::Message
119
126
  yield response
120
- succeed(response)
127
+ when RubyDNS::ResolverFailure
128
+ failure!(:ServFail)
121
129
  else
130
+ # This shouldn't ever happen, but if it does for some reason we shouldn't hang.
122
131
  fail(response)
123
132
  end
124
133
  end
@@ -166,6 +175,8 @@ module RubyDNS
166
175
  #
167
176
  # <tt>options[:ttl]</tt>:: Specify the TTL for the resource
168
177
  # <tt>options[:name]</tt>:: Override the name (question) of the response.
178
+ # <tt>options[:section]</tt>:: Specify whether the response should go in the `:answer`
179
+ # `:authority` or `:additional` section.
169
180
  #
170
181
  # This function can be used to supply multiple responses to a given question.
171
182
  # For example, each argument is expected to be an instantiated resource from
@@ -173,17 +184,27 @@ module RubyDNS
173
184
  def append! (*resources)
174
185
  append_question!
175
186
 
176
- options = resources.last.kind_of?(Hash) ? resources.pop.dup : {}
187
+ if resources.last.kind_of?(Hash)
188
+ options = resources.pop
189
+ else
190
+ options = {}
191
+ end
192
+
193
+ # Use the default options if provided:
194
+ options = options.merge(@options)
195
+
177
196
  options[:ttl] ||= 16000
178
197
  options[:name] ||= @question.to_s + "."
198
+
199
+ method = ("add_" + (options[:section] || 'answer').to_s).to_sym
179
200
 
180
201
  resources.each do |resource|
181
- @server.logger.debug "add_answer: #{resource.inspect} #{resource.class::TypeValue} #{resource.class::ClassValue}"
182
- @answer.add_answer(options[:name], options[:ttl], resource)
202
+ @server.logger.debug "#{method}: #{resource.inspect} #{resource.class::TypeValue} #{resource.class::ClassValue}"
203
+
204
+ @answer.send(method, options[:name], options[:ttl], resource)
183
205
  end
184
206
 
185
- # Raise an exception if there was something wrong with the resource
186
- @answer.encode
207
+ succeed if @deferred
187
208
 
188
209
  true
189
210
  end
@@ -214,6 +235,9 @@ module RubyDNS
214
235
  @answer.rcode = rcode.to_i
215
236
  end
216
237
 
238
+ # The transaction itself has completed, but contains a failure:
239
+ succeed(rcode) if @deferred
240
+
217
241
  true
218
242
  end
219
243