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
@@ -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 valid 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
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()
@@ -462,6 +500,7 @@ module Dnsruby
462
500
  send(key.to_s + '=', args[0][key])
463
501
  end
464
502
  rescue Exception => e
503
+ raise if key.to_s == 'tsig' || ValidationBudget.abort?(e)
465
504
  Dnsruby.log.error{"Argument #{key} not valid : #{e}\n"}
466
505
  end
467
506
  end
@@ -478,54 +517,101 @@ module Dnsruby
478
517
  end
479
518
 
480
519
  def add_config_nameservers # :nodoc: all
481
- 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
482
529
  @config.get_ready
530
+ configure_nameservers
483
531
  end
484
- @configured = true
485
- # Add the Config nameservers
486
- threads = []
487
- @config.nameserver.each do |ns|
488
- threads << Thread.new do
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|
489
542
  PacketSender.new({
490
- server: ns,
491
- port: @port,
492
- dnssec: @dnssec,
493
- use_tcp: @use_tcp,
494
- no_tcp: @no_tcp,
495
- tcp_pipelining: @tcp_pipelining,
496
- tcp_pipelining_max_queries: @tcp_pipelining_max_queries,
497
- packet_timeout: @packet_timeout,
498
- tsig: @tsig,
499
- ignore_truncation: @ignore_truncation,
500
- src_address: @src_address,
501
- src_address6: @src_address6,
502
- src_port: @src_port,
503
- recurse: @recurse,
504
- udp_size: @udp_size,
505
- ignore_config_resolv_errors: true})
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})
506
560
  end
507
- end
508
-
509
- new_resolvers = threads.map(&:value).compact
510
- new_resolvers = new_resolvers.select do |resolver|
511
- !resolver.nil? && !resolver.server.nil?
512
- end
513
- # if new_resolvers.empty?
514
- # raise ArgumentError.new("No valid nameservers found in config")
515
- # end
516
- @single_res_mutex.synchronize { @single_resolvers.concat(new_resolvers) }
517
- if @single_resolvers.nil? || @single_resolvers.empty?
518
- raise ArgumentError.new("No valid nameservers found")
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
519
604
  end
520
605
  end
606
+ private :configure_nameservers
521
607
 
522
608
  def set_config_nameserver(n)
523
- # @TODO@ Should we allow NS RRSet here? If so, then .sort_by {rand}
524
- @config.get_ready unless @configured
525
- @configured = true
526
-
527
- @config.nameserver = n.kind_of?(String) ? [n] : n
528
- 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
529
615
  end
530
616
 
531
617
  def reset_attributes # :nodoc: all
@@ -534,9 +620,8 @@ module Dnsruby
534
620
  # Attributes
535
621
 
536
622
  # do_validation tells the Resolver whether to try to validate the response
537
- # with DNSSEC. This should work for NSEC-signed domains, but NSEC3
538
- # validation is not currently supported. This attribute now defaults to
539
- # 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.
540
625
  @do_validation = false
541
626
  @query_timeout = DefaultQueryTimeout
542
627
  @retry_delay = DefaultRetryDelay
@@ -550,6 +635,7 @@ module Dnsruby
550
635
  @no_tcp = false
551
636
  @tcp_pipelining = false
552
637
  @tcp_pipelining_max_queries = DefaultPipeLiningMaxQueries
638
+ @tcp_idle_timeout = DefaultTcpIdleTimeout
553
639
  @tsig = nil
554
640
  @ignore_truncation = false
555
641
  @config = Config.new()
@@ -561,9 +647,11 @@ module Dnsruby
561
647
  @single_resolvers=[]
562
648
  }
563
649
  @configured = false
650
+ @configuration_error = nil
564
651
  end
565
652
 
566
653
  def update # :nodoc: all
654
+ ensure_current_process
567
655
  # Update any resolvers we have with the latest config
568
656
  @single_res_mutex.synchronize do
569
657
  @single_resolvers.delete(nil) # Just in case...
@@ -579,15 +667,20 @@ module Dnsruby
579
667
  # end
580
668
 
581
669
  def add_server(server)# :nodoc:
582
- @configured = true
583
- res = PacketSender.new(server)
584
- Dnsruby.log_and_raise("Can't create server #{server}", ArgumentError) unless res
585
- update_internal_res(res)
586
- @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
587
680
  end
588
681
 
589
682
  def update_internal_res(res)
590
- [: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,
591
684
  :src_address, :src_address6, :src_port, :recurse,
592
685
  :udp_size, :dnssec].each do |param|
