dnsruby 1.73.1 → 2.0.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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +314 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/dnsruby.gemspec +6 -4
  10. data/lib/dnsruby/cache.rb +279 -49
  11. data/lib/dnsruby/code_mapper.rb +35 -8
  12. data/lib/dnsruby/code_mappers.rb +63 -49
  13. data/lib/dnsruby/config.rb +77 -4
  14. data/lib/dnsruby/deadline_queue.rb +75 -0
  15. data/lib/dnsruby/dnssec.rb +148 -45
  16. data/lib/dnsruby/ipv6.rb +45 -5
  17. data/lib/dnsruby/key_cache.rb +202 -35
  18. data/lib/dnsruby/message/decoder.rb +85 -19
  19. data/lib/dnsruby/message/encoder.rb +27 -16
  20. data/lib/dnsruby/message/header.rb +8 -12
  21. data/lib/dnsruby/message/message.rb +53 -22
  22. data/lib/dnsruby/message/section.rb +12 -34
  23. data/lib/dnsruby/name.rb +5 -2
  24. data/lib/dnsruby/packet_sender.rb +664 -207
  25. data/lib/dnsruby/presentation.rb +147 -0
  26. data/lib/dnsruby/query_control.rb +132 -0
  27. data/lib/dnsruby/recursor.rb +798 -341
  28. data/lib/dnsruby/resolver.rb +472 -176
  29. data/lib/dnsruby/resource/CAA.rb +67 -16
  30. data/lib/dnsruby/resource/DLV.rb +6 -0
  31. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  32. data/lib/dnsruby/resource/DS.rb +27 -28
  33. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  34. data/lib/dnsruby/resource/IN.rb +21 -2
  35. data/lib/dnsruby/resource/MX.rb +2 -2
  36. data/lib/dnsruby/resource/NSEC.rb +6 -10
  37. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  38. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  39. data/lib/dnsruby/resource/OPT.rb +2 -3
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +986 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +53 -2
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +34 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +127 -14
  167. data/.coveralls.yml +0 -2
@@ -15,241 +15,132 @@
15
15
  # ++
16
16
 
17
17
  require_relative 'spec_helper'
18
- require_relative 'test_dnsserver'
19
-
20
- # The TCPPipeliningServer links our NioTcpPipeliningHandler on
21
- # the loopback interface.
22
- class TCPPipeliningServer < Async::DNS::Server
23
- PORT = 53937
24
- IP = '127.0.0.1'
25
-
26
- DEFAULT_MAX_REQUESTS = 4
27
- DEFAULT_TIMEOUT = 3
28
-
29
- @@stats = Stats.new
30
-
31
- def self.stats
32
- @@stats
33
- end
34
-
35
- def initialize(**options)
36
- super(options)
37
-
38
- @handlers = []
39
- @handlers << NioTcpPipeliningHandler.new(self, IP, PORT, DEFAULT_MAX_REQUESTS, DEFAULT_TIMEOUT) #4 max request
40
- end
41
-
42
- def process(name, resource_class, transaction)
43
- @logger.debug "name: #{name}"
44
- transaction.respond!("93.184.216.34", { resource_class: ::Resolv::DNS::Resource::IN::A })
45
- end
46
-
47
- end
18
+ require_relative 'tcp_pipelining_server'
19
+ require 'timeout'
48
20
 
49
21
  class TestTCPPipelining < Minitest::Test
50
-
51
- class << self
52
- attr_accessor :query_id
53
- end
54
-
55
- def self.init
56
- unless @initialized
57
- @initialized = true
58
- @query_id = 0
59
- end
60
- end
61
-
62
- @@server = nil
22
+ WAIT_TIMEOUT = 5
63
23
 
64
24
  def setup
65
- return
66
- self.class.init
67
-
68
- # Instantiate a new server that uses our tcp pipelining handler
69
- # For each query the server sends the query upstream (193.0.14.129)
70
- # options = {
71
- # server_class: TCPPipeliningServer,
72
- # }
73
-
74
- #RubyDNS::run_server(options) || true
75
- if !@@server
76
- @@server = TCPPipeliningServer.new()
77
-
78
- Thread.new do
79
- @@server.run
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)
25
+ @server = PipelineTest::Server.new
26
+ @query_id = 0
27
+ @resolver = Dnsruby::Resolver.new(
28
+ use_tcp: true,
29
+ do_caching: false,
30
+ tcp_pipelining: true,
31
+ dnssec: false,
32
+ retry_times: 1,
33
+ packet_timeout: WAIT_TIMEOUT,
34
+ query_timeout: WAIT_TIMEOUT,
35
+ tcp_pipelining_max_queries: 10,
36
+ nameserver: PipelineTest::Server::IP,
37
+ port: @server.port
38
+ )
94
39
  end
