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,462 @@
1
+ require_relative 'spec_helper'
2
+ require 'open3'
3
+ require 'rbconfig'
4
+
5
+ class TestPipelineRetry < Minitest::Test
6
+ # Native response checking and sending run unchanged. Fake transport methods
7
+ # inject the competing completion at a known boundary, without real sockets.
8
+ # A child owns all singleton tables, cache hooks, and socket constructors.
9
+ FIXTURE = <<~'RUBY'
10
+ require 'dnsruby'
11
+ require 'timeout'
12
+ Dnsruby.log.level = Logger::FATAL
13
+
14
+ class RetrySocket
15
+ attr_accessor :on_send
16
+ attr_reader :writes
17
+ def initialize(tcp)
18
+ @tcp, @closed, @writes = tcp, false, []
19
+ end
20
+ def closed?; @closed; end
21
+ def close; @closed = true; end
22
+ def bind(*); end
23
+ def connect(*); end
24
+ def getsockopt(*)
25
+ raise IOError, 'closed stream' if closed?
26
+ Struct.new(:data).new([@tcp ? Socket::SOCK_STREAM : Socket::SOCK_DGRAM].pack('i'))
27
+ end
28
+ def send(bytes, *)
29
+ raise IOError, 'closed stream' if closed?
30
+ @writes << bytes.dup
31
+ @on_send.call if @on_send
32
+ bytes.bytesize
33
+ end
34
+ def write_nonblock(bytes); send(bytes, 0); end
35
+ end
36
+
37
+ class RetryFixture
38
+ attr_reader :selector, :sender, :original, :udp, :tcp, :replacement, :winner_error
39
+
40
+ def initialize(kind = :formerr, pipeline: true, initial_tcp: kind == :formerr)
41
+ @klass = Dnsruby::SelectThread
42
+ {
43
+ mutex: Mutex.new, sockets: Set.new, query_hash: {}, queries_by_queue: {}.compare_by_identity, socket_hash: {}, timeouts: {},
44
+ socket_is_persistent: {}, socket_remaining_queries: {}, tcp_buffers: {},
45
+ queued_exceptions: [], queued_responses: [], queued_validation_responses: [],
46
+ observers: {}, observer_queue_counts: {}.compare_by_identity, tick_observers: [], select_thread: Thread.current
47
+ }.each { |name, value| @klass.class_variable_set("@@#{name}", value) }
48
+ @selector = @klass.send(:allocate)
49
+ @selector.define_singleton_method(:wake_up_select_thread) {}
50
+ @selector.define_singleton_method(:check_select_thread_synchronized) {}
51
+ @selector.define_singleton_method(:notify_queue_observers) { |*| }
52
+ @deferred_sends = []
53
+ deferred_sends = @deferred_sends
54
+ defer_send = @selector.method(:defer_send)
55
+ @selector.define_singleton_method(:defer_send) do |*args, &operation|
56
+ defer_send.call(*args, &operation).tap { |task| deferred_sends << task }
57
+ end
58
+ selector = @selector
59
+ @klass.define_singleton_method(:instance) { selector }
60
+ @sender = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: pipeline,
61
+ tcp_pipelining_max_queries: :infinite, packet_timeout: 1, dnssec: false)
62
+ @udp, @tcp = RetrySocket.new(false), RetrySocket.new(true)
63
+ udp, tcp = @udp, @tcp
64
+ UDPSocket.define_singleton_method(:new) { |*| udp }
65
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **options| tcp }
66
+ @sender.define_singleton_method(:tcp_pipeline_socket) { |_| [tcp, false] }
67
+ @kind = kind
68
+ @original = settings(RetrySocket.new(initial_tcp), :retry, @sender)
69
+ @original.is_persistent_socket = pipeline && initial_tcp
70
+ @original.query.add_additional(Dnsruby::RR::OPT.new(4096)) if kind == :formerr
71
+ @selector.add_to_select(@original)
72
+ Thread.current[:dnsruby_response_query] = nil
73
+ end
74
+
75
+ def table(name)
76
+ @klass.class_variable_get("@@#{name}")
77
+ end
78
+
79
+ def settings(socket, id, sender = @sender)
80
+ query = Dnsruby::Message.new('retry.example.')
81
+ query.do_validation = query.do_caching = false
82
+ query.header.rd = true
83
+ @klass::QuerySettings.new(query.encode, query, false, Queue.new, id, socket,
84
+ '127.0.0.1', 53, Dnsruby::TransportClock.now + 60, 512, sender)
85
+ end
86
+
87
+ def response(settings = @original)
88
+ settings.query.clone.tap do |message|
89
+ message.header.qr = true
90
+ message.header.rcode = Dnsruby::RCode::NOERROR
91
+ message.header.tc = false
92
+ end
93
+ end
94
+
95
+ def retry_response
96
+ message = response
97
+ if @kind == :formerr
98
+ message.header.rcode = Dnsruby::RCode::FORMERR
99
+ else
100
+ message.header.tc = true
101
+ end
102
+ message
103
+ end
104
+
105
+ def compete(action)
106
+ case action
107
+ when :live
108
+ when :failed
109
+ @winner_error = Dnsruby::SocketEofResolvError.new('original send failed')
110
+ current = @selector.query_settings_for_id(:retry)
111
+ if current && current.socket.nil?
112
+ @selector.fail_query_if_current(:retry, current.client_queue, current, @winner_error)
113
+ else
114
+ @selector.fail_tcp_send(@original, @winner_error)
115
+ end
116
+ when :expired
117
+ table(:timeouts)[:retry] = Dnsruby::TransportClock.now - 1
118
+ @selector.instance_variable_get(:@packet_deadlines)[:retry] = table(:timeouts)[:retry]
119
+ @selector.process_timeouts
120
+ when :replaced
121
+ @replacement = settings(RetrySocket.new(true), :retry)
122
+ @selector.add_to_select(@replacement)
123
+ else
124
+ raise "unknown competing action #{action}"
125
+ end
126
+ end
127
+
128
+ def check_retry(action = nil, wait: true)
129
+ message = retry_response
130
+ wire = message.encode
131
+ if action
132
+ fixture = self
133
+ if @kind == :formerr
134
+ send_retry = @sender.method(:send_async)
135
+ @sender.define_singleton_method(:send_async) do |*args|
136
+ fixture.compete(action)
137
+ send_retry.call(*args)
138
+ end
139
+ else
140
+ # This getter is reached in the native truncation branch immediately
141
+ # before it calls send_async with use_tcp=true.
142
+ truncated = message.header.method(:tc)
143
+ message.header.define_singleton_method(:tc) do
144
+ fixture.compete(action)
145
+ truncated.call
146
+ end
147
+ end
148
+ end
149
+ @selector.process_message(message, wire, @original.socket)
150
+ raise 'response context leaked' if Thread.current[:dnsruby_response_query]
151
+ @deferred_sends.shift.value until !wait || @deferred_sends.empty?
152
+ end
153
+
154
+ def events(settings = @original)
155
+ @selector.send_queued_exceptions
156
+ @selector.send_queued_responses
157
+ @selector.send_queued_validation_responses
158
+ result = []
159
+ result << settings.client_queue.pop(true) until settings.client_queue.empty?
160
+ result
161
+ end
162
+
163
+ def assert_stale(action)
164
+ actual = events
165
+ case action
166
+ when :failed, :expired
167
+ error_class = action == :failed ? Dnsruby::SocketEofResolvError : Dnsruby::ResolvTimeout
168
+ raise "#{action}: wrong terminal deliveries #{actual.inspect}" unless actual.length == 1 &&
169
+ actual.first[0, 3] == [:retry, Dnsruby::Resolver::EventType::ERROR, nil] &&
170
+ actual.first.last.is_a?(error_class)
171
+ raise 'wrong failure owner' if action == :failed && !actual.first.last.equal?(@winner_error)
172
+ raise "#{action}: original query resurrected" if @selector.query_settings_for_id(:retry)
173
+ raise "#{action}: stale timeout retained" if table(:timeouts).key?(:retry)
174
+ when :replaced
175
+ raise 'stale attempt published a delivery' unless actual.empty?
176
+ raise 'replacement registration consumed' unless @selector.query_settings_for_id(:retry).equal?(@replacement)
177
+ raise 'replacement received stale delivery' unless events(@replacement).empty?
178
+ raise 'replacement timeout removed' unless table(:timeouts)[:retry] == @replacement.endtime
179
+ end
180
+ end
181
+
182
+ def assert_success(settings, message)
183
+ @selector.process_message(message, message.encode, settings.socket)
184
+ expected = [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED].map do |type|
185
+ [settings.client_query_id, type, message, nil]
186
+ end
187
+ actual = events(settings)
188
+ raise "wrong success deliveries: #{actual.inspect}" unless actual == expected
189
+ raise 'completed registration retained' if @selector.query_settings_for_id(settings.client_query_id)
190
+ raise 'completed timeout retained' if table(:timeouts).key?(settings.client_query_id)
191
+ end
192
+
193
+ def assert_no_retry_writes
194
+ raise 'stale UDP retry was sent' unless @udp.writes.empty?
195
+ raise 'stale TCP retry was sent' unless @tcp.writes.empty?
196
+ end
197
+ end
198
+ RUBY
199
+
200
+ def test_native_formerr_and_truncated_responses_retry_and_complete
201
+ assert_isolated(<<~'RUBY')
202
+ [:formerr, :truncated].each do |kind|
203
+ [true, false].each do |pipeline|
204
+ fixture = RetryFixture.new(kind, pipeline: pipeline)
205
+ fixture.check_retry
206
+ current = fixture.selector.query_settings_for_id(:retry)
207
+ raise "#{kind}: retry missing" unless current && !current.equal?(fixture.original)
208
+ raise 'retry changed the client queue' unless current.client_queue.equal?(fixture.original.client_queue)
209
+ raise 'premature retry delivery' unless fixture.events.empty?
210
+ raise "#{kind}: retry did not use TCP" unless current.socket.equal?(fixture.tcp) && !fixture.tcp.writes.empty?
211
+ raise 'TCP FORMERR changed transport' unless fixture.udp.writes.empty?
212
+ frame = fixture.tcp.writes.join
213
+ raise 'invalid retry frame' unless frame.unpack1('n') == frame.bytesize - 2
214
+ sent = Dnsruby::Message.decode(frame.byteslice(2..))
215
+ raise 'FORMERR retry retained EDNS' if kind == :formerr && sent.header.arcount != 0
216
+ raise 'retry changed DNS transaction ID' unless sent.header.id == fixture.original.query.header.id
217
+ fixture.assert_success(current, fixture.response(current))
218
+ end
219
+ end
220
+ RUBY
221
+ end
222
+
223
+ def test_native_protocol_retries_get_a_fresh_packet_deadline
224
+ assert_isolated(<<~'RUBY')
225
+ [:formerr, :truncated].each do |kind|
226
+ [true, false].each do |pipeline|
227
+ fixture = RetryFixture.new(kind, pipeline: pipeline)
228
+ crossed = false
229
+ cross_old_deadline = -> do
230
+ crossed = true
231
+ fixture.original.endtime = Dnsruby::TransportClock.now - 1
232
+ fixture.table(:timeouts)[:retry] = fixture.original.endtime
233
+ # No timeout has claimed this attempt: its response is already
234
+ # being processed, and its protocol retry has its own deadline.
235
+ end
236
+ fixture.udp.define_singleton_method(:connect) { |*| cross_old_deadline.call }
237
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **options|
238
+ cross_old_deadline.call
239
+ fixture.tcp
240
+ end
241
+ fixture.sender.define_singleton_method(:tcp_pipeline_socket) do |_|
242
+ cross_old_deadline.call
243
+ [fixture.tcp, false]
244
+ end
245
+ fixture.check_retry
246
+ raise 'connection boundary not exercised' unless crossed
247
+ current = fixture.selector.query_settings_for_id(:retry)
248
+ raise 'live protocol retry discarded' unless current && !current.equal?(fixture.original)
249
+ raise 'retry inherited old deadline' unless current.endtime > Dnsruby::TransportClock.now
250
+ raise 'premature retry event' unless fixture.events.empty?
251
+ raise 'retry was not sent over TCP' if fixture.tcp.writes.empty?
252
+ fixture.assert_success(current, fixture.response(current))
253
+ end
254
+ end
255
+ RUBY
256
+ end
257
+
258
+ def test_deferred_protocol_retry_uses_its_own_packet_deadline
259
+ assert_isolated(<<~'RUBY')
260
+ fixture = RetryFixture.new(:truncated)
261
+ lock = Mutex.new
262
+ fixture.sender.instance_variable_set(:@pipeline_mutex, lock)
263
+ lock.lock
264
+ send_query = fixture.sender.method(:send_dnsruby)
265
+ worker = nil
266
+ fixture.sender.define_singleton_method(:send_dnsruby) { |*args| worker = send_query.call(*args) }
267
+ begin
268
+ fixture.check_retry(wait: false)
269
+ raise 'selector retry was not deferred' unless worker.respond_to?(:value)
270
+ fixture.original.endtime = Dnsruby::TransportClock.now - 1
271
+ fixture.table(:timeouts)[:retry] = fixture.original.endtime
272
+ lock.unlock
273
+ worker.value
274
+ current = fixture.selector.query_settings_for_id(:retry)
275
+ raise 'unclaimed retry discarded after writer wait' unless current && !current.equal?(fixture.original)
276
+ raise 'deferred retry inherited old deadline' unless current.endtime > Dnsruby::TransportClock.now
277
+ raise 'premature retry event' unless fixture.events.empty?
278
+ raise 'deferred retry not sent' if fixture.tcp.writes.empty?
279
+ fixture.assert_success(current, fixture.response(current))
280
+ ensure
281
+ lock.unlock if lock.owned?
282
+ worker.join if worker.is_a?(Thread)
283
+ end
284
+ RUBY
285
+ end
286
+
287
+ def test_native_retries_cannot_resurrect_failed_expired_or_replaced_settings
288
+ assert_isolated(<<~'RUBY')
289
+ [:formerr, :truncated].each do |kind|
290
+ [:failed, :expired, :replaced].each do |action|
291
+ fixture = RetryFixture.new(kind)
292
+ fixture.check_retry(action)
293
+ fixture.assert_stale(action)
294
+ fixture.assert_no_retry_writes
295
+ if action == :replaced
296
+ fixture.assert_success(fixture.replacement, fixture.response(fixture.replacement))
297
+ else
298
+ fixture.selector.process_timeouts
299
+ raise 'second terminal delivery' unless fixture.events.empty?
300
+ end
301
+ end
302
+ end
303
+ RUBY
304
+ end
305
+
306
+ def test_retry_encode_and_oversized_udp_errors_claim_only_the_original_attempt
307
+ assert_isolated(<<~'RUBY')
308
+ [:encode, :oversized_udp].each do |failure|
309
+ [:live, :failed, :expired, :replaced].each do |action|
310
+ fixture = RetryFixture.new
311
+ fixture.sender.no_tcp = true if failure == :oversized_udp
312
+ reached = false
313
+ fixture.sender.define_singleton_method(:make_query_packet) do |*|
314
+ reached = true
315
+ fixture.compete(action)
316
+ if failure == :encode
317
+ raise Dnsruby::EncodeError, 'retry encoding failed'
318
+ else
319
+ 'x' * 65_535
320
+ end
321
+ end
322
+ fixture.check_retry
323
+ raise 'early-error hook not reached' unless reached
324
+ if action == :live
325
+ expected = failure == :encode ? Dnsruby::EncodeError : IOError
326
+ events = fixture.events
327
+ raise "#{failure}: wrong early error" unless events.length == 1 &&
328
+ events.first[1] == Dnsruby::Resolver::EventType::ERROR && events.first.last.is_a?(expected)
329
+ raise 'early error retained original registration' if fixture.selector.query_settings_for_id(:retry)
330
+ fixture.selector.process_timeouts
331
+ raise 'early error delivered twice' unless fixture.events.empty?
332
+ else
333
+ fixture.assert_stale(action)
334
+ end
335
+ fixture.assert_no_retry_writes
336
+ end
337
+ end
338
+ RUBY
339
+ end
340
+
341
+ def test_retry_cache_hit_claims_only_the_original_attempt
342
+ assert_isolated(<<~'RUBY')
343
+ [:live, :failed, :expired, :replaced].each do |action|
344
+ fixture = RetryFixture.new
345
+ fixture.original.query.do_caching = true
346
+ cached = fixture.response
347
+ cached.cached = true
348
+ reached = false
349
+ cache = Dnsruby::PacketSender.class_variable_get(:@@recursive_cache)
350
+ cache.define_singleton_method(:find) do |*|
351
+ reached = true
352
+ fixture.compete(action)
353
+ cached
354
+ end
355
+ fixture.check_retry
356
+ raise 'cache lookup hook not reached' unless reached
357
+ if action == :live
358
+ expected = [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED].map do |type|
359
+ [:retry, type, cached, nil]
360
+ end
361
+ raise 'cache hit delivered incorrectly' unless fixture.events == expected
362
+ raise 'cached retry retained original registration' if fixture.selector.query_settings_for_id(:retry)
363
+ raise 'cached retry retained timeout' if fixture.table(:timeouts).key?(:retry)
364
+ else
365
+ fixture.assert_stale(action)
366
+ end
367
+ fixture.assert_no_retry_writes
368
+ end
369
+ RUBY
370
+ end
371
+
372
+ def test_nonpipeline_retry_write_error_does_not_report_after_another_completion
373
+ assert_isolated(<<~'RUBY')
374
+ [:live, :failed, :expired, :replaced].each do |action|
375
+ fixture = RetryFixture.new(:formerr, pipeline: false, initial_tcp: false)
376
+ reached = false
377
+ fixture.udp.on_send = -> { raise IOError, 'UDP retry send failed' }
378
+ write = fixture.sender.method(:write_udp_packet)
379
+ fixture.sender.define_singleton_method(:write_udp_packet) do |*args|
380
+ begin
381
+ write.call(*args)
382
+ rescue IOError
383
+ # A native write cannot call the selector while holding the send
384
+ # token. Schedule the competing completion after that lock releases
385
+ # but before PacketSender claims the failed attempt.
386
+ reached = true
387
+ fixture.compete(action)
388
+ raise
389
+ end
390
+ end
391
+ fixture.check_retry
392
+ raise 'nonpipeline send hook not reached' unless reached
393
+ raise 'failed retry socket retained' unless fixture.udp.closed?
394
+ if action == :live
395
+ events = fixture.events
396
+ raise 'live retry error not delivered once' unless events.length == 1 &&
397
+ events.first[1] == Dnsruby::Resolver::EventType::ERROR && events.first.last.is_a?(IOError)
398
+ raise 'failed retry registration retained' if fixture.selector.query_settings_for_id(:retry)
399
+ else
400
+ fixture.assert_stale(action)
401
+ end
402
+ end
403
+ RUBY
404
+ end
405
+
406
+ def test_nested_response_checks_restore_context_on_success_and_exception
407
+ assert_isolated(<<~'RUBY')
408
+ [:success, :exception].each do |mode|
409
+ fixture = RetryFixture.new
410
+ selector = fixture.selector
411
+ outer = fixture.original
412
+ inner_sender = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
413
+ inner = fixture.settings(RetrySocket.new(true), :inner, inner_sender)
414
+ selector.add_to_select(inner)
415
+ outer_response, inner_response = fixture.response(outer), fixture.response(inner)
416
+ sentinel = Object.new
417
+ Thread.current[:dnsruby_response_query] = sentinel
418
+ original_outer_check = fixture.sender.method(:check_response)
419
+ original_inner_check = inner_sender.method(:check_response)
420
+ inner_sender.define_singleton_method(:check_response) do |*args|
421
+ raise 'inner context missing' unless response_query_settings(:inner).equal?(inner)
422
+ raise 'wrong ID inherited context' if response_query_settings(:retry)
423
+ raise 'wrong sender inherited context' if fixture.sender.response_query_settings(:inner)
424
+ raise IOError, 'nested response failure' if mode == :exception
425
+ original_inner_check.call(*args)
426
+ end
427
+ fixture.sender.define_singleton_method(:check_response) do |*args|
428
+ raise 'outer context missing' unless response_query_settings(:retry).equal?(outer)
429
+ begin
430
+ selector.process_message(inner_response, inner_response.encode, inner.socket)
431
+ ensure
432
+ raise 'inner check lost outer context' unless Thread.current[:dnsruby_response_query].equal?(outer)
433
+ end
434
+ original_outer_check.call(*args)
435
+ end
436
+ begin
437
+ selector.process_message(outer_response, outer_response.encode, outer.socket)
438
+ raise 'nested exception was swallowed' if mode == :exception
439
+ raise 'inner response missing' unless fixture.events(inner).length == 2
440
+ raise 'outer response missing' unless fixture.events(outer).length == 2
441
+ rescue IOError => error
442
+ raise unless mode == :exception && error.message == 'nested response failure'
443
+ raise 'failed check published a response' unless fixture.events(inner).empty? && fixture.events(outer).empty?
444
+ ensure
445
+ raise 'caller context not restored' unless Thread.current[:dnsruby_response_query].equal?(sentinel)
446
+ Thread.current[:dnsruby_response_query] = nil
447
+ end
448
+ end
449
+ RUBY
450
+ end
451
+
452
+ private
453
+
454
+ def assert_isolated(body)
455
+ options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
456
+ script = FIXTURE + "\nTimeout.timeout(10) do\n" + body + "\nend\n"
457
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
458
+ File.expand_path('../lib', __dir__), '-e', script)
459
+ assert(status.success?, "#{stdout}\n#{stderr}")
460
+ assert_empty(stderr)
461
+ end
462
+ end