593
686
 
@@ -600,10 +693,7 @@ module Dnsruby
600
693
  end
601
694
 
602
695
  def nameserver=(n)
603
- @configured = true
604
- @single_res_mutex.synchronize { @single_resolvers=[] }
605
696
  set_config_nameserver(n)
606
- add_config_nameservers
607
697
  end
608
698
 
609
699
  # --
@@ -634,8 +724,10 @@ module Dnsruby
634
724
  # res.src_port=60015..60115
635
725
  #
636
726
  def src_port=(p)
637
- if Resolver.check_port(p)
638
- @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
639
731
  update
640
732
  end
641
733
  end
@@ -653,15 +745,10 @@ module Dnsruby
653
745
  # res.add_src_port(60015..60115)
654
746
  #
655
747
  def add_src_port(p)
656
- if Resolver.check_port(p, @src_port)
657
- a = Resolver.get_ports_from(p)
658
- a.each do |x|
659
- if (@src_port.length > 0) && (x == 0)
660
- Dnsruby.log_and_raise("src_port of 0 only allowed as only src_port value (currently #{@src_port.length} values",
661
- ArgumentError)
662
- end
663
- @src_port.push(x)
664
- 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
665
752
  end
666
753
  update
667
754
  end
@@ -678,7 +765,10 @@ module Dnsruby
678
765
  return true
679
766
  end
680
767
  if Resolver.port_in_range(p)
681
- 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
682
772
  else
683
773
  Dnsruby.log.error("Illegal port (#{p})")
684
774
  Dnsruby.log_and_raise("Illegal port #{p}", ArgumentError)
@@ -711,6 +801,14 @@ module Dnsruby
711
801
  update
712
802
  end
713
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
+
714
812
  def use_tcp=(on)
715
813
  @use_tcp = on
716
814
  update
@@ -725,10 +823,13 @@ module Dnsruby
725
823
  # Pass in either a Dnsruby::RR::TSIG, or a key_name and key (or just a key)
726
824
  # Pass in nil to stop tsig signing.
727
825
  # * res.tsig=(tsig_rr)
728
- # * res.tsig=(key_name, key) # defaults to hmac-md5
729
- # * 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
730
830
  # * res.tsig=nil # Stop the resolver from signing
731
831
  def tsig=(t)
832
+ Resolver.get_tsig([t]) unless t.nil?
732
833
  @tsig = t
733
834
  update
734
835
  end
@@ -764,9 +865,11 @@ module Dnsruby
764
865
  return nil
765
866
  end
766
867
  else
767
- tsig = RR.new_from_hash(create_tsig_options(args))
868
+ tsig = RR.new_from_hash(create_tsig_options(*args))
768
869
  end
769
- 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})"}
770
873
  tsig
771
874
  end
772
875
 
@@ -815,8 +918,8 @@ module Dnsruby
815
918
  @dnssec = d
816
919
  if d
817
920
  # Set the UDP size (RFC 4035 section 4.1)
818
- if @udp_size < MinDnssecUdpSize
819
- self.udp_size = MinDnssecUdpSize
921
+ if @udp_size < AbsoluteMinDnssecUdpSize
922
+ self.udp_size = AbsoluteMinDnssecUdpSize
820
923
  end
821
924
  end
822
925
  update
@@ -828,37 +931,33 @@ module Dnsruby
828
931
  end
829
932
 
830
933
  def single_res_mutex # :nodoc: all
934
+ ensure_current_process
831
935
  @single_res_mutex
832
936
  end
833
937
 
834
938
  def generate_timeouts(base=0) # :nodoc: all
835
- # These should be be pegged to the single_resolver they are targetting :
836
- # e.g. timeouts[timeout1]=nameserver
837
939
  timeouts = {}
838
940
  retry_delay = @retry_delay
839
- # @single_res_mutex.synchronize {
840
- @retry_times.times do |retry_count|
841
- if retry_count > 0
842
- retry_delay *= 2
843
- end
844
-
845
- @single_resolvers.delete(nil) # Just in case...
846
- @single_resolvers.each_index do |i|
847
- res = @single_resolvers[i]
848
- offset = (i * @retry_delay.to_f / @single_resolvers.length)
849
- if retry_count == 0
850
- 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
851
953
  else
852
- if timeouts.has_key?(base + retry_delay + offset)
853
- Dnsruby.log_and_raise('Duplicate timeout key!')
854
- end
855
- timeouts[base + retry_delay + offset]=[res, retry_count]
954
+ timeouts[deadline] = attempt
856
955
  end
