dnsruby 1.57.0 → 1.58.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/.travis.yml +1 -1
  4. data/README.md +2 -2
  5. data/RELEASE_NOTES.md +16 -0
  6. data/Rakefile +2 -0
  7. data/dnsruby.gemspec +2 -0
  8. data/lib/dnsruby.rb +5 -0
  9. data/lib/dnsruby/bit_mapping.rb +138 -0
  10. data/lib/dnsruby/bitmap.rb +108 -0
  11. data/lib/dnsruby/message/decoder.rb +90 -80
  12. data/lib/dnsruby/message/message.rb +16 -3
  13. data/lib/dnsruby/message/section.rb +3 -3
  14. data/lib/dnsruby/name.rb +2 -2
  15. data/lib/dnsruby/packet_sender.rb +73 -1
  16. data/lib/dnsruby/resolv.rb +56 -42
  17. data/lib/dnsruby/resolver.rb +95 -73
  18. data/lib/dnsruby/resource/GPOS.rb +9 -3
  19. data/lib/dnsruby/resource/HIP.rb +1 -1
  20. data/lib/dnsruby/resource/IN.rb +3 -1
  21. data/lib/dnsruby/resource/NAPTR.rb +2 -2
  22. data/lib/dnsruby/resource/NSEC3.rb +2 -2
  23. data/lib/dnsruby/resource/NXT.rb +302 -0
  24. data/lib/dnsruby/resource/OPT.rb +2 -2
  25. data/lib/dnsruby/resource/TXT.rb +2 -2
  26. data/lib/dnsruby/resource/generic.rb +1 -0
  27. data/lib/dnsruby/resource/type_bitmap.rb +0 -0
  28. data/lib/dnsruby/select_thread.rb +174 -83
  29. data/lib/dnsruby/single_resolver.rb +2 -2
  30. data/lib/dnsruby/version.rb +1 -1
  31. data/lib/dnsruby/zone_reader.rb +19 -9
  32. data/lib/dnsruby/zone_transfer.rb +1 -1
  33. data/test/spec_helper.rb +9 -1
  34. data/test/tc_axfr.rb +17 -4
  35. data/test/tc_gpos.rb +3 -3
  36. data/test/tc_message.rb +7 -1
  37. data/test/tc_nxt.rb +192 -0
  38. data/test/tc_recur.rb +2 -1
  39. data/test/tc_resolv.rb +73 -0
  40. data/test/tc_resolver.rb +22 -4
  41. data/test/tc_rr-opt.rb +9 -8
  42. data/test/tc_rr.rb +22 -0
  43. data/test/tc_single_resolver.rb +15 -15
  44. data/test/tc_soak.rb +154 -65
  45. data/test/tc_soak_base.rb +15 -15
  46. data/test/tc_tcp.rb +1 -1
  47. data/test/tc_tcp_pipelining.rb +203 -0
  48. data/test/tc_zone_reader.rb +73 -0
  49. data/test/test_dnsserver.rb +208 -0
  50. data/test/test_utils.rb +49 -0
  51. data/test/ts_offline.rb +59 -41
  52. data/test/ts_online.rb +92 -63
  53. metadata +40 -3
  54. data/test/tc_dnsruby.rb +0 -51
@@ -20,25 +20,25 @@ class TestSoakBase # < Minitest::Test
20
20
  include Dnsruby
