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
@@ -0,0 +1,663 @@
1
+ require_relative 'spec_helper'
2
+ require 'open3'
3
+ require 'rbconfig'
4
+
5
+ class TestPipelineSend < Minitest::Test
6
+ # Every scenario gets its own selector tables and method hooks. This also
7
+ # prevents the suite's real singleton from consuming fixture-owned events.
8
+ FIXTURE = <<~'RUBY'
9
+ require 'dnsruby'
10
+ require 'timeout'
11
+ Dnsruby.log.level = Logger::FATAL
12
+ K = Dnsruby::SelectThread
13
+ {
14
+ mutex: Mutex.new, query_hash: {}, queries_by_queue: {}.compare_by_identity, socket_hash: {}, timeouts: {}, sockets: Set.new,
15
+ tcp_buffers: {}, socket_is_persistent: {}, socket_remaining_queries: {},
16
+ socket_owners: {}, retiring_sockets: Set.new,
17
+ queued_exceptions: [], queued_responses: [], queued_validation_responses: [],
18
+ observers: {}, observer_queue_counts: {}.compare_by_identity, tick_observers: [], select_thread: nil
19
+ }.each { |name, value| K.class_variable_set("@@#{name}", value) }
20
+ SELECTOR = K.send(:allocate)
21
+ SELECTOR.define_singleton_method(:check_select_thread_synchronized) {}
22
+ SELECTOR.define_singleton_method(:wake_up_select_thread) {}
23
+ SELECTOR.define_singleton_method(:notify_queue_observers) { |*| }
24
+ K.define_singleton_method(:instance) { SELECTOR }
25
+
26
+ class FrameSocket
27
+ attr_accessor :on_write, :limit, :incoming
28
+ attr_reader :written
29
+ def initialize
30
+ @closed = false
31
+ @written = ''.b
32
+ @incoming = ''.b
33
+ end
34
+ def closed?; @closed; end
35
+ def close; @closed = true; end
36
+ def getsockopt(*)
37
+ raise IOError, 'closed stream' if closed?
38
+ Struct.new(:data).new([Socket::SOCK_STREAM].pack('i'))
39
+ end
40
+ def write_nonblock(bytes)
41
+ raise IOError, 'closed stream' if closed?
42
+ count = @limit ? [@limit, bytes.bytesize].min : bytes.bytesize
43
+ @written << bytes.byteslice(0, count)
44
+ @on_write.call(bytes, count) if @on_write
45
+ count
46
+ end
47
+ # Datagram sends share the byte buffer used by stream writes.
48
+ def send(bytes, _flags); write_nonblock(bytes); end
49
+ def recv_nonblock(length)
50
+ raise IOError, 'closed stream' if closed?
51
+ raise IO::EAGAINWaitReadable if @incoming.empty?
52
+ @incoming.slice!(0, length)
53
+ end
54
+ end
55
+
56
+ def query(name = 'pipeline.example.')
57
+ q = Dnsruby::Message.new(name)
58
+ q.do_validation = q.do_caching = false
59
+ q
60
+ end
61
+ def sender(socket, max = :infinite)
62
+ result = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true,
63
+ tcp_pipelining_max_queries: max, packet_timeout: 1)
64
+ result.define_singleton_method(:tcp_pipeline_socket) { |_| [socket, false] }
65
+ result
66
+ end
67
+ def settings(socket, id, q = query, resolver = sender(socket), max = :infinite)
68
+ s = K::QuerySettings.new(q.encode, q, false, Queue.new, id, socket,
69
+ '127.0.0.1', 53, Dnsruby::TransportClock.now + 1, 512, resolver)
70
+ s.is_persistent_socket = true
71
+ s.tcp_pipelining_max_queries = max
72
+ s
73
+ end
74
+ def drain_errors
75
+ SELECTOR.send_queued_exceptions
76
+ end
77
+ def assert_clean(socket)
78
+ [:socket_hash, :tcp_buffers, :socket_is_persistent, :socket_remaining_queries, :socket_owners].each do |name|
79
+ raise "retained #{name}" if K.class_variable_get("@@#{name}").key?(socket)
80
+ end
81
+ raise 'retained socket' if K.class_variable_get(:@@sockets).include?(socket)
82
+ raise 'retained retirement' if K.class_variable_get(:@@retiring_sockets).include?(socket)
83
+ end
84
+ RUBY
85
+
86
+ def test_query_is_registered_before_a_fast_native_response
87
+ run_scenario(<<~'RUBY')
88
+ socket = FrameSocket.new
89
+ writer = sender(socket)
90
+ old = settings(socket, :old, query('older.example.'), writer)
91
+ SELECTOR.add_to_select(old) # The reused socket is already being selected.
92
+ q, queue = query('immediate.example.'), Queue.new
93
+ response = q.clone
94
+ response.header.qr = true
95
+ wire = response.encode
96
+ delivered = Queue.new
97
+ reader = nil
98
+ socket.on_write = ->(_bytes, _count) do
99
+ next unless socket.written.bytesize >= q.encode.bytesize + 2
100
+ socket.incoming = [wire.bytesize].pack('n') + wire
101
+ reader = Thread.new do
102
+ incoming = SELECTOR.get_incoming_data(socket, 0)
103
+ SELECTOR.process_message(*incoming, socket) if incoming
104
+ SELECTOR.send_queued_validation_responses
105
+ delivered << true
106
+ end
107
+ # Native nonblocking writes must not wait for selector callbacks. Check
108
+ # publication inside the write, then let the reader race its return.
109
+ raise 'query was not published before write' unless SELECTOR.query_settings_for_id(:immediate)
110
+ end
111
+ writer.send_dnsruby(q.encode, q, :immediate, queue, true)
112
+ reader.join
113
+ events = 2.times.map { queue.pop(true) }
114
+ raise 'wrong delivery order' unless events.map { |e| e[1] } ==
115
+ [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED]
116
+ raise 'unexpected error' unless events.all? { |e| e.last.nil? }
117
+ raise 'duplicate answer' unless queue.empty?
118
+ raise 'answer still registered' if SELECTOR.query_settings_for_id(:immediate)
119
+ raise 'other query removed' unless SELECTOR.query_settings_for_id(:old).equal?(old)
120
+ RUBY
121
+ end
122
+
123
+ def test_partial_writes_complete_frames_and_ordinary_callers_wait_for_the_writer
124
+ run_scenario(<<~'RUBY')
125
+ socket = FrameSocket.new
126
+ socket.limit = 3
127
+ writer = sender(socket)
128
+ entered, release, returned = Queue.new, Queue.new, Queue.new
129
+ first_write = true
130
+ socket.on_write = ->(*) do
131
+ if first_write
132
+ first_write = false
133
+ entered << true
134
+ release.pop
135
+ end
136
+ end
137
+ a, b = query('first.example.'), query('second.example.')
138
+ t1 = Thread.new { writer.send_dnsruby(a.encode, a, :first, Queue.new, true) }
139
+ entered.pop
140
+ t2 = Thread.new do
141
+ writer.send_dnsruby(b.encode, b, :second, Queue.new, true)
142
+ returned << true
143
+ end
144
+ Thread.pass until t2.status == 'sleep'
145
+ raise 'ordinary caller returned before sending' unless returned.empty?
146
+ release << true
147
+ t1.value
148
+ t2.value
149
+ expected = [a, b].map { |q| [q.encode.bytesize].pack('n') + q.encode }.join
150
+ raise 'interleaved or truncated frames' unless socket.written == expected
151
+ RUBY
152
+ end
153
+
154
+ def test_selector_retry_defers_when_another_writer_owns_the_pipeline
155
+ run_scenario(<<~'RUBY')
156
+ socket = FrameSocket.new
157
+ writer = sender(socket)
158
+ entered, release = Queue.new, Queue.new
159
+ first_write = true
160
+ socket.on_write = ->(*) do
161
+ if first_write
162
+ first_write = false
163
+ entered << true
164
+ release.pop
165
+ end
166
+ end
167
+ first = query('first.example.')
168
+ sending = Thread.new { writer.send_dnsruby(first.encode, first, :first, Queue.new, true) }
169
+ entered.pop
170
+ K.class_variable_set(:@@select_thread, Thread.current)
171
+ retry_query = query('retry.example.')
172
+ old = settings(FrameSocket.new, :retry, retry_query, writer)
173
+ old.is_persistent_socket = false # UDP-to-TCP retry replaces a one-use socket.
174
+ SELECTOR.add_to_select(old)
175
+ worker = writer.send_dnsruby(retry_query.encode, retry_query, :retry, old.client_queue, true)
176
+ raise 'selector send did not defer' unless worker.respond_to?(:value)
177
+ raise 'new writer entered before old frame ended' unless socket.written.bytesize == first.encode.bytesize + 2
178
+ release << true
179
+ sending.value
180
+ worker.value
181
+ replacement = SELECTOR.query_settings_for_id(:retry)
182
+ raise 'retry lost' unless replacement && !replacement.equal?(old) && replacement.socket == socket
183
+ assert_clean(old.socket)
184
+ RUBY
185
+ end
186
+
187
+ def test_deferred_retry_cannot_resurrect_an_expired_or_replaced_query
188
+ run_scenario(<<~'RUBY')
189
+ [:expired, :replaced].each do |kind|
190
+ socket = FrameSocket.new
191
+ writer = sender(socket)
192
+ lock = Mutex.new
193
+ writer.instance_variable_set(:@pipeline_mutex, lock)
194
+ lock.lock
195
+ K.class_variable_set(:@@select_thread, Thread.current)
196
+ old = settings(FrameSocket.new, kind, query("#{kind}.example."), writer)
197
+ SELECTOR.add_to_select(old)
198
+ worker = writer.send_dnsruby(old.query_bytes, old.query, kind, old.client_queue, true)
199
+ if kind == :expired
200
+ old.endtime = Dnsruby::TransportClock.now - 1
201
+ K.class_variable_get(:@@timeouts)[kind] = old.endtime
202
+ SELECTOR.instance_variable_get(:@packet_deadlines)[kind] = K.class_variable_get(:@@timeouts)[kind]
203
+ SELECTOR.process_timeouts
204
+ else
205
+ replacement = settings(FrameSocket.new, kind)
206
+ SELECTOR.add_to_select(replacement)
207
+ end
208
+ lock.unlock
209
+ worker.value
210
+ drain_errors
211
+ raise 'stale retry wrote bytes' unless socket.written.empty?
212
+ if kind == :expired
213
+ raise 'wrong timeout cardinality' unless old.client_queue.length == 1
214
+ raise 'expired retry resurrected' if SELECTOR.query_settings_for_id(kind)
215
+ else
216
+ raise 'replacement lost' unless SELECTOR.query_settings_for_id(kind).equal?(replacement)
217
+ raise 'obsolete retry reported an error' unless old.client_queue.empty?
218
+ end
219
+ end
220
+ RUBY
221
+ end
222
+
223
+ def test_failure_retires_the_stream_and_reports_each_query_once
224
+ run_scenario(<<~'RUBY')
225
+ [:infinite, 3].each do |max|
226
+ [:raise, :zero].each do |failure|
227
+ socket = FrameSocket.new
228
+ writer = sender(socket, max)
229
+ old = settings(socket, :old, query('older.example.'), writer, max)
230
+ SELECTOR.add_to_select(old)
231
+ if failure == :zero
232
+ socket.define_singleton_method(:write_nonblock) { |_| 0 }
233
+ else
234
+ socket.limit = 2
235
+ calls = 0
236
+ socket.on_write = ->(*) { calls += 1; raise IOError, 'write failure' if calls == 2 }
237
+ end
238
+ q, queue = query, Queue.new
239
+ writer.send_dnsruby(q.encode, q, :new, queue, true)
240
+ drain_errors
241
+ [old.client_queue, queue].each do |events|
242
+ raise 'missing or duplicate error' unless events.length == 1
243
+ raise 'wrong error' unless events.pop.last.is_a?(Dnsruby::SocketEofResolvError)
244
+ end
245
+ raise 'failed stream left open' unless socket.closed?
246
+ assert_clean(socket)
247
+ raise 'query retained' if SELECTOR.query_settings_for_id(:old) || SELECTOR.query_settings_for_id(:new)
248
+ end
249
+ end
250
+ RUBY
251
+ end
252
+
253
+ def test_closed_nonpipeline_registration_does_not_attempt_a_write_or_report_twice
254
+ run_scenario(<<~'RUBY')
255
+ socket = FrameSocket.new
256
+ socket.close
257
+ writer = sender(socket)
258
+ writer.tcp_pipelining = false
259
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| socket }
260
+ q, queue = query, Queue.new
261
+ writer.send_dnsruby(q.encode, q, :closed, queue, true)
262
+ drain_errors
263
+ raise 'wrote closed socket' unless socket.written.empty?
264
+ raise 'missing or duplicate error' unless queue.length == 1
265
+ raise 'wrong error' unless queue.pop.last.is_a?(Dnsruby::SocketEofResolvError)
266
+ assert_clean(socket)
267
+ RUBY
268
+ end
269
+
270
+ def test_retry_expiring_during_connect_releases_only_the_unused_socket
271
+ run_scenario(<<~'RUBY')
272
+ [true, false].each do |new_socket|
273
+ socket = FrameSocket.new
274
+ writer = sender(socket)
275
+ old = settings(FrameSocket.new, :retry, query, writer)
276
+ SELECTOR.add_to_select(old)
277
+ other = settings(socket, :other, query('other.example.'), writer)
278
+ SELECTOR.add_to_select(other) unless new_socket
279
+ writer.define_singleton_method(:tcp_pipeline_socket) do |_|
280
+ old.endtime = Dnsruby::TransportClock.now - 1
281
+ K.class_variable_get(:@@timeouts)[:retry] = old.endtime
282
+ SELECTOR.instance_variable_get(:@packet_deadlines)[:retry] = K.class_variable_get(:@@timeouts)[:retry]
283
+ SELECTOR.process_timeouts # The original attempt completes during connect.
284
+ @use_counts[socket] = 1
285
+ [socket, new_socket]
286
+ end
287
+ writer.send_dnsruby(old.query_bytes, old.query, :retry, old.client_queue, true)
288
+ drain_errors
289
+ raise 'expired retry wrote bytes' unless socket.written.empty?
290
+ raise 'missing timeout' unless old.client_queue.length == 1 && old.client_queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
291
+ raise 'expired retry retained' if SELECTOR.query_settings_for_id(:retry)
292
+ if new_socket
293
+ raise 'unused socket leaked' unless socket.closed?
294
+ assert_clean(socket)
295
+ else
296
+ raise 'other pending query damaged' if socket.closed? || !SELECTOR.query_settings_for_id(:other).equal?(other)
297
+ SELECTOR.fail_tcp_send(other, Dnsruby::SocketEofResolvError.new)
298
+ drain_errors
299
+ end
300
+ end
301
+ RUBY
302
+ end
303
+
304
+ def test_configuration_marshal_and_first_send_from_a_stored_sender
305
+ run_scenario(<<~'RUBY')
306
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true)
307
+ raise 'configuration gained a mutex' if writer.instance_variable_defined?(:@pipeline_mutex)
308
+ stored = Marshal.load(Marshal.dump(writer))
309
+ socket = FrameSocket.new
310
+ stored.define_singleton_method(:tcp_pipeline_socket) { |_| [socket, false] }
311
+ q = query
312
+ stored.send_dnsruby(q.encode, q, :stored, Queue.new, true)
313
+ raise 'stored sender cannot send' unless socket.written == [q.encode.bytesize].pack('n') + q.encode
314
+ raise 'stored sender not registered' unless SELECTOR.query_settings_for_id(:stored)
315
+ RUBY
316
+ end
317
+
318
+ def test_blocked_write_and_selector_writer_wait_have_bounded_timeouts
319
+ run_scenario(<<~'RUBY')
320
+ socket = FrameSocket.new
321
+ writer = sender(socket)
322
+ socket.define_singleton_method(:write_nonblock) { |_| raise IO::EAGAINWaitWritable }
323
+ original_select = IO.method(:select)
324
+ IO.define_singleton_method(:select) { |*| nil }
325
+ q, queue = query, Queue.new
326
+ begin
327
+ writer.send_dnsruby(q.encode, q, :blocked, queue, true)
328
+ ensure
329
+ IO.define_singleton_method(:select, original_select)
330
+ end
331
+ drain_errors
332
+ raise 'wrong write timeout' unless queue.length == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
333
+ raise 'blocked stream not closed' unless socket.closed?
334
+ assert_clean(socket)
335
+
336
+ waiting_socket = FrameSocket.new
337
+ waiting = sender(waiting_socket)
338
+ waiting.packet_timeout = 0.02
339
+ lock = Mutex.new
340
+ waiting.instance_variable_set(:@pipeline_mutex, lock)
341
+ lock.lock
342
+ K.class_variable_set(:@@select_thread, Thread.current)
343
+ queue = Queue.new
344
+ worker = waiting.send_dnsruby(q.encode, q, :waiting, queue, true)
345
+ worker.value # Must finish while the other writer still owns the mutex.
346
+ drain_errors
347
+ raise 'wrong writer-wait timeout' unless queue.length == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
348
+ raise 'timed-out sender wrote' unless waiting_socket.written.empty?
349
+ lock.unlock
350
+ RUBY
351
+ end
352
+
353
+ def test_tcp_write_timeout_preserves_the_resolvers_remaining_retries
354
+ run_scenario(<<~'RUBY')
355
+ original_tcp, original_select = Socket.method(:tcp), IO.method(:select)
356
+ begin
357
+ [false, true].each do |pipeline|
358
+ socket = FrameSocket.new
359
+ socket.define_singleton_method(:write_nonblock) { |_| raise IO::EAGAINWaitWritable }
360
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **options| socket }
361
+ IO.define_singleton_method(:select) { |*| nil }
362
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
363
+ tcp_pipelining: pipeline, dnssec: false, packet_timeout: 0.05)
364
+ q, queue = query, Queue.new
365
+ writer.send_async(q, queue, :write_timeout)
366
+ drain_errors
367
+ raise 'missing or duplicate timeout' unless queue.length == 1
368
+ error = queue.pop(true).last
369
+ raise 'write timeout became a connection failure' unless error.is_a?(Dnsruby::ResolvTimeout)
370
+ raise 'stalled socket left open' unless socket.closed?
371
+ assert_clean(socket)
372
+
373
+ # A timeout keeps the next attempt scheduled; a connection error would
374
+ # remove this server and finish the client query prematurely.
375
+ parent = Struct.new(:single_resolvers, :single_res_mutex).new([writer], Mutex.new)
376
+ engine = Dnsruby::ResolverRuby.new(parent)
377
+ client_queue, select_queue = Queue.new, Queue.new
378
+ attempt = [writer, q, :client, 0]
379
+ engine.instance_variable_get(:@query_list)[:client] = [q, client_queue, select_queue, []]
380
+ scheduled = {Dnsruby::TransportClock.now + 60 => [writer, 1]}
381
+ engine.instance_variable_get(:@timeouts)[:client] = [Dnsruby::TransportClock.now + 120, scheduled]
382
+ SELECTOR.add_observer(select_queue, engine)
383
+ engine.handle_error_response(select_queue, attempt, error, nil)
384
+ raise 'remaining retry removed' unless scheduled.size == 1
385
+ raise 'client query stopped early' unless engine.instance_variable_get(:@query_list).key?(:client)
386
+ raise 'premature client error' unless client_queue.empty?
387
+ engine.stop_querying(:client)
388
+ end
389
+ ensure
390
+ Socket.define_singleton_method(:tcp, original_tcp)
391
+ IO.define_singleton_method(:select, original_select)
392
+ end
393
+ RUBY
394
+ end
395
+
396
+ def test_selector_worker_creation_failure_reports_the_original_attempt_once
397
+ run_scenario(<<~'RUBY')
398
+ socket = FrameSocket.new
399
+ writer = sender(socket)
400
+ lock = Mutex.new
401
+ writer.instance_variable_set(:@pipeline_mutex, lock)
402
+ lock.lock
403
+ K.class_variable_set(:@@select_thread, Thread.current)
404
+ old = settings(FrameSocket.new, :retry, query, writer)
405
+ SELECTOR.add_to_select(old)
406
+ factory = Thread.method(:new)
407
+ Thread.define_singleton_method(:new) { |*| raise ThreadError, 'cannot start worker' }
408
+ begin
409
+ writer.send_dnsruby(old.query_bytes, old.query, :retry, old.client_queue, true)
410
+ ensure
411
+ Thread.define_singleton_method(:new, factory)
412
+ lock.unlock
413
+ end
414
+ drain_errors
415
+ raise 'missing worker error' unless old.client_queue.length == 1 && old.client_queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
416
+ raise 'failed worker retained query' if SELECTOR.query_settings_for_id(:retry)
417
+ raise 'failed worker wrote' unless socket.written.empty?
418
+ RUBY
419
+ end
420
+
421
+ def test_fresh_deferred_send_is_cancelled_by_resolver_close_before_writer_or_connect
422
+ run_scenario(<<~'RUBY')
423
+ [:writer, :connect].each do |boundary|
424
+ socket = FrameSocket.new
425
+ writer = sender(socket)
426
+ writer.use_tcp = true
427
+ parent = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
428
+ tcp_pipelining: true, do_caching: false, dnssec: false)
429
+ parent.single_resolvers = [writer]
430
+ entered, release = Queue.new, Queue.new
431
+ lock = Mutex.new
432
+ writer.instance_variable_set(:@pipeline_mutex, lock)
433
+ lock.lock if boundary == :writer
434
+ if boundary == :connect
435
+ writer.define_singleton_method(:tcp_pipeline_socket) do |_|
436
+ entered << true
437
+ release.pop
438
+ [socket, true]
439
+ end
440
+ end
441
+ task = nil
442
+ defer = SELECTOR.method(:defer_send)
443
+ SELECTOR.define_singleton_method(:defer_send) { |*args, &operation| task = defer.call(*args, &operation) }
444
+ K.class_variable_set(:@@select_thread, Thread.current)
445
+ queue = Queue.new
446
+ parent.send_async(query, queue, :closing)
447
+ entered.pop if boundary == :connect
448
+ raise 'fresh attempt has no cancellation owner' if K.class_variable_get(:@@query_hash).empty?
449
+ parent.close
450
+ result = queue.pop(true)
451
+ raise 'close not delivered' unless result.last.is_a?(Dnsruby::OtherResolvError)
452
+ raise 'close retained pending attempt' unless K.class_variable_get(:@@query_hash).empty?
453
+ lock.unlock if boundary == :writer
454
+ release << true if boundary == :connect
455
+ task.value
456
+ drain_errors
457
+ raise 'cancelled send wrote bytes' unless socket.written.empty?
458
+ raise 'close delivered twice' unless queue.empty?
459
+ raise 'unused connection retained' if boundary == :connect && !socket.closed?
460
+ end
461
+ RUBY
462
+ end
463
+
464
+ def test_close_between_registration_and_first_tcp_write_preserves_other_pipeline_queries
465
+ run_scenario(<<~'RUBY')
466
+ socket = FrameSocket.new
467
+ writer = sender(socket)
468
+ writer.use_tcp = true
469
+ other = settings(socket, :other, query('other.example.'), writer)
470
+ SELECTOR.add_to_select(other)
471
+ parent = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
472
+ tcp_pipelining: true, do_caching: false, dnssec: false)
473
+ parent.single_resolvers = [writer]
474
+ register = SELECTOR.method(:register_query_if_current)
475
+ SELECTOR.define_singleton_method(:register_query_if_current) do |current, previous|
476
+ result = register.call(current, previous)
477
+ # Cancel after the atomic registration, before the first native write.
478
+ # Run outside ResolverRuby's caller-side lock, as a selector send does.
479
+ parent.close if result
480
+ result
481
+ end
482
+ K.class_variable_set(:@@select_thread, Thread.current)
483
+ task = nil
484
+ defer = SELECTOR.method(:defer_send)
485
+ SELECTOR.define_singleton_method(:defer_send) { |*args, &operation| task = defer.call(*args, &operation) }
486
+ queue = Queue.new
487
+ parent.send_async(query, queue, :closing)
488
+ task.value
489
+ drain_errors
490
+ raise 'cancelled registered send wrote bytes' unless socket.written.empty?
491
+ raise 'other pipeline query was retired' if socket.closed? || !SELECTOR.query_settings_for_id(:other).equal?(other)
492
+ raise 'wrong close cardinality' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
493
+ RUBY
494
+ end
495
+
496
+ def test_sender_pool_bounds_threads_without_rejecting_bursts
497
+ run_scenario(<<~'RUBY')
498
+ entered, release = Queue.new, Queue.new
499
+ blockers = 4.times.map do
500
+ SELECTOR.defer_send { entered << Thread.current; release.pop }
501
+ end
502
+ workers = 4.times.map { entered.pop }
503
+ raise 'worker count not bounded' unless workers.uniq.size == 4
504
+ completed = Queue.new
505
+ burst = 1000.times.map { SELECTOR.defer_send { completed << Thread.current } }
506
+ raise 'queued sends escaped the bounded workers' unless completed.empty?
507
+ 4.times { release << true }
508
+ (blockers + burst).each(&:value)
509
+ raise 'burst dropped work' unless completed.size == 1000
510
+ raise 'pool expanded under burst' unless 1000.times.map { completed.pop }.all? { |thread| workers.include?(thread) }
511
+ RUBY
512
+ end
513
+
514
+ def test_selector_delegates_uncontended_tcp_connect_and_write_waits_in_both_modes
515
+ run_scenario(<<~'RUBY')
516
+ [false, true].each do |pipeline|
517
+ [:connect, :write].each do |phase|
518
+ socket = FrameSocket.new
519
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: pipeline,
520
+ packet_timeout: 1, dnssec: false)
521
+ entered, release = Queue.new, Queue.new
522
+ connect = -> do
523
+ if phase == :connect
524
+ entered << Thread.current
525
+ release.pop
526
+ end
527
+ socket
528
+ end
529
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| connect.call }
530
+ writer.define_singleton_method(:tcp_pipeline_socket) { |_| [connect.call, true] }
531
+ if phase == :write
532
+ first = true
533
+ native_write = socket.method(:write_nonblock)
534
+ socket.define_singleton_method(:write_nonblock) do |bytes|
535
+ if first
536
+ first = false
537
+ raise IO::EAGAINWaitWritable
538
+ end
539
+ native_write.call(bytes)
540
+ end
541
+ IO.define_singleton_method(:select) do |*|
542
+ entered << Thread.current
543
+ release.pop
544
+ [[], [socket], []]
545
+ end
546
+ end
547
+ K.class_variable_set(:@@select_thread, Thread.current)
548
+ q, queue = query, Queue.new
549
+ task = writer.send_dnsruby(q.encode, q, :blocked, queue, true)
550
+ raise 'TCP send was not delegated' unless task.respond_to?(:value)
551
+ raise 'connect/write still ran on selector' if entered.pop.equal?(Thread.current)
552
+ unrelated = settings(FrameSocket.new, :unrelated)
553
+ unrelated.endtime = Dnsruby::TransportClock.now - 1
554
+ SELECTOR.add_to_select(unrelated)
555
+ SELECTOR.process_timeouts
556
+ raise 'TCP wait blocked another timeout' unless unrelated.client_queue.size == 1
557
+ release << true
558
+ task.value
559
+ raise 'delegated send lost bytes' unless socket.written == [q.encode.bytesize].pack('n') + q.encode
560
+ SELECTOR.remove_id(:blocked)
561
+ end
562
+ end
563
+ RUBY
564
+ end
565
+
566
+ def test_retry_reserves_its_fresh_deadline_before_connecting
567
+ run_scenario(<<~'RUBY')
568
+ [false, true].each do |pipeline|
569
+ socket = FrameSocket.new
570
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: pipeline,
571
+ packet_timeout: 1, dnssec: false)
572
+ old = settings(FrameSocket.new, :retry, query, writer)
573
+ old.endtime = Dnsruby::TransportClock.now - 1
574
+ SELECTOR.add_to_select(old)
575
+ entered, release = Queue.new, Queue.new
576
+ connect = -> { entered << true; release.pop; socket }
577
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| connect.call }
578
+ writer.define_singleton_method(:tcp_pipeline_socket) { |_| [connect.call, true] }
579
+ K.class_variable_set(:@@select_thread, Thread.current)
580
+ Thread.current[:dnsruby_response_query] = old
581
+ task = writer.send_dnsruby(old.query_bytes, old.query, :retry, old.client_queue, true)
582
+ Thread.current[:dnsruby_response_query] = nil
583
+ entered.pop
584
+ pending = SELECTOR.query_settings_for_id(:retry)
585
+ raise 'old deadline retained during connect' unless pending && pending.endtime > Dnsruby::TransportClock.now
586
+ SELECTOR.process_timeouts
587
+ raise 'retry expired against original deadline' unless old.client_queue.empty?
588
+ release << true
589
+ task.value
590
+ raise 'retry was not sent' if socket.written.empty?
591
+ SELECTOR.remove_id(:retry)
592
+ end
593
+ RUBY
594
+ end
595
+
596
+ def test_deferred_sender_fifo_does_not_occupy_other_workers_or_starve_other_senders
597
+ run_scenario(<<~'RUBY')
598
+ entered, release, backlog_release, healthy = Queue.new, Queue.new, Queue.new, Queue.new
599
+ senders = 4.times.map { Object.new }
600
+ initial = senders.map { |sender| SELECTOR.defer_send(sender) { entered << true; release.pop } }
601
+ 4.times { entered.pop }
602
+ backlog = senders.map { |sender| SELECTOR.defer_send(sender) { backlog_release.pop } }
603
+ fifth = SELECTOR.defer_send(Object.new) { healthy << true }
604
+ 4.times { release << true }
605
+ Timeout.timeout(1) { healthy.pop }
606
+ 4.times { backlog_release << true }
607
+ (initial + backlog + [fifth]).each(&:value)
608
+
609
+ one_sender = Object.new
610
+ ordered = Queue.new
611
+ tasks = 1000.times.map { |index| SELECTOR.defer_send(one_sender) { ordered << index } }
612
+ tasks.each(&:value)
613
+ raise 'sender requests reordered' unless 1000.times.map { ordered.pop } == (0...1000).to_a
614
+ RUBY
615
+ end
616
+
617
+ def test_cancellation_after_a_partial_frame_retires_the_corrupt_pipeline
618
+ run_scenario(<<~'RUBY')
619
+ socket = FrameSocket.new
620
+ writer = sender(socket)
621
+ other = settings(socket, :other, query('other.example.'), writer)
622
+ SELECTOR.add_to_select(other)
623
+ entered, release = Queue.new, Queue.new
624
+ native_write = socket.method(:write_nonblock)
625
+ calls = 0
626
+ socket.define_singleton_method(:write_nonblock) do |bytes|
627
+ calls += 1
628
+ raise IO::EAGAINWaitWritable if calls == 2
629
+ native_write.call(bytes.byteslice(0, 2))
630
+ end
631
+ IO.define_singleton_method(:select) do |*|
632
+ entered << true
633
+ release.pop
634
+ [[], [socket], []]
635
+ end
636
+ queue, owner = Queue.new, Object.new
637
+ SELECTOR.add_observer(queue, owner)
638
+ K.class_variable_set(:@@select_thread, Thread.current)
639
+ q = query
640
+ task = writer.send_dnsruby(q.encode, q, :cancelled, queue, true)
641
+ entered.pop
642
+ SELECTOR.remove_observer(queue, owner)
643
+ release << true
644
+ task.value
645
+ drain_errors
646
+ raise 'cancelled frame continued writing' unless socket.written.bytesize == 2
647
+ raise 'corrupt stream reused' unless socket.closed?
648
+ raise 'other query not failed once' unless other.client_queue.size == 1 && other.client_queue.pop.last.is_a?(Dnsruby::SocketEofResolvError)
649
+ raise 'cancelled owner received another result' unless queue.empty?
650
+ assert_clean(socket)
651
+ RUBY
652
+ end
653
+
654
+ private
655
+
656
+ def run_scenario(script)
657
+ options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
658
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
659
+ File.expand_path('../lib', __dir__), '-e', "#{FIXTURE}\nTimeout.timeout(10) do\n#{script}\nend")
660
+ assert(status.success?, "#{stdout}\n#{stderr}")
661
+ assert_empty(stderr)
662
+ end
663
+ end