95
40
 
96
- # Send x number of queries asynchronously to our resolver
97
- def send_async_messages(number_of_messages, queue, wait_seconds = 0)
98
- Dnsruby.log.debug "Sending #{number_of_messages} messages"
99
- number_of_messages.times do
100
- name = "#{self.class.query_id}.com"
101
- Dnsruby.log.debug "Sending #{name}"
102
- message = Dnsruby::Message.new(name)
103
- # self.class.query_id identifies our query, must be different for each message
104
- @@resolver.send_async(message, queue, self.class.query_id)
105
- self.class.query_id += 1
41
+ def teardown
42
+ @resolver.close if @resolver
43
+ ensure
44
+ @server.stop if @server
45
+ wait_for_client_close if @resolver
46
+ end
106
47
 
107
- # Note: For 0, we don't sleep at all instead of sleeping 0 since sleeping 0
108
- # involves yielding the CPU.
109
- sleep wait_seconds unless wait_seconds == 0
48
+ def send_async_messages(count, queue)
49
+ Array.new(count) do
50
+ id = @query_id
51
+ @query_id += 1
52
+ message = Dnsruby::Message.new("pipeline-#{id}.example.invalid")
53
+ message.do_caching = false
54
+ message.do_validation = false
55
+ @resolver.send_async(message, queue, id)
56
+ id
110
57
  end
111
58
  end
112
59
 
113
- # Verify x responses with no exception
114
- def verify_responses(number_of_messages, queue)
115
- number_of_messages.times do
116
- _response_id, response, exception = queue.pop
117
- assert_nil(exception)
118
- assert(response.is_a?(Dnsruby::Message))
60
+ def responses_for(ids, queue)
61
+ responses = Timeout.timeout(WAIT_TIMEOUT + 1) do
62
+ Array.new(ids.length) { queue.pop }
119
63
  end
64
+ @server.stats # Propagate any exception in the server thread.
65
+ assert_equal ids.sort, responses.map(&:first).sort
66
+ responses.sort_by(&:first)
120
67
  end
121
68
 
122
- def accept_wait(accept_count, max)
123
- i = 0
124
- while TCPPipeliningServer.stats.accept_count < accept_count
125
- sleep 0.5
126
- i+=0.5
127
- assert(i<max, "Max wait for accept reached #{TCPPipeliningServer.stats.accept_count} accepts < #{accept_count}")
69
+ def verify_responses(ids, queue)
70
+ responses_for(ids, queue).each do |id, response, exception|
71
+ assert_nil exception
72
+ assert_instance_of Dnsruby::Message, response
73
+ assert_equal "pipeline-#{id}.example.invalid", response.question.first.qname.to_s
74
+ assert_equal PipelineTest::Server::ANSWER, response.answer.first.address.to_s
128
75
  end
129
76
  end
130
77
 
131
- def connection_wait(connection_count, max)
132
- i = 0
133
- while TCPPipeliningServer.stats.connections > connection_count
134
- sleep 0.5
135
- i+=0.5
136
- assert(i<max, "Max wait for connection reached: #{TCPPipeliningServer.stats.connections} active connections > #{connection_count}")
78
+ def wait_for_timeouts(count)
79
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + WAIT_TIMEOUT
80
+ until @server.stats[:timeouts] == count && @server.stats[:connections].zero?
81
+ flunk "Timed out waiting for #{count} connection expiry/expiries: #{@server.stats}" if
82
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
83
+ sleep 0.01
137
84
  end
85
+ # A server-side close does not mean the client's select thread has seen
86
+ # the FIN yet. Reconnect only after it has processed EOF.
87
+ wait_for_client_close
138
88
  end
139
89
 