857
956
  end
858
957
  end
859
- # }
860
958
  timeouts
861
959
  end
960
+
862
961
  end
863
962
 
864
963
 
@@ -874,7 +973,49 @@ module Dnsruby
874
973
  # @mutex=Mutex.new
875
974
  @query_list = {}
876
975
  @timeouts = {}
877
- 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
+
878
1019
  def send_async(msg, client_queue, client_query_id=nil)
879
1020
  # This is the whole point of the Resolver class.
880
1021
  # We want to use multiple SingleResolvers to run a query.
@@ -893,58 +1034,87 @@ module Dnsruby
893
1034
  unless client_queue.kind_of?(Queue)
894
1035
  Dnsruby.log_and_raise('Wrong type for client_queue in Resolver# send_async')
895
1036
  # @TODO@ Handle different queue tuples - push this to generic send_error method
896
- 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}')])
897
1038
  return
898
1039
  end
899
1040
 
900
1041
  unless msg.kind_of?Message
901
1042
  Dnsruby.log.error{'Wrong type for msg in Resolver# send_async'}
902
1043
  # @TODO@ Handle different queue tuples - push this to generic send_error method
903
- 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}")])
904
1045
  return
905
1046
  end
906
1047
 
907
1048
  # According to RFC 6891, a UDP payload size >512 bytes requires an
908
1049
  # EDNS OPT RR in the message.
909
- if @parent.udp_size > 512 && !msg.get_opt
1050
+ if @parent.udp_size > 512 && !msg.get_opt && !msg.send_raw && !msg.signed? && !msg.tsig
910
1051
  TheLog.debug("Automatically adding EDNS OPT RR for udp_size=#{ @parent.udp_size }")
911
1052
  msg.add_additional(Dnsruby::RR::OPT.new(@parent.udp_size))
912
1053
  end
913
1054
 
914
1055
  begin
915
- 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
916
1063
  rescue EncodeError => err
917
1064
  Dnsruby.log.error { "Can't encode " + msg.to_s + " : #{err}" }
918
- client_queue.push([client_query_id, err])
1065
+ client_queue.push([client_query_id, nil, err])
919
1066
  return
920
1067
  end
921
1068
 
922
- tick_needed = false
1069
+ st = SelectThread.instance
923
1070
  # add to our data structures
924
1071
  # @mutex.synchronize{
925
1072
  @parent.single_res_mutex.synchronize {
926
- tick_needed = true if @query_list.empty?
927
1073
  if @query_list.has_key?(client_query_id)
928
1074
  Dnsruby.log.error("Duplicate query id requested (#{client_query_id}")
929
1075
  # @TODO@ Handle different queue tuples - push this to generic send_error method
930
- 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')])
931
1077
  return
932
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
933
1087
  outstanding = []
934
- @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
935
1090
 
936
- query_timeout = Time.now + @parent.query_timeout
1091
+ query_timeout = TransportClock.now + @parent.query_timeout
937
1092
  if @parent.query_timeout == 0
938
- query_timeout = Time.now + 31536000 # a year from now
1093
+ query_timeout = TransportClock.now + 31536000 # a year from now
939
1094
  end
940
- @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)
941
1112
  }
942
1113
 
943
- # Now do querying stuff using SingleResolver
944
- # All this will be handled by the tick method (if we have 0 as the first timeout)
945
- st = SelectThread.instance
946
- st.add_observer(q, self)
947
- 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)
948
1118
  client_query_id
949
1119
  end
950
1120
 
@@ -953,8 +1123,8 @@ module Dnsruby
953
1123
  # These are created at the same time in case the parameters change during the life of the query.
954
1124
  #
955
1125
  # These should be absolute, rather than relative
956
- # The first value should be Time.now[
957
- @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
958
1128
  end
959
1129
 
960
1130
  # Close the Resolver. Unfinished queries are terminated with OtherResolvError.
@@ -966,15 +1136,30 @@ module Dnsruby
966
1136
  send_result_and_stop_querying(client_queue, client_query_id, q, nil,
967
1137
  OtherResolvError.new('Resolver closing!'))
968
1138
  end
1139
+ @parent.single_resolvers.compact.each(&:close)
969
1140
  }
970
1141
  end
971
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
+
972
1158
  # MUST BE CALLED IN A SYNCHRONIZED BLOCK!
973
1159
  #
974
1160
  # Send the result back to the client, and close the socket for that query by removing
