dnsruby 1.57.0 → 1.58.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 (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