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
@@ -13,12 +13,14 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  # ++
16
+ require 'dnsruby/query_control'
17
+
16
18
  module Dnsruby
17
19
  # Dnsruby::Recursor - Perform recursive dns lookups
18
20
  #
19
21
  # require 'Dnsruby'
20
22
  # rec = Dnsruby::Recursor.new()
21
- # answer = rec.recurse("rob.com.au")
23
+ # answer = rec.query("rob.com.au")
22
24
  #
23
25
  # This module uses a Dnsruby::Resolver to perform recursive queries.
24
26
  #
@@ -111,40 +113,161 @@ module Dnsruby
111
113
  # ;;; FINALLY, THE ANSWER!
112
114
  # Now,DNSSEC validation is performed (unless disabled).
113
115
  class Recursor
116
+ CACHE_LIMIT = 16_384
117
+ ADDRESS_CACHE_LIMIT = 256
118
+ DEFAULT_RECURSION_LIMITS = {
119
+ timeout: 30.0, max_queries: 128, max_steps: 256, max_workers: 8
120
+ }.freeze
121
+
122
+ # TransmissionBudget carries this across address and validation workers.
123
+ class Budget # :nodoc:
124
+ attr_reader :limits, :deadline
125
+
126
+ def initialize(limits)
127
+ @limits = limits
128
+ @deadline = TransportClock.now + limits[:timeout]
129
+ @steps = @workers = 0
130
+ @mutex = Mutex.new
131
+ end
132
+
133
+ def step!
134
+ QueryControl.current&.check!
135
+ @mutex.synchronize do
136
+ raise RecursionLimitError, 'Recursive lookup work limit exceeded' if @steps >= @limits[:max_steps]
137
+ @steps += 1
138
+ end
139
+ end
140
+
141
+ def reserve_worker
142
+ @mutex.synchronize do
143
+ return false if @workers >= @limits[:max_workers]
144
+ @workers += 1
145
+ true
146
+ end
147
+ end
148
+
149
+ def release_worker
150
+ @mutex.synchronize { @workers -= 1 }
151
+ end
152
+
153
+ # Zero-TTL priming data remains usable by the lookup that received it,
154
+ # including its address workers, but never by an independent lookup.
155
+ def retain_hints(generation, hints)
156
+ @mutex.synchronize { @hints = [generation, hints] }
157
+ end
158
+
159
+ def hints_for(generation)
160
+ @mutex.synchronize do
161
+ if @hints && @hints.first == generation &&
162
+ @hints.last.any? { |_name, addresses| !addresses.current_values.empty? }
163
+ @hints.last
164
+ end
165
+ end
166
+ end
167
+
168
+ def with_dependency(name, type, klass)
169
+ previous = Thread.current[:dnsruby_recursion_dependencies] || []
170
+ identity = [Name.create(name).canonical, Types.new(type).code, Classes.new(klass).code]
171
+ return nil if previous.include?(identity)
172
+ Thread.current[:dnsruby_recursion_dependencies] = previous + [identity]
173
+ yield
174
+ ensure
175
+ Thread.current[:dnsruby_recursion_dependencies] = previous
176
+ end
177
+ end
178
+ private_constant :Budget
179
+
180
+ def recursion_limits
181
+ @recursion_limits || DEFAULT_RECURSION_LIMITS
182
+ end
183
+
184
+ def recursion_limits=(settings)
185
+ unless settings.is_a?(Hash) && (settings.keys - DEFAULT_RECURSION_LIMITS.keys).empty?
186
+ raise ArgumentError, 'Unknown recursive lookup limit'
187
+ end
188
+ limits = DEFAULT_RECURSION_LIMITS.merge(settings)
189
+ limits.each do |key, value|
190
+ valid = if key == :timeout
191
+ value.is_a?(Numeric) && value.finite? && value > 0
192
+ else
193
+ value.is_a?(Integer) && value > 0
194
+ end
195
+ raise ArgumentError, "Invalid recursive lookup limit #{key}" unless valid
196
+ end
197
+ @recursion_limits = limits.freeze
198
+ end
199
+
200
+ def self.current_budget
201
+ SelectThread::TransmissionBudget.current&.context(:recursion) { nil }
202
+ end
203
+ private_class_method :current_budget
204
+
205
+ def self.with_limits(resolver, limits = DEFAULT_RECURSION_LIMITS)
206
+ parent = QueryControl.current
207
+ parent&.check!
208
+ transmissions = SelectThread::TransmissionBudget.current || SelectThread::TransmissionBudget.new
209
+ budget = transmissions.context(:recursion) { Budget.new(limits) }
210
+ transmissions.limit_total!(budget.limits[:max_queries], RecursionLimitError)
211
+ timeout = resolver.query_timeout if resolver.respond_to?(:query_timeout)
212
+ deadline = budget.deadline
213
+ deadline = [deadline, TransportClock.now + timeout].min if timeout && timeout > 0
214
+ control = QueryControl.new(deadline: deadline, parent: parent)
215
+ SelectThread::TransmissionBudget.with(transmissions) do
216
+ QueryControl.with(control) { yield budget }
217
+ end
218
+ rescue QueryControl::DeadlineExceeded
219
+ parent&.check!
220
+ raise ResolvTimeout, 'Recursive query timed out'
221
+ ensure
222
+ control&.cancel
223
+ end
224
+ private_class_method :with_limits
225
+
114
226
  class AddressCache # :nodoc: all
115
227
  # Like an array, but stores the expiration of each record.
116
228
  def initialize(*args)
117
229
  @hash = Hash.new # stores addresses against their expiration
230
+ @fresh = {} # Zero-TTL addresses belong only to the current lookup.
118
231
  @mutex = Mutex.new # This class is thread-safe
119
232
  end
120
233
  def push(item)
121
234
  address, ttl = item
122
235
  expiration = Time.now + ttl
123
236
  @mutex.synchronize {
124
- @hash[address] = expiration
237
+ @hash.delete(address)
238
+ @fresh.delete(address)
239
+ if ttl <= 0
240
+ @fresh[address] = true
241
+ else
242
+ @hash[address] = expiration
243
+ end
244
+ @hash.shift while @hash.length > ADDRESS_CACHE_LIMIT
245
+ @fresh.shift while @fresh.length > ADDRESS_CACHE_LIMIT
125
246
  }
126
247
  end
127
248
  def values
128
- ret =[]
129
- keys_to_delete = []
130
- @mutex.synchronize {
131
- @hash.keys.each {|address|
132
- if (@hash[address] > Time.now)
133
- ret.push(address)
134
- else
135
- keys_to_delete.push(address)
136
- end
137
- }
138
- keys_to_delete.each {|key|
139
- @hash.delete(key)
140
- }
141
- }
142
- return ret
249
+ @mutex.synchronize { expire; @hash.keys }
250
+ end
251
+ def current_values
252
+ @mutex.synchronize { expire; @hash.keys | @fresh.keys }
253
+ end
254
+ # Shared caches contain only reusable addresses. Copies also keep later
255
+ # updates to one lookup's working map out of another lookup's view.
256
+ def snapshot(include_current: false)
257
+ copy = self.class.new
258
+ @mutex.synchronize do
259
+ expire
260
+ copy.instance_variable_set(:@hash, @hash.dup)
261
+ copy.instance_variable_set(:@fresh, @fresh.dup) if include_current
262
+ end
263
+ copy
264
+ end
265
+ private def expire
266
+ now = Time.now
267
+ @hash.delete_if { |_address, expiration| expiration <= now }
143
268
  end
144
269
  def length
145
- @mutex.synchronize {
146
- return @hash.length
147
- }
270
+ values.length
148
271
  end
149
272
  def each()
