rubydns 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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