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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +0 -2
  3. data/.simplecov +15 -0
  4. data/.travis.yml +13 -7
  5. data/Gemfile +5 -6
  6. data/README.md +118 -41
  7. data/Rakefile +8 -3
  8. data/celluloid-dns.gemspec +29 -18
  9. data/lib/celluloid/dns.rb +30 -7
  10. data/lib/celluloid/dns/chunked.rb +34 -0
  11. data/lib/celluloid/dns/extensions/resolv.rb +136 -0
  12. data/lib/celluloid/dns/extensions/string.rb +28 -0
  13. data/lib/celluloid/dns/handler.rb +198 -0
  14. data/lib/celluloid/dns/logger.rb +31 -0
  15. data/lib/celluloid/dns/message.rb +76 -0
  16. data/lib/celluloid/dns/replace.rb +54 -0
  17. data/lib/celluloid/dns/resolver.rb +288 -0
  18. data/lib/celluloid/dns/server.rb +151 -27
  19. data/lib/celluloid/dns/system.rb +146 -0
  20. data/lib/celluloid/dns/transaction.rb +202 -0
  21. data/lib/celluloid/dns/transport.rb +75 -0
  22. data/lib/celluloid/dns/version.rb +23 -3
  23. data/spec/celluloid/dns/celluloid_bug_spec.rb +92 -0
  24. data/spec/celluloid/dns/hosts.txt +2 -0
  25. data/spec/celluloid/dns/ipv6_spec.rb +70 -0
  26. data/spec/celluloid/dns/message_spec.rb +56 -0
  27. data/spec/celluloid/dns/origin_spec.rb +106 -0
  28. data/spec/celluloid/dns/replace_spec.rb +42 -0
  29. data/spec/celluloid/dns/resolver_performance_spec.rb +110 -0
  30. data/spec/celluloid/dns/resolver_spec.rb +152 -0
  31. data/spec/celluloid/dns/server/bind9/generate-local.rb +25 -0
  32. data/spec/celluloid/dns/server/bind9/local.zone +5014 -0
  33. data/spec/celluloid/dns/server/bind9/named.conf +14 -0
  34. data/spec/celluloid/dns/server/bind9/named.run +0 -0
  35. data/spec/celluloid/dns/server/million.rb +85 -0
  36. data/spec/celluloid/dns/server_performance_spec.rb +139 -0
  37. data/spec/celluloid/dns/slow_server_spec.rb +91 -0
  38. data/spec/celluloid/dns/socket_spec.rb +71 -0
  39. data/spec/celluloid/dns/system_spec.rb +60 -0
  40. data/spec/celluloid/dns/transaction_spec.rb +138 -0
  41. data/spec/celluloid/dns/truncation_spec.rb +62 -0
  42. metadata +124 -56
  43. data/.coveralls.yml +0 -1
  44. data/.gitignore +0 -17
  45. data/CHANGES.md +0 -3
  46. data/LICENSE.txt +0 -22
  47. data/lib/celluloid/dns/request.rb +0 -46
  48. data/spec/celluloid/dns/server_spec.rb +0 -26
  49. data/spec/spec_helper.rb +0 -5
  50. data/tasks/rspec.task +0 -7