150
273
  values.each {|v|
@@ -159,9 +282,15 @@ module Dnsruby
159
282
 
160
283
  # For guarding access to shared caches.
161
284
  @@mutex = Mutex.new # :nodoc: all
285
+ @@hints_mutex = Mutex.new
286
+ @@hints_generation = 0
287
+ @@stale_hints = nil
162
288
  @@hints = nil
163
289
  @@authority_cache = Hash.new
164
290
  @@zones_cache = nil
291
+ # Keep delegation lifetimes independent of the A/AAAA glue lifetimes.
292
+ # The stored Hash identity prevents stale metadata from expiring a replacement.
293
+ @@zone_expirations = {}
165
294
  @@nameservers = nil
166
295
 
167
296
  def dnssec=(dnssec_on)
@@ -173,14 +302,22 @@ module Dnsruby
173
302
  if (res)
174
303
  @resolver = res
175
304
  else
176
- if (defined?@@nameservers && @@nameservers.length > 0)
177
- @resolver = Resolver.new({:nameserver => @@nameservers})
305
+ nameservers = @@mutex.synchronize do
306
+ # Hints already contain addresses. Re-resolving their owner names
307
+ # would start separate bootstrap queries outside this lookup.
308
+ if @@hints && !@@hints.equal?(@@stale_hints)
309
+ @@hints.values.flat_map(&:values).uniq
310
+ end
311
+ end
312
+ if nameservers && !nameservers.empty?
313
+ @resolver = Resolver.new({:nameserver => nameservers})
178
314
  else
179
315
  @resolver = Resolver.new
180
316
  end
181
317
  end
182
318
  @resolver.dnssec = @dnssec
183
319
  @ipv6_ok = false
320
+ @response_cache = Cache::ResolutionCache.new
184
321
  end
185
322
  # Initialize the hint servers. Recursive queries need a starting name
186
323
  # server to work off of. This method takes a list of IP addresses to use
@@ -195,41 +332,95 @@ module Dnsruby
195
332
  # ftp://ftp.internic.net/domain/named.root
196
333
  #
197
334
  def hints=(hints)
198
- Recursor.set_hints(hints, @resolver)
335
+ Recursor.send(:with_limits, @resolver, recursion_limits) { Recursor.set_hints(hints, @resolver) }
199
336
  end
200
- def Recursor.set_hints(hints, resolver)
201
- TheLog.debug(";; hints(#{hints.inspect})\n")
202
- @resolver = resolver
203
- if (resolver.single_resolvers.length == 0)
204
- resolver = Resolver.new()
205
- resolver.dnssec = @dnssec
206
- end
207
- if (hints && hints.length > 0)
208
- resolver.nameservers=hints
209
- if (String === hints)
210
- hints = [hints]
337
+ def self.with_hints_configuration
338
+ # Bootstrap can wait on DNS. Keep it outside the cache mutex, and allow
339
+ # a query waiting for another bootstrap to honour its own deadline.
340
+ until @@hints_mutex.try_lock
341
+ QueryControl.current&.check!
342
+ sleep 0.01
343
+ end
344
+ begin
345
+ QueryControl.current&.check!
346
+ yield
347
+ ensure
348
+ @@hints_mutex.unlock
349
+ end
350
+ end
351
+ private_class_method :with_hints_configuration
352
+
353
+ def self.reset_hints
354
+ with_hints_configuration do
355
+ @@mutex.synchronize do
356
+ # Existing queries may still hold the previous root bootstrap.
357
+ # Keep it readable until a replacement is ready, but require the
358
+ # next query to refresh it and prevent old work from seeding caches.
359
+ @@stale_hints = @@hints
360
+ @@zones_cache = @@hints && {'.' => @@hints}
361
+ @@nameservers = nil
362
+ @@zone_expirations = {}
363
+ @@authority_cache = {}
364
+ @@hints_generation += 1
365
+ Cache.invalidate_resolutions
211
366
  end
212
- hints.each {|hint|
213
- @@hints = Hash.new
214
- @@hints[hint]=hint
215
- }
216
367
  end
217
- if (!hints && @@nameservers)
218
- @@hints=(@@nameservers)
219
- else
220
- @@nameservers=(hints)
221
- @@hints = hints
368
+ end
369
+
370
+ def self.ensure_hints(resolver)
371
+ generation = @@mutex.synchronize do
372
+ if hints_ready?
373
+ current_budget&.retain_hints(@@hints_generation, @@hints)
374
+ @@hints_generation
375
+ elsif current_budget&.hints_for(@@hints_generation)
376
+ @@hints_generation
377
+ end
378
+ end
379
+ return generation if generation
380
+ with_hints_configuration do
381
+ configured = @@mutex.synchronize { hints_ready? || current_budget&.hints_for(@@hints_generation) }
382
+ configure_hints({}, resolver) unless configured
383
+ @@mutex.synchronize { @@hints_generation }
384
+ end
385
+ end
386
+ private_class_method :ensure_hints
387
+
388
+ # Called with the cache mutex held. A retained bootstrap is usable only
389
+ # while at least one root address is live and no reset has superseded it.
390
+ def self.hints_ready?
391
+ lifetime = @@zone_expirations['.']
392
+ return false if lifetime && lifetime.first.equal?(@@hints) && lifetime[1] <= Time.now
393
+ @@hints && @@zones_cache && !@@hints.equal?(@@stale_hints) &&
394
+ @@hints.any? { |_name, addresses| !addresses.values.empty? }
395
+ end
396
+ private_class_method :hints_ready?
397
+
398
+ # Caller holds the cache mutex. Fresh hints belong to this query's budget.
399
+ def self.query_hints
400
+ current_budget&.hints_for(@@hints_generation) || @@hints
401
+ end
402
+ private_class_method :query_hints
403
+
404
+ def self.set_hints(hints, resolver)
405
+ with_limits(resolver) do
406
+ with_hints_configuration { configure_hints(hints, resolver) }
222
407
  end
223
- TheLog.debug(";; verifying (root) zone...\n")
224
- # bind always asks one of the hint servers
225
- # for who it thinks is authoritative for
226
- # the (root) zone as a sanity check.
227
- # Nice idea.
408
+ end
228
409
 
229
- # if (!@@hints || @@hints.length == 0)
410
+ def self.configure_hints(hints, resolver)
411
+ TheLog.debug(";; hints(#{hints.inspect})\n")
412
+ temporary_resolver = nil
413
+ if resolver.single_resolvers.empty?
414
+ resolver = temporary_resolver = Resolver.new
415
+ end
416
+ resolver.nameservers = hints if hints && !hints.empty?
230
417
  resolver.recurse = true
418
+ budget = current_budget
419
+ budget&.step!
231
420
  packet=resolver.query_no_validation_or_recursion(".", "NS", "IN")
421
+ received_at = Time.now
232
422
  hints = Hash.new
423
+ root_expiration = nil
233
424
  if (packet)
234
425
  if (ans = packet.answer)
235
426
  ans.each do |rr|
@@ -240,98 +431,77 @@ module Dnsruby
240
431
  server.sub!(/\.$/,"")
241
432
  TheLog.debug(";; FOUND HINT: #{server}\n")
242
433
  hints[server] = AddressCache.new
434
+ receipt = rr.instance_variable_get(:@dnsruby_received_at)
435
+ expiration = (receipt ? Time.at(receipt) : received_at) + rr.ttl
436
+ root_expiration = [root_expiration, expiration].compact.min
243
437
  end
244
438
  end
245
- if ((packet.additional.length == 0) ||
246
- ((packet.additional.length == 1) && (packet.additional()[0].type == Types.OPT)))
247
- # Some resolvers (e.g. 8.8.8.8) do not send an additional section -
248
- # need to make explicit queries for these :(
249
- # Probably best to limit the number of outstanding queries - extremely bursty behaviour otherwise
250
- # What happens if we select only name
251
- q = Queue.new
252
- hints.keys.each {|server|
253
- # Query for the server address and add it to hints.
254
- ['A', 'AAAA'].each {|type|
255
- msg = Message.new
256
- msg.do_caching = @do_caching
257
- msg.header.rd = false
258
- msg.do_validation = false
259
- msg.add_question(server, type, 'IN')
260
- if (@dnssec)
261
- msg.header.cd = true # We do our own validation by default
439
+ packet.additional.each { |rr| add_to_hints(hints, rr) }
440
+ missing = hints.keys.select { |server| hints[server].current_values.empty? }
441
+ unless missing.empty?
442
+ # Resolve missing root addresses in bounded batches. All of the
443
+ # queries keep the bootstrap's deadline and transmission budget.
444
+ control = QueryControl.current
445
+ q = control ? QueryControl::Queue.new : Queue.new
446
+ batch_size = budget ? budget.limits[:max_workers] : DEFAULT_RECURSION_LIMITS[:max_workers]
447
+ missing.product(['A', 'AAAA']).each_slice(batch_size) do |batch|
448
+ query_ids = []
449
+ begin
450
+ batch.each do |server, type|
451
+ budget&.step!
452
+ msg = Message.new
453
+ msg.do_caching = @do_caching
454
+ msg.header.rd = false
455
+ msg.do_validation = false
456
+ msg.add_question(server, type, 'IN')
457
+ msg.header.cd = true if @dnssec
458
+ control&.check!
459
+ query_ids << resolver.send_async(msg, q)
460
+ end
461
+ query_ids.length.times do
462
+ _id, result, error = control ? control.pop(q) : q.pop
463
+ raise error if error.is_a?(RecursionLimitError)
464
+ result&.answer&.each { |rr| add_to_hints(hints, rr) }
465
+ end
466
+ ensure
467
+ if control && (engine = resolver.instance_variable_get(:@resolver_ruby))
468
+ query_ids.each { |id| engine.cancel_query(id, q) }
262
469
  end
263
- resolver.send_async(msg, q)
264
- }
265
- }
266
- (hints.length * 2).times {
267
- _id, result, _error = q.pop
268
- if (result)
269
- result.answer.each {|rr|
270
- TheLog.debug(";; NS address: " + rr.inspect+"\n")
271
- add_to_hints(hints, rr)
272
- }
273
470
  end
274
- }
275
- else
276
- packet.additional.each do |rr|
277
- TheLog.debug(";; ADDITIONAL: "+rr.inspect+"\n")
278
- add_to_hints(hints, rr)
279
-
280
471
  end
281
472
  end
282
473
  end
283
474
  # foreach my $server (keys %hints) {
284
475
  hints.keys.each do |server|
285
- if (!hints[server] || hints[server].length == 0)
476
+ if (!hints[server] || hints[server].current_values.empty?)
286
477
  # Wipe the servers without lookups
287
478
  hints.delete(server)
288
479
  end
289
480
  end
290
- @@hints = hints
291
- else
292
- @@hints = {}
293
- end
294
- if (@@hints.size > 0)
295
- TheLog.info(";; USING THE FOLLOWING HINT IPS:\n")
296
- @@hints.values.each do |ips|
297
- ips.each do |server|
298
- TheLog.info(";; #{server}\n")
299
- end
300
- end
301
- else
302
- raise ResolvError.new( "Server ["+(@@nameservers)[0].to_s+".] did not give answers")
303
481
  end
482
+ raise ResolvError, 'Nameserver did not supply usable root hints' if hints.empty?
304
483
 
305
- # Disable recursion flag.
306
- resolver.recurse = false
307
- # end
308
-
309
- # return $self->nameservers( map { @{ $_ } } values %{ $self->{'hints'} } );
310
- if (Array === @@hints)
311
- temp = []
312
- @@hints.each {|hint|
313
- temp.push(hint)
314
- }
315
- @@hints = Hash.new
316
- count = 0
317
- temp.each {|hint|
318
- print "Adding hint : #{temp[count]}\n"
319
- @@hints[count] = temp[count]
320
- count += 1
321
- }
484
+ # Publish a complete bootstrap, replacing delegations learned through the
485
+ # old hints. An unsuccessful bootstrap leaves the previous state intact.
486
+ @@mutex.synchronize do
487
+ fresh_hints = hints
488
+ @@hints = hints.transform_values(&:snapshot)
489
+ @@stale_hints = nil
490
+ @@hints_generation += 1
491
+ @@nameservers = hints.keys
492
+ @@zones_cache = {'.' => @@hints}
493
+ @@zone_expirations = {'.' => [@@hints, root_expiration]}
494
+ budget&.retain_hints(@@hints_generation, fresh_hints)
495
+ @@authority_cache = {}
496
+ Cache.invalidate_resolutions
322
497
  end
323
- if (String === @@hints)
324
- temp = @@hints
325
- @@hints = Hash.new
326
- @@hints[0] = temp
327
- end
328
- # @@nameservers = @@hints.values
329
- @@nameservers=[]
330
- @@hints.each {|key, value|
331
- @@nameservers.push(key)
332
- }
333
- return @@nameservers
498
+ @@nameservers
499
+ ensure
500
+ resolver.recurse = false if resolver
501
+ temporary_resolver&.close
334
502
  end
503
+ private_class_method :configure_hints
504
+
335
505
 
336
506
  def Recursor.add_to_hints(hints, rr)
337
507
  server = rr.name.to_s.downcase
@@ -378,13 +548,64 @@ module Dnsruby
378
548
  return @callback
379
549
  end
380
550
 
381
- def Recursor.clear_caches(resolver = Resolver.new)
382
- resolver.dnssec = @dnssec
383
- Recursor.set_hints(Hash.new, resolver)
384
- @@zones_cache = Hash.new # key zone_name, values Hash of servers and AddressCaches
385
- @@zones_cache["."] = @@hints
551
+ def Recursor.clear_caches(resolver = nil)
552
+ temporary_resolver = resolver.nil?
553
+ resolver ||= Resolver.new
554
+ Recursor.set_hints({}, resolver)
555
+ ensure
556
+ resolver&.close if temporary_resolver
557
+ end
386
558
 
387
- @@authority_cache = Hash.new
559
+ # These helpers are called with the shared cache mutex held. Root hints
560
+ # are retained as the recursion bootstrap; learned delegations are bounded.
561
+ def self.cache_authority(name, addresses) # :nodoc:
562
+ unless @@mutex.owned?
563
+ return @@mutex.synchronize { cache_authority(name, addresses) }
564
+ end
565
+ generation = Thread.current[:dnsruby_recursor_generation]
566
+ return if generation && generation != @@hints_generation
567
+ @@authority_cache.delete(name)
568
+ @@authority_cache[name] = addresses.is_a?(AddressCache) ? addresses.snapshot : addresses.dup
569
+ @@authority_cache.shift while @@authority_cache.length > CACHE_LIMIT
570
+ end
571
+
572
+ def self.cache_zone(name, authorities, ttl, parent: nil) # :nodoc:
573
+ unless @@mutex.owned?
574
+ return @@mutex.synchronize { cache_zone(name, authorities, ttl, parent: parent) }
575
+ end
576
+ generation = Thread.current[:dnsruby_recursor_generation]
577
+ return if generation && generation != @@hints_generation
578
+ name = Name.create(name)
579
+ name.absolute = true
580
+ name = name.to_s(true).downcase
581
+ if parent
582
+ parent = Name.create(parent)
583
+ parent.absolute = true
584
+ parent = parent.to_s(true).downcase
585
+ end
586
+ return if name == "."
587
+ @@zones_cache.delete(name)
588
+ @@zone_expirations.delete(name)
589
+ return if ttl.nil? || ttl <= 0
590
+ expiration = Time.now + ttl
591
+ if parent
592
+ lifetime = @@zone_expirations[parent]
593
+ # Retain the parent's deadline, not a reference to its cache entry.
594
+ # LRU eviction must not invalidate descendants already learned, while
595
+ # a descendant cannot extend its route beyond the parent admission.
596
+ return unless lifetime && lifetime.first.equal?(@@zones_cache[parent])
597
+ expiration = [expiration, lifetime[1]].min
598
+ return if expiration <= Time.now
599
+ end
600
+ snapshot = authorities.transform_values { |addresses| addresses.is_a?(AddressCache) ? addresses.snapshot : addresses.dup }
601
+ @@zones_cache[name] = snapshot
602
+ @@zone_expirations[name] = [snapshot, expiration]
603
+ while @@zones_cache.length > CACHE_LIMIT
604
+ oldest = @@zones_cache.each_key.find { |zone| zone != "." }
605
+ break unless oldest
606
+ @@zones_cache.delete(oldest)
607
+ @@zone_expirations.delete(oldest)
608
+ end
388
609
  end
389
610
 
390
611
  def query_no_validation_or_recursion(name, type=Types.A, klass=Classes.IN) # :nodoc: all
@@ -400,71 +621,231 @@ module Dnsruby
400
621
  # The Recursor maintains a cache of known nameservers.
401
622
  # DNSSEC validation is performed unless true is passed as the fourth parameter.
402
623
  def query(name, type=Types.A, klass=Classes.IN, no_validation = false)
624
+ Recursor.send(:with_limits, @resolver, recursion_limits) do
625
+ if no_validation
626
+ query_internal(name, type, klass, no_validation)
627
+ else
628
+ transmissions = SelectThread::TransmissionBudget.current
629
+ budget = transmissions.context(:validation) { ValidationBudget.current || ValidationBudget.new }
630
+ ValidationBudget.with(budget) { query_internal(name, type, klass, no_validation) }
631
+ end
632
+ end
633
+ end
634
+
635
+ def query_internal(name, type, klass, no_validation)
636
+ previous_generation = Thread.current[:dnsruby_recursor_generation]
637
+ QueryControl.current&.check!
638
+ unless SelectThread::TransmissionBudget.current
639
+ return SelectThread::TransmissionBudget.with(SelectThread::TransmissionBudget.new) do
640
+ query_internal(name, type, klass, no_validation)
641
+ end
642
+ end
403
643
  # @TODO@ PROVIDE AN ASYNCHRONOUS SEND WHICH RETURNS MESSAGE WITH ERROR!!!
404
644
 
405
- # Make sure the hint servers are initialized.
406
- @@mutex.synchronize {
407
- self.hints=(Hash.new) unless @@hints
408
- }
645
+ Thread.current[:dnsruby_recursor_generation] = Recursor.send(:ensure_hints, @resolver)
409
646
  @resolver.recurse = false
410
- # Make sure the authority cache is clean.
411
- # It is only used to store A and AAAA records of
412
- # the suposedly authoritative name servers.
413
- # TTLs are respected
414
- @@mutex.synchronize {
415
- if (!@@zones_cache)
416
- Recursor.clear_caches(@resolver)
417
- end
418
- }
647
+
648
+ query_message = Message.new(name, type, klass)
649
+ query_message.do_validation = !no_validation
650
+ scope = recursion_cache_scope(no_validation)
651
+ if scope && (cached = @response_cache.find(query_message, scope))
652
+ response, error = cached
653
+ error.response = response if error.is_a?(ResolvError)
654
+ raise error if error
655
+ return response
656
+ end
419
657
 
420
658
  # So we have normal hashes, but the array of addresses at the end is now an AddressCache
421
659
  # which respects the ttls of the A/AAAA records
422
660
 
423
661
  # Now see if we already know the zone in question
424
662
  # Otherwise, see if we know any of its parents (will know at least ".")
425
- known_zone, known_authorities = get_closest_known_zone_authorities_for(name) # ".", @hints if nothing else
663
+ authority_name = name
664
+ if Types.new(type) == Types::DS
665
+ # DS records belong to the parent side of a delegation (RFC 4035
666
+ # section 3.1.4). A cached child authority cannot answer for them.
667
+ authority_name = Name.create(name)
668
+ authority_name = authority_name.strip_label unless authority_name.labels.empty?
669
+ end
670
+ known_zone, known_authorities = get_closest_known_zone_authorities_for(authority_name) # ".", @hints if nothing else
426
671
 
427
672
  # Seed name servers with the closest known authority
428
673
  # ret = _dorecursion( name, type, klass, ".", @hints, 0)
429
- ret = _dorecursion( name, type, klass, known_zone, known_authorities, 0, no_validation)
430
- Dnssec.validate(ret) if !no_validation
431
- # print "\n\nRESPONSE:\n#{ret}\n"
432
- return ret
674
+ begin
675
+ ret = follow_aliases(name, type, klass, known_zone, known_authorities, 0, no_validation)
676
+ Dnssec.validate(ret) if ret && !no_validation
677
+ if scope && scope == recursion_cache_scope(no_validation)
678
+ @response_cache.add(query_message, scope, ret, nil,
679
+ control: QueryControl.current, no_response: ret.nil?)
680
+ end
681
+ ret
682
+ rescue StandardError => error
683
+ if scope && scope == recursion_cache_scope(no_validation)
684
+ failed_response = ret || (error.response if error.is_a?(ResolvError))
685
+ @response_cache.add(query_message, scope, failed_response, error, control: QueryControl.current)
686
+ end
687
+ raise
688
+ end
689
+ ensure
690
+ Thread.current[:dnsruby_recursor_generation] = previous_generation
433
691
  end
434
692
 
435
- def get_closest_known_zone_for(n) # :nodoc:
436
- # Find the closest parent of name that we know
437
- # e.g. for nominet.org.uk, try nominet.org.uk., org.uk., uk., .
438
- # does @zones_cache contain the name we're after
439
- if (Name === n)
440
- n = n.to_s # @TODO@ This is a bit crap!
693
+ private :query_internal
694
+
695
+ # An authoritative server may finish its answer at an alias whose target
696
+ # belongs to another zone. Continue within this lookup's budgets, retaining
697
+ # the caller's question and every answer in the alias chain.
698
+ def follow_aliases(name, type, klass, known_zone, authorities, depth, no_validation)
699
+ original_name = Name.create(name)
700
+ original_name.absolute = true
701
+ qtype, qclass = Types.new(type), Classes.new(klass)
702
+ current_name = original_name
703
+ responses = []
704
+ seen = {}
705
+ loop do
706
+ QueryControl.current&.check!
707
+ begin
708
+ packet = _dorecursion(current_name.to_s(true), type, klass, known_zone, authorities, depth, no_validation)
709
+ rescue ResolvError => error
710
+ if error.response && !responses.empty?
711
+ error.response = combine_alias_responses(responses + [error.response], original_name, qtype, qclass)
712
+ end
713
+ raise
714
+ end
715
+ return nil unless packet
716
+ responses << packet
717
+ # Explicit alias queries and ANY request the records themselves.
718
+ if [Types::CNAME, Types::DNAME, Types::ANY].include?(qtype) ||
719
+ packet.rcode != RCode::NOERROR || Cache.negative_soa(packet)
720
+ return combine_alias_responses(responses, original_name, qtype, qclass)
721
+ end
722
+ target, complete = alias_target(packet, current_name, qtype, qclass, seen)
723
+ if complete || target == current_name
724
+ return combine_alias_responses(responses, original_name, qtype, qclass)
725
+ end
726
+ current_name = target
727
+ depth += 1
728
+ authority_name = qtype == Types::DS && !current_name.labels.empty? ? current_name.strip_label : current_name
729
+ known_zone, authorities = get_closest_known_zone_authorities_for(authority_name)
441
730
  end
731
+ end
732
+ private :follow_aliases
733
+
734
+ def alias_target(packet, name, type, klass, seen)
735
+ loop do
736
+ QueryControl.current&.check!
737
+ records = packet.answer.select { |rr| rr.klass == klass && rr.name == name }
738
+ return [name, true] if records.any? { |rr| rr.type == type }
739
+ aliases = records.select { |rr| rr.type == Types::CNAME }
740
+ if aliases.empty?
741
+ dnames = packet.answer.select do |rr|
742
+ rr.type == Types::DNAME && rr.klass == klass && name.subdomain_of?(rr.name)
743
+ end
744
+ dname = dnames.max_by { |rr| rr.name.labels.length }
745
+ return [name, false] unless dname
746
+ if dnames.any? { |rr| rr.name == dname.name && rr.domainname != dname.domainname }
747
+ raise ResolvError, 'Conflicting DNAME targets in recursive answer'
748
+ end
749
+ begin
750
+ target = Name.new(name.labels.first(name.labels.length - dname.name.labels.length) +
751
+ dname.domainname.labels, true)
752
+ rescue ResolvError => error
753
+ raise YXDomain, "DNAME target is too long: #{error.message}"
754
+ end
755
+ cname = RR.create(name: name, type: Types::CNAME, klass: klass, ttl: dname.ttl, domainname: target)
756
+ packet.add_answer(cname)
757
+ else
758
+ targets = aliases.map(&:domainname).uniq
759
+ raise ResolvError, 'Conflicting CNAME targets in recursive answer' unless targets.length == 1
760
+ target = targets.first
761
+ end
762
+ Recursor.send(:current_budget)&.step!
763
+ raise RecursionLimitError, 'Recursive alias chain is too long' if seen.length >= 255
764
+ raise RecursionLimitError, 'Recursive alias loop' if seen[name.canonical]
765
+ seen[name.canonical] = true
766
+ name = target
767
+ end
768
+ end
769
+ private :alias_target
770
+
771
+ def combine_alias_responses(responses, name, type, klass)
772
+ return responses.first if responses.length == 1
773
+ result = responses.last.clone
774
+ result.question.replace([Question.new(name, type, klass)])
775
+ result.answer.replace(responses.flat_map(&:answer))
776
+ # Earlier wildcard answers can require their own denial proofs when the
777
+ # completed chain is verified. Keep those proofs with their signatures.
778
+ proofs = responses[0...-1].flat_map(&:authority).select do |rr|
779
+ [Types::NSEC, Types::NSEC3].include?(rr.type) ||
780
+ (rr.type == Types::RRSIG && [Types::NSEC, Types::NSEC3].include?(rr.type_covered))
781
+ end
782
+ result.authority.concat(proofs)
783
+ result.header.id = responses.first.header.id
784
+ # The assembled response is no longer one authority's signed message.
785
+ # DNSSEC is checked over the complete answer before it is returned.
786
+ result.header.aa = result.header.ad = false
787
+ expirations = responses.filter_map { |packet| packet.instance_variable_get(:@dnsruby_authenticated_expiration) }
788
+ result.instance_variable_set(:@dnsruby_authenticated_expiration, expirations.min) unless expirations.empty?
789
+ result.answerfrom = responses.last.answerfrom
790
+ result.answerip = responses.last.answerip
791
+ result.update_counts
792
+ result
793
+ end
794
+ private :combine_alias_responses
795
+
796
+ HOP_SETTINGS = %i[port use_tcp no_tcp tcp_pipelining tcp_pipelining_max_queries
797
+ tcp_idle_timeout packet_timeout query_timeout retry_times retry_delay
798
+ ignore_truncation udp_size src_address src_address6 src_port].freeze
799
+ private_constant :HOP_SETTINGS
800
+
801
+ def hop_settings
802
+ HOP_SETTINGS.each_with_object({}) do |field, settings|
803
+ next unless @resolver && @resolver.respond_to?(field)
804
+ value = @resolver.public_send(field)
805
+ settings[field] = value.is_a?(Array) || value.is_a?(String) ? value.dup : value
806
+ end
807
+ end
808
+ private :hop_settings
809
+
810
+ def recursion_cache_scope(no_validation)
811
+ return unless @resolver.respond_to?(:do_caching) && @resolver.do_caching
812
+ return if @resolver.tsig
813
+ hints = @@mutex.synchronize do
814
+ @@hints.map { |name, addresses| [name, addresses.values.map(&:to_s).sort] }.sort_by(&:first)
815
+ end
816
+ [no_validation, @dnssec, @ipv6_ok, hints, @resolver.object_id, hop_settings, recursion_limits,
817
+ @resolver.single_resolvers.map { |sender| [sender.server.to_s.dup, sender.port] }.sort,
818
+ Cache.validation_scope]
819
+ end
820
+ private :recursion_cache_scope
821
+
822
+ def get_closest_known_zone_for(n) # :nodoc:
442
823
  if (n == nil)
443
824
  TheLog.error("Name is nil")
444
825
  raise ResolvError.new("Nameserver invalid!")
445
826
  end
446
- name = n.tr("","")
447
- if (name[name.length-1] != ".")
448
- name = name + "."
449
- end
450
-
451
- while (true)
452
- # print "Checking for known zone : #{name}\n"
453
- zone = nil
827
+ candidate = Name.create(n)
828
+ candidate.absolute = true
829
+ loop do
830
+ QueryControl.current&.check!
831
+ name = candidate.to_s(true).downcase
454
832
  @@mutex.synchronize{
833
+ if name == '.' && Recursor.send(:current_budget)&.hints_for(@@hints_generation)
834
+ return name
835
+ end
455
836
  zone = @@zones_cache[name]
837
+ lifetime = @@zone_expirations[name]
838
+ if lifetime && lifetime[0].equal?(zone) && lifetime[1] <= Time.now
839
+ @@zones_cache.delete(name)
840
+ @@zone_expirations.delete(name)
841
+ zone = nil
842
+ end
456
843
  if (zone != nil)
457
844
  return name
458
845
  end
459
846
  }
460
- return false if name=="."
461
- # strip the name up to the first dot
462
- first_dot = name.index(".")
463
- if (first_dot == (name.length-1))
464
- name = "."
465
- else
466
- name = name[first_dot+1, name.length]
467
- end
847
+ return false if candidate.labels.empty?
848
+ candidate = candidate.strip_label
468
849
  end
469
850
  end
470
851
 
@@ -472,10 +853,16 @@ module Dnsruby
472
853
  done = false
473
854
  known_authorities, known_zone = nil
474
855
  while (!done)
856
+ QueryControl.current&.check!
475
857
  known_zone = get_closest_known_zone_for(name)
476
858
  # print "GOT KNOWN ZONE : #{known_zone}\n"
859
+ observed_authorities = nil
477
860
  @@mutex.synchronize {
478
- known_authorities = @@zones_cache[known_zone] # ".", @hints if nothing else
861
+ observed_authorities = known_zone == '.' ? Recursor.send(:query_hints) : @@zones_cache[known_zone]
862
+ known_authorities = observed_authorities&.dup # isolate each recursion's working map
863
+ known_authorities&.transform_values! do |addresses|
864
+ addresses.is_a?(AddressCache) ? addresses.snapshot(include_current: known_zone == '.') : addresses.dup
865
+ end
479
866
  }
480
867
  # print "Known authorities : #{known_authorities}\n"
481
868
 
@@ -485,7 +872,10 @@ module Dnsruby
485
872
  done = true
486
873
  else
487
874
  @@mutex.synchronize{
488
- @@zones_cache.delete(known_zone)
875
+ if @@zones_cache[known_zone].equal?(observed_authorities)
876
+ @@zones_cache.delete(known_zone)
877
+ @@zone_expirations.delete(known_zone)
878
+ end
489
879
  }
490
880
  end
491
881
  end
@@ -493,6 +883,8 @@ module Dnsruby
493
883
  end
494
884
 
495
885
  def _dorecursion(name, type, klass, known_zone, known_authorities, depth, no_validation) # :nodoc:
886
+ QueryControl.current&.check!
887
+ Recursor.send(:current_budget)&.step!
496
888
 
497
889
  if ( depth > 255 )
498
890
  TheLog.debug(";; _dorecursion() Recursion too deep, aborting...\n")
@@ -502,110 +894,187 @@ module Dnsruby
502
894
 
503
895
  known_zone = known_zone.sub(/\.*$/, ".")
504
896
 
505
- ns = [] # Array of AddressCaches (was array of array of addresses)
897
+ ns = [] # Snapshot each address list once for this hop.
898
+ missing_authorities = []
506
899
  @@mutex.synchronize{
507
900
  # Get IPs from authorities
508
901
  known_authorities.keys.each do |ns_rec|
509
- if (known_authorities[ns_rec] != nil && known_authorities[ns_rec] != [] )
510
- @@authority_cache[ns_rec] = known_authorities[ns_rec]
511
- ns.push(@@authority_cache[ns_rec])
512
- elsif (@@authority_cache[ns_rec]!=nil && @@authority_cache[ns_rec]!=[])
513
- known_authorities[ns_rec] = @@authority_cache[ns_rec]
514
- ns.push(@@authority_cache[ns_rec])
902
+ addresses = known_authorities[ns_rec]
903
+ cached = @@authority_cache[ns_rec]
904
+ values = addresses.is_a?(AddressCache) ? addresses.current_values : Array(addresses)
905
+ cached_values = if values.empty?
906
+ cached.respond_to?(:values) ? cached.values : Array(cached)
907
+ end
908
+ if !values.empty?
909
+ Recursor.cache_authority(ns_rec, known_authorities[ns_rec])
910
+ ns.push(values)
911
+ elsif !cached_values.empty?
912
+ known_authorities[ns_rec] = cached.is_a?(AddressCache) ? cached.snapshot : cached.dup
913
+ ns.push(cached_values)
914
+ else
915
+ @@authority_cache.delete(ns_rec)
916
+ missing_authorities << ns_rec
515
917
  end
516
918
  end
919
+ }
920
+
921
+ if known_zone == '.'
922
+ if ns.empty?
923
+ # Root addresses can expire after query admission, including while
924
+ # finding an address for a delegated server. Refresh the bootstrap
925
+ # instead of recursively looking up root servers against empty hints.
926
+ Recursor.send(:ensure_hints, @resolver)
927
+ ns = @@mutex.synchronize { Recursor.send(:query_hints).values.map(&:current_values).reject(&:empty?) }
928
+ raise ResolvError, 'Root hints expired before use' if ns.empty?
929
+ # Retain this query's original generation: a refresh or reset must
930
+ # not let earlier traversal work populate the replacement caches.
931
+ end
932
+ # Missing root addresses need bootstrap refresh, not hostname lookup
933
+ # through the same root servers after their other addresses fail.
934
+ missing_authorities.clear
935
+ end
936
+
937
+ if (ns.length == 0)
938
+ found_auth = 0
939
+ TheLog.debug(";; _dorecursion() Failed to extract nameserver IPs:")
940
+ TheLog.debug(known_authorities.inspect + @@authority_cache.inspect)
941
+ missing_authorities.each do |ns_rec|
942
+ TheLog.debug(";; _dorecursion() Manual lookup for authority [#{ns_rec}]")
517
943
 
518
- if (ns.length == 0)
519
- found_auth = 0
520
- TheLog.debug(";; _dorecursion() Failed to extract nameserver IPs:")
521
- TheLog.debug(known_authorities.inspect + @@authority_cache.inspect)
522
- known_authorities.keys.each do |ns_rec|
523
- if (known_authorities[ns_rec]==nil || known_authorities[ns_rec]==[])
524
- TheLog.debug(";; _dorecursion() Manual lookup for authority [#{ns_rec}]")
525
-
526
- auth_packet=nil
527
- ans=[]
528
-
529
- # Don't query for V6 if its not there.
530
- # Do this in parallel
531
- ip_mutex = Mutex.new
532
- ip6_thread = Thread.start {
533
- if ( @ipv6_ok)
534
- auth_packet = _dorecursion(ns_rec,"AAAA", klass, # packet
535
- ".", # known_zone
536
- @@hints, # known_authorities
537
- depth+1); # depth
538
- ip_mutex.synchronize {
539
- ans.push(auth_packet.answer) if auth_packet
540
- }
944
+ # Recursive lookups must run outside the shared cache mutex.
945
+ # Propagate cancellation and validation policy into both workers.
946
+ control = QueryControl.new(parent: QueryControl.current)
947
+ transmission_budget = SelectThread::TransmissionBudget.current
948
+ hints_generation = Thread.current[:dnsruby_recursor_generation]
949
+ hints = @@mutex.synchronize { Recursor.send(:query_hints).dup }
950
+ workers = []
951
+ budget = Recursor.send(:current_budget)
952
+ dependencies = Thread.current[:dnsruby_recursion_dependencies]
953
+ results = QueryControl::Queue.new
954
+ ans = []
955
+ begin
956
+ address_types = @ipv6_ok ? ['AAAA', 'A'] : ['A']
957
+ address_types.each do |address_type|
958
+ lookup = lambda do
959
+ previous_dependencies = Thread.current[:dnsruby_recursion_dependencies]
960
+ Thread.current[:dnsruby_recursion_dependencies] = dependencies
961
+ begin
962
+ answer = SelectThread::TransmissionBudget.with(transmission_budget) do
963
+ QueryControl.with(control) do
964
+ local_hints = hints.transform_values do |addresses|
965
+ addresses.is_a?(AddressCache) ? addresses.snapshot(include_current: true) : addresses.dup
966
+ end
967
+ recurse = -> { follow_aliases(ns_rec, address_type, klass, '.', local_hints,
968
+ depth + 1, no_validation) }
969
+ budget ? budget.with_dependency(ns_rec, address_type, klass, &recurse) : recurse.call
970
+ end
971
+ end
972
+ results << [answer, nil]
973
+ rescue StandardError => error
974
+ results << [nil, error]
975
+ ensure
976
+ Thread.current[:dnsruby_recursion_dependencies] = previous_dependencies
977
+ end
978
+ end
979
+ reserved = !budget || budget.reserve_worker
980
+ if reserved
981
+ begin
982
+ workers << Thread.new(lookup) do |operation|
983
+ Thread.current.report_on_exception = false
984
+ Thread.current[:dnsruby_recursor_generation] = hints_generation
985
+ begin
986
+ operation.call
987
+ ensure
988
+ budget&.release_worker
989
+ end
990
+ end
991
+ rescue Exception
992
+ budget&.release_worker
993
+ raise
994
+ end
995
+ else
996
+ lookup.call
997
+ end
998
+ end
999
+ address_types.length.times do
1000
+ answer, error = control.pop(results)
1001
+ if error
1002
+ # A failed family or nameserver must not hide a usable
1003
+ # alternative. Budget failures and cancellation still stop
1004
+ # the entire operation, as do unexpected programming errors.
1005
+ raise error if error.is_a?(VerifyError) || error.is_a?(RecursionLimitError)
1006
+ case error
1007
+ when ResolvError, ResolvTimeout, IOError, SystemCallError
1008
+ TheLog.debug(";; _dorecursion() Address lookup failed: #{error}")
1009
+ else
1010
+ raise error
541
1011
  end
542
- }
543
-
544
- ip4_thread = Thread.start {
545
- auth_packet = _dorecursion(ns_rec,"A",klass, # packet
546
- ".", # known_zone
547
- @@hints, # known_authorities
548
- depth+1); # depth
549
-
550
- ip_mutex.synchronize {
551
- ans.push(auth_packet.answer ) if auth_packet
552
- }
553
- }
554
- ip6_thread.join
555
- ip4_thread.join
556
-
557
- if ( ans.length > 0 )
558
- TheLog.debug(";; _dorecursion() Answers found for [#{ns_rec}]")
559
- # foreach my $rr (@ans) {
560
- ans.each do |rr_arr|
561
- rr_arr.each do |rr|
562
- TheLog.debug(";; RR:" + rr.inspect + "")
563
- if (rr.type == Types::CNAME)
564
- # Follow CNAME
565
- server = rr.name.to_s.downcase
566
- if (server)
567
- server.sub!(/\.*$/, ".")
568
- if (server == ns_rec)
569
- cname = rr.cname.downcase
570
- cname.sub!(/\.*$/, ".")
571
- TheLog.debug(";; _dorecursion() Following CNAME ns [#{ns_rec}] -> [#{cname}]")
572
- if (!(known_authorities[cname]))
573
- known_authorities[cname] = AddressCache.new
574
- end
575
- known_authorities.delete(ns_rec)
576
- next
1012
+ elsif answer
1013
+ ans << answer.answer
1014
+ end
1015
+ end
1016
+ ensure
1017
+ control.cancel
1018
+ # A failed lookup must not leave its sibling running.
1019
+ workers.each { |worker| worker.join rescue nil }
1020
+ end
1021
+
1022
+ if ( ans.length > 0 )
1023
+ TheLog.debug(";; _dorecursion() Answers found for [#{ns_rec}]")
1024
+ # foreach my $rr (@ans) {
1025
+ @@mutex.synchronize do
1026
+ ans.each do |rr_arr|
1027
+ rr_arr.each do |rr|
1028
+ TheLog.debug(";; RR:" + rr.inspect + "")
1029
+ if (rr.type == Types::CNAME)
1030
+ # Follow CNAME
1031
+ server = rr.name.to_s.downcase
1032
+ if (server)
1033
+ server.sub!(/\.*$/, ".")
1034
+ if (server == ns_rec)
1035
+ cname = rr.cname.to_s.downcase
1036
+ cname.sub!(/\.*$/, ".")
1037
+ TheLog.debug(";; _dorecursion() Following CNAME ns [#{ns_rec}] -> [#{cname}]")
1038
+ if (!(known_authorities[cname]))
1039
+ known_authorities[cname] = AddressCache.new
577
1040
  end
1041
+ known_authorities.delete(ns_rec)
1042
+ next
578
1043
  end
579
- elsif (rr.type == Types::A || rr.type == Types::AAAA )
580
- server = rr.name.to_s.downcase
581
- if (server)
582
- server.sub!(/\.*$/, ".")
583
- if (known_authorities[server]!=nil)
584
- ip = rr.address.to_s
585
- TheLog.debug(";; _dorecursion() Found ns: #{server} IN A #{ip}")
586
- @@authority_cache[server] = known_authorities[server]
587
- @@authority_cache[ns_rec].push([ip, rr.ttl])
588
- found_auth+=1
589
- next
1044
+ end
1045
+ elsif (rr.type == Types::A || rr.type == Types::AAAA )
1046
+ server = rr.name.to_s.downcase
1047
+ if (server)
1048
+ server.sub!(/\.*$/, ".")
1049
+ if known_authorities.key?(server)
1050
+ ip = rr.address.to_s
1051
+ TheLog.debug(";; _dorecursion() Found ns: #{server} IN A #{ip}")
1052
+ addresses = known_authorities[server]
1053
+ unless addresses.is_a?(AddressCache)
1054
+ addresses = known_authorities[server] = AddressCache.new
590
1055
  end
1056
+ addresses.push([ip, rr.ttl])
1057
+ Recursor.cache_authority(server, addresses)
1058
+ found_auth+=1
1059
+ next
591
1060
  end
592
1061
  end
593
- TheLog.debug(";; _dorecursion() Ignoring useless answer: " + rr.inspect + "")
594
1062
  end
1063
+ TheLog.debug(";; _dorecursion() Ignoring useless answer: " + rr.inspect + "")
595
1064
  end
596
- else
597
- TheLog.debug(";; _dorecursion() Could not find A records for [#{ns_rec}]")
598
1065
  end
599
1066
  end
1067
+ else
1068
+ TheLog.debug(";; _dorecursion() Could not find A records for [#{ns_rec}]")
600
1069
  end
601
- if (found_auth > 0)
602
- TheLog.debug(";; _dorecursion() Found #{found_auth} new NS authorities...")
603
- return _dorecursion( name, type, klass, known_zone, known_authorities, depth+1)
604
- end
605
- TheLog.debug(";; _dorecursion() No authority information could be obtained.")
606
- return nil
607
1070
  end
608
- }
1071
+ if (found_auth > 0)
1072
+ TheLog.debug(";; _dorecursion() Found #{found_auth} new NS authorities...")
1073
+ return _dorecursion(name, type, klass, known_zone, known_authorities, depth + 1, no_validation)
1074
+ end
1075
+ TheLog.debug(";; _dorecursion() No authority information could be obtained.")
1076
+ return nil
1077
+ end
609
1078
 
