rubydns 0.1.8

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.
@@ -0,0 +1,108 @@
1
+ # Copyright (c) 2009 Samuel Williams. Released under the GNU GPLv3.
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ # Thanks to "jmorgan" who provided some basic ideas for how to do this
17
+ # using Ruby: http://half-penny.org/computing/simple-ruby-dns-server
18
+
19
+ require 'rubydns/version'
20
+ require 'rubydns/resolv'
21
+ require 'rubydns/server'
22
+
23
+ require 'logger'
24
+
25
+ require 'rexec'
26
+ require 'rexec/daemon'
27
+
28
+ module RubyDNS
29
+
30
+ # Run a server with the given rules. A number of options can be supplied:
31
+ #
32
+ # <tt>:interfaces</tt>:: A set of sockets or addresses as defined below.
33
+ #
34
+ # One important feature of DNS is the port it runs on. The <tt>options[:listen]</tt>
35
+ # allows you to specify a set of network interfaces and ports to run the server on. This
36
+ # must be a list of <tt>[protocol, interface address, port]</tt>.
37
+ #
38
+ # INTERFACES = [[:udp, "0.0.0.0", 5300]]
39
+ # RubyDNS::run_server(:listen => INTERFACES) do
40
+ # ...
41
+ # end
42
+ #
43
+ # You can specify already connected sockets if need be:
44
+ #
45
+ # socket = UDPSocket.new; socket.bind("0.0.0.0", 53)
46
+ # Process::Sys.setuid(server_uid)
47
+ # INTERFACES = [socket]
48
+ #
49
+ # The default interface is <tt>[[:udp, "0.0.0.0", 53]]</tt>. The server typically needs
50
+ # to run as root for this to work, since port 53 is privileged.
51
+ #
52
+ def self.run_server (options = {}, &block)
53
+ server = RubyDNS::Server.new(&block)
54
+ threads = ThreadGroup.new
55
+
56
+ server.logger.info "Starting server..."
57
+
58
+ options[:listen] ||= [[:udp, "0.0.0.0", 53]]
59
+
60
+ sockets = []
61
+
62
+ # Setup server sockets
63
+ options[:listen].each do |spec|
64
+ if spec.kind_of?(BasicSocket)
65
+ sockets << spec
66
+ elsif spec[0] == :udp
67
+ socket = UDPSocket.new
68
+ socket.bind(spec[1], spec[2])
69
+
70
+ sockets << socket
71
+ elsif spec[0] == :tcp
72
+ server.logger.warn "Sorry, TCP is not currently supported!"
73
+ end
74
+ end
75
+
76
+ begin
77
+ # Listen for incoming packets
78
+ while true
79
+ ready = IO.select(sockets)
80
+
81
+ ready[0].each do |socket|
82
+ packet, sender = socket.recvfrom(1024*5)
83
+ server.logger.debug "Receiving incoming query..."
84
+
85
+ thr = Thread.new do
86
+ begin
87
+ result = server.receive_data(packet)
88
+
89
+ server.logger.debug "Sending result to #{sender.inspect}:"
90
+ server.logger.debug "#{result.inspect}"
91
+ socket.send(result, 0, sender[2], sender[1])
92
+ rescue
93
+ server.logger.error "Error processing request!"
94
+ server.logger.error "#{$!.class}: #{$!.message}"
95
+ $!.backtrace.each { |at| server.logger.error at }
96
+ end
97
+ end
98
+
99
+ threads.add thr
100
+ end
101
+ end
102
+ rescue Interrupt
103
+ server.logger.info "Server interrupted - stopping #{threads.list.size} request(s)."
104
+ threads.list.each { |thr| thr.join }
105
+ end
106
+ end
107
+ end
108
+
@@ -0,0 +1,76 @@
1
+ # Copyright (c) 2009 Samuel Williams. Released under the GNU GPLv3.
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ # Resolv rdoc
17
+ # http://www.ruby-doc.org/stdlib/libdoc/resolv/rdoc/index.html
18
+
19
+ require 'resolv'
20
+
21
+ class Resolv
22
+ class DNS
23
+ # Queries the given DNS server and returns its response in its entirety.
24
+ # This allows such responses to be passed upstream with little or no
25
+ # modification/reinterpretation.
26
+ def query(name, typeclass)
27
+ lazy_initialize
28
+ requester = make_requester
29
+ senders = {}
30
+ begin
31
+ @config.resolv(name) {|candidate, tout, nameserver|
32
+ msg = Message.new
33
+ msg.rd = 1
34
+ msg.add_question(candidate, typeclass)
35
+ unless sender = senders[[candidate, nameserver]]
36
+ sender = senders[[candidate, nameserver]] =
37
+ requester.sender(msg, candidate, nameserver)
38
+ end
39
+ reply, reply_name = requester.request(sender, tout)
40
+
41
+ return reply, reply_name
42
+ }
43
+ ensure
44
+ requester.close
45
+ end
46
+ end
47
+
48
+ class Message
49
+ # Merge the given message with this message. A number of heuristics are
50
+ # applied in order to ensure that the result makes sense. For example,
51
+ # If the current message is not recursive but is being merged with a
52
+ # message that was recursive, this bit is maintained. If either message
53
+ # is authoritive, then the result is also authoritive.
54
+ #
55
+ # Modifies the current message in place.
56
+ def merge! (other)
57
+ # Authoritive Answer
58
+ @aa = @aa && other.aa
59
+
60
+ @additional += other.additional
61
+ @answer += other.answer
62
+ @authority += other.authority
63
+ @question += other.question
64
+
65
+ # Recursion Available
66
+ @ra = @ra || other.ra
67
+
68
+ # Result Code (Error Code)
69
+ @rcode = other.rcode unless other.rcode == 0
70
+
71
+ # Recursion Desired
72
+ @rd = @rd || other.rd
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,170 @@
1
+ # Copyright (c) 2009 Samuel Williams. Released under the GNU GPLv3.
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ require 'rubydns/transaction'
17
+
18
+ module RubyDNS
19
+
20
+ # This class provides the core of the DSL. It contains a list of rules which
21
+ # are used to match against incoming DNS questions. These rules are used to
22
+ # generate responses which are either DNS resource records or failures.
23
+ class Server
24
+
25
+ # Instantiate a server with a block
26
+ #
27
+ # server = Server.new do
28
+ # match(/server.mydomain.com/, :A) do |transaction|
29
+ # transaction.respond!("1.2.3.4")
30
+ # end
31
+ # end
32
+ #
33
+ def initialize(&block)
34
+ @rules = []
35
+ @otherwise = nil
36
+
37
+ @logger = Logger.new($stderr)
38
+
39
+ if block_given?
40
+ instance_eval &block
41
+ end
42
+ end
43
+
44
+ attr :logger, true
45
+
46
+ # This function connects a pattern with a block. A pattern is either
47
+ # a String or a Regex instance. Optionally, a second argument can be
48
+ # provided which is either a String, Symbol or Array of resource record
49
+ # types which the rule matches against.
50
+ #
51
+ # match("www.google.com")
52
+ # match("gmail.com", :MX)
53
+ # match(/g?mail.(com|org|net)/, [:MX, :A])
54
+ #
55
+ def match (*pattern, &block)
56
+ # Normalize pattern
57
+ case pattern[1]
58
+ when nil
59
+ # Do nothing
60
+ when String
61
+ pattern[1] = pattern[1].upcase
62
+ when Symbol
63
+ pattern[1] = pattern[1].to_s.upcase
64
+ when Array
65
+ pattern[1] = pattern[1].collect { |v| v.to_s.upcase }
66
+ end
67
+
68
+ @rules << [pattern, Proc.new(&block)]
69
+ end
70
+
71
+ # Specify a default block to execute if all other rules fail to match.
72
+ # This block is typially used to pass the request on to another server
73
+ # (i.e. recursive request).
74
+ #
75
+ # otherwise do |transaction|
76
+ # transaction.passthrough!($R)
77
+ # end
78
+ #
79
+ def otherwise(&block)
80
+ @otherwise = Proc.new(&block)
81
+ end
82
+
83
+ # Give a name and a record type, try to match a rule and use it for
84
+ # processing the given arguments.
85
+ #
86
+ # If a rule returns false, it is considered that the rule failed and
87
+ # futher matching is carried out.
88
+ def process(name, record_type, *args)
89
+ @logger.debug "Searching for #{name} #{record_type}"
90
+
91
+ @rules.each do |rule|
92
+ @logger.debug "Checking rule #{rule[0].inspect}..."
93
+
94
+ pattern = rule[0]
95
+
96
+ # Match failed against record_type?
97
+ case pattern[1]
98
+ when String
99
+ next if pattern[1] != record_type
100
+ @logger.debug "Resource type #{record_type} matched"
101
+ when Array
102
+ next if pattern[1].include?(record_type)
103
+ @logger.debug "Resource type #{record_type} matched #{pattern[1].inspect}"
104
+ end
105
+
106
+ # Match succeeded against name?
107
+ case pattern[0]
108
+ when Regexp
109
+ match_data = pattern[0].match(name)
110
+ if match_data
111
+ @logger.debug "Query #{name} matched #{pattern[0].to_s} with result #{match_data.inspect}"
112
+ if rule[1].call(match_data, *args)
113
+ @logger.debug "Rule returned successfully"
114
+ return
115
+ end
116
+ else
117
+ @logger.debug "Query #{name} failed to match against #{pattern[0].to_s}"
118
+ end
119
+ when String
120
+ if pattern[0] == name
121
+ @logger.debug "Query #{name} matched #{pattern[0]}"
122
+ if rule[1].call(*args)
123
+ @logger.debug "Rule returned successfully"
124
+ return
125
+ end
126
+ else
127
+ @logger.debug "Query #{name} failed to match against #{pattern[0]}"
128
+ end
129
+ end
130
+ end
131
+
132
+ if @otherwise
133
+ @otherwise.call(*args)
134
+ else
135
+ @logger.warn "Failed to handle #{name} #{record_type}!"
136
+ end
137
+ end
138
+
139
+ # Process an incoming DNS message. Returns a serialized message to be
140
+ # sent back to the client.
141
+ def receive_data(data)
142
+ query = Resolv::DNS::Message::decode(data)
143
+
144
+ # Setup answer
145
+ answer = Resolv::DNS::Message::new(query.id)
146
+ answer.qr = 1 # 0 = Query, 1 = Response
147
+ answer.opcode = query.opcode # Type of Query; copy from query
148
+ answer.aa = 1 # Is this an authoritative response: 0 = No, 1 = Yes
149
+ answer.rd = query.rd # Is Recursion Desired, copied from query
150
+ answer.ra = 0 # Does name server support recursion: 0 = No, 1 = Yes
151
+ answer.rcode = 0 # Response code: 0 = No errors
152
+
153
+ query.each_question do |question, resource_class| # There may be multiple questions per query
154
+ transaction = Transaction.new(self, query, question, resource_class, answer)
155
+
156
+ begin
157
+ transaction.process
158
+ rescue
159
+ @logger.error "Exception thrown while processing #{transaction}!"
160
+ @logger.error "#{$!.class}: #{$!.message}"
161
+ $!.backtrace.each { |at| @logger.error at }
162
+
163
+ answer.rcode = Resolv::DNS::RCode::ServFail
164
+ end
165
+ end
166
+
167
+ return answer.encode
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,208 @@
1
+ # Copyright (c) 2009 Samuel Williams. Released under the GNU GPLv3.
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ module RubyDNS
17
+
18
+ # Turn a symbol or string name into a resource class. For example,
19
+ # convert <tt>:A</tt> into <tt>Resolv::DNS::Resource::IN::A</tt>
20
+ def self.lookup_resource_class(klass)
21
+ return nil if klass == nil
22
+
23
+ if Symbol === klass
24
+ klass = klass.to_s
25
+ end
26
+
27
+ if String === klass
28
+ if Resolv::DNS::Resource.const_defined?(klass)
29
+ return Resolv::DNS::Resource.const_get(klass)
30
+ elsif Resolv::DNS::Resource::IN.const_defined?(klass)
31
+ return Resolv::DNS::Resource::IN.const_get(klass)
32
+ end
33
+ end
34
+
35
+ return klass
36
+ end
37
+
38
+ # This class provides all details of a single DNS question and answer. This
39
+ # is used by the DSL to provide DNS related functionality.
40
+ class Transaction
41
+ def initialize(server, query, question, resource_class, answer)
42
+ @server = server
43
+ @query = query
44
+ @question = question
45
+ @resource_class = resource_class
46
+ @answer = answer
47
+
48
+ @question_appended = false
49
+ end
50
+
51
+ # The resource_class that was requested. This is typically used to generate a
52
+ # response.
53
+ attr :resource_class
54
+
55
+ # The incoming query which is a set of questions.
56
+ attr :query
57
+
58
+ # The question that this transaction represents.
59
+ attr :question
60
+
61
+ # The current full answer to the incoming query.
62
+ attr :answer
63
+
64
+ # Return the type of record (eg. <tt>A</tt>, <tt>MX</tt>) as a <tt>String</tt>.
65
+ def record_type
66
+ @resource_class.name.split("::").last
67
+ end
68
+
69
+ # Return the name of the question, which is typically the requested hostname.
70
+ def name
71
+ @question.to_s
72
+ end
73
+
74
+ # Suitable for debugging purposes
75
+ def to_s
76
+ "#{name} #{record_type}"
77
+ end
78
+
79
+ # Run a new query through the rules with the given name and resource type. The
80
+ # results of this query are appended to the current transactions <tt>answer</tt>.
81
+ def append_query!(name, resource_type = nil)
82
+ Transaction.new(@server, @query, name, RubyDNS.lookup_resource_class(resource_type) || @resource_class, @answer).process
83
+ end
84
+
85
+ def process
86
+ @server.process(name, record_type, self)
87
+ end
88
+
89
+ # Use the given resolver to respond to the question. This will <tt>query</tt>
90
+ # the resolver and <tt>merge!</tt> the answer if one is received. If recursion is
91
+ # not requested, the result is <tt>failure!(:Refused)</tt>. If the resolver does
92
+ # not respond, the result is <tt>failure!(:NXDomain)</tt>
93
+ #
94
+ # If a block is supplied, this function yields with the reply and reply_name if
95
+ # successful. This could be used, for example, to update a cache.
96
+ def passthrough! (resolver, &block)
97
+ # Were we asked to recursively find this name?
98
+ if @query.rd
99
+ reply, reply_name = resolver.query(name, resource_class)
100
+
101
+ if reply
102
+ if block_given?
103
+ yield(reply, reply_name)
104
+ end
105
+
106
+ @answer.merge!(reply)
107
+ else
108
+ failure!(:NXDomain)
109
+ end
110
+ else
111
+ failure!(:Refused)
112
+ end
113
+
114
+ true
115
+ end
116
+
117
+ # Respond to the given query with a resource record. The arguments to this
118
+ # function depend on the <tt>resource_class</tt> requested. The last argument
119
+ # can optionally be a hash of options.
120
+ #
121
+ # <tt>options[:resource_class]</tt>:: Override the default <tt>resource_class</tt>
122
+ # <tt>options[:ttl]</tt>:: Specify the TTL for the resource
123
+ # <tt>options[:name]</tt>:: Override the name (question) of the response.
124
+ #
125
+ # for A records:: <tt>respond!("1.2.3.4")</tt>
126
+ # for MX records:: <tt>respond!("mail.blah.com", 10)</tt>
127
+ #
128
+ # This function instantiates the resource class with the supplied arguments, and
129
+ # then passes it to <tt>append!</tt>.
130
+ #
131
+ # See <tt>Resolv::DNS::Resource</tt> for more information about the various
132
+ # <tt>resource_class</tt>s available.
133
+ # http://www.ruby-doc.org/stdlib/libdoc/resolv/rdoc/index.html
134
+ def respond! (*data)
135
+ options = data.last.kind_of?(Hash) ? data.pop : {}
136
+
137
+ case options[:resource_class]
138
+ when nil
139
+ append!(@resource_class.new(*data), options)
140
+ when Class
141
+ append!(options[:resource_class].new(*data), options)
142
+ else
143
+ raise ArgumentError, "Could not instantiate resource!"
144
+ end
145
+ end
146
+
147
+ # Append a given set of resources to the answer. The last argument can
148
+ # optionally be a hash of options.
149
+ #
150
+ # <tt>options[:ttl]</tt>:: Specify the TTL for the resource
151
+ # <tt>options[:name]</tt>:: Override the name (question) of the response.
152
+ #
153
+ # This function can be used to supply multiple responses to a given question.
154
+ # For example, each argument is expected to be an instantiated resource from
155
+ # <tt>Resolv::DNS::Resource</tt> module.
156
+ def append! (*resources)
157
+ append_question!
158
+
159
+ options = resources.last.kind_of?(Hash) ? resources.pop.dup : {}
160
+ options[:ttl] ||= 16000
161
+ options[:name] ||= @question.to_s + "."
162
+
163
+ resources.each do |resource|
164
+ @answer.add_answer(options[:name], options[:ttl], resource)
165
+ end
166
+
167
+ @answer.encode
168
+
169
+ true
170
+ end
171
+
172
+ # This function indicates that there was a failure to resolve the given
173
+ # question. The single argument must be an integer error code, typically
174
+ # given by the constants in <tt>Resolv::DNS::RCode</tt>.
175
+ #
176
+ # The easiest way to use this function it to simply supply a symbol. Here is
177
+ # a list of the most commonly used ones:
178
+ #
179
+ # <tt>:NoError</tt>:: No error occurred.
180
+ # <tt>:FormErr</tt>:: The incoming data was not formatted correctly.
181
+ # <tt>:ServFail</tt>:: The operation caused a server failure (internal error, etc).
182
+ # <tt>:NXDomain</tt>:: Non-eXistant Domain (domain record does not exist).
183
+ # <tt>:NotImp</tt>:: The operation requested is not implemented.
184
+ # <tt>:Refused</tt>:: The operation was refused by the server.
185
+ # <tt>:NotAuth</tt>:: The server is not authoritive for the zone.
186
+ #
187
+ # See http://www.rfc-editor.org/rfc/rfc2929.txt for more information
188
+ # about DNS error codes (specifically, page 3).
189
+ def failure! (rcode)
190
+ append_question!
191
+
192
+ if rcode.kind_of? Symbol
193
+ @answer.rcode = Resolv::DNS::RCode.const_get(rcode)
194
+ else
195
+ @answer.rcode = rcode.to_i
196
+ end
197
+
198
+ true
199
+ end
200
+
201
+ protected
202
+ def append_question!
203
+ if @answer.question.size == 0
204
+ @answer.add_question(@question, @resource_class) unless @question_appended
205
+ end
206
+ end
207
+ end
208
+ end