975
1161
  # the query from the select thread.
976
1162
  def send_result_and_stop_querying(client_queue, client_query_id, select_queue, msg, error) # :nodoc: all
977
- stop_querying(client_query_id)
978
1163
  send_result(client_queue, client_query_id, select_queue, msg, error)
979
1164
  end
980
1165
 
@@ -983,13 +1168,51 @@ module Dnsruby
983
1168
  # Stops send any more packets for a client-level query
984
1169
  def stop_querying(client_query_id) # :nodoc: all
985
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
986
1207
  end
987
1208
 
988
1209
  # MUST BE CALLED IN A SYNCHRONIZED BLOCK!
989
1210
  #
990
1211
  # Sends the result to the client's queue, and removes the queue observer from the select thread
991
1212
  def send_result(client_queue, client_query_id, select_queue, msg, error) # :nodoc: all
992
- 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)
993
1216
  # We might still get some callbacks, which we should ignore
994
1217
  st = SelectThread.instance
995
1218
  st.remove_observer(select_queue, self)
@@ -999,50 +1222,78 @@ module Dnsruby
999
1222
  # }
1000
1223
  # Return the response to the client
1001
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
1002
1244
  end
1245
+ private :update_tick_deadline
1003
1246
 
1004
- # 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.
1005
1248
  # It should arguably be called from another worker thread... (which also handles the queue)
1006
1249
  # Each tick, we check if any timeouts have occurred. If so, we take the appropriate action :
1007
1250
  # Return a timeout to the client, or send a new query
1008
- def tick # :nodoc: all
1251
+ def tick(client_query_id = nil) # :nodoc: all
1009
1252
  # Handle the tick
1010
1253
  # Do we have any retries due to be sent yet?
1011
1254
  # @mutex.synchronize{
1255
+ sends = []
1012
1256
  @parent.single_res_mutex.synchronize {
1013
- time_now = Time.now
1014
- @timeouts.keys.each do |client_query_id|
1015
- msg, client_queue, select_queue, outstanding = @query_list[client_query_id]
1016
- query_timeout, timeouts = @timeouts[client_query_id]
1017
- if query_timeout < Time.now
1018
- # 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'
1019
1272
  send_result_and_stop_querying(client_queue, client_query_id, select_queue, nil,
1020
- ResolvTimeout.new('Query timed out'))
1273
+ ResolvTimeout.new(reason))
1021
1274
  next
1022
1275
  end
1023
- timeouts_done = []
1024
- timeouts.keys.sort.each do |timeout|
1025
- if timeout < time_now
1026
- # Send the next query
1027
- 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|
1028
1282
  id = [res, msg, client_query_id, retry_count]
1029
1283
  Dnsruby.log.debug("Sending msg to #{res.server}")
1030
- # We should keep a list of the queries which are outstanding
1031
1284
  outstanding.push(id)
1032
- timeouts_done.push(timeout)
1033
- timeouts.delete(timeout)
1034
-
1035
- # Pick a new QID here @TODO@ !!!
1036
- # msg.header.id = rand(65535);
1037
- # print "New query : #{new_msg}\n"
1038
- res.send_async(msg, select_queue, id)
1039
- else
1040
- 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
1041
1291
  end
1042
1292
  end
1043
- timeouts_done.each { |t| timeouts.delete(t) }
1293
+ update_tick_deadline(client_query_id)
1044
1294
  end
1045
1295
  }
1296
+ sends.each(&:call)
1046
1297
  end
1047
1298
 
1048
1299
  # This method is called by the SelectThread (in the select thread) when the queue has a new item on it.
@@ -1070,9 +1321,6 @@ module Dnsruby
1070
1321
  # can leave handleError alone
1071
1322
  # but need to change handleResponse to stop sending, rather than send_result_and_stop_querying.
1072
1323
  #
1073
- # @TODO@ Also, we could really do with a MaxValidationTimeout - if validation not OK within
1074
- # this time, then raise Timeout (and stop validation)?
1075
- #
1076
1324
  # @TODO@ Also, should there be some facility to stop validator following same chain
1077
1325
  # concurrently?
1078
1326
  #
@@ -1089,12 +1337,29 @@ module Dnsruby
1089
1337
  end
1090
1338
  # @mutex.synchronize{
