async-dns 0.10.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.travis.yml +18 -0
  4. data/Gemfile +14 -0
  5. data/README.md +144 -0
  6. data/Rakefile +32 -0
  7. data/async-dns.gemspec +31 -0
  8. data/lib/async/dns.rb +36 -0
  9. data/lib/async/dns/chunked.rb +34 -0
  10. data/lib/async/dns/extensions/resolv.rb +136 -0
  11. data/lib/async/dns/extensions/string.rb +28 -0
  12. data/lib/async/dns/handler.rb +229 -0
  13. data/lib/async/dns/logger.rb +31 -0
  14. data/lib/async/dns/message.rb +75 -0
  15. data/lib/async/dns/replace.rb +54 -0
  16. data/lib/async/dns/resolver.rb +280 -0
  17. data/lib/async/dns/server.rb +154 -0
  18. data/lib/async/dns/system.rb +146 -0
  19. data/lib/async/dns/transaction.rb +202 -0
  20. data/lib/async/dns/transport.rb +78 -0
  21. data/lib/async/dns/version.rb +25 -0
  22. data/spec/async/dns/handler_spec.rb +58 -0
  23. data/spec/async/dns/hosts.txt +2 -0
  24. data/spec/async/dns/ipv6_spec.rb +78 -0
  25. data/spec/async/dns/message_spec.rb +56 -0
  26. data/spec/async/dns/origin_spec.rb +106 -0
  27. data/spec/async/dns/replace_spec.rb +44 -0
  28. data/spec/async/dns/resolver_performance_spec.rb +110 -0
  29. data/spec/async/dns/resolver_spec.rb +151 -0
  30. data/spec/async/dns/server/bind9/generate-local.rb +25 -0
  31. data/spec/async/dns/server/bind9/local.zone +5014 -0
  32. data/spec/async/dns/server/bind9/named.conf +14 -0
  33. data/spec/async/dns/server/bind9/named.run +0 -0
  34. data/spec/async/dns/server/million.rb +85 -0
  35. data/spec/async/dns/server_performance_spec.rb +138 -0
  36. data/spec/async/dns/slow_server_spec.rb +97 -0
  37. data/spec/async/dns/socket_spec.rb +70 -0
  38. data/spec/async/dns/system_spec.rb +57 -0
  39. data/spec/async/dns/transaction_spec.rb +140 -0
  40. data/spec/async/dns/truncation_spec.rb +61 -0
  41. data/spec/spec_helper.rb +60 -0
  42. metadata +175 -0
@@ -0,0 +1,154 @@
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 'async'
22
+
23
+ require_relative 'transaction'
24
+ require_relative 'logger'
25
+
26
+ module Async::DNS
27
+ class Server
28
+ # The default server interfaces
29
+ DEFAULT_INTERFACES = [[:udp, "0.0.0.0", 53], [:tcp, "0.0.0.0", 53]]
30
+
31
+ # Instantiate a server with a block
32
+ #
33
+ # server = Server.new do
34
+ # match(/server.mydomain.com/, IN::A) do |transaction|
35
+ # transaction.respond!("1.2.3.4")
36
+ # end
37
+ # end
38
+ #
39
+ def initialize(listen: DEFAULT_INTERFACES, origin: '.', logger: Async.logger)
40
+ @interfaces = listen
41
+ @origin = origin
42
+ @logger = logger
43
+
44
+ @handlers = []
45
+ end
46
+
47
+ # Records are relative to this origin:
48
+ attr_accessor :origin
49
+
50
+ attr_accessor :logger
51
+
52
+ # Fire the named event as part of running the server.
53
+ def fire(event_name)
54
+ end
55
+
56
+ # Give a name and a record type, try to match a rule and use it for processing the given arguments.
57
+ def process(name, resource_class, transaction)
58
+ raise NotImplementedError.new
59
+ end
60
+
61
+ # Process an incoming DNS message. Returns a serialized message to be sent back to the client.
62
+ def process_query(query, options = {}, &block)
63
+ start_time = Time.now
64
+
65
+ # Setup response
66
+ response = Resolv::DNS::Message::new(query.id)
67
+ response.qr = 1 # 0 = Query, 1 = Response
68
+ response.opcode = query.opcode # Type of Query; copy from query
69
+ response.aa = 1 # Is this an authoritative response: 0 = No, 1 = Yes
70
+ response.rd = query.rd # Is Recursion Desired, copied from query
71
+ response.ra = 0 # Does name server support recursion: 0 = No, 1 = Yes
72
+ response.rcode = 0 # Response code: 0 = No errors
73
+
74
+ transaction = nil
75
+
76
+ begin
77
+ query.question.each do |question, resource_class|
78
+ begin
79
+ question = question.without_origin(@origin)
80
+
81
+ @logger.debug {"<#{query.id}> Processing question #{question} #{resource_class}..."}
82
+
83
+ transaction = Transaction.new(self, query, question, resource_class, response, options)
84
+
85
+ transaction.process
86
+ rescue Resolv::DNS::OriginError
87
+ # This is triggered if the question is not part of the specified @origin:
88
+ @logger.debug {"<#{query.id}> Skipping question #{question} #{resource_class} because #{$!}"}
89
+ end
90
+ end
91
+ rescue StandardError => error
92
+ @logger.error "<#{query.id}> Exception thrown while processing #{transaction}!"
93
+ Async::DNS.log_exception(@logger, error)
94
+
95
+ response.rcode = Resolv::DNS::RCode::ServFail
96
+ end
97
+
98
+ end_time = Time.now
99
+ @logger.debug {"<#{query.id}> Time to process request: #{end_time - start_time}s"}
100
+
101
+ return response
102
+ end
103
+
104
+ # Setup all specified interfaces and begin accepting incoming connections.
105
+ def run(*args)
106
+ @logger.info "Starting Async::DNS server (v#{Async::DNS::VERSION})..."
107
+
108
+ setup_handlers if @handlers.empty?
109
+
110
+ Async::Reactor.run do
111
+ @handlers.each do |handler|
112
+ handler.run(*args)
113
+ end
114
+
115
+ fire(:start)
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def setup_handlers
122
+ fire(:setup)
123
+
124
+ # Setup server sockets
125
+ @interfaces.each do |spec|
126
+ if spec.is_a?(BasicSocket)
127
+ spec.do_not_reverse_lookup
128
+ protocol = spec.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE).unpack("i")[0]
129
+ ip = spec.local_address.ip_address
130
+ port = spec.local_address.ip_port
131
+
132
+ case protocol
133
+ when Socket::SOCK_DGRAM
134
+ @logger.info "<> Attaching to pre-existing UDP socket #{ip}:#{port}"
135
+ @handlers << UDPSocketHandler.new(self, spec)
136
+ when Socket::SOCK_STREAM
137
+ @logger.info "<> Attaching to pre-existing TCP socket #{ip}:#{port}"
138
+ @handlers << TCPSocketHandler.new(self, spec)
139
+ else
140
+ raise ArgumentError.new("Unknown socket protocol: #{protocol}")
141
+ end
142
+ elsif spec[0] == :udp
143
+ @logger.info "<> Listening on #{spec.join(':')}"
144
+ @handlers << UDPServerHandler.new(self, spec[1], spec[2])
145
+ elsif spec[0] == :tcp
146
+ @logger.info "<> Listening on #{spec.join(':')}"
147
+ @handlers << TCPServerHandler.new(self, spec[1], spec[2])
148
+ else
149
+ raise ArgumentError.new("Invalid connection specification: #{spec.inspect}")
150
+ end
151
+ end
152
+ end
153
+ end
154
+ 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 Async::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 Async::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