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,383 @@
1
+ require_relative 'tc_pipeline_send'
2
+
3
+ class TestResolverConcurrency < Minitest::Test
4
+ def test_busy_resolver_dispatches_a_new_query_without_waiting_for_a_tick
5
+ isolated(<<~'RUBY')
6
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
7
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
8
+ writer = resolver.single_resolvers.first
9
+ sent = []
10
+ writer.define_singleton_method(:send_async) { |_message, _queue, id| sent << id }
11
+ slow_results, fast_results = Queue.new, Queue.new
12
+ resolver.send_async(query('pending.example.'), slow_results, :pending)
13
+ raise 'first query not dispatched' unless sent.size == 1
14
+ resolver.query_timeout = 0.1
15
+ resolver.send_async(query('fast.example.'), fast_results, :fast)
16
+ raise 'busy resolver left new query waiting for a tick' unless sent.size == 2
17
+ fast = SELECTOR.query_settings_for_id(sent.last)
18
+ answer = fast.query.clone
19
+ answer.header.qr = true
20
+ SELECTOR.push_response_for_query(fast, answer, fast.query, writer)
21
+ SELECTOR.send_queued_responses
22
+ raise 'fast query did not finish independently' unless fast_results.size == 1 &&
23
+ fast_results.pop.last.nil? && slow_results.empty?
24
+ SELECTOR.send_tick_to_observers
25
+ raise 'selector tick resent initial attempt' unless sent.size == 2
26
+ resolver.close
27
+ RUBY
28
+ end
29
+
30
+ def test_concurrent_admission_and_selector_ticks_dispatch_each_attempt_once
31
+ isolated(<<~'RUBY')
32
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
33
+ counts, lock = Hash.new(0), Mutex.new
34
+ resolver.single_resolvers.first.define_singleton_method(:send_async) do |_message, _queue, id|
35
+ lock.synchronize { counts[id[2]] += 1 }
36
+ Thread.pass
37
+ end
38
+ gate = Queue.new
39
+ callers = 20.times.map do |index|
40
+ Thread.new { gate.pop; resolver.send_async(query("parallel#{index}.example."), Queue.new, index) }
41
+ end
42
+ ticking = Thread.new { 100.times { SELECTOR.send_tick_to_observers; Thread.pass } }
43
+ callers.length.times { gate << true }
44
+ callers.each(&:value)
45
+ ticking.value
46
+ raise 'lost or duplicate initial attempts' unless counts.size == callers.size && counts.values.all? { |count| count == 1 }
47
+ resolver.close
48
+ RUBY
49
+ end
50
+
51
+ def test_concurrent_first_use_publishes_one_complete_configuration_and_engine
52
+ isolated(<<~'RUBY')
53
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
54
+ Dnsruby::Config.define_singleton_method(:default_config_hash) { { nameserver: ['127.0.0.1'] } }
55
+ original = Dnsruby::PacketSender.method(:new)
56
+ entered, release = Queue.new, Queue.new
57
+ creations = 0
58
+ Dnsruby::PacketSender.define_singleton_method(:new) do |*args|
59
+ creations += 1
60
+ entered << true
61
+ release.pop
62
+ original.call(*args).tap do |sender|
63
+ sender.define_singleton_method(:send_async) do |message, queue, id|
64
+ answer = message.clone
65
+ answer.header.qr = true
66
+ SELECTOR.push_response_to_select(id, queue, answer, message, self)
67
+ end
68
+ end
69
+ end
70
+ resolver = Dnsruby::Resolver.new
71
+ resolver.dnssec = false
72
+ queues = [Queue.new, Queue.new]
73
+ first = Thread.new { resolver.send_async(query('first.example.'), queues[0], :first) }
74
+ entered.pop
75
+ second = Thread.new { resolver.send_async(query('second.example.'), queues[1], :second) }
76
+ Thread.pass until second.status == 'sleep'
77
+ raise 'partial configuration published' if resolver.instance_variable_get(:@configured)
78
+ release << true
79
+ first.value
80
+ second.value
81
+ SELECTOR.send_tick_to_observers
82
+ SELECTOR.send_queued_responses
83
+ raise 'initialized more than once' unless creations == 1 && resolver.single_resolvers.length == 1
84
+ queues.each do |queue|
85
+ raise 'missing or duplicate completion' unless queue.size == 1
86
+ raise 'cold query failed' unless queue.pop.last.nil?
87
+ end
88
+ resolver.close
89
+ RUBY
90
+ end
91
+
92
+ def test_sender_publication_uses_settings_changed_during_construction
93
+ isolated(<<~'RUBY')
94
+ original = Dnsruby::PacketSender.method(:new)
95
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
96
+ entered, release = Queue.new, Queue.new
97
+ Dnsruby::PacketSender.define_singleton_method(:new) do |*args|
98
+ original.call(*args).tap do
99
+ entered << true
100
+ release.pop
101
+ end
102
+ end
103
+ reconfigure = Thread.new { resolver.nameserver = '127.0.0.2' }
104
+ entered.pop
105
+ resolver.port = 1053
106
+ resolver.use_tcp = true
107
+ resolver.no_tcp = true
108
+ resolver.tcp_pipelining = true
109
+ resolver.tcp_pipelining_max_queries = 11
110
+ resolver.packet_timeout = 3
111
+ resolver.tsig = ['example.', 'c2VjcmV0']
112
+ resolver.ignore_truncation = true
113
+ resolver.src_address = '127.0.0.3'
114
+ resolver.src_address6 = '::1'
115
+ resolver.src_port = 50123
116
+ resolver.recurse = false
117
+ resolver.udp_size = 1452
118
+ resolver.dnssec = true
119
+ release << true
120
+ reconfigure.value
121
+ sender = resolver.single_resolvers.first
122
+ [:port, :use_tcp, :no_tcp, :tcp_pipelining, :tcp_pipelining_max_queries,
123
+ :packet_timeout, :ignore_truncation, :src_address, :src_port, :recurse,
124
+ :udp_size, :dnssec].each do |option|
125
+ raise "stale #{option}" unless sender.public_send(option) == resolver.public_send(option)
126
+ end
127
+ raise 'stale IPv6 source' unless sender.instance_variable_get(:@src_address6) == '::1'
128
+ raise 'stale TSIG key' unless sender.tsig.name.to_s == 'example' &&
129
+ sender.tsig.algorithm.to_s == 'hmac-sha256'
130
+ resolver.close
131
+ RUBY
132
+ end
133
+
134
+ def test_initialization_failure_is_shared_and_an_explicit_reconfiguration_recovers
135
+ isolated(<<~'RUBY')
136
+ Thread.report_on_exception = false
137
+ Dnsruby::Config.define_singleton_method(:default_config_hash) { { nameserver: ['127.0.0.1'] } }
138
+ original = Dnsruby::PacketSender.method(:new)
139
+ entered, release = Queue.new, Queue.new
140
+ creations = 0
141
+ Dnsruby::PacketSender.define_singleton_method(:new) do |*|
142
+ creations += 1
143
+ entered << true
144
+ release.pop
145
+ raise ArgumentError, 'unusable nameserver'
146
+ end
147
+ resolver = Dnsruby::Resolver.new
148
+ resolver.query_timeout = 0.02
149
+ queues = [Queue.new, Queue.new]
150
+ first = Thread.new { resolver.send_async(query, queues[0], :first) }
151
+ entered.pop
152
+ second = Thread.new { resolver.send_async(query, queues[1], :second) }
153
+ Thread.pass until second.status == 'sleep'
154
+ release << true
155
+ first.value
156
+ second.value
157
+ raise 'failed initialization repeated' unless creations == 1
158
+ sleep 0.03
159
+ SELECTOR.send_tick_to_observers
160
+ queues.each do |queue|
161
+ raise 'failed configuration changed async completion' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
162
+ end
163
+ SELECTOR.send_tick_to_observers
164
+ raise 'initialization failure completed twice' unless queues.all?(&:empty?)
165
+ Dnsruby::PacketSender.define_singleton_method(:new, original)
166
+ resolver.nameserver = '127.0.0.1'
167
+ raise 'explicit reconfiguration failed' unless resolver.single_resolvers.length == 1
168
+ raise 'failure retained' if resolver.instance_variable_get(:@configuration_error)
169
+ RUBY
170
+ end
171
+
172
+ def test_an_explicitly_empty_resolver_completes_each_query_once_without_a_timer_thread
173
+ isolated(<<~'RUBY')
174
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
175
+ resolver = Dnsruby::Resolver.new
176
+ resolver.single_resolvers = []
177
+ resolver.query_timeout = 0.02
178
+ queue = Queue.new
179
+ resolver.send_async(query, queue, :empty)
180
+ sleep 0.03
181
+ SELECTOR.send_tick_to_observers
182
+ raise 'empty resolver did not time out once' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
183
+ SELECTOR.send_tick_to_observers
184
+ raise 'empty resolver completed twice' unless queue.empty?
185
+ resolver.close
186
+ RUBY
187
+ end
188
+
189
+ def test_hostname_reconfiguration_does_not_hold_the_selector_callback_lock
190
+ isolated(<<~'RUBY')
191
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
192
+ parent = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
193
+ parent.single_resolvers.first.define_singleton_method(:send_async) { |*| }
194
+ parent.send_async(query('pending.example.'), Queue.new, :pending)
195
+ bootstrap = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
196
+ queued = Queue.new
197
+ bootstrap.single_resolvers.first.define_singleton_method(:send_async) do |message, queue, id|
198
+ answer = message.clone
199
+ answer.header.qr = true
200
+ answer.add_answer(Dnsruby::RR.create(name: message.question.first.qname, type: 'A', address: '127.0.0.2'))
201
+ SELECTOR.push_response_to_select(id, queue, answer, message, self)
202
+ queued << true
203
+ end
204
+ Dnsruby::Config.define_singleton_method(:nameserver_resolver) { bootstrap }
205
+ reconfigure = Thread.new { parent.nameserver = 'resolver.example.' }
206
+ queued.pop
207
+ # A selector loop ticks observers before dispatching the bootstrap answer.
208
+ # Holding the parent query mutex while joining the constructor deadlocks.
209
+ SELECTOR.send_tick_to_observers
210
+ SELECTOR.send_queued_responses
211
+ reconfigure.value
212
+ raise 'hostname was not configured' unless parent.single_resolvers.first.server == '127.0.0.2'
213
+ parent.close
214
+ bootstrap.close
215
+ RUBY
216
+ end
217
+
218
+ def test_every_configuration_entry_point_recovers_an_initially_empty_resolver
219
+ isolated(<<~'RUBY')
220
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
221
+ [:add_server, :single_resolvers=, :nameserver=].each do |entry|
222
+ resolver = Dnsruby::Resolver.new(nameserver: [], dnssec: false)
223
+ raise 'fixture did not record empty configuration' unless resolver.instance_variable_get(:@configuration_error)
224
+ value = entry == :single_resolvers= ? [Dnsruby::PacketSender.new(server: '127.0.0.1')] : '127.0.0.1'
225
+ resolver.public_send(entry, value)
226
+ resolver.add_config_nameservers
227
+ raise 'failed initialization retained after recovery' if resolver.instance_variable_get(:@configuration_error)
228
+ raise 'server missing after recovery' unless resolver.single_resolvers.size == 1
229
+ resolver.single_resolvers.first.define_singleton_method(:send_async) do |message, queue, id|
230
+ answer = message.clone
231
+ answer.header.qr = true
232
+ SELECTOR.push_response_to_select(id, queue, answer, message, self)
233
+ end
234
+ queue = Queue.new
235
+ resolver.send_async(query, queue, :recovered)
236
+ SELECTOR.send_queued_responses
237
+ raise 'recovered query failed' unless queue.size == 1 && queue.pop.last.nil?
238
+ resolver.close
239
+ end
240
+ RUBY
241
+ end
242
+
243
+ def test_close_cannot_observe_a_query_before_its_selector_observer_exists
244
+ isolated(<<~'RUBY')
245
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
246
+ resolver.single_resolvers.first.define_singleton_method(:send_async) { |*| }
247
+ entered, release, errors = Queue.new, Queue.new, Queue.new
248
+ add_observer = SELECTOR.method(:add_observer)
249
+ SELECTOR.define_singleton_method(:add_observer) do |*args|
250
+ entered << true
251
+ release.pop
252
+ add_observer.call(*args)
253
+ end
254
+ queue = Queue.new
255
+ sending = Thread.new { resolver.send_async(query, queue, :closing) }
256
+ entered.pop
257
+ closing = Thread.new do
258
+ resolver.close
259
+ rescue StandardError => error
260
+ errors << error
261
+ end
262
+ Thread.pass until !closing.alive? || closing.status == 'sleep'
263
+ raise 'close saw an unpublished observer' unless closing.alive? && errors.empty?
264
+ release << true
265
+ sending.value
266
+ closing.value
267
+ raise 'concurrent close raised' unless errors.empty?
268
+ raise 'query did not complete once' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
269
+ raise 'observer retained after close' unless K.class_variable_get(:@@observers).empty?
270
+ RUBY
271
+ end
272
+
273
+ def test_close_and_id_reuse_ignore_an_old_received_response
274
+ assert_stale_query_event_is_ignored(:RECEIVED)
275
+ end
276
+
277
+ def test_close_and_id_reuse_ignore_an_old_received_error
278
+ assert_stale_query_event_is_ignored(:RECEIVED, :ServFail)
279
+ end
280
+
281
+ def test_close_and_id_reuse_ignore_an_old_transport_error
282
+ assert_stale_query_event_is_ignored(:ERROR, :ServFail)
283
+ end
284
+
285
+ def test_close_and_id_reuse_ignore_an_old_validated_response
286
+ assert_stale_query_event_is_ignored(:VALIDATED)
287
+ end
288
+
289
+ def test_close_and_id_reuse_ignore_an_old_validation_error
290
+ assert_stale_query_event_is_ignored(:VALIDATED, :ServFail)
291
+ end
292
+
293
+ private
294
+
295
+ def assert_stale_query_event_is_ignored(event_type, error_type = nil)
296
+ isolated(<<~RUBY + <<~'RUBY')
297
+ stale_event_type = Dnsruby::Resolver::EventType::#{event_type}
298
+ stale_error = #{error_type ? "Dnsruby::#{error_type}.new" : 'nil'}
299
+ RUBY
300
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
301
+ Thread.report_on_exception = false
302
+ options = { nameserver: '127.0.0.1', dnssec: false, do_caching: false,
303
+ packet_timeout: 60, query_timeout: 60 }
304
+ resolver = Dnsruby::Resolver.new(options)
305
+ unrelated = Dnsruby::Resolver.new(options)
306
+ sends = Queue.new
307
+ [resolver, unrelated].each do |parent|
308
+ parent.single_resolvers.first.define_singleton_method(:send_async) do |message, queue, id|
309
+ sends << [message, queue, id, self]
310
+ end
311
+ end
312
+ queue, keeper_queue, unrelated_queue = Queue.new, Queue.new, Queue.new
313
+ resolver.send_async(query('old.example.'), queue, :reused)
314
+ old_message, old_queue, old_id, = sends.pop
315
+ response = old_message.clone
316
+ response.header.qr = true
317
+ response.header.rcode = Dnsruby::RCode::SERVFAIL if stale_error
318
+ old_queue << [old_id, stale_event_type, response, stale_error]
319
+
320
+ engine = resolver.instance_variable_get(:@resolver_ruby)
321
+ original = engine.method(:handle_queue_event)
322
+ entered, release = Queue.new, Queue.new
323
+ engine.define_singleton_method(:handle_queue_event) do |*args|
324
+ if args.first.equal?(old_queue)
325
+ entered << true
326
+ release.pop
327
+ end
328
+ original.call(*args)
329
+ end
330
+ callback = Thread.new do
331
+ # Exercise the real observer lookup, then pause the callback before it
332
+ # can acquire the resolver lock. close cannot retract that lookup.
333
+ SELECTOR.notify_queue_observers(old_queue, old_id)
334
+ raise 'stale event completed the replacement query' unless queue.empty?
335
+ pending = engine.instance_variable_get(:@timeouts)[:reused]
336
+ replacement = engine.instance_variable_get(:@query_list)[:reused]
337
+ raise 'stale event stopped the replacement send' unless pending && replacement &&
338
+ replacement[3].size == 1 && SELECTOR.query_settings_for_id(replacement[3].first)
339
+
340
+ # The same selector dispatch must still serve the replacement and a
341
+ # different Resolver. No sockets or timing-dependent timeout is needed.
342
+ SELECTOR.send_tick_to_observers
343
+ until sends.empty?
344
+ message, select_queue, id, sender = sends.pop
345
+ answer = message.clone
346
+ answer.header.qr = true
347
+ SELECTOR.push_response_to_select(id, select_queue, answer, message, sender)
348
+ end
349
+ SELECTOR.send_queued_responses
350
+ end
351
+ entered.pop
352
+ resolver.close
353
+ raise 'close did not complete the old query once' unless
354
+ queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
355
+
356
+ # Both new queries dispatch immediately, even while the old observer's
357
+ # callback is paused. That stale callback must preserve their attempts.
358
+ resolver.send_async(query('keeper.example.'), keeper_queue, :keeper)
359
+ resolver.send_async(query('new.example.'), queue, :reused)
360
+ unrelated.send_async(query('unrelated.example.'), unrelated_queue, :unrelated)
361
+ release << true
362
+ callback.value
363
+ [[queue, :reused, 'new.example'], [keeper_queue, :keeper, 'keeper.example'],
364
+ [unrelated_queue, :unrelated, 'unrelated.example']].each do |results, expected_id, name|
365
+ raise 'query did not complete exactly once' unless results.size == 1
366
+ id, answer, error = results.pop
367
+ raise 'wrong query completion' unless id == expected_id && error.nil? &&
368
+ answer.question.first.qname.to_s == name
369
+ end
370
+ raise 'completed queries retained selector ownership' unless K.class_variable_get(:@@query_hash).empty?
371
+ resolver.close
372
+ unrelated.close
373
+ RUBY
374
+ end
375
+
376
+ def isolated(body)
377
+ options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
378
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
379
+ File.expand_path('../lib', __dir__), '-e', "#{TestPipelineSend::FIXTURE}\nTimeout.timeout(10) do\n#{body}\nend")
380
+ assert(status.success?, "#{stdout}\n#{stderr}")
381
+ assert_empty(stderr)
382
+ end
383
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'spec_helper'
2
+ require 'timeout'
3
+
4
+ class ResolverConfigurationContractsTest < Minitest::Test
5
+ def test_single_resolver_keeps_only_its_configured_server
6
+ Dnsruby::Config.stub(:default_config_hash, {nameserver: %w[192.0.2.1 192.0.2.2]}) do
7
+ resolver = Dnsruby::SingleResolver.new
8
+ assert_equal(['192.0.2.1'], resolver.single_resolvers.map(&:server))
9
+ resolver.server = '198.51.100.1'
10
+ assert_equal(['198.51.100.1'], resolver.single_resolvers.map(&:server))
11
+ assert_equal('198.51.100.1', resolver.server)
12
+ assert_equal(['198.51.100.1'], resolver.config.nameserver)
13
+ ensure
14
+ resolver&.close
15
+ end
16
+ end
17
+
18
+ def test_zero_attempt_plan_completes_once_without_sending
19
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', retry_times: 0, query_timeout: 0, do_caching: false)
20
+ queue = Queue.new
21
+ resolver.single_resolvers.first.define_singleton_method(:prepare_send_async) { |*| raise 'Unexpected send' }
22
+ resolver.send_async(Dnsruby::Message.new('example.'), queue, :empty)
23
+ id, response, error = Timeout.timeout(1) { queue.pop }
24
+ assert_equal(:empty, id)
25
+ assert_nil(response)
26
+ assert_kind_of(Dnsruby::ResolvTimeout, error)
27
+ assert(queue.empty?)
28
+ ensure
29
+ resolver&.close
30
+ end
31
+ end
@@ -0,0 +1,211 @@
1
+ require_relative 'tc_pipeline_send'
2
+
3
+ class TestResolverValidation < Minitest::Test
4
+ FIXTURE = <<~'RUBY'
5
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
6
+ clock = 1000.0
7
+ # Advance slightly on reads so an immediately scheduled send is due on
8
+ # the next tick, without sleeps or dependence on the host's clock.
9
+ Dnsruby::TransportClock.define_singleton_method(:now) { clock += 0.0001 }
10
+ validators = []
11
+ Dnsruby::ValidatorThread.define_method(:run) { validators << self }
12
+ Dnsruby::Dnssec.define_singleton_method(:validate_with_query) do |_query, answer|
13
+ answer.security_level = Dnsruby::Message::SecurityLevel::SECURE
14
+ end
15
+ resolver = Dnsruby::Resolver.new(nameserver: ['127.0.0.1', '127.0.0.2'],
16
+ dnssec: true, do_validation: true, do_caching: false,
17
+ retry_times: 2, retry_delay: 1, query_timeout: 10)
18
+ resolver.single_resolvers.each { |writer| writer.define_singleton_method(:send_async) { |*| } }
19
+ results = Queue.new
20
+
21
+ def start_query(resolver, results, name = 'validation.example.')
22
+ request = query(name)
23
+ request.do_validation = true
24
+ resolver.send_async(request, results, :client)
25
+ SELECTOR.send_tick_to_observers
26
+ request
27
+ end
28
+
29
+ def accept_answer(settings, rcode = Dnsruby::RCode::NOERROR)
30
+ answer = settings.query.clone
31
+ answer.header.qr = true
32
+ answer.header.rcode = rcode
33
+ SELECTOR.push_response_for_query(settings, answer, settings.query, settings.single_resolver)
34
+ SELECTOR.send_queued_responses
35
+ answer
36
+ end
37
+
38
+ def attempts
39
+ K.class_variable_get(:@@query_hash).values
40
+ end
41
+
42
+ def expect_completion(results, error_class = nil, name = 'validation.example')
43
+ raise 'missing or duplicate completion' unless results.size == 1
44
+ id, answer, error = results.pop
45
+ raise 'wrong completion id' unless id == :client
46
+ if error_class
47
+ raise "wrong error: #{error.inspect}" unless error.is_a?(error_class)
48
+ else
49
+ raise "unexpected error: #{error.inspect}" if error
50
+ raise 'wrong answer' unless answer.question.first.qname.to_s == name
51
+ end
52
+ end
53
+
54
+ def expect_clean(resolver)
55
+ engine = resolver.instance_variable_get(:@resolver_ruby)
56
+ tables = [:@query_list, :@timeouts]
57
+ tables << :@validation_queries if engine.instance_variable_defined?(:@validation_queries)
58
+ tables.each do |table|
59
+ raise "retained #{table}" unless engine.instance_variable_get(table).empty?
60
+ end
61
+ raise 'retained attempts' unless attempts.empty?
62
+ raise 'retained observer' unless K.class_variable_get(:@@observers).empty?
63
+ end
64
+ RUBY
65
+
66
+ def test_total_deadline_applies_during_positive_and_nxdomain_validation
67
+ [0, 3].each do |rcode|
68
+ isolated("rcode = #{rcode}\n" + <<~'RUBY')
69
+ start_query(resolver, results)
70
+ accept_answer(attempts.first, rcode)
71
+ raise 'validation not pending' unless validators.size == 1 && results.empty?
72
+ clock += 11
73
+ SELECTOR.send_tick_to_observers
74
+ expect_completion(results, Dnsruby::ResolvTimeout)
75
+ validators.shift.do_validate
76
+ SELECTOR.send_queued_validation_responses
77
+ raise 'late validation completed twice' unless results.empty?
78
+ expect_clean(resolver)
79
+ RUBY
80
+ end
81
+ end
82
+
83
+ def test_validation_receives_the_original_transmission_budget
84
+ isolated(<<~'RUBY')
85
+ start_query(resolver, results)
86
+ settings = attempts.first
87
+ engine = resolver.instance_variable_get(:@resolver_ruby)
88
+ original = engine.instance_variable_get(:@transmission_budgets).fetch(:client)
89
+ accept_answer(settings)
90
+ raise 'validation lost transmission history' unless
91
+ validators.first.instance_variable_get(:@transmission_budget).equal?(original)
92
+ raise 'replacement queue inherited old transmission history' if
93
+ engine.validation_transmission_budget(Queue.new, settings.client_query_id)
94
+ resolver.close
95
+ RUBY
96
+ end
97
+
98
+ def test_validation_finishing_after_deadline_times_out_even_before_next_tick
99
+ isolated(<<~'RUBY')
100
+ start_query(resolver, results)
101
+ accept_answer(attempts.first)
102
+ clock += 11
103
+ validators.shift.do_validate
104
+ SELECTOR.send_queued_validation_responses
105
+ expect_completion(results, Dnsruby::ResolvTimeout)
106
+ expect_clean(resolver)
107
+ RUBY
108
+ end
109
+
110
+ def test_competing_error_or_answer_cannot_replace_the_accepted_validator
111
+ [:servfail, :socket_error, :positive, :nxdomain, :validation_error].each do |event|
112
+ isolated("competing = :#{event}\n" + <<~'RUBY')
113
+ start_query(resolver, results)
114
+ clock += 0.6
115
+ SELECTOR.send_tick_to_observers
116
+ first, second = attempts
117
+ raise 'second server was not queried' unless first && second
118
+ accept_answer(first)
119
+ raise 'competing sends not canceled' unless attempts.empty?
120
+ clock += 3
121
+ SELECTOR.send_tick_to_observers
122
+ raise 'retry continued during validation' unless attempts.empty?
123
+
124
+ # A reply/error may already have been claimed and queued when the first
125
+ # answer wins. Exercise those callbacks even though its send is canceled.
126
+ response = second.query.clone
127
+ response.header.qr = true
128
+ kind = Dnsruby::Resolver::EventType::RECEIVED
129
+ error = case competing
130
+ when :servfail then Dnsruby::ServFail.new
131
+ when :socket_error
132
+ kind = Dnsruby::Resolver::EventType::ERROR
133
+ Dnsruby::OtherResolvError.new('peer closed')
134
+ when :nxdomain
135
+ response.header.rcode = Dnsruby::RCode::NXDOMAIN
136
+ Dnsruby::NXDomain.new
137
+ when :validation_error
138
+ kind = Dnsruby::Resolver::EventType::VALIDATED
139
+ Dnsruby::VerifyError.new('other answer failed')
140
+ end
141
+ second.client_queue << [second.client_query_id, kind, response, error]
142
+ SELECTOR.notify_queue_observers(second.client_queue, second.client_query_id)
143
+ if competing == :positive || competing == :nxdomain
144
+ SELECTOR.push_validation_response_to_select(second.client_query_id,
145
+ second.client_queue, response, nil, second.query, second.single_resolver)
146
+ SELECTOR.send_queued_validation_responses
147
+ end
148
+ raise 'competing event completed query' unless results.empty?
149
+ validators.shift.do_validate
150
+ SELECTOR.send_queued_validation_responses
151
+ expect_completion(results)
152
+ expect_clean(resolver)
153
+ RUBY
154
+ end
155
+ end
156
+
157
+ def test_accepted_nxdomain_and_validation_failure_keep_their_error_semantics
158
+ [:nxdomain, :bogus].each do |outcome|
159
+ isolated("outcome = :#{outcome}\n" + <<~'RUBY')
160
+ start_query(resolver, results)
161
+ accept_answer(attempts.first, outcome == :nxdomain ? 3 : 0)
162
+ if outcome == :bogus
163
+ Dnsruby::Dnssec.define_singleton_method(:validate_with_query) { |*| raise Dnsruby::VerifyError, 'bad signature' }
164
+ end
165
+ validators.shift.do_validate
166
+ SELECTOR.send_queued_validation_responses
167
+ expect_completion(results, outcome == :nxdomain ? Dnsruby::NXDomain : Dnsruby::VerifyError)
168
+ expect_clean(resolver)
169
+ RUBY
170
+ end
171
+ end
172
+
173
+ def test_timeout_or_close_and_id_reuse_ignore_the_old_validator
174
+ [:timeout, :close].each do |finish|
175
+ isolated("finish = :#{finish}\n" + <<~'RUBY')
176
+ start_query(resolver, results)
177
+ accept_answer(attempts.first)
178
+ old_validator = validators.shift
179
+ if finish == :timeout
180
+ clock += 11
181
+ SELECTOR.send_tick_to_observers
182
+ else
183
+ resolver.close
184
+ end
185
+ expect_completion(results, finish == :timeout ? Dnsruby::ResolvTimeout : Dnsruby::OtherResolvError)
186
+ expect_clean(resolver)
187
+
188
+ start_query(resolver, results, 'replacement.example.')
189
+ old_validator.do_validate
190
+ SELECTOR.send_queued_validation_responses
191
+ raise 'old validator completed reused id' unless results.empty?
192
+ accept_answer(attempts.first)
193
+ validators.shift.do_validate
194
+ SELECTOR.send_queued_validation_responses
195
+ expect_completion(results, nil, 'replacement.example')
196
+ expect_clean(resolver)
197
+ RUBY
198
+ end
199
+ end
200
+
201
+ private
202
+
203
+ def isolated(body)
204
+ options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
205
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
206
+ File.expand_path('../lib', __dir__), '-e',
207
+ "#{TestPipelineSend::FIXTURE}\nTimeout.timeout(10) do\n#{FIXTURE}\n#{body}\nend")
208
+ assert(status.success?, "#{stdout}\n#{stderr}")
209
+ assert_empty(stderr)
210
+ end
211
+ end