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
@@ -35,13 +35,10 @@ module Dnsruby
35
35
  #
36
36
  # Note also that these timeouts are distinct from the SingleResolver's packet_timeout
37
37
  #
38
- # Timeouts apply to the initial query and response. If DNSSEC validation is to
39
- # be performed, then additional queries may be required (these are performed automatically
40
- # by Dnsruby). Each additional query will be performed with its own timeouts.
41
- # So, even with a query_timeout of 5 seconds, a response which required extensive
42
- # validation may take several times that long.
43
- # (Future versions of Dnsruby may expose finer-grained events for client tracking of
44
- # responses and validation)
38
+ # The total query_timeout includes local DNSSEC validation. Validation may
39
+ # require additional queries, each with its own packet timeout, but the caller
40
+ # still receives a timeout when the original total deadline expires. Late
41
+ # validation results cannot complete the query a second time.
45
42
  #
46
43
  # == Methods
47
44
  #
@@ -68,11 +65,35 @@ module Dnsruby
68
65
  # * Dnsruby::Resolver#send_async(msg, response_queue, query_id)
69
66
  #
70
67
  # == Event Loop
71
- # Dnsruby runs a pure Ruby event loop to handle I/O in a single thread.
68
+ # Dnsruby runs a shared pure Ruby select loop for responses and timeouts.
69
+ # TCP sends initiated by the selector run in a shared worker pool.
72
70
  # Support for EventMachine has been deprecated.
73
71
  class Resolver
72
+ CONFIGURATION_MUTEX_CREATION = Mutex.new
73
+ private_constant :CONFIGURATION_MUTEX_CREATION
74
+
75
+ def ensure_current_process
76
+ return if @process_pid == Process.pid
77
+ CONFIGURATION_MUTEX_CREATION.synchronize do
78
+ if @process_pid != Process.pid
79
+ @single_res_mutex = Mutex.new
80
+ @configuration_mutex = nil
81
+ @resolver_ruby.reset_attributes if @resolver_ruby
82
+ @process_pid = Process.pid
83
+ end
84
+ end
85
+ end
86
+ private :ensure_current_process
87
+
88
+ def configuration_mutex
89
+ ensure_current_process
90
+ CONFIGURATION_MUTEX_CREATION.synchronize { @configuration_mutex ||= Mutex.new }
91
+ end
92
+ private :configuration_mutex
93
+
74
94
  DefaultQueryTimeout = 0
75
95
  DefaultPacketTimeout = 5
96
+ DefaultTcpIdleTimeout = 10
76
97
  DefaultRetryTimes = 1
77
98
  DefaultRetryDelay = 5
78
99
  DefaultPipeLiningMaxQueries = 5
@@ -80,7 +101,7 @@ module Dnsruby
80
101
  DefaultDnssec = false
81
102
  AbsoluteMinDnssecUdpSize = 1220
82
103
  MinDnssecUdpSize = 4096
83
- DefaultUDPSize = MinDnssecUdpSize
104
+ DefaultUDPSize = 1232
84
105
 
85
106
  class EventType
86
107
  RECEIVED = 0
@@ -102,6 +123,9 @@ module Dnsruby
102
123
  # before closing, :infinite for infinite number of requests per connection
103
124
  attr_reader :tcp_pipelining_max_queries
104
125
 
126
+ # Close a reusable TCP connection after this many seconds without queries.
127
+ attr_reader :tcp_idle_timeout
128
+
105
129
  # If no_tcp==true, then ONLY UDP will be used as a transport.
106
130
  # This should not generally be used, but is provided as a debugging aid.
107
131
  attr_reader :no_tcp
@@ -122,7 +146,9 @@ module Dnsruby
122
146
  # Should the Recursion Desired bit be set?
123
147
  attr_reader :recurse
124
148
 
125
- # The maximum UDP size to be used
149
+ # The advertised UDP response size, defaulting to 1232 bytes. Larger sizes
150
+ # may be configured explicitly. Truncated responses are retried over TCP
151
+ # unless no_tcp or ignore_truncation is set.
126
152
  attr_reader :udp_size
127
153
 
128
154
  # The current Config
@@ -136,10 +162,14 @@ module Dnsruby
136
162
  # The array of SingleResolvers used for sending query messages
137
163
  # attr_accessor :single_resolvers # :nodoc:
138
164
  def single_resolvers=(s) # :nodoc:
139
- @configured = true
140
- # @single_res_mutex.synchronize {
141
- @single_resolvers = s
142
- # }
165
+ configuration_mutex.synchronize do
166
+ @single_res_mutex.synchronize do
167
+ (@single_resolvers - s).compact.each(&:close)
168
+ @single_resolvers = s
169
+ @configuration_error = nil
170
+ @configured = true
171
+ end
172
+ end
143
173
  end
144
174
  def single_resolvers # :nodoc:
145
175
  unless @configured
@@ -161,6 +191,7 @@ module Dnsruby
161
191
  # The query will be tried across nameservers retry_times times, with a delay of retry_delay seconds
162
192
  # between each retry. The first time round, retry_delay will be divided by the number of nameservers
163
193
  # being targetted, and a new nameserver will be queried with the resultant delay.
194
+ # RFC 9520 caps attempts at three per server address and transport, including protocol retries.
164
195
  attr_accessor :retry_times, :retry_delay
165
196
 
166
197
  # Use DNSSEC for this Resolver
@@ -244,10 +275,16 @@ module Dnsruby
244
275
  # end
245
276
  def send_message(message)
246
277
  Dnsruby.log.debug{'Resolver : sending message'}
247
- q = Queue.new
248
- send_async(message, q)
278
+ control = QueryControl.current
279
+ q = control ? QueryControl::Queue.new : Queue.new
280
+ control&.check!
281
+ query_id = send_async(message, q)
249
282
 
250
- _id, result, error = q.pop
283
+ begin
284
+ _id, result, error = control ? control.pop(q) : q.pop
285
+ ensure
286
+ @resolver_ruby.cancel_query(query_id, q) if control && @resolver_ruby
287
+ end
251
288
 
252
289
  if error
253
290
  error.response = result if error.is_a?(ResolvError)
@@ -389,27 +426,26 @@ module Dnsruby
389
426
  # end
390
427
  #
391
428
  def send_async(msg, client_queue, client_query_id = nil)
392
- unless @configured
429
+ begin
393
430
  add_config_nameservers
431
+ rescue ArgumentError => error
432
+ # Constructor options historically tolerate an unusable nameserver and
433
+ # queries then report an asynchronous timeout. Keep that completion path
434
+ # for every waiter on the same failed initialization.
435
+ raise unless error.equal?(@configuration_error) && @single_resolvers.empty?
394
436
  end
