dnsruby 1.74.0 → 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 +302 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/demo/rubydig.rb +0 -2
  10. data/dnsruby.gemspec +5 -4
  11. data/lib/dnsruby/cache.rb +279 -47
  12. data/lib/dnsruby/code_mapper.rb +35 -8
  13. data/lib/dnsruby/code_mappers.rb +63 -49
  14. data/lib/dnsruby/config.rb +75 -4
  15. data/lib/dnsruby/deadline_queue.rb +75 -0
  16. data/lib/dnsruby/dnssec.rb +148 -45
  17. data/lib/dnsruby/ipv6.rb +45 -5
  18. data/lib/dnsruby/key_cache.rb +202 -35
  19. data/lib/dnsruby/message/decoder.rb +85 -19
  20. data/lib/dnsruby/message/encoder.rb +27 -16
  21. data/lib/dnsruby/message/header.rb +8 -12
  22. data/lib/dnsruby/message/message.rb +53 -22
  23. data/lib/dnsruby/message/section.rb +12 -34
  24. data/lib/dnsruby/name.rb +5 -2
  25. data/lib/dnsruby/packet_sender.rb +645 -203
  26. data/lib/dnsruby/presentation.rb +147 -0
  27. data/lib/dnsruby/query_control.rb +132 -0
  28. data/lib/dnsruby/recursor.rb +795 -339
  29. data/lib/dnsruby/resolver.rb +468 -186
  30. data/lib/dnsruby/resource/CAA.rb +67 -16
  31. data/lib/dnsruby/resource/DLV.rb +6 -0
  32. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  33. data/lib/dnsruby/resource/DS.rb +27 -28
  34. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  35. data/lib/dnsruby/resource/IN.rb +19 -0
  36. data/lib/dnsruby/resource/MX.rb +2 -2
  37. data/lib/dnsruby/resource/NSEC.rb +6 -10
  38. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  39. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  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 +979 -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 +42 -20
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +21 -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 +105 -10
  167. data/.coveralls.yml +0 -2