610
1079
  # Cut the deck of IPs in a random place.
611
1080
  TheLog.debug(";; _dorecursion() cutting deck of (" + ns.length.to_s + ") authorities...")
@@ -617,17 +1086,17 @@ module Dnsruby
617
1086
  nameservers = []
618
1087
  ns.each do |nss|
619
1088
  nss.each {|n|
1089
+ Recursor.send(:current_budget)&.step!
620
1090
  nameservers.push(n.to_s)
621
1091
  }
622
1092
  end
623
- resolver = Resolver.new({:nameserver=>nameservers})
624
- resolver.dnssec = @dnssec
1093
+ # TSIG belongs to the configured upstream, not to arbitrary authorities.
1094
+ resolver = Resolver.new(hop_settings.merge(nameserver: nameservers))
1095
+ # Validation needs signatures even when requesting DNSSEC records was not
1096
+ # explicitly enabled. Non-validating lookups retain the caller's setting.
1097
+ resolver.dnssec = @dnssec || !no_validation
625
1098
  resolver.recurse = false
626
- servers = []
627
- resolver.single_resolvers.each {|s|
628
- servers.push(s.server)
629
- }
630
- resolver.retry_delay = nameservers.length
1099
+ authority_error = nil
631
1100
  begin
632
1101
  # Should construct packet ourselves and clear RD bit