21
21
  Rrs = [
22
22
  {
23
- :type => Types.A,
24
- :name => 'a.t.dnsruby.validation-test-servers.nominet.org.uk',
25
- :address => '10.0.1.128'
23
+ :type => Types.A,
24
+ :name => 'ns1.google.com.',
25
+ :address => '10.0.1.128'
26
26
  },
27
27
  {
28
- :type => Types::MX,
29
- :name => 'mx.t.dnsruby.validation-test-servers.nominet.org.uk',
30
- :exchange => 'a.t.dnsruby.validation-test-servers.nominet.org.uk',
31
- :preference => 10
28
+ :type => Types::MX,
29
+ :name => 'ns1.google.com.',
30
+ :exchange => 'ns1.google.com.',
31
+ :preference => 10
32
32
  },
33
33
  {
34
- :type => 'CNAME',
35
- :name => 'cname.t.dnsruby.validation-test-servers.nominet.org.uk',
36
- :domainname => 'a.t.dnsruby.validation-test-servers.nominet.org.uk'
34
+ :type => 'CNAME',
35
+ :name => 'ns1.google.com.',
36
+ :domainname => 'a.t.dnsruby.validation-test-servers.nominet.org.uk'
37
37
  },
38
38
  {
39
- :type => Types.TXT,
40
- :name => 'txt.t.dnsruby.validation-test-servers.nominet.org.uk',
41
- :strings => ['Net-DNS']
39
+ :type => Types.TXT,
40
+ :name => 'ns1.google.com.',
41
+ :strings => ['Net-DNS']
42
42
  }
43
43
  ]
44
44
 
@@ -92,7 +92,7 @@ class TestSoakBase # < Minitest::Test
92
92
  assert(num_in_progress==0)
93
93
  stop=Time.now
94
94
  time_taken=stop-start
95
- p "Query count : #{num_sent}, #{timed_out} timed out. #{time_taken} time taken"
95
+ puts "Query count : #{num_sent}, #{timed_out} timed out. #{time_taken} time taken"
96
96
  assert(timed_out < num_sent * 0.1, "#{timed_out} of #{num_sent} timed out!")
97
97
  end
98
98
 
@@ -143,7 +143,7 @@ class TestSoakBase # < Minitest::Test
143
143
  assert(num_in_progress==0)
144
144
  stop=Time.now
145
145
  time_taken=stop-start
146
- p "Query count : #{num_sent}, #{timed_out} timed out, #{error_count} other errors. #{time_taken} time taken"
146
+ puts "Query count : #{num_sent}, #{timed_out} timed out, #{error_count} other errors. #{time_taken} time taken"
147
147
  assert(timed_out < num_sent * 0.1, "#{timed_out} of #{num_sent} timed out!")
148
148
  assert(error_count == 0)
149
149
  end
@@ -80,7 +80,7 @@ class TestTcp < Minitest::Test
80
80
 
81
81
  class HackMessage < Dnsruby::Message
82
82
  def wipe_additional
83
- @additional = Dnsruby::Message::Section.new(self)
83
+ @additional = Dnsruby::Section.new(self)
84
84
  end
85
85
 
86
86
  # Decode the encoded message