395
- # @single_res_mutex.synchronize {
396
- unless @resolver_ruby # @TODO@ Synchronize this?
397
- @resolver_ruby = ResolverRuby.new(self)
398
- end
399
- # }
400
- client_query_id = @resolver_ruby.send_async(msg, client_queue, client_query_id)
401
- if @single_resolvers.length == 0
402
- Thread.start {
403
- sleep(@query_timeout == 0 ? 1 : @query_timeout)
404
- client_queue.push([client_query_id, nil, ResolvTimeout.new('Query timed out - no nameservers configured')])
405
- }
406
- end
407
- client_query_id
437
+ engine = @single_res_mutex.synchronize { @resolver_ruby ||= ResolverRuby.new(self) }
438
+ engine.send_async(msg, client_queue, client_query_id)
408
439
  end
409
440
 
410
441
  # Close the Resolver. Unfinished queries are terminated with OtherResolvError.
411
442
  def close
412
- @resolver_ruby.close if @resolver_ruby
443
+ ensure_current_process
444
+ if @resolver_ruby
445
+ @resolver_ruby.close
446
+ else
447
+ @single_res_mutex.synchronize { @single_resolvers.compact.each(&:close) }
448
+ end
413
449
  end
414
450
 
415
451
  # Create a new Resolver object. If no parameters are passed in, then the default
@@ -435,12 +471,14 @@ module Dnsruby
435
471
  # * :do_caching
436
472
  # * :tcp_pipelining
437
473
  # * :tcp_pipelining_max_queries - can be a number or :infinite symbol
474
+ # * :tcp_idle_timeout - positive seconds, defaults to 10
438
475
  def initialize(*args)
439
- # @TODO@ Should we allow :namesver to be an RRSet of NS records? Would then need to randomly order them?
476
+ # @TODO@ Should we allow :nameserver to be an RRSet of NS records? Would then need to randomly order them?
440
477
  @resolver_ruby = nil
441
478
  @src_address = nil
442
479
  @src_address6 = nil
443
480
  @single_res_mutex = Mutex.new
481
+ @process_pid = Process.pid
444
482
  @configured = false
445
483
  @do_caching = true
446
484
  @config = Config.new()
@@ -453,6 +491,7 @@ module Dnsruby
453
491
  begin
454
492
  if key == :config_info
455
493
  @config.set_config_info(args[0][:config_info])
494
+ add_config_nameservers
456
495
  elsif key == :nameserver
457
496
  set_config_nameserver(args[0][:nameserver])
458
497
  elsif key == :nameservers
@@ -461,6 +500,7 @@ module Dnsruby
461
500
  send(key.to_s + '=', args[0][key])
462
501
  end
463
502
  rescue Exception => e
503
+ raise if key.to_s == 'tsig' || ValidationBudget.abort?(e)
464
504
  Dnsruby.log.error{"Argument #{key} not valid : #{e}\n"}
465
505
  end
466
506
  end
@@ -477,41 +517,101 @@ module Dnsruby
477
517
  end
478
518
 
479
519
  def add_config_nameservers # :nodoc: all
480
- unless @configured
520
+ ensure_current_process
521
+ # Queries can keep using the published configuration while a replacement
522
+ # resolves nameserver hostnames. First-use callers still share one setup.
523
+ return if @single_res_mutex.synchronize { @configured && !@configuration_error }
524
+ configuration_mutex.synchronize do
525
+ if @configured
526
+ raise @configuration_error if @configuration_error
527
+ return
528
+ end
481
529
  @config.get_ready
530
+ configure_nameservers
482
531
  end
483
- @configured = true
484
- @single_res_mutex.synchronize {
485
- # Add the Config nameservers
486
- @config.nameserver.each do |ns|
487
- res = PacketSender.new({
488
- server: ns,
489
- port: @port,
490
- dnssec: @dnssec,
491
- use_tcp: @use_tcp,
492
- no_tcp: @no_tcp,
493
- tcp_pipelining: @tcp_pipelining,
494
- tcp_pipelining_max_queries: @tcp_pipelining_max_queries,
495
- packet_timeout: @packet_timeout,
496
- tsig: @tsig,
497
- ignore_truncation: @ignore_truncation,
498
- src_address: @src_address,
499
- src_address6: @src_address6,
500
- src_port: @src_port,
501
- recurse: @recurse,
502
- udp_size: @udp_size})
503
- @single_resolvers.push(res) if res
532
+ end
533
+
534
+ # Serialize configuration separately from query state. Constructing a sender
535
+ # can resolve a hostname through this same selector, whose callbacks need
536
+ # @single_res_mutex. Only publishing the finished list takes that mutex.
537
+ def configure_nameservers(replace = false, nameservers = @config.nameserver)
538
+ new_resolvers = []
539
+ published = false
540
+ begin
541
+ build_sender = lambda do |ns|
542
+ PacketSender.new({
543
+ server: ns,
544
+ port: @port,
545
+ dnssec: @dnssec,
546
+ use_tcp: @use_tcp,
547
+ no_tcp: @no_tcp,
548
+ tcp_pipelining: @tcp_pipelining,
549
+ tcp_pipelining_max_queries: @tcp_pipelining_max_queries,
550
+ tcp_idle_timeout: @tcp_idle_timeout,
551
+ packet_timeout: @packet_timeout,
552
+ tsig: @tsig,
553
+ ignore_truncation: @ignore_truncation,
554
+ src_address: @src_address,
555
+ src_address6: @src_address6,
556
+ src_port: @src_port,
557
+ recurse: @recurse,
558
+ udp_size: @udp_size,
559
+ ignore_config_resolv_errors: true})
504
560
  end
505
- }
561
+ if QueryControl.current || SelectThread::TransmissionBudget.current
562
+ # DNSSEC may discover NS names without glue. Resolve those names in
563
+ # this thread so inherited deadlines, work and transmission limits
564
+ # remain shared, without creating another discovery worker pool.
565
+ nameservers.each do |ns|
566
+ QueryControl.current&.check!
567
+ ValidationBudget.current&.check!(1)
568
+ new_resolvers << build_sender.call(ns)
569
+ end
570
+ else
571
+ threads = nameservers.map { |ns| Thread.new { build_sender.call(ns) } }
572
+ new_resolvers = threads.map(&:value).compact
573
+ end
574
+ new_resolvers = new_resolvers.select do |resolver|
575
+ !resolver.nil? && !resolver.server.nil?
576
+ end
577
+ @single_res_mutex.synchronize do
578
+ # Construction may resolve a hostname and run alongside setters.
579
+ # Publish only after applying the settings current under this lock.
580
+ new_resolvers.each { |resolver| update_internal_res(resolver) }
581
+ complete = replace ? new_resolvers : @single_resolvers + new_resolvers
582
+ raise ArgumentError, "No valid nameservers found" if complete.empty?
583
+ @config.nameserver = nameservers if replace
584
+ (@single_resolvers - complete).compact.each(&:close)
585
+ @single_resolvers = complete
586
+ published = true
587
+ @configuration_error = nil
588
+ @configured = true
589
+ end
590
+ rescue Exception => error
591
+ new_resolvers.compact.each(&:close) unless published
592
+ @single_res_mutex.synchronize do
593
+ if ValidationBudget.abort?(error)
594
+ # Exhaustion belongs to the enclosing lookup, not the server
595
+ # configuration. A later lookup must be able to configure again.
596
+ @configuration_error = nil
597
+ @configured = !@single_resolvers.empty?
598
+ else
599
+ @configuration_error = error if @single_resolvers.empty?
600
+ @configured = true
601
+ end
602
+ end
603
+ raise
604
+ end
506
605
  end