@@ -1,27 +1,151 @@
1
- module Celluloid
2
- module DNS
3
- class Server
4
- # Maximum UDP packet we'll accept
5
- MAX_PACKET_SIZE = 512
6
-
7
- include Celluloid::IO
8
-
9
- def initialize(addr, port, &block)
10
- @block = block
11
-
12
- # Create a non-blocking Celluloid::IO::UDPSocket
13
- @socket = UDPSocket.new
14
- @socket.bind(addr, port)
15
-
16
- async.run
17
- end
18
-
19
- def run
20
- loop do
21
- data, (_, port, addr) = @socket.recvfrom(MAX_PACKET_SIZE)
22
- @block.call Request.new(addr, port, @socket, data)
23
- end
24
- end
25
- end
26
- end
27
- end
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 'celluloid/io'
22
+
23
+ require_relative 'transaction'
24
+ require_relative 'logger'
25
+
26
+ module Celluloid::DNS
27
+ class Server
28
+ include Celluloid::IO
29
+
30
+ # The default server interfaces
31
+ DEFAULT_INTERFACES = [[:udp, "0.0.0.0", 53], [:tcp, "0.0.0.0", 53]]
32
+
33
+ # Instantiate a server with a block
34
+ #
35
+ # server = Server.new do
36
+ # match(/server.mydomain.com/, IN::A) do |transaction|
37
+ # transaction.respond!("1.2.3.4")
38
+ # end
39
+ # end
40
+ #
41
+ def initialize(options = {})
42
+ @logger = options[:logger] || Celluloid.logger
43
+ @interfaces = options[:listen] || DEFAULT_INTERFACES
44
+
45
+ @origin = options[:origin] || '.'
46
+ end
47
+
48
+ # Records are relative to this origin:
49
+ attr_accessor :origin
50
+
51
+ attr_accessor :logger
52
+
53
+ # Fire the named event as part of running the server.
54
+ def fire(event_name)
55
+ end
56
+
57
+ finalizer def stop
58
+ # Celluloid.logger.debug(self.class.name) {"-> Shutdown..."}
59
+
60
+ fire(:stop)
61
+
62
+ # Celluloid.logger.debug(self.class.name) {"<- Shutdown..."}
63
+ end
64
+
65
+ # Give a name and a record type, try to match a rule and use it for processing the given arguments.
66
+ def process(name, resource_class, transaction)
67
+ raise NotImplementedError.new
68
+ end
69
+
70
+ # Process an incoming DNS message. Returns a serialized message to be sent back to the client.
71
+ def process_query(query, options = {}, &block)
72
+ start_time = Time.now
73
+
74
+ # Setup response
75
+ response = Resolv::DNS::Message::new(query.id)
76
+ response.qr = 1 # 0 = Query, 1 = Response
77
+ response.opcode = query.opcode # Type of Query; copy from query
78
+ response.aa = 1 # Is this an authoritative response: 0 = No, 1 = Yes
79
+ response.rd = query.rd # Is Recursion Desired, copied from query
80
+ response.ra = 0 # Does name server support recursion: 0 = No, 1 = Yes
81
+ response.rcode = 0 # Response code: 0 = No errors
82
+
83
+ transaction = nil
84
+
85
+ begin
86
+ query.question.each do |question, resource_class|
87
+ begin
88
+ question = question.without_origin(@origin)
89
+
90
+ @logger.debug {"<#{query.id}> Processing question #{question} #{resource_class}..."}
91
+
92
+ transaction = Transaction.new(self, query, question, resource_class, response, options)
93
+
94
+ transaction.process
95
+ rescue Resolv::DNS::OriginError
96
+ # This is triggered if the question is not part of the specified @origin:
97
+ @logger.debug {"<#{query.id}> Skipping question #{question} #{resource_class} because #{$!}"}
98
+ end
99
+ end
100
+ rescue StandardError => error
101
+ @logger.error "<#{query.id}> Exception thrown while processing #{transaction}!"
102
+ Celluloid::DNS.log_exception(@logger, error)
103
+
104
+ response.rcode = Resolv::DNS::RCode::ServFail
105
+ end
106
+
107
+ end_time = Time.now
108
+ @logger.debug {"<#{query.id}> Time to process request: #{end_time - start_time}s"}
109
+
110
+ return response
111
+ end
112
+
113
+ # Setup all specified interfaces and begin accepting incoming connections.
114
+ def run
115
+ @logger.info "Starting Celluloid::DNS server (v#{Celluloid::DNS::VERSION})..."
116
+
117
+ fire(:setup)
118
+
119
+ # Setup server sockets
120
+ @interfaces.each do |spec|
121
+ if spec.is_a?(BasicSocket)
122
+ spec.do_not_reverse_lookup
123
+ protocol = spec.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE).unpack("i")[0]
124
+ ip = spec.local_address.ip_address
125
+ port = spec.local_address.ip_port
126
+
127
+ case protocol
128
+ when Socket::SOCK_DGRAM
129
+ @logger.info "<> Attaching to pre-existing UDP socket #{ip}:#{port}"
130
+ link UDPSocketHandler.new(self, Celluloid::IO::Socket.try_convert(spec))
131
+ when Socket::SOCK_STREAM
132
+ @logger.info "<> Attaching to pre-existing TCP socket #{ip}:#{port}"
133
+ link TCPSocketHandler.new(self, Celluloid::IO::Socket.try_convert(spec))
134
+ else
135
+ raise ArgumentError.new("Unknown socket protocol: #{protocol}")
136
+ end
137
+ elsif spec[0] == :udp
138
+ @logger.info "<> Listening on #{spec.join(':')}"
139
+ link UDPHandler.new(self, spec[1], spec[2])
140
+ elsif spec[0] == :tcp
141
+ @logger.info "<> Listening on #{spec.join(':')}"
142
+ link TCPHandler.new(self, spec[1], spec[2])
143
+ else
144
+ raise ArgumentError.new("Invalid connection specification: #{spec.inspect}")
145
+ end
146
+ end
147
+
148
+ fire(:start)
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,146 @@
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 Celluloid::DNS
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
@@ -0,0 +1,202 @@
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
+ module Celluloid::DNS
22
+
23
+ # This class provides all details of a single DNS question and response. This is used by the DSL to provide DNS related functionality.
24
+ #
25
+ # The main functions to complete the transaction are: {#append!} (evaluate a new query and append the results), {#passthrough!} (pass the query to an upstream server), {#respond!} (compute a specific response) and {#fail!} (fail with an error code).
26
+ class Transaction
27
+ # The default time used for responses (24 hours).
28
+ DEFAULT_TTL = 86400
29
+
30
+ def initialize(server, query, question, resource_class, response, options = {})
31
+ @server = server
32
+ @query = query
33
+ @question = question
34
+ @resource_class = resource_class
35
+ @response = response
36
+
37
+ @options = options
38
+ end
39
+
40
+ # The resource_class that was requested. This is typically used to generate a response.
41
+ attr :resource_class
42
+
43
+ # The incoming query which is a set of questions.
44
+ attr :query
45
+
46
+ # The question that this transaction represents.
47
+ attr :question
48
+
49
+ # The current full response to the incoming query.
50
+ attr :response
51
+
52
+ # Any options or configuration associated with the given transaction.
53
+ attr :options
54
+
55
+ def [] key
56
+ @options[key]
57
+ end
58
+
59
+ # The name of the question, which is typically the requested hostname.
60
+ def name
61
+ @question.to_s
62
+ end
63
+
64
+ # Shows the question name and resource class. Suitable for debugging purposes.
65
+ def to_s
66
+ "#{name} #{@resource_class.name}"
67
+ end
68
+
69
+ # Run a new query through the rules with the given name and resource type. The results of this query are appended to the current transaction's `response`.
70
+ def append!(name, resource_class = nil, options = {})
71
+ Transaction.new(@server, @query, name, resource_class || @resource_class, @response, options).process
72
+ end
73
+
74
+ # Use the given resolver to respond to the question. Uses `passthrough` to do the lookup and merges the result.
75
+ #
76
+ # If a block is supplied, this function yields with the `response` message if successful. This could be used, for example, to update a cache or modify the reply.
77
+ #
78
+ # If recursion is not requested, the result is `fail!(:Refused)`. This check is ignored if an explicit `options[:name]` or `options[:force]` is given.
79
+ #
80
+ # If the resolver can't reach upstream servers, `fail!(:ServFail)` is invoked.
81
+ def passthrough!(resolver, options = {}, &block)
82
+ if @query.rd || options[:force] || options[:name]
83
+ response = passthrough(resolver, options)
84
+
85
+ if response
86
+ yield response if block_given?
87
+
88
+ # Recursion is available and is being used:
89
+ # See issue #26 for more details.
90
+ @response.ra = 1
91
+ @response.merge!(response)
92
+ else
93
+ fail!(:ServFail)
94
+ end
95
+ else
96
+ fail!(:Refused)
97
+ end
98
+ end
99
+
100
+ # Use the given resolver to respond to the question.
101
+ #
102
+ # A block must be supplied, and provided a valid response is received from the upstream server, this function yields with the reply and reply_name.
103
+ #
104
+ # If `options[:name]` is provided, this overrides the default query name sent to the upstream server. The same logic applies to `options[:resource_class]`.
105
+ def passthrough(resolver, options = {})
106
+ query_name = options[:name] || name
107
+ query_resource_class = options[:resource_class] || resource_class
108
+
109
+ resolver.query(query_name, query_resource_class)
110
+ end
111
+
112
+ # Respond to the given query with a resource record. The arguments to this function depend on the `resource_class` requested. This function instantiates the resource class with the supplied arguments, and then passes it to {#append!}.
113
+ #
114
+ # e.g. For A records: `respond!("1.2.3.4")`, For MX records: `respond!(10, Name.create("mail.blah.com"))`
115
+
116
+ # The last argument can optionally be a hash of `options`. If `options[:resource_class]` is provided, it overrides the default resource class of transaction. Additional `options` are passed to {#append!}.
117
+ #
118
+ # See `Resolv::DNS::Resource` for more information about the various `resource_classes` available (http://www.ruby-doc.org/stdlib/libdoc/resolv/rdoc/index.html).
119
+ def respond!(*args)
120
+ append_question!
121
+
122
+ options = args.last.kind_of?(Hash) ? args.pop : {}
123
+ resource_class = options[:resource_class] || @resource_class
124
+
125
+ if resource_class == nil
126
+ raise ArgumentError.new("Could not instantiate resource #{resource_class}!")
127
+ end
128
+
129
+ resource = resource_class.new(*args)
130
+
131
+ add([resource], options)
132
+ end
133
+
134
+ # Append a list of resources.
135
+ #
136
+ # By default resources are appended to the `answers` section, but this can be changed by setting `options[:section]` to either `:authority` or `:additional`.
137
+ #
138
+ # The time-to-live (TTL) of the resources can be specified using `options[:ttl]` and defaults to `DEFAULT_TTL`.
139
+ def add(resources, options = {})
140
+ # Use the default options if provided:
141
+ options = options.merge(@options)
142
+
143
+ ttl = options[:ttl] || DEFAULT_TTL
144
+ name = options[:name] || @question.to_s + "."
145
+
146
+ section = (options[:section] || 'answer').to_sym
147
+ method = "add_#{section}".to_sym
148
+
149
+ resources.each do |resource|
150
+ @server.logger.debug "#{method}: #{resource.inspect} #{resource.class::TypeValue} #{resource.class::ClassValue}"
151
+
152
+ @response.send(method, name, ttl, resource)
153
+ end
154
+ end
155
+
156
+ # This function indicates that there was a failure to resolve the given question. The single argument must be an integer error code, typically given by the constants in {Resolv::DNS::RCode}.
157
+ #
158
+ # The easiest way to use this function it to simply supply a symbol. Here is a list of the most commonly used ones:
159
+ #
160
+ # - `:NoError`: No error occurred.
161
+ # - `:FormErr`: The incoming data was not formatted correctly.
162
+ # - `:ServFail`: The operation caused a server failure (internal error, etc).
163
+ # - `:NXDomain`: Non-eXistant Domain (domain record does not exist).
164
+ # - `:NotImp`: The operation requested is not implemented.
165
+ # - `:Refused`: The operation was refused by the server.
166
+ # - `:NotAuth`: The server is not authoritive for the zone.
167
+ #
168
+ # See [RFC2929](http://www.rfc-editor.org/rfc/rfc2929.txt) for more information about DNS error codes (specifically, page 3).
169
+ #
170
+ # **This function will complete deferred transactions.**
171
+ def fail!(rcode)
172
+ append_question!
173
+
174
+ if rcode.kind_of? Symbol
175
+ @response.rcode = Resolv::DNS::RCode.const_get(rcode)
176
+ else
177
+ @response.rcode = rcode.to_i
178
+ end
179
+ end
180
+
181
+ # @deprecated
182
+ def failure!(*args)
183
+ @server.logger.warn "failure! is deprecated, use fail! instead"
184
+
185
+ fail!(*args)
186
+ end
187
+
188
+ # A helper method to process the transaction on the given server. Unless the transaction is deferred, it will {#succeed} on completion.
189
+ def process
190
+ @server.process(name, @resource_class, self)
191
+ end
192
+
193
+ protected
194
+
195
+ # A typical response to a DNS request includes both the question and response. This helper appends the question unless it looks like the user is already managing that aspect of the response.
196
+ def append_question!
197
+ if @response.question.size == 0
198
+ @response.add_question(@question, @resource_class)
199
+ end
200
+ end
201
+ end
202
+ end