633
1102
  query = Message.new(name, type, klass)
@@ -642,7 +1111,29 @@ module Dnsruby
642
1111
  rescue ResolvTimeout, IOError
643
1112
  # TheLog.debug(";; nameserver #{levelns.to_s} didn't respond")
644
1113
  # next
1114
+ QueryControl.current&.check!
645
1115
  TheLog.debug("No response!")
1116
+ packet = nil
1117
+ rescue ServFail, Refused, NotImp, FormErr, SocketEofResolvError => error
1118
+ raise if missing_authorities.empty?
1119
+ QueryControl.current&.check!
1120
+ authority_error = error
1121
+ packet = nil
1122
+ ensure
1123
+ resolver.close
1124
+ end
1125
+ if packet.nil? && !missing_authorities.empty?
1126
+ # A referral may contain both glue and names that need address
1127
+ # discovery. Try the latter if all immediately usable servers fail.
1128
+ unresolved = known_authorities.select { |server, _| missing_authorities.include?(server) }
1129
+ begin
1130
+ alternative = _dorecursion(name, type, klass, known_zone, unresolved, depth + 1, no_validation)
1131
+ return alternative if alternative
1132
+ rescue ResolvTimeout, IOError, ServFail, Refused, NotImp, FormErr, SocketEofResolvError => error
1133
+ QueryControl.current&.check!
1134
+ raise(authority_error || error)
1135
+ end
1136
+ raise authority_error if authority_error
646
1137
  return nil