606
+ private :configure_nameservers
507
607
 
508
608
  def set_config_nameserver(n)
509
- # @TODO@ Should we allow NS RRSet here? If so, then .sort_by {rand}
510
- @config.get_ready unless @configured
511
- @configured = true
512
-
513
- @config.nameserver = n.kind_of?(String) ? [n] : n
514
- add_config_nameservers
609
+ configuration_mutex.synchronize do
610
+ @config.get_ready unless @configured
611
+ nameservers = n.kind_of?(String) ? [n] : n
612
+ @config.check_ns(nameservers)
613
+ configure_nameservers(true, nameservers)
614
+ end
515
615
  end
516
616
 
517
617
  def reset_attributes # :nodoc: all
@@ -520,9 +620,8 @@ module Dnsruby
520
620
  # Attributes
521
621
 
522
622
  # do_validation tells the Resolver whether to try to validate the response
523
- # with DNSSEC. This should work for NSEC-signed domains, but NSEC3
524
- # validation is not currently supported. This attribute now defaults to
525
- # false. Please let me know if you require NSEC3 validation.
623
+ # with DNSSEC, including NSEC and NSEC3 proofs. Local validation defaults
624
+ # to false and is separate from requesting DNSSEC records.
526
625
  @do_validation = false
527
626
  @query_timeout = DefaultQueryTimeout
528
627
  @retry_delay = DefaultRetryDelay
@@ -536,6 +635,7 @@ module Dnsruby
536
635
  @no_tcp = false
537
636
  @tcp_pipelining = false
538
637
  @tcp_pipelining_max_queries = DefaultPipeLiningMaxQueries
638
+ @tcp_idle_timeout = DefaultTcpIdleTimeout
539
639
  @tsig = nil
540
640
  @ignore_truncation = false
541
641
  @config = Config.new()
@@ -547,9 +647,11 @@ module Dnsruby
547
647
  @single_resolvers=[]
548
648
  }
549
649
  @configured = false
650
+ @configuration_error = nil
550
651
  end
551
652
 
552
653
  def update # :nodoc: all
654
+ ensure_current_process
553
655
  # Update any resolvers we have with the latest config
554
656
  @single_res_mutex.synchronize do
555
657
  @single_resolvers.delete(nil) # Just in case...
@@ -565,15 +667,20 @@ module Dnsruby
565
667
  # end
566
668
 
567
669
  def add_server(server)# :nodoc:
568
- @configured = true
569
- res = PacketSender.new(server)
570
- Dnsruby.log_and_raise("Can't create server #{server}", ArgumentError) unless res
571
- update_internal_res(res)
572
- @single_res_mutex.synchronize { @single_resolvers.push(res) }
670
+ configuration_mutex.synchronize do
671
+ res = PacketSender.new(server)
672
+ Dnsruby.log_and_raise("Can't create server #{server}", ArgumentError) unless res
673
+ @single_res_mutex.synchronize do
674
+ update_internal_res(res)
675
+ @single_resolvers.push(res)
676
+ @configuration_error = nil
677
+ @configured = true
678
+ end
679
+ end
573
680
  end
574
681
 
575
682
  def update_internal_res(res)