1091
1339
  @parent.single_res_mutex.synchronize {
1092
- if @query_list[client_query_id] == nil
1093
- # print "Dead query response - ignoring\n"
1094
- 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
1095
1360
  return
1096
1361
  end
1097
- _msg, _client_queue, _select_queue, outstanding = @query_list[client_query_id]
1362
+ _msg, _client_queue, _select_queue, outstanding = current
1098
1363
  if event_type == Resolver::EventType::RECEIVED ||
1099
1364
  event_type == Resolver::EventType::ERROR
1100
1365
  unless outstanding.include?(id)
@@ -1134,7 +1399,20 @@ module Dnsruby
1134
1399
  # Check what sort of error it was :
1135
1400
  resolver, _msg, client_query_id, _retry_count = query_id
1136
1401
  _msg, client_queue, select_queue, outstanding = @query_list[client_query_id]
1137
- 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.
1138
1416
  # - if it was a timeout, then check which number it was, and how many retries are expected on that server
1139
1417
  # - if it was the last retry, on the last server, then return a timeout to the client (and clean up)
1140
1418
  # - otherwise, continue
@@ -1150,10 +1428,10 @@ module Dnsruby
1150
1428
  # - if it was an NXDomain, then return that to the client, and stop all new queries (and clean up)
1151
1429
  # send_result_and_stop_querying(client_queue, client_query_id, select_queue, response, error)
1152
1430
  increment_resolver_priority(resolver) unless response.cached
1153
- stop_querying(client_query_id)
1431
+ begin_validation(client_query_id, query_id, select_queue)
1154
1432
  # @TODO@ Does the client want notified at this point?
1155
- elsif error.kind_of?(EncodeError)
1156
- 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'}
1157
1435
  send_result_and_stop_querying(client_queue, client_query_id, select_queue, response, error)
1158
1436
  else
1159
1437
  # - if it was any other error, then remove that server from the list for that query
@@ -1163,11 +1441,14 @@ module Dnsruby
1163
1441
  Dnsruby.log.debug{"Removing #{resolver.server} from resolver list for this query"}
1164
1442
  if timeouts
1165
1443
  timeouts[1].each do |key, value|
1166
- res = value[0]
1167
- 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
1168
1448
  timeouts[1].delete(key)
1169
1449
  end
1170
1450
  end
1451
+ update_tick_deadline(client_query_id)
1171
1452
  end
1172
1453
  # Also stick it to the back of the list for future queries
1173
1454
  demote_resolver(resolver)
@@ -1191,7 +1472,7 @@ module Dnsruby
1191
1472
  TheLog.debug("Incrementing resolver priority for #{res.server}\n")
1192
1473
  # @parent.single_res_mutex.synchronize {
1193
1474
  index = @parent.single_resolvers.index(res)
1194
- if index > 0
1475
+ if index && index > 0
1195
1476
  @parent.single_resolvers.delete(res)
1196
1477
  @parent.single_resolvers.insert(index-1,res)
1197
1478
  end
@@ -1203,7 +1484,7 @@ module Dnsruby
1203
1484
  TheLog.debug("Decrementing resolver priority for #{res.server}\n")
1204
1485
  # @parent.single_res_mutex.synchronize {
1205
1486
  index = @parent.single_resolvers.index(res)
1206
- if index < @parent.single_resolvers.length
1487
+ if index && index < @parent.single_resolvers.length - 1
1207
1488
  @parent.single_resolvers.delete(res)
1208
1489
  @parent.single_resolvers.insert(index+1,res)
1209
1490
  end
@@ -1214,8 +1495,9 @@ module Dnsruby
1214
1495
  def demote_resolver(res)
1215
1496
  TheLog.debug("Demoting resolver priority for #{res.server} to bottom\n")
1216
1497
  # @parent.single_res_mutex.synchronize {
1217
- @parent.single_resolvers.delete(res)
1218
- @parent.single_resolvers.push(res)
1498
+ if @parent.single_resolvers.delete(res)
1499
+ @parent.single_resolvers.push(res)
1500
+ end
1219
1501
  # }
1220
1502
  end
1221
1503
 
@@ -1230,7 +1512,7 @@ module Dnsruby
1230
1512
  if s_queue != select_queue
1231
1513
  Dnsruby.log_and_raise("Serious internal error : expected select queue #{s_queue}, got #{select_queue}")
1232
1514
  end
1233
- stop_querying(client_query_id)
1515
+ begin_validation(client_query_id, query_id, select_queue)
1234
1516
  # @TODO@ Does the client want notified at this point?
1235
1517
  # client_queue.push([client_query_id, Resolver::EventType::RECEIVED, msg, nil])
1236
1518
  # }