647
1138
  end
648
1139
  if (packet) # @TODO@ Check that the packet *is* actually authoritative!!
@@ -666,80 +1157,11 @@ module Dnsruby
666
1157
  TheLog.debug(";; _dorecursion() Authoritative answer found")
667
1158
  return packet
668
1159
  elsif (authority.length > 0)
669
- auth = Hash.new
670
- # foreach my $rr (@authority) {
671
- authority.each do |rr|
672
- if (rr.type.to_s =~ /^(NS|SOA)$/)
673
- server = (rr.type == Types::NS ? rr.nsdname : rr.mname).to_s.downcase
674
- server.sub!(/\.*$/, ".")
675
- of = rr.name.to_s.downcase
676
- of.sub!(/\.*$/, ".")
677
- TheLog.debug(";; _dorecursion() Received authority [#{of}] [" + rr.type().to_s + "] [#{server}]")
678
- if (of.length <= known_zone.length)
679
- TheLog.debug(";; _dorecursion() Deadbeat name server did not provide new information.")
680
- next
681
- # elsif (of =~ /#{known_zone}/)
682
- elsif (of == known_zone || known_zone.length == 1 || of.end_with?(".#{known_zone}"))
683
- TheLog.debug(";; _dorecursion() FOUND closer authority for [#{of}] at [#{server}].")
684
- auth[server] ||= AddressCache.new #[] @TODO@ If there is no additional record for this, then we want to use the authority!
685
- if (rr.type == Types.NS)
686
- if ((packet.additional.rrset(rr.nsdname, Types::A).length == 0) &&
687
- (packet.additional.rrset(rr.nsdname, Types::AAAA).length == 0))
688
- auth[server].push([rr.nsdname, rr.ttl])
689
- end
690
- end
691
- else
692
- TheLog.debug(";; _dorecursion() Confused name server [" + @answerfrom.to_s + "] thinks " + of.to_s + " is closer than " + known_zone.to_s + "?")
693
- return nil
694
- end
695
- else
696
- TheLog.debug(";; _dorecursion() Ignoring NON NS entry found in authority section: " + rr.inspect)
697
- end
698
- end
699
- # foreach my $rr ($packet->additional)
700
- packet.additional.each do |rr|
701
- if (rr.type == Types::CNAME)
702
- # Store this CNAME into %auth too
703
- server = rr.name.to_s.downcase
704
- if (server)
705
- server.sub!(/\.*$/, ".")
706
- if (auth[server]!=nil && auth[server].length > 0)
707
- cname = rr.cname.to_s.downcase
708
- cname.sub!(/\.*$/, ".")
709
- TheLog.debug(";; _dorecursion() FOUND CNAME authority: " + rr.string)
710
- auth[cname] ||= AddressCache.new # []
711
- auth[server] = auth[cname]
712
- next
713
- end
714
-
715
- end
716
- elsif (rr.type == Types::A || rr.type == Types::AAAA)
717
- server = rr.name.to_s.downcase
718
- if (server)
719
- server.sub!(/\.*$/, ".")
720
- if (auth[server]!=nil)
721
- if (rr.type == Types::A)
722
- TheLog.debug(";; _dorecursion() STORING: #{server} IN A " + rr.address.to_s)
723
- end
724
- if (rr.type == Types::AAAA)
725
- TheLog.debug(";; _dorecursion() STORING: #{server} IN AAAA " + rr.address.to_s)
726
- end
727
- auth[server].push([rr.address.to_s, rr.ttl])
728
- next
729
- end
730
- end
731
- end
732
- TheLog.debug(";; _dorecursion() Ignoring useless: " + rr.inspect)
733
- end
734
- if (of =~ /#{known_zone}/)
735
- # print "Adding #{of} with :\n#{auth}\nto zones_cache\n"
736
- @@mutex.synchronize{
737
- @@zones_cache[of]=auth
738
- }
739
- return _dorecursion( name, type, klass, of, auth, depth+1, no_validation)
740
- else
741
- return _dorecursion( name, type, klass, known_zone, known_authorities, depth+1, no_validation )
742
- end
1160
+ referral = referral_authorities(packet, known_zone, name, klass)
1161
+ return nil unless referral
1162
+ of, auth, delegation_ttl = referral
1163
+ @@mutex.synchronize { Recursor.cache_zone(of, auth.dup, delegation_ttl, parent: known_zone) }
1164
+ return _dorecursion(name, type, klass, of, auth, depth + 1, no_validation)
743
1165
  end
744
1166
  end
745
1167
  end
@@ -747,6 +1169,40 @@ module Dnsruby
747
1169
  return nil
748
1170
  end
749
1171
 
1172
+ # A referral advances one enclosing delegation. SOA records describe
1173
+ # negative answers; unrelated NS owners must not join this delegation.
1174
+ def referral_authorities(packet, known_zone, question_name, question_class) #:nodoc:
1175
+ parent = Name.create(known_zone)
1176
+ parent.absolute = true
1177
+ question = Name.create(question_name)
1178
+ question.absolute = true
1179
+ klass = Classes.new(question_class)
1180
+ candidates = packet.authority.select do |rr|
1181
+ rr.type == Types::NS && rr.klass == klass && rr.name.subdomain_of?(parent) &&
1182
+ (question == rr.name || question.subdomain_of?(rr.name))
1183
+ end
1184
+ delegation = candidates.max_by { |rr| rr.name.labels.length }&.name
1185
+ return unless delegation
1186
+ records = candidates.select { |rr| rr.name == delegation }
1187
+ authorities = {}
1188
+ records.each do |rr|
1189
+ server = rr.nsdname.to_s(true).downcase
1190
+ addresses = authorities[server] ||= AddressCache.new
1191
+ # RFC 9471 permits sibling glue. Trust addresses within the responding
1192
+ # parent's zone, then look up out-of-bailiwick names independently.
1193
+ glue = packet.additional.select do |address|
1194
+ [Types::A, Types::AAAA].include?(address.type) && address.klass == klass &&
1195
+ address.name == rr.nsdname &&
1196
+ (address.name == parent || address.name.subdomain_of?(parent))
1197
+ end
1198
+ # Leave missing addresses empty so _dorecursion resolves them with the
1199
+ # same transport settings, deadline and transmission budget. Passing a
1200
+ # hostname to Resolver.new would start a separate bootstrap lookup.
1201
+ glue.each { |address| addresses.push([address.address.to_s, address.ttl]) }
1202
+ end
1203
+ [delegation.to_s(true).downcase, authorities, records.map(&:ttl).min]
1204
+ end
1205
+
750
1206
  def prune_rrsets_to_rfc5452(packet, zone)
751
1207
  # Now prune the response of any unrelated rrsets (RFC5452 section6)
752
1208
  # "One very simple way to achieve this is to only accept data if it is