576
- [:port, :use_tcp, :no_tcp, :tcp_pipelining, :tcp_pipelining_max_queries, :tsig, :ignore_truncation, :packet_timeout,
683
+ [:port, :use_tcp, :no_tcp, :tcp_pipelining, :tcp_pipelining_max_queries, :tcp_idle_timeout, :tsig, :ignore_truncation, :packet_timeout,
577
684
  :src_address, :src_address6, :src_port, :recurse,
578
685
  :udp_size, :dnssec].each do |param|
579
686
 
@@ -586,10 +693,7 @@ module Dnsruby
586
693
  end
587
694
 
588
695
  def nameserver=(n)
589
- @configured = true
590
- @single_res_mutex.synchronize { @single_resolvers=[] }
591
696
  set_config_nameserver(n)
592
- add_config_nameservers
593
697
  end
594
698
 
595
699
  # --
@@ -620,8 +724,10 @@ module Dnsruby
620
724
  # res.src_port=60015..60115
621
725
  #
622
726
  def src_port=(p)
623
- if Resolver.check_port(p)
624
- @src_port = Resolver.get_ports_from(p)
727
+ ports = Resolver.get_ports_from(p)
728
+ raise ArgumentError, 'Source port pool must not be empty' if ports.empty?
729
+ if Resolver.check_port(ports)
730
+ @src_port = ports
625
731
  update
626
732
  end
627
733
  end
@@ -639,15 +745,10 @@ module Dnsruby
639
745
  # res.add_src_port(60015..60115)
640
746
  #
641
747
  def add_src_port(p)
642
- if Resolver.check_port(p, @src_port)
643
- a = Resolver.get_ports_from(p)
644
- a.each do |x|
645
- if (@src_port.length > 0) && (x == 0)
646
- Dnsruby.log_and_raise("src_port of 0 only allowed as only src_port value (currently #{@src_port.length} values",
647
- ArgumentError)
648
- end
649
- @src_port.push(x)
650
- end
748
+ additions = Resolver.get_ports_from(p)
749
+ ports = @src_port == [0] && !additions.empty? && !additions.include?(0) ? [] : @src_port
750
+ if Resolver.check_port(additions, ports)
751
+ @src_port = ports + additions
651
752
  end
652
753
  update
653
754
  end
@@ -664,7 +765,10 @@ module Dnsruby
664
765
  return true
665
766
  end
666
767
  if Resolver.port_in_range(p)
667
- return ! ((p == 0) && (src_port.length > 0))
768
+ if (p == 0 && !src_port.empty?) || (p != 0 && src_port.include?(0))
769
+ raise ArgumentError, 'Source port 0 must be the only member of a source port pool'
770
+ end
771
+ return true
668
772
  else
669
773
  Dnsruby.log.error("Illegal port (#{p})")
670
774
  Dnsruby.log_and_raise("Illegal port #{p}", ArgumentError)
@@ -697,6 +801,14 @@ module Dnsruby
697
801
  update
698
802
  end
699
803
 
804
+ def tcp_idle_timeout=(seconds)
805
+ unless seconds.is_a?(Numeric) && seconds.finite? && seconds > 0
806
+ raise ArgumentError, 'TCP idle timeout must be a positive finite number'
807
+ end
808
+ @tcp_idle_timeout = seconds
809
+ update
810
+ end
811
+
700
812
  def use_tcp=(on)
701
813
  @use_tcp = on
702
814
  update
@@ -711,10 +823,13 @@ module Dnsruby
711
823
  # Pass in either a Dnsruby::RR::TSIG, or a key_name and key (or just a key)
712
824
  # Pass in nil to stop tsig signing.
713
825
  # * res.tsig=(tsig_rr)
714
- # * res.tsig=(key_name, key) # defaults to hmac-md5
715
- # * res.tsig=(key_name, key, alg) # e.g. alg = 'hmac-sha1'
826
+ # Update MD5 server keys to SHA-256 when upgrading from 1.74.0, or select
827
+ # hmac-md5 explicitly for a legacy server. MD5 use logs a warning.
828
+ # * res.tsig = key_name, key # defaults to hmac-sha256
829
+ # * res.tsig = key_name, key, algorithm # must match the server's key
716
830
  # * res.tsig=nil # Stop the resolver from signing
717
831
  def tsig=(t)
832
+ Resolver.get_tsig([t]) unless t.nil?
718
833
  @tsig = t
719
834
  update
720
835
  end
@@ -750,9 +865,11 @@ module Dnsruby
750
865
  return nil
751
866
  end
752
867
  else
753
- tsig = RR.new_from_hash(create_tsig_options(args))
868
+ tsig = RR.new_from_hash(create_tsig_options(*args))
754
869
  end
755
- Dnsruby.log.info{"TSIG signing now using #{tsig.name}, key=#{tsig.key}"}
870
+ raise ArgumentError, 'Expected a TSIG record or a key name, secret and optional algorithm' unless tsig
871
+ tsig.warn_if_legacy_algorithm
872
+ Dnsruby.log.info{"TSIG signing now using #{tsig.name} (#{tsig.algorithm})"}
756
873
  tsig
757
874
  end
758
875
 
@@ -801,8 +918,8 @@ module Dnsruby
801
918
  @dnssec = d
802
919
  if d
803
920
  # Set the UDP size (RFC 4035 section 4.1)
804
- if @udp_size < MinDnssecUdpSize
805
- self.udp_size = MinDnssecUdpSize
921
+ if @udp_size < AbsoluteMinDnssecUdpSize
922
+ self.udp_size = AbsoluteMinDnssecUdpSize
806
923
  end
807
924
  end
808
925
  update
@@ -814,37 +931,33 @@ module Dnsruby
814
931
  end
815
932
 
816
933
  def single_res_mutex # :nodoc: all
934
+ ensure_current_process
817
935
  @single_res_mutex
818
936
  end
819
937
 
820
938
  def generate_timeouts(base=0) # :nodoc: all
821
- # These should be be pegged to the single_resolver they are targetting :
822
- # e.g. timeouts[timeout1]=nameserver
823
939
  timeouts = {}
824
940
  retry_delay = @retry_delay
825
- # @single_res_mutex.synchronize {
826
- @retry_times.times do |retry_count|
827
- if retry_count > 0
828
- retry_delay *= 2
829
- end
830
-
831
- @single_resolvers.delete(nil) # Just in case...
832
- @single_resolvers.each_index do |i|
833
- res = @single_resolvers[i]
834
- offset = (i * @retry_delay.to_f / @single_resolvers.length)
835
- if retry_count == 0
836
- timeouts[base + offset]=[res, retry_count]
941
+ @single_resolvers.delete(nil)
942
+ [@retry_times, 3].min.times do |retry_count|
943
+ retry_delay *= 2 if retry_count > 0
944
+ @single_resolvers.each_with_index do |res, i|
945
+ offset = i * @retry_delay.to_f / @single_resolvers.length
946
+ deadline = base + offset + (retry_count == 0 ? 0 : retry_delay)
947
+ attempt = [res, retry_count]
948
+ if (existing = timeouts[deadline])
949
+ # Keep the established single-attempt shape for distinct deadlines.
950
+ # Zero delays (or rounded floating-point deadlines) need a bucket.
951
+ existing = timeouts[deadline] = [existing] unless existing.first.is_a?(Array)
952
+ existing << attempt
837
953
  else
838
- if timeouts.has_key?(base + retry_delay + offset)
839
- Dnsruby.log_and_raise('Duplicate timeout key!')
840
- end
841
- timeouts[base + retry_delay + offset]=[res, retry_count]
954
+ timeouts[deadline] = attempt
842
955
  end
843
956
  end
844
957
  end
845
- # }
846
958
  timeouts
847
959
  end
960
+
848
961
  end
849
962
 
850
963
 
@@ -860,7 +973,49 @@ module Dnsruby
860
973
  # @mutex=Mutex.new
861
974
  @query_list = {}
862
975
  @timeouts = {}
863
- end
976
+ @tick_deadlines = DeadlineQueue.new
977
+ @validation_queries = {}
978
+ @transmission_budgets = {}
979
+ @response_cache = Cache::ResolutionCache.new
980
+ @query_cache_scopes = {}
981
+ end
982
+ # Completed failures belong to one resolver configuration and validation
983
+ # policy. Snapshot them at admission, and decline insertion if it changes.
984
+ def response_cache_scope(query)
985
+ return unless query.do_caching && @parent.do_caching && !query.send_raw &&
986
+ query.header.opcode == OpCode.QUERY && !query.is_a?(Update) &&
987
+ !query.tsig && !query.signed? && !@parent.tsig && query.question.length == 1 &&
988
+ query.answer.empty? && query.authority.empty? &&
989
+ query.additional.all? { |rr| rr.type == Types::OPT }
990
+ return if [Types::AXFR, Types::IXFR].include?(query.question.first.qtype)
991
+ senders = @parent.single_resolvers
992
+ return if senders.empty? || senders.any?(&:tsig)
993
+ settings = senders.map do |sender|
994
+ [sender.server.to_s.dup, sender.port, sender.use_tcp, sender.no_tcp,
995
+ sender.dnssec, sender.packet_timeout, sender.src_address.to_s.dup,
996
+ sender.instance_variable_get(:@src_address6).to_s.dup,
997
+ sender.src_port.dup, sender.ignore_truncation]
998
+ end.sort_by(&:inspect)
999
+ [query.do_validation, query.header.rd, query.header.cd, query.header.ad,
1000
+ query.additional.map { |rr|
1001
+ encoded = MessageEncoder.new { |encoder| rr.encode_rdata(encoder) }.to_s
1002
+ [rr.payloadsize, rr.ttl, encoded]
1003
+ }, settings, @parent.query_timeout, @parent.retry_times, @parent.retry_delay,
1004
+ Cache.validation_scope]
1005
+ end
1006
+
1007
+ def cached_completion(query, scope)
1008
+ @response_cache.find(query, scope)
1009
+ end
1010
+
1011
+ def cache_completion(client_query_id, response, error)
1012
+ current = @query_list[client_query_id]
1013
+ scope = @query_cache_scopes.delete(client_query_id)
1014
+ return unless current && scope && scope == response_cache_scope(current[0])
1015
+ @response_cache.add(current[0], scope, response, error, control: current[4])
1016
+ end
1017
+ private :response_cache_scope, :cached_completion, :cache_completion
1018
+
864
1019
  def send_async(msg, client_queue, client_query_id=nil)
865
1020
  # This is the whole point of the Resolver class.
866
1021
  # We want to use multiple SingleResolvers to run a query.
@@ -879,58 +1034,87 @@ module Dnsruby
879
1034
  unless client_queue.kind_of?(Queue)
880
1035
  Dnsruby.log_and_raise('Wrong type for client_queue in Resolver# send_async')
881
1036
  # @TODO@ Handle different queue tuples - push this to generic send_error method
882
- client_queue.push([client_query_id, ArgumentError.new('Wrong type of client_queue passed to Dnsruby::Resolver# send_async - should have been Queue, was #{client_queue.class}')])
1037
+ client_queue.push([client_query_id, nil, ArgumentError.new('Wrong type of client_queue passed to Dnsruby::Resolver# send_async - should have been Queue, was #{client_queue.class}')])
883
1038
  return
884
1039
  end
885
1040
 
886
1041
  unless msg.kind_of?Message
887
1042
  Dnsruby.log.error{'Wrong type for msg in Resolver# send_async'}
888
1043
  # @TODO@ Handle different queue tuples - push this to generic send_error method
889
- client_queue.push([client_query_id, ArgumentError.new("Wrong type of msg passed to Dnsruby::Resolver# send_async - should have been Message, was #{msg.class}")])
1044
+ client_queue.push([client_query_id, nil, ArgumentError.new("Wrong type of msg passed to Dnsruby::Resolver# send_async - should have been Message, was #{msg.class}")])
890
1045
  return
891
1046
  end
892
1047
 
893
1048
  # According to RFC 6891, a UDP payload size >512 bytes requires an
894
1049
  # EDNS OPT RR in the message.
895
- if @parent.udp_size > 512 && !msg.get_opt
1050
+ if @parent.udp_size > 512 && !msg.get_opt && !msg.send_raw && !msg.signed? && !msg.tsig
896
1051
  TheLog.debug("Automatically adding EDNS OPT RR for udp_size=#{ @parent.udp_size }")
897
1052
  msg.add_additional(Dnsruby::RR::OPT.new(@parent.udp_size))
898
1053
  end
899
1054
 
900
1055
  begin
901
- msg.encode
1056
+ # Encoding may sign the Message, even for this size check. Set the
1057
+ # header and EDNS options first so the signature covers the final query.
1058
+ sender = @parent.single_resolvers.first
1059
+ wire = sender ? sender.make_query_packet(msg) : msg.encode
1060
+ if wire.bytesize > 65_535
1061
+ raise EncodeError, 'DNS message exceeds the 65535-byte wire limit'
1062
+ end
902
1063
  rescue EncodeError => err
903
1064
  Dnsruby.log.error { "Can't encode " + msg.to_s + " : #{err}" }
904
- client_queue.push([client_query_id, err])
1065
+ client_queue.push([client_query_id, nil, err])
905
1066
  return
906
1067
  end
907
1068
 
908
- tick_needed = false
1069
+ st = SelectThread.instance
909
1070
  # add to our data structures
910
1071
  # @mutex.synchronize{
911
1072
  @parent.single_res_mutex.synchronize {
912
- tick_needed = true if @query_list.empty?
913
1073
  if @query_list.has_key?(client_query_id)
914
1074
  Dnsruby.log.error("Duplicate query id requested (#{client_query_id}")
915
1075
  # @TODO@ Handle different queue tuples - push this to generic send_error method
916
- client_queue.push([client_query_id, ArgumentError.new('Client query ID already in use')])
1076
+ client_queue.push([client_query_id, nil, ArgumentError.new('Client query ID already in use')])
917
1077
  return
918
1078
  end
1079
+ scope = response_cache_scope(msg)
1080
+ control = QueryControl.current
1081
+ control&.check!
1082
+ if (cached = cached_completion(msg, scope))
1083
+ client_queue.push([client_query_id, *cached])
1084
+ return client_query_id
1085
+ end
1086
+ @query_cache_scopes[client_query_id] = scope
919
1087
  outstanding = []
920
- @query_list[client_query_id]=[msg, client_queue, q, outstanding]
1088
+ @query_list[client_query_id]=[msg, client_queue, q, outstanding, control]
1089
+ @transmission_budgets[client_query_id] = SelectThread::TransmissionBudget.current || SelectThread::TransmissionBudget.new
921
1090
 
922
- query_timeout = Time.now + @parent.query_timeout
1091
+ query_timeout = TransportClock.now + @parent.query_timeout
923
1092
  if @parent.query_timeout == 0
924
- query_timeout = Time.now + 31536000 # a year from now
1093
+ query_timeout = TransportClock.now + 31536000 # a year from now
925
1094
  end
926
- @timeouts[client_query_id] = [query_timeout, generate_timeouts]
1095
+ retries = generate_timeouts
1096
+ # An explicitly empty nameserver list uses the ordinary query deadline
1097
+ # and completion path. No independent timer can publish a second result.
1098
+ no_nameservers = @parent.single_resolvers.empty?
1099
+ if no_nameservers && @parent.query_timeout == 0
1100
+ query_timeout = TransportClock.now + 1
1101
+ elsif retries.empty? && !no_nameservers
1102
+ # A caller may disable all attempts with retry_times=0. Complete
1103
+ # through the normal timeout path instead of leaving an orphan.
1104
+ query_timeout = TransportClock.now
1105
+ end
1106
+ query_timeout = [query_timeout, control&.deadline].compact.min
1107
+ @timeouts[client_query_id] = [query_timeout, retries, no_nameservers]
1108
+ update_tick_deadline(client_query_id)
1109
+ # Publish the observer together with its logical query. close must never
1110
+ # see a query whose selector ownership has not yet been registered.
1111
+ st.add_observer(q, self)
927
1112
  }
928
1113
 
929
- # Now do querying stuff using SingleResolver
930
- # All this will be handled by the tick method (if we have 0 as the first timeout)
931
- st = SelectThread.instance
932
- st.add_observer(q, self)
933
- tick if tick_needed
1114
+ # Dispatch this query immediately, even while earlier queries are pending.
1115
+ # The same locked retry bookkeeping lets a concurrent selector tick win
1116
+ # safely, while socket I/O still happens after releasing the query lock.
1117
+ tick(client_query_id)
934
1118
  client_query_id
935
1119
  end
936
1120
 
@@ -939,8 +1123,8 @@ module Dnsruby
939
1123
  # These are created at the same time in case the parameters change during the life of the query.
940
1124
  #
941
1125
  # These should be absolute, rather than relative
942
- # The first value should be Time.now[
943
- @parent.generate_timeouts(Time.now)
1126
+ # All retry and query deadlines use the same monotonic clock.
1127
+ @parent.generate_timeouts(TransportClock.now).sort.to_h
944
1128
  end
945
1129
 
946
1130
  # Close the Resolver. Unfinished queries are terminated with OtherResolvError.
@@ -952,15 +1136,30 @@ module Dnsruby
952
1136
  send_result_and_stop_querying(client_queue, client_query_id, q, nil,
953
1137
  OtherResolvError.new('Resolver closing!'))
954
1138
  end
1139
+ @parent.single_resolvers.compact.each(&:close)
955
1140
  }