@@ -0,0 +1,203 @@
1
+ # --
2
+ # Copyright 2015 Verisign
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ++
16
+
17
+ require_relative 'spec_helper'
18
+ require_relative 'test_dnsserver'
19
+
20
+ # The TCPPipeliningServer links our TCPPipeliningHandler on
21
+ # the loopback interface.
22
+ class TCPPipeliningServer < RubyDNS::RuleBasedServer
23
+
24
+ PORT = 53937
25
+ IP = '127.0.0.1'
26
+
27
+ @@stats = Stats.new
28
+
29
+ def self.stats
30
+ @@stats
31
+ end
32
+
33
+ def run
34
+ fire(:setup)
35
+
36
+ link TCPPipeliningHandler.new(self,
37
+ IP,
38
+ PORT,
39
+ TCPPipeliningHandler::DEFAULT_MAX_REQUESTS,
40
+ TCPPipeliningHandler::DEFAULT_TIMEOUT)
41
+
42
+ fire(:start)
43
+ end
44
+ end
45
+
46
+ class TestTCPPipelining < Minitest::Test
47
+
48
+ QUERIES = %w(psi.net passport.net verisigninc.com google.com yahoo.com apple.com)
49
+
50
+ class << self
51
+ attr_accessor :query_id
52
+ end
53
+
54
+ def self.init
55
+ unless @initialized
56
+ Celluloid.boot
57
+ # By default, Celluloid logs output to console. Use Dnsruby.log instead
58
+ Celluloid.logger = Dnsruby.log
59
+ @initialized = true
60
+ @query_id = 0
61
+ end
62
+ end
63
+
64
+ def setup
65
+ self.class.init
66
+ @@upstream ||= RubyDNS::Resolver.new([
67
+ [:udp, '193.0.14.129', 53],
68
+ [:tcp, '193.0.14.129', 53]])
69
+
70
+ # Instantiate a new server that uses our tcp pipelining handler
71
+ # For each query the server sends the query upstream (193.0.14.129)
72
+ options = {
73
+ server_class: TCPPipeliningServer,
74
+ asynchronous: true
75
+ }
76
+
77
+ @@supervisor ||= RubyDNS::run_server(options) do
78
+ otherwise do |transaction|
79
+ transaction.passthrough!(@@upstream)
80
+ end
81
+ end
82
+
83
+ # Instantiate our resolver. The resolver will use the same pipeline as much as possible.
84
+ # If a timeout occurs or max_request_per_connection a new connection should be initiated
85
+ @@resolver ||= Dnsruby::Resolver.new(
86
+ use_tcp: true,
87
+ do_caching: false,
88
+ tcp_pipelining: true,
89
+ dnssec: false,
90
+ packet_timeout: 10,
91
+ tcp_pipelining_max_queries: 10,
92
+ nameserver: TCPPipeliningServer::IP,
93
+ port: TCPPipeliningServer::PORT)
94
+ end
95
+
96
+ # Send x number of queries asynchronously to our resolver
97
+ def send_async_messages(number_of_messages, queue, wait_seconds = 0)
98
+ query_cycler = QUERIES.cycle
99
+ number_of_messages.times do
100
+ message = Dnsruby::Message.new(query_cycler.next)
101
+ # self.class.query_id identifies our query, must be different for each message
102
+ @@resolver.send_async(message, queue, self.class.query_id)
103
+ self.class.query_id += 1
104
+
105
+ # Note: For 0, we don't sleep at all instead of sleeping 0 since sleeping 0
106
+ # involves yielding the CPU.
107
+ sleep wait_seconds unless wait_seconds == 0
108
+ end
109
+ end
110
+
111
+ # Verify x responses with no exception
112
+ def verify_responses(number_of_messages, queue)
113
+ number_of_messages.times do
114
+ _response_id, response, exception = queue.pop
115
+ assert_nil(exception)
116
+ assert(response.is_a?(Dnsruby::Message))
117
+ end
118
+ end
119
+
120
+ # This test initiates multiple asynchronous requests and verifies they go on the same tcp
121
+ # pipeline or a new one depending on timeouts
122
+ def test_TCP_pipelining_timeout
123
+ accept_count = TCPPipeliningServer.stats.accept_count
124
+ timeout_count = TCPPipeliningServer.stats.timeout_count
125
+
126
+ # This is the main queue used to communicate between Dnsruby in async mode and the client
127
+ query_queue = Queue.new
128
+
129
+ # Test basic pipelining. All request should go on the same tcp connection.
130
+ # TCPPipeliningServer.stats.accept_count should be 1.
131
+ send_async_messages(3, query_queue)
132
+ verify_responses(3, query_queue)
133
+
134
+ assert_equal(accept_count + 1, TCPPipeliningServer.stats.accept_count)
135
+
136
+ # Wait for the timeout to occur (5s) and check timeout_count
137
+ sleep TCPPipeliningHandler::DEFAULT_TIMEOUT + 0.5
138
+
139
+ assert_equal(timeout_count + 1, TCPPipeliningServer.stats.timeout_count)
140
+
141
+ # Initiate another 3 queries, check accept_count and timeout_count
142
+ send_async_messages(3, query_queue)
143
+ verify_responses(3, query_queue)
144
+
145
+ assert_equal(accept_count + 2, TCPPipeliningServer.stats.accept_count)
146
+
147
+ # Wait for the timeout to occur and check timeout_count
148
+ sleep TCPPipeliningHandler::DEFAULT_TIMEOUT + 0.5
149
+
150
+ assert_equal(timeout_count + 2, TCPPipeliningServer.stats.timeout_count)
151
+ end
152
+
153
+ # Test timeout occurs and new connection is initiated inbetween 2 sends
154
+ def test_TCP_pipelining_timeout_in_send
155
+ accept_count = TCPPipeliningServer.stats.accept_count
156
+ timeout_count = TCPPipeliningServer.stats.timeout_count
157
+
158
+ query_queue = Queue.new
159
+
160
+ # Initiate another 3 queries but wait 3s after each query.
161
+ # Check accept_count. Wait for timeout and verify we got 2 additional timeouts.
162
+ send_async_messages(3, query_queue, TCPPipeliningHandler::DEFAULT_TIMEOUT / 2.0 + 0.5)
163
+ verify_responses(3, query_queue)
164
+
165
+ assert_equal(accept_count + 2, TCPPipeliningServer.stats.accept_count)
166
+
167
+ sleep TCPPipeliningHandler::DEFAULT_TIMEOUT + 0.5
168
+
169
+ assert_equal(timeout_count + 2, TCPPipeliningServer.stats.timeout_count)
170
+ end
171
+
172
+ # Test that we get a SocketEofResolvError if the servers closes the socket before
173
+ # all queries are answered
174
+ def test_TCP_pipelining_socket_eof
175
+ accept_count = TCPPipeliningServer.stats.accept_count
176
+ timeout_count = TCPPipeliningServer.stats.timeout_count
177
+ max_count = TCPPipeliningServer.stats.max_count
178
+
179
+ query_queue = Queue.new
180
+
181
+ # Issue 6 queries. Only 4 should be replied since max_request_per_connection = 4
182
+ # Verify we get Dnsruby::SocketEofResolvError on the last 2.
183
+ # Verify we got max_count was incremented
184
+ send_async_messages(6, query_queue)
185
+
186
+ step = 0
187
+ 6.times do
188
+ _response_id, response, exception = query_queue.pop
189
+ if step < TCPPipeliningHandler::DEFAULT_MAX_REQUESTS
190
+ assert_nil(exception)
191
+ assert(response.is_a?(Dnsruby::Message))
192
+ else
193
+ assert_equal(Dnsruby::SocketEofResolvError, exception.class)
194
+ assert_nil(response)
195
+ end
196
+ step += 1
197
+ end
198
+
199
+ assert_equal(accept_count + 1, TCPPipeliningServer.stats.accept_count)
200
+ assert_equal(timeout_count, TCPPipeliningServer.stats.timeout_count)
201
+ assert_equal(max_count + 1, TCPPipeliningServer.stats.max_count)
202
+ end
203
+ end
@@ -0,0 +1,73 @@
1
+
2
+ require_relative 'spec_helper'
3
+
4
+ include Dnsruby
5
+
6
+ class ZoneReaderTest < Minitest::Test
7
+
8
+ def setup
9
+ @zone_data = <<ZONEDATA
10
+ $TTL 3600
11
+ ; Comment
12
+ ; Comment with whitespace in front
13
+ @ IN SOA ns1.example.com. hostmaster.example.com. (
14
+ 1993112101
15
+ 10800
16
+ 3600
17
+ 604800
18
+ 7200
19
+ )
20
+ IN NS ns1.example.com.
21
+ IN NS ns2.example.com.
22
+ IN MX 10 mx.example.com.
23
+ IN TXT "v=spf1 mx ~all"
24
+ www IN A 192.0.2.10
25
+ IN AAAA 2001:DB8::10
26
+ ftp.example.com. IN CNAME www
27
+ db IN CNAME www.example.com.
28
+ ZONEDATA
29
+
30
+ @zone_file = Tempfile.new('zonefile')
31
+ @zone_file << @zone_data
32
+ @zone_file.flush
33
+ @zone_file.rewind
34
+ @reader = Dnsruby::ZoneReader.new("example.com.")
35
+ end
36
+
37
+ def teardown
38
+ @zone_file.close
39
+ @zone_file.unlink
40
+ end
41
+
42
+ def check_zone_data_is_valid(zone)
43
+ assert_equal(1993112101, zone[0].serial)
44
+ assert_equal("ns1.example.com.", zone[1].rdata)
45
+ assert_equal("ns2.example.com.", zone[2].rdata)
46
+ assert_equal("10 mx.example.com.", zone[3].rdata)
47
+ assert_equal("\"v=spf1 mx ~all\"", zone[4].rdata)
48
+ assert_equal("192.0.2.10", zone[5].rdata)
49
+ assert_equal("2001:DB8::10", zone[6].rdata)
50
+ assert_equal("www.example.com.", zone[7].rdata)
51
+ assert_equal("www.example.com.", zone[8].rdata)
52
+ end
53
+
54
+ def test_process_file_with_filename
55
+ zone = @reader.process_file(@zone_file.path)
56
+ check_zone_data_is_valid(zone)
57
+ end
58
+
59
+ def test_process_file_with_file_object
60
+ zone = @reader.process_file(@zone_file)
61
+ check_zone_data_is_valid(zone)
62
+ assert_equal(false, @zone_file.closed?)
63
+ end
64
+
65
+ def test_process_file_with_stringio_object
66
+ stringio = StringIO.new(@zone_data)
67
+ zone = @reader.process_file(stringio)
68
+ check_zone_data_is_valid(zone)
69
+ assert_equal(false, stringio.closed?)
70
+ stringio.close
71
+ end
72
+ end
73
+
@@ -0,0 +1,208 @@
1
+ # --
2
+ # Copyright 2015 Verisign
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ++
16
+
17
+ require 'rubydns'
18
+ require 'nio'
19
+ require 'socket'
20
+
21
+ # TCPPipeliningHandler accepts new tcp connection and reads data from the sockets until
22
+ # either the client closes the connection, @max_requests_per_connection is reached
23
+ # or @timeout is attained.
24
+
25
+ class NioTcpPipeliningHandler < RubyDNS::GenericHandler
26
+
27
+ DEFAULT_MAX_REQUESTS = 4
28
+
29
+ # TODO Add timeout
30
+ def initialize(server, host, port, max_requests = DEFAULT_MAX_REQUESTS)
31
+ super(server)
32
+ @max_requests_per_connection = max_requests
33
+ @socket = TCPServer.new(host, port)
34
+ @count = {}
35
+
36
+ @selector = NIO::Selector.new
37
+ monitor = @selector.register(@socket, :r)
38
+ monitor.value = proc { accept }
39
+
40
+ async.run
41
+ end
42
+
43
+ finalizer :finalize
44
+
45
+ def finalize
46
+ @socket.close if @socket
47
+ @selector.close
48
+ @selector_thread.join
49
+ end
50
+
51
+ def run
52
+ @logger.debug "Running selector thread"
53
+ @selector_thread = create_selector_thread
54
+ end
55
+
56
+ def accept
57
+ handle_connection(@socket.accept)
58
+ end
59
+
60
+ def process_socket(socket)
61
+ @logger.debug "Processing socket"
62
+ _, _remote_port, remote_host = socket.peeraddr
63
+ options = { peer: remote_host }
64
+
65
+ input_data = RubyDNS::StreamTransport.read_chunk(socket)
66
+ response = process_query(input_data, options)
67
+ RubyDNS::StreamTransport.write_message(socket, response)
68
+
69
+ @count[socket] ||= 0
70
+ @count[socket] += 1
71
+
72
+ if @count[socket] >= @max_requests_per_connection
73
+ _, port, host = socket.peeraddr
74
+ @logger.debug("*** max request for #{host}:#{port}")
75
+ remove(socket)
76
+ end
77
+ rescue EOFError
78
+ _, port, host = socket.peeraddr
79
+ @logger.debug("*** #{host}:#{port} disconnected")
80
+
81
+ remove(socket)
82
+ end
83
+
84
+ def remove(socket)
85
+ @logger.debug("Removing socket from selector")
86
+ socket.close rescue nil
87
+ @selector.deregister(socket)
88
+ @count.delete(socket)
89
+ end
90
+
91
+ def create_selector_thread
92
+ Thread.new do
93
+ loop do
94
+ @selector.select { |monitor| monitor.value.call(monitor) }
95
+ break if @selector.closed?
96
+ end
97
+ end
98
+ end
99
+
100
+ def handle_connection(socket)
101
+ @logger.debug "New connection"
102
+ @server.class.stats.increment_connection
103
+
104
+ @logger.debug "Add socket to @selector"
105
+ monitor = @selector.register(socket, :r)
106
+ monitor.value = proc { process_socket(socket) }
107
+ end
108
+ end
109
+
110
+ class TCPPipeliningHandler < RubyDNS::GenericHandler
111
+ DEFAULT_MAX_REQUESTS = 4
112
+ DEFAULT_TIMEOUT = 3.0
113
+
114
+ def initialize(server, host, port, max_requests = DEFAULT_MAX_REQUESTS, timeout = DEFAULT_TIMEOUT)
115
+ super(server)
116
+ @timeout = timeout
117
+ @max_requests_per_connection = max_requests
118
+ @socket = TCPServer.new(host, port)
119
+
120
+ async.run
121
+ end
122
+
123
+ finalizer :finalize
124
+
125
+ def finalize
126
+ @socket.close if @socket
127
+ end
128
+
129
+ def run
130
+ loop { async.handle_connection(@socket.accept) }
131
+ end
132
+
133
+
134
+ def handle_connection(socket)
135
+ _, _remote_port, remote_host = socket.peeraddr
136
+ options = { peer: remote_host }
137
+
138
+ @logger.debug "New connection"
139
+ @server.class.stats.increment_connection
140
+
141
+ timeout = @timeout
142
+ msg_count = 0
143
+
144
+ loop do
145
+ start_time = Time.now
146
+ @logger.debug "Waiting for #{timeout} max"
147
+ sockets = ::IO.select([socket], nil , nil, timeout)
148
+ duration = Time.now - start_time
149
+
150
+ @logger.debug "Slept for #{duration}"
151
+
152
+ timeout -= duration
153
+
154
+ if sockets
155
+ input_data = RubyDNS::StreamTransport.read_chunk(socket)
156
+ response = process_query(input_data, options)
157
+ RubyDNS::StreamTransport.write_message(socket, response)
158
+
159
+ msg_count += 1
160
+ @logger.debug "Responded to message #{msg_count}"
161
+ else
162
+ @logger.debug "TCP session timeout!"
163
+ @server.class.stats.increment_timeout
164
+ break
165
+ end
166
+
167
+ if msg_count >= @max_requests_per_connection
168
+ @logger.debug "Max number of requests attained (#{@max_requests_per_connection})"
169
+ @server.class.stats.increment_max
170
+ break
171
+ end
172
+
173
+ end
174
+ rescue EOFError
175
+ @logger.warn "TCP session ended (closed by client)"
176
+ rescue DecodeError
177
+ @logger.warn "Could not decode incoming TCP data!"
178
+ ensure
179
+ socket.close
180
+ end
181
+ end
182
+
183
+ # Stats collects statistics from our tcp handler
184
+ class Stats
185
+ def initialize()
186
+ @mutex = Mutex.new
187
+ @accept_count = 0
188
+ @timeout_count = 0
189
+ @max_count = 0
190
+ end
191
+
192
+ def increment_max; @mutex.synchronize { @max_count += 1 } end
193
+ def increment_timeout; @mutex.synchronize { @timeout_count += 1 } end
194
+ def increment_connection; @mutex.synchronize { @accept_count += 1 } end
195
+
196
+ def accept_count
197
+ @mutex.synchronize { @accept_count }
198
+ end
199
+
200
+ def timeout_count
201
+ @mutex.synchronize { @timeout_count }
202
+ end
203
+
204
+ def max_count
205
+ @mutex.synchronize { @max_count }
206
+ end
207
+
208
+ end