@@ -0,0 +1,712 @@
1
+ require_relative 'spec_helper'
2
+ require 'minitest/mock'
3
+ require 'timeout'
4
+ require 'socket'
5
+ require 'open3'
6
+ require 'rbconfig'
7
+
8
+ class TestMalformedTcp < Minitest::Test
9
+ # Keep selector fixtures off IO.select: a live resolver in another test may
10
+ # share these tables. Only the dedicated allocated receiver uses the fakes.
11
+ class SocketBuffer
12
+ attr_reader :closed
13
+ alias closed? closed
14
+ attr_accessor :udp_response
15
+
16
+ def initialize(bytes = ''.b)
17
+ @bytes = bytes.dup
18
+ @closed = false
19
+ end
20
+
21
+ def recv_nonblock(length)
22
+ @bytes.slice!(0, [length, 1].min) # Fragment prefix and body alike.
23
+ end
24
+
25
+ def close
26
+ @closed = true
27
+ end
28
+
29
+ def recvfrom_nonblock(_length)
30
+ @udp_response
31
+ end
32
+ end
33
+
34
+ def setup
35
+ @selector_class = Dnsruby::SelectThread
36
+ @temporary_variables = []
37
+ { :@@mutex => Mutex.new, :@@query_hash => {}, :@@queries_by_queue => {}.compare_by_identity, :@@socket_hash => {},
38
+ :@@timeouts => {}, :@@sockets => Set.new, :@@tcp_buffers => {},
39
+ :@@socket_owners => {}, :@@retiring_sockets => Set.new,
40
+ :@@socket_is_persistent => {}, :@@socket_remaining_queries => {}, :@@queued_exceptions => [] }.each do |name, value|
41
+ next if @selector_class.class_variable_defined?(name)
42
+ @selector_class.class_variable_set(name, value)
43
+ @temporary_variables << name
44
+ end
45
+ @selector = @selector_class.send(:allocate)
46
+ @selector.define_singleton_method(:tcp?) { |_| true }
47
+ @selector.define_singleton_method(:notify_queue_observers) { |*| }
48
+ @registered = []
49
+ end
50
+
51
+ def teardown
52
+ table(:mutex).synchronize do
53
+ @registered.each do |settings|
54
+ table(:query_hash).delete(settings.client_query_id)
55
+ table(:queries_by_queue).delete(settings.client_queue)
56
+ table(:timeouts).delete(settings.client_query_id)
57
+ [:socket_hash, :tcp_buffers, :socket_is_persistent, :socket_remaining_queries, :socket_owners].each do |name|
58
+ table(name).delete(settings.socket)
59
+ end
60
+ table(:retiring_sockets).delete(settings.socket)
61
+ end
62
+ end
63
+ @temporary_variables.each { |name| @selector_class.remove_class_variable(name) }
64
+ end
65
+
66
+ def test_short_matching_frames_report_only_the_matching_pipeline_query
67
+ [2, 4, 11].each do |length|
68
+ socket = SocketBuffer.new
69
+ first = register(socket, 12_345)
70
+ second = register(socket, 12_346)
71
+ packet = [12_346, 0x8000, 0, 0, 0, 0].pack('n6')[0, length]
72
+ @selector.stub(:tcp_read, packet) { assert_nil(@selector.get_incoming_data(socket, 0)) }
73
+ event = second.client_queue.pop(true)
74
+ assert_equal(second.client_query_id, event[0])
75
+ assert_kind_of(Dnsruby::DecodeError, event.last)
76
+ assert(first.client_queue.empty?)
77
+ assert_same(first, table(:query_hash)[first.client_query_id])
78
+ refute(socket.closed)
79
+ end
80
+ end
81
+
82
+ def test_unknown_and_unidentifiable_pipeline_frames_leave_queries_active
83
+ socket = SocketBuffer.new
84
+ first = register(socket, 12_350)
85
+ second = register(socket, 12_351)
86
+ packets = ['', 'x', [12_352].pack('n'),
87
+ [12_352, 0x8000, 1, 0, 0, 0].pack('n6'),
88
+ [12_352, 0x8000, 0, 0, 0, 0].pack('n6')]
89
+ packets.each do |packet|
90
+ @selector.stub(:tcp_read, packet) { assert_nil(@selector.get_incoming_data(socket, 0)) }
91
+ [first, second].each do |settings|
92
+ assert(settings.client_queue.empty?)
93
+ assert_same(settings, table(:query_hash)[settings.client_query_id])
94
+ end
95
+ end
96
+ refute(socket.closed)
97
+ end
98
+
99
+ def test_unidentifiable_frame_on_single_query_reports_decode_error
100
+ ['', 'x'].each do |packet|
101
+ settings = register(SocketBuffer.new, 12_355)
102
+ @selector.stub(:tcp_read, packet) { assert_nil(@selector.get_incoming_data(settings.socket, 0)) }
103
+ assert_kind_of(Dnsruby::DecodeError, settings.client_queue.pop(true).last)
104
+ end
105
+ end
106
+
107
+ def test_duplicate_ids_on_one_pipeline_socket_are_ambiguous
108
+ socket = SocketBuffer.new
109
+ first = register(socket, 12_356)
110
+ second = register(socket, 12_356)
111
+ [2, 12].each do |length|
112
+ packet = [12_356, 0x8000, 1, 0, 0, 0].pack('n6')[0, length]
113
+ @selector.stub(:tcp_read, packet) { assert_nil(@selector.get_incoming_data(socket, 0)) }
114
+ [first, second].each do |settings|
115
+ assert(settings.client_queue.empty?)
116
+ assert_same(settings, table(:query_hash)[settings.client_query_id])
117
+ end
118
+ end
119
+ end
120
+
121
+ def test_valid_in_order_replies_with_duplicate_ids_keep_existing_behavior
122
+ socket = SocketBuffer.new
123
+ first = register(socket, 12_357)
124
+ second = register(socket, 12_357)
125
+ second.query = Dnsruby::Message.new('second.example.')
126
+ second.query.header.id = 12_357
127
+ delivered = []
128
+ @selector.stub(:push_to_client, ->(*args) { delivered << args }) do
129
+ [first, second].each do |settings|
130
+ settings.single_resolver = Dnsruby::PacketSender.allocate
131
+ response = settings.query.clone
132
+ response.header.qr = true
133
+ @selector.stub(:tcp_read, response.encode) do
134
+ decoded, bytes = @selector.get_incoming_data(socket, 0)
135
+ @selector.send_response_to_client(decoded, bytes, socket)
136
+ end
137
+ end
138
+ end
139
+ assert_equal([first.client_query_id, second.client_query_id], delivered.map(&:first))
140
+ assert(delivered.all? { |args| args[3].nil? })
141
+ assert_equal([], table(:socket_hash)[socket])
142
+ refute(socket.closed)
143
+ end
144
+
145
+ def test_reverse_order_duplicate_ids_match_question_before_tsig_checks
146
+ socket = SocketBuffer.new
147
+ first = register(socket, 12_358)
148
+ second = register(socket, 12_358)
149
+ second.query = Dnsruby::Message.new('second.example.')
150
+ second.query.header.id = 12_358
151
+ [first, second].each do |settings|
152
+ settings.single_resolver = Dnsruby::PacketSender.allocate
153
+ settings.single_resolver.define_singleton_method(:check_tsig) do |request, response, _|
154
+ raise 'TSIG checked against the wrong query' unless request.question.first.qname == response.question.first.qname
155
+ :okay
156
+ end
157
+ end
158
+ delivered = []
159
+ @selector.stub(:push_to_client, ->(*args) { delivered << args }) do
160
+ [second, first].each do |settings|
161
+ response = settings.query.clone
162
+ response.header.qr = true
163
+ @selector.send_response_to_client(response, response.encode, socket)
164
+ end
165
+ end
166
+ assert_equal([second.client_query_id, first.client_query_id], delivered.map(&:first))
167
+ assert(delivered.all? { |args| args[3].nil? })
168
+ assert_equal([], table(:socket_hash)[socket])
169
+ end
170
+
171
+ def test_matching_ids_on_other_sockets_do_not_supply_tcp_peer_metadata
172
+ first = register(SocketBuffer.new, 12_360, '192.0.2.1', 53)
173
+ second = register(SocketBuffer.new, 12_360, '192.0.2.2', 54)
174
+ response = second.query.clone
175
+ response.header.qr = true
176
+ wire = response.encode
177
+ @selector.stub(:tcp_read, wire) do
178
+ decoded, bytes = @selector.get_incoming_data(second.socket, 0)
179
+ assert_equal('192.0.2.2', decoded.answerip)
180
+ assert_equal(wire, bytes)
181
+ end
182
+ # The same ID on another socket does not make an unsolicited frame ours.
183
+ second.query.header.id = 12_361
184
+ @selector.stub(:tcp_read, wire) { assert_nil(@selector.get_incoming_data(second.socket, 0)) }
185
+ assert(first.client_queue.empty?)
186
+ assert(second.client_queue.empty?)
187
+ end
188
+
189
+ def test_malformed_udp_requires_matching_id_address_and_port
190
+ socket = SocketBuffer.new
191
+ settings = register(socket, 12_370, '192.0.2.1', 53)
192
+ [
193
+ [12_371, '192.0.2.1', 53],
194
+ [12_370, '192.0.2.2', 53],
195
+ [12_370, '192.0.2.1', 54],
196
+ ].each do |id, source, port|
197
+ socket.udp_response = [[id, 0x8000, 1, 0, 0, 0].pack('n6'), ['AF_INET', port, source, source]]
198
+ @selector.stub(:tcp?, false) { assert_nil(@selector.get_incoming_data(socket, 512)) }
199
+ assert(settings.client_queue.empty?)
200
+ assert_same(settings, table(:query_hash)[settings.client_query_id])
201
+ end
202
+ end
203
+
204
+ def test_malformed_truncated_tcp_response_is_terminal_without_retrying
205
+ settings = register(SocketBuffer.new, 12_372)
206
+ sender = Object.new
207
+ sender.define_singleton_method(:send_async) { |*| raise 'Unexpected TCP retry' }
208
+ settings.single_resolver = sender
209
+ packet = [settings.query.header.id, 0x8200, 1, 0, 0, 0].pack('n6')
210
+ @selector.stub(:tcp_read, packet) do
211
+ assert_nil(@selector.get_incoming_data(settings.socket, 0))
212
+ # A second buffered frame after terminal delivery cannot start a retry.
213
+ assert_nil(@selector.get_incoming_data(settings.socket, 0))
214
+ end
215
+ assert_kind_of(Dnsruby::DecodeError, settings.client_queue.pop(true).last)
216
+ assert(settings.client_queue.empty?)
217
+ refute(table(:query_hash).key?(settings.client_query_id))
218
+ end
219
+
220
+ def test_malformed_truncated_udp_respects_transport_options
221
+ [[true, false], [false, true], [true, true]].each do |no_tcp, ignore_truncation|
222
+ socket = SocketBuffer.new
223
+ settings = register(socket, 12_373)
224
+ settings.ignore_truncation = ignore_truncation
225
+ sender = Object.new
226
+ sender.define_singleton_method(:no_tcp) { no_tcp }
227
+ sender.define_singleton_method(:send_async) { |*| raise 'Unexpected UDP fallback' }
228
+ settings.single_resolver = sender
229
+ packet = settings.query.encode
230
+ packet[2, 2] = [0x8200].pack('n')
231
+ packet[6, 2] = [1].pack('n') # A complete matching question, then a missing answer.
232
+ socket.udp_response = [packet, ['AF_INET', 53, '127.0.0.1', '127.0.0.1']]
233
+ @selector.stub(:tcp?, false) { assert_nil(@selector.get_incoming_data(socket, 512)) }
234
+ assert_kind_of(Dnsruby::DecodeError, settings.client_queue.pop(true).last)
235
+ refute(table(:query_hash).key?(settings.client_query_id))
236
+ end
237
+ end
238
+
239
+ def test_fragmented_zero_one_and_two_byte_frames_do_not_consume_next_frame
240
+ [0, 1, 2].each do |length|
241
+ short = 'x' * length
242
+ valid = Dnsruby::Message.new('following.example.').encode
243
+ socket = SocketBuffer.new([short.bytesize].pack('n') + short + [valid.bytesize].pack('n') + valid)
244
+ # Register only for fixture cleanup; tcp_read itself does not decode.
245
+ register(socket, 12_380 + length)
246
+ frames = []
247
+ (short.bytesize + valid.bytesize + 8).times do
248
+ frame = @selector.tcp_read(socket)
249
+ frames << frame if frame
250
+ break if frames.length == 2
251
+ end
252
+ assert_equal([short, valid], frames)
253
+ refute(socket.closed)
254
+ refute(table(:tcp_buffers).key?(socket))
255
+ end
256
+ end
257
+
258
+ def test_error_for_removed_query_is_harmless
259
+ socket = SocketBuffer.new
260
+ assert_nil(@selector.send_exception_to_client(Dnsruby::DecodeError.new, socket, Object.new))
261
+ @selector.stub(:tcp_read, Dnsruby::Message.new.encode) do
262
+ assert_nil(@selector.get_incoming_data(socket, 0))
263
+ end
264
+ end
265
+
266
+ def test_eof_after_fragment_fails_all_pending_queries_and_clears_framing
267
+ socket = SocketBuffer.new("\x00".b)
268
+ first = register(socket, 12_390)
269
+ second = register(socket, 12_391)
270
+ assert_equal(false, @selector.tcp_read(socket))
271
+ assert_equal(false, @selector.tcp_read(socket))
272
+ [first, second].each do |settings|
273
+ assert_kind_of(Dnsruby::SocketEofResolvError, settings.client_queue.pop(true).last)
274
+ refute(table(:query_hash).key?(settings.client_query_id))
275
+ end
276
+ assert(socket.closed)
277
+ [:socket_hash, :tcp_buffers, :socket_is_persistent, :socket_remaining_queries].each do |name|
278
+ refute(table(name).key?(socket))
279
+ end
280
+ end
281
+
282
+ def test_idle_selector_drains_deliveries_registered_at_shutdown_boundary
283
+ # The singleton tables and IO.select stub must be isolated from any real
284
+ # resolver running elsewhere in the suite. No network sockets are needed.
285
+ script = <<~'RUBY'
286
+ require 'dnsruby'
287
+ require 'timeout'
288
+ klass = Dnsruby::SelectThread
289
+ [:closed_query, :response, :validation].each do |kind|
290
+ selector = klass.send(:allocate)
291
+ wakeup = Object.new
292
+ wakeup.define_singleton_method(:closed?) { false }
293
+ socket = Object.new
294
+ socket.define_singleton_method(:closed?) { true }
295
+ query = Dnsruby::Message.new('late.example.')
296
+ query.do_validation = query.do_caching = false
297
+ response = query.clone
298
+ response.header.qr = true
299
+ queue = Queue.new
300
+ settings = Dnsruby::SelectThread::QuerySettings.new
301
+ settings.query, settings.client_query_id = query, :late
302
+ settings.client_queue, settings.socket = queue, socket
303
+ settings.endtime = Dnsruby::TransportClock.now + 60
304
+ {
305
+ mutex: Mutex.new, sockets: Set.new([wakeup]), timeouts: {}, query_hash: {}, queries_by_queue: {}.compare_by_identity,
306
+ socket_hash: {}, socket_is_persistent: {}, socket_remaining_queries: {},
307
+ tcp_buffers: {}, select_thread: Thread.current, observers: {}, observer_queue_counts: {}.compare_by_identity,
308
+ queued_exceptions: [], queued_responses: [], queued_validation_responses: [],
309
+ tick_observers: [], wakeup_sockets: [wakeup, wakeup]
310
+ }.each { |name, value| klass.class_variable_set("@@#{name}", value) }
311
+ selector.define_singleton_method(:wake_up_select_thread) {}
312
+ selector.define_singleton_method(:send_tick_to_observers) {}
313
+ selector.define_singleton_method(:notify_queue_observers) { |*| }
314
+ iterations = 0
315
+ selector.define_singleton_method(:process_timeouts) do
316
+ # IO.select has returned on the last otherwise-idle iteration. A
317
+ # producer gets the GVL here, just before the shutdown mutex is taken.
318
+ if iterations == 11
319
+ case kind
320
+ when :closed_query then add_to_select(settings)
321
+ when :response then push_response_to_select(:late, queue, response, query, nil)
322
+ when :validation then push_validation_response_to_select(:late, queue, response, nil, query, nil)
323
+ end
324
+ end
325
+ end
326
+ original_select = IO.method(:select)
327
+ IO.define_singleton_method(:select) { |*| iterations += 1; nil }
328
+ begin
329
+ Timeout.timeout(2) { selector.do_select }
330
+ ensure
331
+ IO.define_singleton_method(:select, original_select)
332
+ end
333
+ expected = kind == :response ? 2 : 1
334
+ raise "#{kind}: stranded delivery" unless queue.length == expected
335
+ if kind == :closed_query
336
+ raise 'wrong error' unless queue.pop.last.is_a?(Dnsruby::SocketEofResolvError)
337
+ end
338
+ [:queued_exceptions, :queued_responses, :queued_validation_responses].each do |name|
339
+ raise "#{kind}: pending #{name}" unless klass.class_variable_get("@@#{name}").empty?
340
+ end
341
+ # Publish the stop decision under the mutex, before the Ruby thread exits.
342
+ raise 'stopping selector still advertised as running' if selector.select_thread_alive?
343
+ end
344
+ RUBY
345
+ assert_selector_subprocess(script)
346
+ end
347
+
348
+ def test_simultaneous_delivery_producers_start_only_one_selector
349
+ assert_selector_subprocess(<<~'RUBY')
350
+ klass = Dnsruby::SelectThread
351
+ selector = klass.send(:allocate)
352
+ original_new = Thread.method(:new)
353
+ dead_worker = original_new.call {}.tap(&:join)
354
+ {
355
+ mutex: Mutex.new, select_thread: dead_worker, queued_exceptions: [],
356
+ queued_responses: [], queued_validation_responses: []
357
+ }.each { |name, value| klass.class_variable_set("@@#{name}", value) }
358
+ selector.define_singleton_method(:wake_up_select_thread) {}
359
+ factory_entered, release_factory = Queue.new, Queue.new
360
+ workers, release_workers = Queue.new, Queue.new
361
+ selector.define_singleton_method(:do_select) do
362
+ workers << Thread.current
363
+ release_workers.pop
364
+ end
365
+ Thread.define_singleton_method(:new) do |*args, &block|
366
+ if Thread.current[:selector_producer]
367
+ factory_entered << true
368
+ release_factory.pop
369
+ end
370
+ original_new.call(*args, &block)
371
+ end
372
+ producers = []
373
+ started = Queue.new
374
+ begin
375
+ [:exception, :response, :validation].each do |kind|
376
+ producers << original_new.call do
377
+ Thread.current[:selector_producer] = true
378
+ query = Dnsruby::Message.new('parallel.example.')
379
+ started << kind
380
+ case kind
381
+ when :exception
382
+ selector.push_exception_to_select(kind, Queue.new, Dnsruby::OtherResolvError.new, nil)
383
+ when :response
384
+ selector.push_response_to_select(kind, Queue.new, query, query, nil)
385
+ when :validation
386
+ selector.push_validation_response_to_select(kind, Queue.new, query, nil, query, nil)
387
+ end
388
+ end
389
+ # Hold the first producer inside Thread.new before starting its peers.
390
+ factory_entered.pop if kind == :exception
391
+ end
392
+ 3.times { started.pop }
393
+ # Each producer is now blocked either at the selector mutex or at the
394
+ # intercepted thread factory. No timing delay determines the outcome.
395
+ Thread.pass until producers.all? { |thread| thread.status == 'sleep' }
396
+ factories = 1 + factory_entered.length
397
+ 3.times { release_factory << true }
398
+ producers.each(&:value)
399
+ # Every created worker must enter do_select before checking its count.
400
+ active_workers = factories.times.map { workers.pop }
401
+ raise "started #{factories} selector workers" unless factories == 1
402
+ counts = [:queued_exceptions, :queued_responses, :queued_validation_responses].map do |name|
403
+ klass.class_variable_get("@@#{name}").length
404
+ end
405
+ raise "lost producer work: #{counts.inspect}" unless counts == [1, 1, 1]
406
+ active_workers.each { release_workers << true }
407
+ active_workers.each(&:value)
408
+ ensure
409
+ Thread.define_singleton_method(:new, original_new)
410
+ producers.each { |thread| thread.kill if thread.alive? }
411
+ worker = klass.class_variable_get(:@@select_thread)
412
+ worker.kill if worker && worker.alive?
413
+ until workers.empty?
414
+ thread = workers.pop(true)
415
+ thread.kill if thread.alive?
416
+ end
417
+ Array(active_workers).each { |thread| thread.kill if thread.alive? }
418
+ end
419
+ RUBY
420
+ end
421
+
422
+ def test_stopped_selector_restarts_before_the_old_ruby_thread_exits
423
+ assert_selector_subprocess(<<~'RUBY')
424
+ klass = Dnsruby::SelectThread
425
+ original_select = IO.method(:select)
426
+ IO.define_singleton_method(:select) { |*| nil }
427
+ begin
428
+ [:exception, :response, :validation, :closed_registration].each do |kind|
429
+ selector = klass.send(:allocate)
430
+ wakeup = Object.new
431
+ wakeup.define_singleton_method(:closed?) { false }
432
+ {
433
+ mutex: Mutex.new, sockets: Set.new([wakeup]), timeouts: {}, query_hash: {}, queries_by_queue: {}.compare_by_identity,
434
+ socket_hash: {}, socket_is_persistent: {}, socket_remaining_queries: {},
435
+ tcp_buffers: {}, select_thread: nil, observers: {}, observer_queue_counts: {}.compare_by_identity, tick_observers: [],
436
+ queued_exceptions: [], queued_responses: [], queued_validation_responses: [],
437
+ wakeup_sockets: [wakeup, wakeup]
438
+ }.each { |name, value| klass.class_variable_set("@@#{name}", value) }
439
+ selector.define_singleton_method(:wake_up_select_thread) {}
440
+ selector.define_singleton_method(:send_tick_to_observers) {}
441
+ selector.define_singleton_method(:notify_queue_observers) { |*| }
442
+ stopped, release_old, start_old, replacements = Queue.new, Queue.new, Queue.new, Queue.new
443
+ implementation = selector.method(:do_select)
444
+ old_worker = nil
445
+ replacement = nil
446
+ selector.define_singleton_method(:do_select) do
447
+ implementation.call
448
+ if Thread.current.equal?(old_worker)
449
+ stopped << true
450
+ release_old.pop
451
+ else
452
+ replacements << Thread.current
453
+ end
454
+ end
455
+ begin
456
+ old_worker = Thread.new { start_old.pop; selector.do_select }
457
+ klass.class_variable_set(:@@select_thread, old_worker)
458
+ start_old << true
459
+ stopped.pop
460
+ raise 'old worker exited before publication' unless old_worker.alive?
461
+ query = Dnsruby::Message.new('late.example.')
462
+ query.do_validation = query.do_caching = false
463
+ response = query.clone
464
+ response.header.qr = true
465
+ queue = Queue.new
466
+ error = Dnsruby::OtherResolvError.new('late error')
467
+ case kind
468
+ when :exception
469
+ selector.push_exception_to_select(:late, queue, error, nil)
470
+ when :response
471
+ selector.push_response_to_select(:late, queue, response, query, nil)
472
+ when :validation
473
+ selector.push_validation_response_to_select(:late, queue, response, nil, query, nil)
474
+ when :closed_registration
475
+ closed_socket = Object.new
476
+ closed_socket.define_singleton_method(:closed?) { true }
477
+ settings = klass::QuerySettings.new(query.encode, query, false, queue, :late,
478
+ closed_socket, '127.0.0.1', 53, Dnsruby::TransportClock.now + 60, 512, nil)
479
+ selector.add_to_select(settings)
480
+ end
481
+ expected_types = case kind
482
+ when :exception, :closed_registration then [Dnsruby::Resolver::EventType::ERROR]
483
+ when :response then [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED]
484
+ when :validation then [Dnsruby::Resolver::EventType::VALIDATED]
485
+ end
486
+ events = Timeout.timeout(2, Timeout::Error, "#{kind}: stranded delivery while old worker is alive") do
487
+ expected_types.map { queue.pop }
488
+ end
489
+ replacement = Timeout.timeout(2, Timeout::Error, "#{kind}: replacement did not stop") { replacements.pop }
490
+ replacement.value
491
+ raise "#{kind}: old worker already exited" unless old_worker.alive?
492
+ raise "#{kind}: wrong delivery types" unless events.map { |event| event[1] } == expected_types
493
+ raise "#{kind}: wrong client ID" unless events.all? { |event| event[0] == :late }
494
+ raise "#{kind}: duplicate delivery" unless queue.empty?
495
+ case kind
496
+ when :exception
497
+ raise 'wrong exception' unless events.first.last.equal?(error)
498
+ when :closed_registration
499
+ raise 'wrong EOF error' unless events.first.last.is_a?(Dnsruby::SocketEofResolvError)
500
+ raise 'closed registration retained' unless klass.class_variable_get(:@@query_hash).empty? &&
501
+ klass.class_variable_get(:@@timeouts).empty? &&
502
+ !klass.class_variable_get(:@@sockets).include?(closed_socket)
503
+ else
504
+ raise "#{kind}: wrong response" unless events.all? { |event| event[2].equal?(response) && event.last.nil? }
505
+ end
506
+ [:queued_exceptions, :queued_responses, :queued_validation_responses].each do |name|
507
+ raise "#{kind}: pending #{name}" unless klass.class_variable_get("@@#{name}").empty?
508
+ end
509
+ ensure
510
+ release_old << true
511
+ old_worker.kill if old_worker && old_worker.alive?
512
+ old_worker.join if old_worker
513
+ worker = klass.class_variable_get(:@@select_thread)
514
+ worker.kill if worker && worker.alive?
515
+ replacement.kill if replacement && replacement.alive?
516
+ end
517
+ end
518
+ ensure
519
+ IO.define_singleton_method(:select, original_select)
520
+ end
521
+ RUBY
522
+ end
523
+
524
+ def test_send_failure_on_reused_pipeline_reports_the_registered_query
525
+ sender = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true)
526
+ socket = Object.new
527
+ socket.define_singleton_method(:write_nonblock) { |*| raise IOError, 'closed during send' }
528
+ queue = Queue.new
529
+ query = Dnsruby::Message.new('late.example.')
530
+ errors = []
531
+ selector = Object.new
532
+ current = nil
533
+ selector.define_singleton_method(:query_settings_for_id) { |_| current }
534
+ selector.define_singleton_method(:reserve_send) { |settings, _previous| current = settings; true }
535
+ selector.define_singleton_method(:selector_thread?) { false }
536
+ selector.define_singleton_method(:register_query_if_current) { |settings, _| current = settings; true }
537
+ selector.define_singleton_method(:fail_tcp_send) do |settings, error|
538
+ errors << [settings.client_query_id, settings.client_queue, error, nil]
539
+ end
540
+ Dnsruby::SelectThread.stub(:instance, selector) do
541
+ sender.stub(:tcp_pipeline_socket, [socket, false]) do
542
+ sender.send_dnsruby(query.encode, query, :late_send, queue, true)
543
+ end
544
+ end
545
+ assert_equal(1, errors.length)
546
+ assert_equal([:late_send, queue], errors.first[0, 2])
547
+ assert_kind_of(Dnsruby::SocketEofResolvError, errors.first[2])
548
+ assert_nil(errors.first[3])
549
+ end
550
+
551
+ def test_eof_callback_registration_gets_error_without_retaining_closed_socket
552
+ # add_to_select queues the second EOF in a class variable. Run the complete
553
+ # callback scenario in a child so a live singleton cannot steal its queue.
554
+ assert_selector_subprocess(<<~'RUBY')
555
+ klass = Dnsruby::SelectThread
556
+ {
557
+ mutex: Mutex.new, query_hash: {}, queries_by_queue: {}.compare_by_identity, socket_hash: {}, timeouts: {}, sockets: Set.new,
558
+ tcp_buffers: {}, socket_is_persistent: {}, socket_remaining_queries: {}, queued_exceptions: []
559
+ }.each { |name, value| klass.class_variable_set("@@#{name}", value) }
560
+ selector = klass.send(:allocate)
561
+ selector.define_singleton_method(:check_select_thread_synchronized) {}
562
+ selector.define_singleton_method(:wake_up_select_thread) {}
563
+ closed = false
564
+ socket = Object.new
565
+ socket.define_singleton_method(:closed?) { closed }
566
+ socket.define_singleton_method(:close) { closed = true }
567
+ socket.define_singleton_method(:recv_nonblock) { |_| ''.b }
568
+ first, second = [:first, :second].map do |id|
569
+ settings = klass::QuerySettings.new
570
+ settings.query = Dnsruby::Message.new("#{id}.example.")
571
+ settings.client_query_id = [Object.new, settings.query]
572
+ settings.client_queue, settings.socket = Queue.new, socket
573
+ settings.endtime = Dnsruby::TransportClock.now + 60
574
+ settings.is_persistent_socket = true
575
+ settings.tcp_pipelining_max_queries = :infinite
576
+ settings
577
+ end
578
+ selector.add_to_select(first)
579
+ notifications = []
580
+ selector.define_singleton_method(:notify_queue_observers) do |_queue, id|
581
+ raise 'callback under selector mutex' if klass.class_variable_get(:@@mutex).owned?
582
+ notifications << id
583
+ # A sender resumes registration during an earlier query's EOF callback.
584
+ add_to_select(second) if id == first.client_query_id
585
+ end
586
+ raise 'unexpected EOF result' unless selector.tcp_read(socket) == false
587
+ selector.send_queued_exceptions
588
+ [first, second].each do |settings|
589
+ event = settings.client_queue.pop(true)
590
+ raise 'wrong EOF error' unless event.last.is_a?(Dnsruby::SocketEofResolvError)
591
+ raise 'duplicate EOF' unless settings.client_queue.empty?
592
+ [:query_hash, :timeouts].each do |name|
593
+ raise "retained #{name}" if klass.class_variable_get("@@#{name}").key?(settings.client_query_id)
594
+ end
595
+ end
596
+ raise 'missing callback' unless notifications == [first.client_query_id, second.client_query_id]
597
+ [:socket_hash, :tcp_buffers, :socket_is_persistent, :socket_remaining_queries].each do |name|
598
+ raise "retained #{name}" if klass.class_variable_get("@@#{name}").key?(socket)
599
+ end
600
+ raise 'retained socket' if klass.class_variable_get(:@@sockets).include?(socket)
601
+ raise 'undelivered EOF' unless klass.class_variable_get(:@@queued_exceptions).empty?
602
+ RUBY
603
+ end
604
+
605
+ private
606
+
607
+ def assert_selector_subprocess(script)
608
+ options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
609
+ # A child-side deadline also bounds broken synchronization regressions;
610
+ # capture3 otherwise waits forever for a child blocked at a test gate.
611
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
612
+ File.expand_path('../lib', __dir__), '-r', 'dnsruby', '-r', 'timeout',
613
+ '-e', "Timeout.timeout(10) do\n#{script}\nend")
614
+ assert(status.success?, "#{stdout}\n#{stderr}")
615
+ assert_empty(stderr)
616
+ end
617
+
618
+ def table(name)
619
+ @selector_class.class_variable_get("@@#{name}")
620
+ end
621
+
622
+ def register(socket, id, destination = '127.0.0.1', port = 53)
623
+ settings = Dnsruby::SelectThread::QuerySettings.new
624
+ settings.query = Dnsruby::Message.new('malformed.example.')
625
+ settings.query.header.id = id
626
+ settings.client_query_id = [Object.new, settings.query]
627
+ settings.client_queue = Queue.new
628
+ settings.socket = socket
629
+ settings.dest_server = destination
630
+ settings.dest_port = port
631
+ table(:mutex).synchronize do
632
+ table(:query_hash)[settings.client_query_id] = settings
633
+ (table(:queries_by_queue)[settings.client_queue] ||= Set.new) << settings.client_query_id
634
+ (table(:socket_hash)[socket] ||= []) << settings.client_query_id
635
+ table(:socket_is_persistent)[socket] = true
636
+ end
637
+ @registered << settings
638
+ settings
639
+ end
640
+ end
641
+
642
+ class TestMalformedTcpTransport < Minitest::Test
643
+ def test_malformed_query_does_not_block_an_unrelated_query_or_its_timeout
644
+ [:two_bytes, :four_bytes, :truncated, :unknown_id].each do |kind|
645
+ listener = TCPServer.new('127.0.0.1', 0)
646
+ connections = []
647
+ exchanges = []
648
+ release = Queue.new
649
+ server = Thread.new do
650
+ begin
651
+ # Both queries must already be outstanding when the malformed reply
652
+ # arrives; resolver retry scheduling is separate from selector liveness.
653
+ requests = 2.times.map do
654
+ connection = listener.accept
655
+ connections << connection
656
+ length = connection.read(2).unpack('n').first
657
+ [connection, Dnsruby::Message.decode(connection.read(length))]
658
+ end
659
+ requests.each do |connection, response|
660
+ if response.question.first.qname.to_s == 'broken.example'
661
+ wire = case kind
662
+ when :two_bytes
663
+ [response.header.id].pack('n')
664
+ when :four_bytes
665
+ [response.header.id, 0x8000].pack('n2')
666
+ when :truncated
667
+ [response.header.id, 0x8200, 1, 0, 0, 0].pack('n6')
668
+ when :unknown_id
669
+ [(response.header.id + 1) % 65_536, 0x8000, 0, 0, 0, 0].pack('n6')
670
+ end
671
+ else
672
+ response.header.qr = true
673
+ response.add_answer(Dnsruby::RR.create('healthy.example. 60 IN A 192.0.2.25'))
674
+ wire = response.encode
675
+ end
676
+ exchanges << [response.question.first.qname.to_s, wire.bytesize]
677
+ connection.write([wire.bytesize].pack('n') + wire)
678
+ end
679
+ release.pop # The unanswered query must time out while its peer stays open.
680
+ rescue StandardError => error
681
+ error
682
+ end
683
+ end
684
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', port: listener.addr[1],
685
+ use_tcp: true, tcp_pipelining: false, do_caching: false, dnssec: false, retry_times: 1,
686
+ # The second async query is sent on the next resolver tick (up to 0.5s).
687
+ packet_timeout: 1, query_timeout: 2)
688
+ queue = Queue.new
689
+ [:broken, :healthy].each do |id|
690
+ query = Dnsruby::Message.new("#{id}.example.")
691
+ query.do_caching = false
692
+ query.do_validation = false
693
+ resolver.send_async(query, queue, id)
694
+ end
695
+ results = Timeout.timeout(5) { 2.times.map { queue.pop } }
696
+ broken = results.find { |item| item.first == :broken }
697
+ healthy = results.find { |item| item.first == :healthy }
698
+ assert_kind_of(kind == :unknown_id ? Dnsruby::ResolvTimeout : Dnsruby::DecodeError, broken.last)
699
+ assert_nil(healthy.last, "#{kind}: #{exchanges.inspect}, server status: #{server.status.inspect}")
700
+ assert_equal(['192.0.2.25'], healthy[1].answer.map { |rr| rr.address.to_s })
701
+ assert(Dnsruby::SelectThread.instance.select_thread_alive?)
702
+ release << true
703
+ server_result = Timeout.timeout(5) { server.value }
704
+ raise server_result if server_result.is_a?(StandardError)
705
+ ensure
706
+ resolver.close if resolver
707
+ server.kill.join if server
708
+ connections.each { |connection| connection.close unless connection.closed? } if connections
709
+ listener.close if listener && !listener.closed?
710
+ end
711
+ end
712
+ end