956
1141
  end
957
1142
 
1143
+ # Cancel only the nested discovery query owned by this synchronous waiter;
1144
+ # never close a shared resolver or disturb unrelated callers.
1145
+ def cancel_query(client_query_id, client_queue)
1146
+ @parent.single_res_mutex.synchronize do
1147
+ current = @query_list[client_query_id]
1148
+ if current && current[1].equal?(client_queue)
1149
+ stop_querying(client_query_id)
1150
+ @validation_queries.delete(client_query_id)
1151
+ @query_cache_scopes.delete(client_query_id)
1152
+ SelectThread.instance.remove_observer(current[2], self)
1153
+ @query_list.delete(client_query_id)
1154
+ end
1155
+ end
1156
+ end
1157
+
958
1158
  # MUST BE CALLED IN A SYNCHRONIZED BLOCK!
959
1159
  #
960
1160
  # Send the result back to the client, and close the socket for that query by removing
961
1161
  # the query from the select thread.
962
1162
  def send_result_and_stop_querying(client_queue, client_query_id, select_queue, msg, error) # :nodoc: all
963
- stop_querying(client_query_id)
964
1163
  send_result(client_queue, client_query_id, select_queue, msg, error)
965
1164
  end
966
1165
 
@@ -969,13 +1168,51 @@ module Dnsruby
969
1168
  # Stops send any more packets for a client-level query