140
- def timeout_wait(timeout_count, max)
141
- i = 0
142
- while TCPPipeliningServer.stats.timeout_count < timeout_count
143
- sleep 0.5
144
- i+=0.5
145
- assert(i<max, "Max wait for timeout reached #{TCPPipeliningServer.stats.timeout_count} timeounts < #{timeout_count}")
90
+ def wait_for_client_close
91
+ sender = @resolver.single_resolvers.first
92
+ socket = sender.instance_variable_get(:@pipeline_socket)
93
+ Timeout.timeout(WAIT_TIMEOUT) do
94
+ sleep 0.01 while socket && !socket.closed?
146
95
  end
147
96
  end
148
97
 
149
- # This test initiates multiple asynchronous requests and verifies they go on the same tcp
150
- # pipeline or a new one depending on timeouts
98
+ # Multiple outstanding queries share one TCP connection; after it expires,
99
+ # the next batch must use a new connection.
151
100
  def test_TCP_pipelining_timeout
152
- return
153
- Dnsruby.log.debug "test_TCP_pipelining_timeout"
154
- connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
155
-
156
- accept_count = TCPPipeliningServer.stats.accept_count
157
- timeout_count = TCPPipeliningServer.stats.timeout_count
158
-
159
- # This is the main queue used to communicate between Dnsruby in async mode and the client
160
- query_queue = Queue.new
161
-
162
- # Test basic pipelining. All request should go on the same tcp connection.
163
- # TCPPipeliningServer.stats.accept_count should be 1.
164
- send_async_messages(3, query_queue)
165
- verify_responses(3, query_queue)
166
-
167
- assert_equal(accept_count + 1, TCPPipeliningServer.stats.accept_count)
168
-
169
- connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
170
- timeout_wait(timeout_count + 1, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
171
-
172
- assert_equal(timeout_count + 1, TCPPipeliningServer.stats.timeout_count)
173
-
174
- # Initiate another 3 queries, check accept_count and timeout_count
175
- send_async_messages(3, query_queue)
176
- verify_responses(3, query_queue)
177
-
178
- connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
179
- timeout_wait(timeout_count + 2, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
180
-
181
- assert_equal(accept_count + 2, TCPPipeliningServer.stats.accept_count)
182
- assert_equal(timeout_count + 2, TCPPipeliningServer.stats.timeout_count)
183
-
184
- connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
101
+ queue = Queue.new
102
+ verify_responses(send_async_messages(3, queue), queue)
103
+ assert_equal 1, @server.stats[:accepts]
104
+ wait_for_timeouts(1)
105
+
106
+ verify_responses(send_async_messages(3, queue), queue)
107
+ assert_equal 2, @server.stats[:accepts]
108
+ wait_for_timeouts(2)
109
+ assert_equal 2, @server.stats[:timeouts]
185
110
  end
186
111
 
187
- # Test timeout occurs and new connection is initiated inbetween 2 sends
112
+ # The server closes the pipeline between sends, and a single subsequent
113
+ # query must reconnect successfully.
188
114
  def test_TCP_pipelining_timeout_in_send
189
- return
190
- Dnsruby.log.debug "test_TCP_pipelining_timeout_in_send"
191
- connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
192
-
193
- accept_count = TCPPipeliningServer.stats.accept_count
194
- timeout_count = TCPPipeliningServer.stats.timeout_count
195
-
196
- query_queue = Queue.new
197
-
198
- # Initiate another 2 queries wait and then send a final query
199
- # Check accept_count. Wait for timeout and verify we got 2 additional timeouts.
200
- send_async_messages(2, query_queue)
201
- verify_responses(2, query_queue)
202
-
203
- accept_wait(accept_count+1, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
204
- connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
205
-
206
- send_async_messages(1, query_queue)
207
-
208
- verify_responses(1, query_queue)
209
-
210
- assert_equal(accept_count + 2, TCPPipeliningServer.stats.accept_count)
211
-
212
- connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
213
-
214
- timeout_wait(timeout_count + 2, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
115
+ queue = Queue.new
116
+ verify_responses(send_async_messages(2, queue), queue)
117
+ assert_equal 1, @server.stats[:accepts]
118
+ wait_for_timeouts(1)
119
+
120
+ verify_responses(send_async_messages(1, queue), queue)
121
+ assert_equal 2, @server.stats[:accepts]
122
+ wait_for_timeouts(2)
123
+ assert_equal 2, @server.stats[:timeouts]
215
124
  end
216
125
 
217
- # Test that we get a SocketEofResolvError if the servers closes the socket before
218
- # all queries are answered
126
+ # The peer answers only four of six outstanding queries before closing.
127
+ # Each unanswered query must receive EOF, rather than a resolver timeout.
219
128
  def test_TCP_pipelining_socket_eof
220
- return
221
- connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
222
-
223
- query_queue = Queue.new
224
-
225
- # Issue 6 queries. Only 4 should be replied since max_request_per_connection = 4
226
- # Verify we get Dnsruby::SocketEofResolvError on the last 2.
227
- # Verify we got max_count was incremented
228
- send_async_messages(6, query_queue)
229
-
230
- responses = []
231
-
232
- 6.times do
233
- response = query_queue.pop
234
- responses << response
235
- end
236
-
237
- responses.sort_by { |response| response[0] }
238
-
239
- step = 0
240
-
241
- responses.each do | response |
242
- _response_id, response, exception = response
243
- if step < TCPPipeliningServer::DEFAULT_MAX_REQUESTS
244
- assert_nil(exception, "Exception not nil for msg #{step} < #{TCPPipeliningServer::DEFAULT_MAX_REQUESTS} requests")
245
- assert(response.is_a?(Dnsruby::Message))
129
+ queue = Queue.new
130
+ ids = send_async_messages(6, queue)
131
+ responses_for(ids, queue).each do |id, response, exception|
132
+ if id < PipelineTest::Server::MAX_REQUESTS
133
+ assert_nil exception
134
+ assert_instance_of Dnsruby::Message, response
135
+ assert_equal "pipeline-#{id}.example.invalid", response.question.first.qname.to_s
136
+ assert_equal PipelineTest::Server::ANSWER, response.answer.first.address.to_s
246
137
  else
247
- assert_equal(Dnsruby::SocketEofResolvError, exception.class)
248
- assert_nil(response)
138
+ assert_instance_of Dnsruby::SocketEofResolvError, exception
139
+ assert_nil response
249
140
  end
250
- step += 1
251
141
  end
252
-
253
- connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
142
+ wait_for_timeouts(1)
143
+ assert_equal 1, @server.stats[:accepts]
144
+ assert_equal 1, @server.stats[:limits]
254
145
  end
255
146
  end
@@ -0,0 +1,179 @@
1
+ require_relative 'spec_helper'
2
+ require 'open3'
3
+ require 'rbconfig'
4
+
5
+ class TestTransportFork < Minitest::Test
6
+ def test_child_discards_parent_transactions_descriptors_and_worker_state
7
+ skip 'fork unavailable' unless Process.respond_to?(:fork)
8
+ code = <<~'CHILD'
9
+ require 'dnsruby'
10
+ require 'timeout'
11
+ Dnsruby.log.level = Logger::FATAL
12
+ # Own dispatch explicitly: no DNS service or elapsed timeout is needed.
13
+ class Dnsruby::SelectThread
14
+ def get_socket_pair
15
+ Socket.pair(:UNIX, :STREAM, 0)
16
+ end
17
+
18
+ def do_select
19
+ sleep
20
+ end
21
+ end
22
+ Timeout.timeout(10) do
23
+ selector = Dnsruby::SelectThread.instance
24
+ klass = Dnsruby::SelectThread
25
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
26
+ sender = resolver.single_resolvers.first
27
+ sender.define_singleton_method(:send_async) { |*| }
28
+ request = Dnsruby::Message.new('fork.example.')
29
+ request.do_caching = request.do_validation = false
30
+ results = Queue.new
31
+ resolver.send_async(request, results, :reused)
32
+ engine = resolver.instance_variable_get(:@resolver_ruby)
33
+ inherited_attempt = engine.instance_variable_get(:@query_list)[:reused][3].first
34
+ socket, peer = Socket.pair(:UNIX, :STREAM, 0)
35
+ pending_socket, pending_peer = Socket.pair(:UNIX, :STREAM, 0)
36
+ selector.track_socket(pending_socket) # A socket still connecting/writing.
37
+ settings = klass::QuerySettings.new(request.encode, request, false, Queue.new,
38
+ :parent_stream, socket, '127.0.0.1', 53, Dnsruby::TransportClock.now + 60, 512, sender)
39
+ settings.is_persistent_socket = true
40
+ settings.tcp_pipelining_max_queries = :infinite
41
+ selector.add_to_select(settings)
42
+ sender.instance_variable_set(:@pipeline_socket, socket)
43
+ sender.instance_variable_set(:@use_counts, { socket => 1 })
44
+ writer_lock = Mutex.new
45
+ writer_lock.lock
46
+ sender.instance_variable_set(:@pipeline_mutex, writer_lock)
47
+ wakeups = klass.class_variable_get(:@@wakeup_sockets).dup
48
+ started, release = Queue.new, Queue.new
49
+ selector.defer_send(sender) { started << true; release.pop }
50
+ started.pop
51
+
52
+ child_status = ForkTestHelper.run do
53
+ begin
54
+ # A retained reference must be as safe as fetching the singleton.
55
+ raise 'inherited transaction survived' if selector.query_settings_for_id(:parent_stream)
56
+ raise 'singleton changed' unless Dnsruby::SelectThread.instance.equal?(selector)
57
+ raise 'inherited descriptors open' unless ([socket, pending_socket] + wakeups).all?(&:closed?)
58
+ [:query_hash, :socket_hash, :timeouts, :observers, :observer_queue_counts,
59
+ :tcp_buffers, :socket_owners, :socket_remaining_queries,
60
+ :queued_exceptions, :queued_responses, :queued_validation_responses,
61
+ :validations, :tick_observers].each do |name|
62
+ raise "inherited #{name}" unless klass.class_variable_get("@@#{name}").empty?
63
+ end
64
+ raise 'inherited sockets still selected' unless klass.class_variable_get(:@@sockets).size == 1
65
+ raise 'new wakeup pair unavailable' unless klass.class_variable_get(:@@wakeup_sockets).none?(&:closed?)
66
+ raise 'inherited workers retained' if selector.instance_variable_get(:@send_workers)
67
+ selector.defer_send(sender) { :child_work }.value == :child_work or raise 'new worker failed'
68
+
69
+ # A real pipeline send must choose a child-owned connection before
70
+ # taking the writer lock that the parent held when it forked.
71
+ child_socket, child_peer = Socket.pair(:UNIX, :STREAM, 0)
72
+ sender.tcp_pipelining = true
73
+ sender.define_singleton_method(:connect_tcp) do |*|
74
+ selector.track_socket(child_socket)
75
+ end
76
+ direct_results = Queue.new
77
+ sender.send_dnsruby(request.encode, request, :child_stream, direct_results, true)
78
+ raise 'child did not open its own pipeline' unless sender.instance_variable_get(:@pipeline_socket).equal?(child_socket)
79
+ raise 'inherited writer mutex retained' if sender.instance_variable_get(:@pipeline_mutex).equal?(writer_lock)
80
+ raise 'inherited use counts retained' if sender.instance_variable_get(:@use_counts).key?(socket)
81
+ frame_size = child_peer.read(2).unpack1('n')
82
+ child_request = Dnsruby::Message.decode(child_peer.read(frame_size))
83
+ raise 'child frame incorrect' unless child_request.question == request.question
84
+ direct_answer = request.clone
85
+ direct_answer.header.qr = true
86
+ selector.process_message(direct_answer, direct_answer.encode, child_socket)
87
+ selector.send_queued_validation_responses
88
+ raise 'child pipeline response failed' unless direct_results.size == 2 &&
89
+ 2.times.map { direct_results.pop }.all? { |event| event.last.nil? }
90
+ sender.close
91
+ child_peer.close
92
+
93
+ # Resolver IDs also belong to the parent and can be reused by child
94
+ # transactions without delivering a spurious duplicate-ID failure.
95
+ resolver.send_async(request.clone, results, :reused)
96
+ current = engine.instance_variable_get(:@query_list)[:reused]
97
+ raise 'child query rejected' unless current && results.empty?
98
+ pending = selector.query_settings_for_id(current[3].first)
99
+ raise 'child attempt absent' unless pending
100
+ answer = request.clone
101
+ answer.header.qr = true
102
+ answer.add_answer(Dnsruby::RR.create('fork.example. 60 IN A 192.0.2.1'))
103
+ selector.push_response_for_query(pending, answer, pending.query, sender)
104
+ selector.send_queued_responses
105
+ raise 'child completion failed' unless results.size == 1 && results.pop.last.nil?
106
+ resolver.close
107
+ exit! 0
108
+ rescue Exception => error
109
+ warn error.full_message
110
+ exit! 1
111
+ end
112
+ end
113
+ raise 'fork child failed' unless child_status.success?
114
+ raise 'parent descriptor closed' if ([socket, pending_socket] + wakeups).any?(&:closed?)
115
+ raise 'parent transaction discarded' unless selector.query_settings_for_id(inherited_attempt)
116
+ raise 'parent observer discarded' unless klass.class_variable_get(:@@observers).size == 1
117
+ raise 'parent received child result' unless results.empty?
118
+ peer.write('parent')
119
+ raise 'parent connection damaged' unless socket.read(6) == 'parent'
120
+ release << true
121
+ writer_lock.unlock
122
+ resolver.close
123
+ [socket, peer, pending_socket, pending_peer].each { |io| io.close unless io.closed? }
124
+ end
125
+ CHILD
126
+ source = "require #{File.expand_path('fork_test_helper', __dir__).inspect}\n#{code}"
127
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-I',
128
+ File.expand_path('../lib', __dir__), '-e', source)
129
+ assert(status.success?, "#{stdout}\n#{stderr}")
130
+ assert_empty(stderr)
131
+ end
132
+
133
+ def test_fork_timeout_reaps_the_child_and_closes_its_output_pipes
134
+ skip 'fork unavailable' unless Process.respond_to?(:fork)
135
+ source = <<~RUBY
136
+ require #{File.expand_path('fork_test_helper', __dir__).inspect}
137
+ # Start the timer service before forking, as the transport tests do.
138
+ Timeout.timeout(3) do
139
+ status = ForkTestHelper.run(timeout: 0.05) { sleep }
140
+ raise 'stalled child reported success' unless status.exitstatus == 1
141
+ end
142
+ begin
143
+ Process.wait(-1, Process::WNOHANG)
144
+ raise 'child was not reaped'
145
+ rescue Errno::ECHILD
146
+ end
147
+ RUBY
148
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-e', source)
149
+ assert(status.success?, "#{stdout}\n#{stderr}")
150
+ assert_match(/Timeout::Error/, stderr)
151
+ end
152
+
153
+ def test_parent_interruption_also_reaps_its_waiting_child
154
+ skip 'fork unavailable' unless Process.respond_to?(:fork)
155
+ source = <<~RUBY
156
+ require #{File.expand_path('fork_test_helper', __dir__).inspect}
157
+ class ParentInterrupted < StandardError; end
158
+ # This thread exists only in the parent, so the child cannot inherit
159
+ # a pending Timeout request and finish before the parent is interrupted.
160
+ interrupter = Thread.new { sleep 0.1; Thread.main.raise ParentInterrupted }
161
+ begin
162
+ ForkTestHelper.run { sleep }
163
+ raise 'parent was not interrupted'
164
+ rescue ParentInterrupted
165
+ ensure
166
+ interrupter.kill
167
+ interrupter.join
168
+ end
169
+ begin
170
+ Process.wait(-1, Process::WNOHANG)
171
+ raise 'child was not reaped'
172
+ rescue Errno::ECHILD
173
+ end
174
+ RUBY
175
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-e', source)
176
+ assert(status.success?, "#{stdout}\n#{stderr}")
177
+ assert_empty(stderr)
178
+ end
179
+ end
data/test/tc_tsig.rb CHANGED
@@ -16,11 +16,11 @@
16
16
 
17
17
  require_relative 'spec_helper'
18
18
 
19
- require "digest/md5"
20
19
  class TestTSig < Minitest::Test
21
20
 
22
21
  include Dnsruby
23
22
 
23
+ # The online fixture must configure this key for HMAC-SHA256 on the server.
24
24
  KEY_NAME="rubytsig"
25
25
  KEY = "8n6gugn4aJ7MazyNlMccGKH1WxD2B3UvN/O/RA6iBupO2/03u9CTa3Ewz3gBWTSBCH3crY4Kk+tigNdeJBAvrw=="
26
26
  def is_empty(string)
@@ -52,7 +52,7 @@ class TestTSig < Minitest::Test
52
52
  :type => "TSIG",
53
53
  :ttl => 0,
54
54
  :klass => "ANY",
55
- :algorithm => "hmac-md5",
55
+ :algorithm => "hmac-sha256",
56
56
  :fudge => 300,
57
57
  :key => KEY,
58
58
  :error => 0