970
1169
  def stop_querying(client_query_id) # :nodoc: all
971
1170
  @timeouts.delete(client_query_id)
1171
+ @tick_deadlines.delete(client_query_id)
1172
+ @transmission_budgets.delete(client_query_id)
1173
+ end
1174
+
1175
+ # Accept one answer for validation, without losing the total query deadline.
1176
+ # The observer remains registered so ticks and this validator can finish the
1177
+ # logical query. Competing attempts, including queued sends, no longer own it.
1178
+ # MUST BE CALLED WITH THE PARENT MUTEX HELD.
1179
+ def begin_validation(client_query_id, query_id, select_queue)
1180
+ @validation_queries[client_query_id] = query_id
1181
+ @timeouts[client_query_id][1].clear
1182
+ update_tick_deadline(client_query_id)
1183
+ SelectThread.instance.cancel_queries_for_queue(select_queue)
1184
+ @query_list[client_query_id][3].clear
1185
+ end
1186
+
1187
+ # Called outside the selector lock. A completed or replaced transaction
1188
+ # cannot start validation, and validation keeps the original query deadline.
1189
+ def validation_deadline(select_queue, query_id)
1190
+ client_query_id = query_id[2]
1191
+ @parent.single_res_mutex.synchronize do
1192
+ current = @query_list[client_query_id]
1193
+ if current && current[2].equal?(select_queue) && @validation_queries[client_query_id] == query_id
1194
+ @timeouts[client_query_id][0]
1195
+ end
1196
+ end
1197
+ end
1198
+
1199
+ def validation_transmission_budget(select_queue, query_id)
1200
+ client_query_id = query_id[2]
1201
+ @parent.single_res_mutex.synchronize do
1202
+ current = @query_list[client_query_id]
1203
+ if current && current[2].equal?(select_queue) && @validation_queries[client_query_id] == query_id
1204
+ @transmission_budgets[client_query_id]
1205
+ end
1206
+ end
972
1207
  end
973
1208
 
974
1209
  # MUST BE CALLED IN A SYNCHRONIZED BLOCK!
975
1210
  #
976
1211
  # Sends the result to the client's queue, and removes the queue observer from the select thread
977
1212
  def send_result(client_queue, client_query_id, select_queue, msg, error) # :nodoc: all
978
- stop_querying(client_query_id) # @TODO@ !
1213
+ cache_completion(client_query_id, msg, error)
1214
+ stop_querying(client_query_id)
1215
+ @validation_queries.delete(client_query_id)
979
1216
  # We might still get some callbacks, which we should ignore
980
1217
  st = SelectThread.instance
981
1218
  st.remove_observer(select_queue, self)
@@ -985,50 +1222,78 @@ module Dnsruby
985
1222
  # }
986
1223
  # Return the response to the client
987
1224
  client_queue.push([client_query_id, msg, error])
1225
+ rescue ClosedQueueError
1226
+ # The query and its observers are already removed. A closed client queue
1227
+ # must not stop the shared selector thread.
1228
+ Dnsruby.log.debug { "Discarding completion for closed client queue" }
1229
+ end
1230
+
1231
+ # Earliest retry or total query deadline, using the same lock as admission.
1232
+ # The selector must call this outside its own lock.
1233
+ def next_tick_at # :nodoc: all
1234
+ @parent.single_res_mutex.synchronize { @tick_deadlines.first_deadline }
1235
+ end
1236
+
1237
+ # Only a changed query's finite retry plan needs inspection. The selector
1238
+ # reads the minimum in constant time, without scanning all pending queries.
1239
+ # MUST BE CALLED WITH THE PARENT MUTEX HELD.
1240
+ def update_tick_deadline(client_query_id)
1241
+ if (entry = @timeouts[client_query_id])
1242
+ @tick_deadlines[client_query_id] = [entry[0], entry[1].each_key.first].compact.min
1243
+ end
988
1244
  end
1245
+ private :update_tick_deadline
989
1246
 
990
- # This method is called twice a second from the select loop, in the select thread.
1247
+ # This method is called when a retry or query deadline is due.
991
1248
  # It should arguably be called from another worker thread... (which also handles the queue)
992
1249
  # Each tick, we check if any timeouts have occurred. If so, we take the appropriate action :
993
1250
  # Return a timeout to the client, or send a new query
994
- def tick # :nodoc: all
1251
+ def tick(client_query_id = nil) # :nodoc: all
995
1252
  # Handle the tick
996
1253
  # Do we have any retries due to be sent yet?
997
1254
  # @mutex.synchronize{
1255
+ sends = []
998
1256
  @parent.single_res_mutex.synchronize {
999
- time_now = Time.now
1000
- @timeouts.keys.each do |client_query_id|
1001
- msg, client_queue, select_queue, outstanding = @query_list[client_query_id]
1002
- query_timeout, timeouts = @timeouts[client_query_id]
1003
- if query_timeout < Time.now
1004
- # Time the query out
1257
+ time_now = TransportClock.now
1258
+ query_ids = client_query_id.nil? ? [] : [client_query_id]
1259
+ if client_query_id.nil?
1260
+ while (first = @tick_deadlines.first) && first[1] <= time_now
1261
+ query_ids << first[0]
1262
+ @tick_deadlines.delete(first[0])
1263
+ end
1264
+ end
1265
+ query_ids.each do |client_query_id|
1266
+ next unless @timeouts.key?(client_query_id)
1267
+ msg, client_queue, select_queue, outstanding, control = @query_list[client_query_id]
1268
+ query_timeout, timeouts, no_nameservers = @timeouts[client_query_id]
1269
+ if query_timeout <= time_now
1270
+ # Time the query out through the same exactly-once completion path.
1271
+ reason = no_nameservers ? 'Query timed out - no valid nameservers configured' : 'Query timed out'
1005
1272
  send_result_and_stop_querying(client_queue, client_query_id, select_queue, nil,
1006
- ResolvTimeout.new('Query timed out'))
1273
+ ResolvTimeout.new(reason))
1007
1274
  next
1008
1275
  end
1009
- timeouts_done = []
1010
- timeouts.keys.sort.each do |timeout|
1011
- if timeout < time_now
1012
- # Send the next query
1013
- res, retry_count = timeouts[timeout]
1276
+ # The finite retry plan is ordered once when admitted. Hash deletion
1277
+ # retains that order, so no unrelated retry plan needs to be sorted.
1278
+ while (timeout = timeouts.each_key.first) && timeout <= time_now
1279
+ attempts = timeouts.delete(timeout)
1280
+ attempts = [attempts] unless attempts.first.is_a?(Array)
1281
+ attempts.each do |res, retry_count|
1014
1282
  id = [res, msg, client_query_id, retry_count]
1015
1283
  Dnsruby.log.debug("Sending msg to #{res.server}")
1016
- # We should keep a list of the queries which are outstanding
1017
1284
  outstanding.push(id)
1018
- timeouts_done.push(timeout)
1019
- timeouts.delete(timeout)
1020
-
1021
- # Pick a new QID here @TODO@ !!!
1022
- # msg.header.id = rand(65535);
1023
- # print "New query : #{new_msg}\n"
1024
- res.send_async(msg, select_queue, id)
1025
- else
1026
- break
1285
+ # A query keeps its original retry plan after reconfiguration.
1286
+ # Retired senders must not create unreachable reusable sockets.
1287
+ retired = !@parent.single_resolvers.include?(res)
1288
+ operation = res.prepare_send_async(msg, select_queue, id, retired, control,
1289
+ (@transmission_budgets[client_query_id] ||= SelectThread::TransmissionBudget.new))
1290
+ sends << operation if operation
1027
1291
  end
1028
1292
  end
1029
- timeouts_done.each { |t| timeouts.delete(t) }
1293
+ update_tick_deadline(client_query_id)
1030
1294
  end
1031
1295
  }
1296
+ sends.each(&:call)
1032
1297
  end
1033
1298
 
1034
1299
  # This method is called by the SelectThread (in the select thread) when the queue has a new item on it.
@@ -1056,9 +1321,6 @@ module Dnsruby
1056
1321
  # can leave handleError alone
1057
1322
  # but need to change handleResponse to stop sending, rather than send_result_and_stop_querying.
1058
1323
  #
1059
- # @TODO@ Also, we could really do with a MaxValidationTimeout - if validation not OK within
1060
- # this time, then raise Timeout (and stop validation)?
1061
- #
1062
1324
  # @TODO@ Also, should there be some facility to stop validator following same chain
1063
1325
  # concurrently?
1064
1326
  #
@@ -1075,12 +1337,29 @@ module Dnsruby
1075
1337
  end
1076
1338
  # @mutex.synchronize{
1077
1339
  @parent.single_res_mutex.synchronize {
1078
- if @query_list[client_query_id] == nil
1079
- # print "Dead query response - ignoring\n"
1080
- Dnsruby.log.debug{'Ignoring response for dead query'}
1340
+ current = @query_list[client_query_id]
1341
+ # An observer selected before completion can arrive after the caller
1342
+ # reuses its ID. Only the original logical query owns this queue.
1343
+ unless current && current[2].equal?(queue)
1344
+ Dnsruby.log.debug{'Ignoring response for completed query'}
1345
+ return
1346
+ end
1347
+ deadline, _retries, no_nameservers = @timeouts[client_query_id]
1348
+ if deadline && deadline <= TransportClock.now
1349
+ reason = no_nameservers ? 'Query timed out - no valid nameservers configured' : 'Query timed out'
1350
+ send_result_and_stop_querying(current[1], client_query_id, queue, nil,
1351
+ ResolvTimeout.new(reason))
1352
+ return
1353
+ end
1354
+ validation_query = @validation_queries[client_query_id]
1355
+ if validation_query
1356
+ # A response already accepted for validation owns completion. Errors
1357
+ # and already-queued answers from competing attempts cannot replace it.
1358
+ return unless event_type == Resolver::EventType::VALIDATED && validation_query == event_id
1359
+ elsif event_type == Resolver::EventType::VALIDATED
1081
1360
  return
1082
1361
  end
1083
- _msg, _client_queue, _select_queue, outstanding = @query_list[client_query_id]
1362
+ _msg, _client_queue, _select_queue, outstanding = current
1084
1363
  if event_type == Resolver::EventType::RECEIVED ||
1085
1364
  event_type == Resolver::EventType::ERROR
1086
1365
  unless outstanding.include?(id)
@@ -1120,7 +1399,20 @@ module Dnsruby
1120
1399
  # Check what sort of error it was :
1121
1400
  resolver, _msg, client_query_id, _retry_count = query_id
1122
1401
  _msg, client_queue, select_queue, outstanding = @query_list[client_query_id]
1123
- if error.kind_of?(ResolvTimeout)
1402
+ if error.kind_of?(SocketEofResolvError) && _msg.header.opcode != OpCode.QUERY
1403
+ # An UPDATE or other operation may already have taken effect. An
1404
+ # ambiguous EOF ends the whole logical query, including other servers'
1405
+ # scheduled and outstanding attempts; do not replay it elsewhere.
1406
+ send_result_and_stop_querying(client_queue, client_query_id, select_queue, response, error)
1407
+ return
1408
+ end
1409
+ retryable_eof = error.kind_of?(SocketEofResolvError)
1410
+ if error.kind_of?(ResolvTimeout) || retryable_eof
1411
+ # A server can close a TCP connection before answering (RFC 7766
1412
+ # section 6.2.4). Keep the finite retry plan and original total deadline,
1413
+ # just as for a lost UDP packet; reconnecting must not add new retries.
1414
+ # An UPDATE may already have changed the server and must not be replayed
1415
+ # merely because its reply was lost.
1124
1416
  # - if it was a timeout, then check which number it was, and how many retries are expected on that server
1125
1417
  # - if it was the last retry, on the last server, then return a timeout to the client (and clean up)
1126
1418
  # - otherwise, continue
@@ -1136,10 +1428,10 @@ module Dnsruby
1136
1428
  # - if it was an NXDomain, then return that to the client, and stop all new queries (and clean up)
1137
1429
  # send_result_and_stop_querying(client_queue, client_query_id, select_queue, response, error)
1138
1430
  increment_resolver_priority(resolver) unless response.cached
1139
- stop_querying(client_query_id)
1431
+ begin_validation(client_query_id, query_id, select_queue)
1140
1432
  # @TODO@ Does the client want notified at this point?
1141
- elsif error.kind_of?(EncodeError)
1142
- Dnsruby.log.debug{'Encode error - sending to client'}
1433
+ elsif error.kind_of?(EncodeError) || error.kind_of?(RecursionLimitError)
1434
+ Dnsruby.log.debug{'Query cannot continue - sending error to client'}
1143
1435
  send_result_and_stop_querying(client_queue, client_query_id, select_queue, response, error)
1144
1436
  else
1145
1437
  # - if it was any other error, then remove that server from the list for that query
@@ -1149,11 +1441,14 @@ module Dnsruby
1149
1441
  Dnsruby.log.debug{"Removing #{resolver.server} from resolver list for this query"}
1150
1442
  if timeouts
1151
1443
  timeouts[1].each do |key, value|
1152
- res = value[0]
1153
- if res == resolver
1444
+ if value.first.is_a?(Array)
1445
+ value.reject! { |res, _retry| res == resolver }
1446
+ timeouts[1].delete(key) if value.empty?
1447
+ elsif value.first == resolver
1154
1448
  timeouts[1].delete(key)
1155
1449
  end
1156
1450
  end
1451
+ update_tick_deadline(client_query_id)
1157
1452
  end
1158
1453
  # Also stick it to the back of the list for future queries
1159
1454
  demote_resolver(resolver)
@@ -1177,7 +1472,7 @@ module Dnsruby
1177
1472
  TheLog.debug("Incrementing resolver priority for #{res.server}\n")
1178
1473
  # @parent.single_res_mutex.synchronize {
1179
1474
  index = @parent.single_resolvers.index(res)
1180
- if index > 0
1475
+ if index && index > 0
1181
1476
  @parent.single_resolvers.delete(res)
1182
1477
  @parent.single_resolvers.insert(index-1,res)
1183
1478
  end
@@ -1189,7 +1484,7 @@ module Dnsruby
1189
1484
  TheLog.debug("Decrementing resolver priority for #{res.server}\n")
1190
1485
  # @parent.single_res_mutex.synchronize {
1191
1486
  index = @parent.single_resolvers.index(res)
1192
- if index < @parent.single_resolvers.length
1487
+ if index && index < @parent.single_resolvers.length - 1
1193
1488
  @parent.single_resolvers.delete(res)
1194
1489
  @parent.single_resolvers.insert(index+1,res)
1195
1490
  end
@@ -1200,8 +1495,9 @@ module Dnsruby
1200
1495
  def demote_resolver(res)
1201
1496
  TheLog.debug("Demoting resolver priority for #{res.server} to bottom\n")
1202
1497
  # @parent.single_res_mutex.synchronize {
1203
- @parent.single_resolvers.delete(res)
1204
- @parent.single_resolvers.push(res)
1498
+ if @parent.single_resolvers.delete(res)
1499
+ @parent.single_resolvers.push(res)
1500
+ end
1205
1501
  # }
1206
1502
  end
1207
1503
 
@@ -1216,7 +1512,7 @@ module Dnsruby
1216
1512
  if s_queue != select_queue
1217
1513
  Dnsruby.log_and_raise("Serious internal error : expected select queue #{s_queue}, got #{select_queue}")
1218
1514
  end
1219
- stop_querying(client_query_id)
1515
+ begin_validation(client_query_id, query_id, select_queue)
1220
1516
  # @TODO@ Does the client want notified at this point?
1221
1517
  # client_queue.push([client_query_id, Resolver::EventType::RECEIVED, msg, nil])
1222
1518
  # }