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
@@ -23,10 +23,22 @@ end
23
23
  require 'set'
24
24
  require 'singleton'
25
25
  require 'dnsruby/validator_thread.rb'
26
+ require 'dnsruby/transport_clock'
27
+ require 'dnsruby/query_control'
28
+ require 'dnsruby/deadline_queue'
26
29
  module Dnsruby
27
30
  class SelectThread #:nodoc: all
28
31
  class SelectWakeup < RuntimeError; end
29
32
  include Singleton
33
+
34
+ class << self
35
+ alias_method :instance_without_fork_check, :instance
36
+ private :instance_without_fork_check
37
+
38
+ def instance
39
+ instance_without_fork_check.tap { |selector| selector.send(:ensure_current_process) }
40
+ end
41
+ end
30
42
  # This singleton class runs a continuous select loop which
31
43
  # listens for responses on all of the in-use sockets.
32
44
  # When a new query is sent, the thread is woken up, and
@@ -37,24 +49,40 @@ module Dnsruby
37
49
  #
38
50
  # But how do we find the response queue for a particular query?
39
51
  # Hash of client_id->[query, client_queue, socket]
40
- # and socket->[client_id]
41
- #
42
- # @todo@ should we implement some of cancel function?
52
+ # and socket->[client_ids], including multiple queries per TCP pipeline.
53
+ #
54
+ # @@mutex owns the attempt, queue and socket indexes. Cancellation may take
55
+ # a SendToken lock while holding @@mutex; a token-protected write must not
56
+ # acquire @@mutex. The selector never waits for a pipeline writer mutex.
57
+ # Blocking I/O and client callbacks run outside @@mutex.
43
58
 
44
59
  def initialize
60
+ @process_mutex ||= Mutex.new
61
+ @send_queue = @send_workers = @send_groups = nil
62
+ @packet_deadlines = DeadlineQueue.new
63
+ @idle_deadlines = DeadlineQueue.new
64
+ @created_sockets = ObjectSpace::WeakMap.new
45
65
  @@mutex = Mutex.new
46
66
  @@mutex.synchronize {
47
67
  @@in_select=false
48
- # @@notifier,@@notified=IO.pipe
49
68
  @@sockets = Set.new
50
69
  @@timeouts = Hash.new
51
- # @@mutex.synchronize do
52
70
  @@query_hash = Hash.new
71
+ @@queries_by_queue = {}.compare_by_identity
72
+ @@queries_by_sender = {}.compare_by_identity
53
73
  @@socket_hash = Hash.new
54
74
  @@socket_is_persistent = Hash.new
75
+ @@socket_owners = Hash.new
76
+ @@sockets_by_sender = {}.compare_by_identity
77
+ @@retiring_sockets = Set.new
55
78
  @@observers = Hash.new
79
+ @@observer_queue_counts = {}.compare_by_identity
80
+ @@observed_queues = ObjectSpace::WeakMap.new
81
+ @@validation_pid = Process.pid
82
+ @@validations = []
56
83
  @@tcp_buffers=Hash.new
57
84
  @@socket_remaining_queries = Hash.new
85
+ @@socket_idle_deadlines = Hash.new
58
86
  @@tick_observers = []
59
87
  @@queued_exceptions=[]
60
88
  @@queued_responses=[]
@@ -64,15 +92,47 @@ module Dnsruby
64
92
 
65
93
  # Suppress reverse lookups
66
94
  BasicSocket.do_not_reverse_lookup = true
67
- # end
68
95
  # Now start the select thread
96
+ @process_pid = Process.pid
69
97
  @@select_thread = Thread.new { do_select }
70
-
71
- # # Start the validator thread
72
- # @@validator = ValidatorThread.instance
73
98
  }
74
99
  end
75
100
 
101
+ # Only the calling thread survives fork. Discard all parent transactions
102
+ # before restarting any workers, and close the child's descriptor copies
103
+ # without shutting down the connections still used by the parent.
104
+ def ensure_current_process
105
+ return unless @process_pid && @process_pid != Process.pid
106
+ @process_mutex.synchronize do
107
+ if @process_pid != Process.pid
108
+ sockets = @@sockets.to_a + @@socket_hash.keys + @@wakeup_sockets + @created_sockets.keys
109
+ sockets.uniq.each do |socket|
110
+ begin
111
+ socket.close unless socket.closed?
112
+ rescue IOError, SystemCallError
113
+ end
114
+ end
115
+ Thread.current[:dnsruby_response_query] = nil
116
+ initialize
117
+ end
118
+ end
119
+ end
120
+ private :ensure_current_process
121
+
122
+ # Also protect callers retaining a selector reference across fork.
123
+ def state_mutex
124
+ ensure_current_process
125
+ @@mutex
126
+ end
127
+ private :state_mutex
128
+
129
+ # Keep weak references from allocation through connect/write, before a
130
+ # socket joins the read set. A fork must close those descriptor copies too.
131
+ def track_socket(socket)
132
+ state_mutex.synchronize { (@created_sockets ||= ObjectSpace::WeakMap.new)[socket] = true }
133
+ socket
134
+ end
135
+
76
136
  def get_socket_pair
77
137
  # Emulate socketpair on platforms which don't support it
78
138
  srv = nil
@@ -94,7 +154,8 @@ module Dnsruby
94
154
  class QuerySettings
95
155
  attr_accessor :query_bytes, :query, :ignore_truncation, :client_queue,
96
156
  :client_query_id, :socket, :dest_server, :dest_port, :endtime, :udp_packet_size,
97
- :single_resolver, :is_persistent_socket, :tcp_pipelining_max_queries
157
+ :single_resolver, :is_persistent_socket, :tcp_pipelining_max_queries, :send_token,
158
+ :retire_connection, :disable_edns, :authentication_error, :cache_scope, :transport_configuration
98
159
  # new(query_bytes, query, ignore_truncation, client_queue, client_query_id,
99
160
  # socket, dest_server, dest_port, endtime, , udp_packet_size, single_resolver)
100
161
  def initialize(*args)
@@ -111,9 +172,230 @@ module Dnsruby
111
172
  @single_resolver = args[10]
112
173
  @is_persistent_socket = false
113
174
  @tcp_pipelining_max_queries = nil
175
+ @send_token = SendToken.new
176
+ @retire_connection = false
177
+ @disable_edns = false
178
+ end
179
+
180
+ def no_tcp
181
+ transport_configuration ? transport_configuration[:no_tcp] : single_resolver.no_tcp
182
+ end
183
+
184
+ def tcp_idle_timeout
185
+ transport_configuration ? transport_configuration[:tcp_idle_timeout] : single_resolver.tcp_idle_timeout
186
+ end
187
+ end
188
+
189
+ # Look up client queries by socket and DNS message ID. Helper callers may
190
+ # register several queries with the same DNS ID; normal TCP sends keep IDs
191
+ # unique per connection. Hold @@mutex when updating the index.
192
+ class SocketQueries < Set
193
+ def initialize
194
+ super
195
+ @by_header_id = {}
196
+ @header_ids = {}
197
+ end
198
+
199
+ def add_query(settings)
200
+ id, header_id = settings.client_query_id, settings.query.header.id
201
+ add(id)
202
+ @header_ids[id] = header_id
203
+ (@by_header_id[header_id] ||= Set.new).add(id)
204
+ end
205
+
206
+ def matching(header_id)
207
+ (@by_header_id[header_id] || []).to_a
208
+ end
209
+
210
+ def delete(id)
211
+ header_id = @header_ids.delete(id)
212
+ if (ids = @by_header_id[header_id])
213
+ ids.delete(id)
214
+ @by_header_id.delete(header_id) if ids.empty?
215
+ end
216
+ super
217
+ end
218
+ end
219
+ private_constant :SocketQueries
220
+
221
+ # RFC 9520 section 3.1 limits one resolution to three attempts per
222
+ # server address and transport, including protocol-driven retries.
223
+ class TransmissionBudget
224
+ def self.current
225
+ Thread.current[:dnsruby_transmission_budget]
226
+ end
227
+
228
+ def self.with(budget)
229
+ previous = current
230
+ Thread.current[:dnsruby_transmission_budget] = budget
231
+ yield
232
+ ensure
233
+ Thread.current[:dnsruby_transmission_budget] = previous
234
+ end
235
+
236
+ def initialize
237
+ @counts = {}
238
+ @contexts = {}
239
+ @total = 0
240
+ @mutex = Mutex.new
241
+ end
242
+
243
+ # Supporting queries and validator workers already share this object.
244
+ def context(key)
245
+ @mutex.synchronize { @contexts[key] ||= yield }
246
+ end
247
+
248
+ def limit_total!(maximum, error_class)
249
+ @mutex.synchronize do
250
+ @maximum = [@maximum, maximum].compact.min
251
+ @limit_error_class = error_class
252
+ end
253
+ end
254
+
255
+ def admit(server, port, tcp, question = nil)
256
+ identity = question && [question.qname.canonical, question.qtype.code, question.qclass.code]
257
+ key = [server.to_s.dup.freeze, port, !!tcp, identity]
258
+ @mutex.synchronize do
259
+ count = @counts[key] || 0
260
+ return false if count >= 3
261
+ if @maximum && @total >= @maximum
262
+ raise @limit_error_class, 'Recursive query transmission limit exceeded'
263
+ end
264
+ @total += 1
265
+ @counts[key] = count + 1
266
+ true
267
+ end
268
+ end
269
+ end
270
+
271
+ # Cancellation and each nonblocking write are ordered without holding the
272
+ # selector mutex during I/O. Waiting for a connect or a writable socket never
273
+ # holds this lock, so closing a Resolver remains responsive.
274
+ class SendToken
275
+ attr_reader :control, :transmission_budget
276
+
277
+ def initialize(control = QueryControl.current, transmission_budget = nil)
278
+ @mutex = Mutex.new
279
+ @cancelled = false
280
+ @control = control
281
+ @transmission_budget = transmission_budget || TransmissionBudget.current || TransmissionBudget.new
282
+ end
283
+
284
+ def cancel
285
+ @mutex.synchronize { @cancelled = true }
286
+ end
287
+
288
+ def cancelled?
289
+ @control&.check!
290
+ @mutex.synchronize { @cancelled }
291
+ rescue QueryControl::Stopped
292
+ true
293
+ end
294
+
295
+ def write
296
+ @mutex.synchronize do
297
+ return false if @cancelled
298
+ begin
299
+ @control&.check!
300
+ rescue QueryControl::Stopped
301
+ return false
302
+ end
303
+ yield
304
+ end
305
+ end
306
+ end
307
+
308
+ # Register ownership before work leaves the selector, including attempts
309
+ # which have not acquired a socket yet. Nil is never a live attempt token.
310
+ def reserve_send(settings, previous_settings = nil)
311
+ state_mutex.synchronize do
312
+ return false unless @@query_hash[settings.client_query_id].equal?(previous_settings)
313
+ if previous_settings
314
+ settings.retire_connection = previous_settings.retire_connection
315
+ remove_id_from_mutex_synchronized_block(settings.client_query_id, settings.send_token)
316
+ end
317
+ @@query_hash[settings.client_query_id] = settings
318
+ (@@queries_by_queue[settings.client_queue] ||= Set.new) << settings.client_query_id
319
+ ((@@queries_by_sender ||= {}.compare_by_identity)[settings.single_resolver] ||= Set.new) << settings.client_query_id
320
+ @@timeouts[settings.client_query_id] = settings.endtime
321
+ (@packet_deadlines ||= DeadlineQueue.new)[settings.client_query_id] = settings.endtime
322
+ check_select_thread_synchronized
323
+ end
324
+ wake_up_select_thread
325
+ true
326
+ end
327
+
328
+ class SendTask
329
+ def initialize(&operation)
330
+ @operation = operation
331
+ @result = Queue.new
332
+ end
333
+
334
+ def run
335
+ @result << [@operation.call, nil]
336
+ rescue Exception => error
337
+ @result << [nil, error]
338
+ end
339
+
340
+ def value
341
+ result, error = @result.pop
342
+ raise error if error
343
+ result
114
344
  end
115
345
  end
116
346
 
347
+ # A finite pool keeps slow connects/writes off the selector without creating
348
+ # an unbounded number of threads. Already-owned attempts can queue without
349
+ # introducing a new admission limit; each retains its original deadline.
350
+ def defer_send(sender = nil, &operation)
351
+ task = SendTask.new(&operation)
352
+ state_mutex.synchronize do
353
+ unless @send_queue && @send_workers.any?(&:alive?)
354
+ queue = Queue.new
355
+ workers = []
356
+ begin
357
+ 4.times { workers << Thread.new { while (work = queue.pop); work.run; end } }
358
+ rescue ThreadError
359
+ workers.length.times { queue << nil }
360
+ raise
361
+ end
362
+ @send_queue = queue
363
+ @send_workers = workers
364
+ @send_groups = {}
365
+ end
366
+ if sender
367
+ group = @send_groups[sender]
368
+ if group
369
+ group << task
370
+ else
371
+ @send_groups[sender] = [task]
372
+ # One worker owns each sender's FIFO. Other tasks for the same
373
+ # pipeline do not occupy workers just to wait for its writer lock.
374
+ @send_queue << SendTask.new { run_send_group(sender) }
375
+ end
376
+ else
377
+ @send_queue << task
378
+ end
379
+ end
380
+ task
381
+ end
382
+
383
+ def run_send_group(sender)
384
+ task = state_mutex.synchronize { @send_groups[sender].shift }
385
+ task.run
386
+ state_mutex.synchronize do
387
+ if @send_groups[sender].empty?
388
+ @send_groups.delete(sender)
389
+ else
390
+ # Give another sender a turn without allowing two workers to send on
391
+ # this sender concurrently. A continuously busy pipeline cannot starve
392
+ # a different server's connection behind its entire backlog.
393
+ @send_queue << SendTask.new { run_send_group(sender) }
394
+ end
395
+ end
396
+ end
397
+ private :run_send_group
398
+
117
399
  def tcp?(socket)
118
400
  type = socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE)
119
401
  [Socket::SOCK_STREAM].pack("i") == type.data
@@ -126,31 +408,203 @@ module Dnsruby
126
408
 
127
409
  def add_to_select(query_settings)
128
410
  # Add the query to sockets, and then wake the select thread up
129
- @@mutex.synchronize {
411
+ state_mutex.synchronize {
130
412
  check_select_thread_synchronized
131
- # @TODO@ This assumes that all client_query_ids are unique!
132
- # Would be a good idea at least to check this...
133
- @@query_hash[query_settings.client_query_id]=query_settings
134
- @@socket_hash[query_settings.socket] ||= []
135
- @@socket_hash[query_settings.socket] << query_settings.client_query_id
136
- @@socket_remaining_queries[query_settings.socket] ||= query_settings.tcp_pipelining_max_queries if query_settings.tcp_pipelining_max_queries != :infinite
137
- @@timeouts[query_settings.client_query_id]=query_settings.endtime
138
- @@sockets << query_settings.socket
139
- @@socket_is_persistent[query_settings.socket] = query_settings.is_persistent_socket
413
+ add_query_from_mutex_synchronized_block(query_settings)
140
414
  }
141
415
  wake_up_select_thread
142
416
  end
143
417
 
144
- def wake_up_select_thread
145
- begin
146
- @@wakeup_sockets[0].send("wakeup!", 0)
147
- rescue Exception
148
- # do nothing
418
+ def query_settings_for_id(client_query_id)
419
+ state_mutex.synchronize { @@query_hash[client_query_id] }
420
+ end
421
+
422
+ def selector_thread?
423
+ state_mutex.synchronize { @@select_thread.equal?(Thread.current) }
424
+ end
425
+
426
+ # Reserve a pipeline query before writing, or replace a protocol retry.
427
+ # A deferred retry must not replace an attempt already completed or replaced.
428
+ def register_query_if_current(query_settings, previous_settings)
429
+ registered = state_mutex.synchronize do
430
+ return false unless @@query_hash[query_settings.client_query_id].equal?(previous_settings)
431
+ # Selection happens outside this mutex. A timeout or close may retire
432
+ # that stream before admission, so leave the reservation untouched and
433
+ # let the sender choose a new connection under the original deadline.
434
+ if query_settings.is_persistent_socket &&
435
+ (query_settings.socket.closed? || (@@retiring_sockets ||= Set.new).include?(query_settings.socket))
436
+ return :retry_connection
437
+ end
438
+ query_settings.retire_connection = previous_settings.retire_connection if previous_settings
439
+ check_select_thread_synchronized
440
+ add_query_from_mutex_synchronized_block(query_settings)
441
+ end
442
+ wake_up_select_thread
443
+ registered
444
+ end
445
+
446
+ # Called only with @@mutex held. Retrying a client ID also retires its old
447
+ # socket membership, so stale EOF/timeout work cannot consume the retry.
448
+ def add_query_from_mutex_synchronized_block(settings)
449
+ id = settings.client_query_id
450
+ remove_id_from_mutex_synchronized_block(id, settings.send_token) if @@query_hash[id]
451
+ if settings.socket.closed?
452
+ @@queued_exceptions << [id, settings.client_queue,
453
+ SocketEofResolvError.new("TCP socket closed before all answers received"), nil]
454
+ return false
455
+ end
456
+ @@query_hash[id] = settings
457
+ (@@queries_by_queue[settings.client_queue] ||= Set.new) << id
458
+ ((@@queries_by_sender ||= {}.compare_by_identity)[settings.single_resolver] ||= Set.new) << id
459
+ membership = @@socket_hash[settings.socket]
460
+ unless membership.is_a?(SocketQueries)
461
+ indexed = SocketQueries.new
462
+ (membership || []).each { |member| indexed.add_query(@@query_hash[member]) if @@query_hash[member] }
463
+ membership = @@socket_hash[settings.socket] = indexed
464
+ end
465
+ membership.add_query(settings)
466
+ (@@socket_idle_deadlines ||= {}).delete(settings.socket)
467
+ @idle_deadlines&.delete(settings.socket)
468
+ if settings.tcp_pipelining_max_queries != :infinite
469
+ @@socket_remaining_queries[settings.socket] ||= settings.tcp_pipelining_max_queries
470
+ end
471
+ @@timeouts[id] = settings.endtime
472
+ (@packet_deadlines ||= DeadlineQueue.new)[id] = settings.endtime
473
+ @@sockets << settings.socket
474
+ @@socket_is_persistent[settings.socket] = settings.is_persistent_socket
475
+ previous_owner = (@@socket_owners ||= {})[settings.socket]
476
+ if previous_owner && !previous_owner.equal?(settings.single_resolver) &&
477
+ (previous_sockets = (@@sockets_by_sender ||= {}.compare_by_identity)[previous_owner])
478
+ previous_sockets.delete(settings.socket)
479
+ @@sockets_by_sender.delete(previous_owner) if previous_sockets.empty?
149
480
  end
481
+ (@@socket_owners ||= {})[settings.socket] = settings.single_resolver
482
+ ((@@sockets_by_sender ||= {}.compare_by_identity)[settings.single_resolver] ||= Set.new) << settings.socket
483
+ (@@retiring_sockets ||= Set.new) << settings.socket if settings.retire_connection
484
+ true
485
+ end
486
+
487
+ # Explicit close retires streams owned by this sender, including idle and
488
+ # rolled-over pipelines. Other logical queries sharing a sender may finish,
489
+ # but future sends must choose a fresh connection.
490
+ def retire_sender(sender)
491
+ state_mutex.synchronize do
492
+ refresh_validation_process
493
+ # A connect can be in progress without a socket in our read set yet.
494
+ # Carry retirement through reservation replacements so its eventual
495
+ # registration drains and closes, while later fresh sends can reuse.
496
+ ((@@queries_by_sender ||= {}.compare_by_identity)[sender] || []).each do |id|
497
+ settings = @@query_hash[id]
498
+ settings.retire_connection = true if settings
499
+ end
500
+ ((@@sockets_by_sender ||= {}.compare_by_identity)[sender] || []).to_a.each do |socket|
501
+ mark_socket_for_retirement(socket)
502
+ end
503
+ @@validations.each do |queue, owner, control|
504
+ # A Resolver owns its validation through the queue observer. Removing
505
+ # that observer cancels its work; retiring a shared or reconfigured
506
+ # sender must let other accepted responses finish. Direct PacketSender
507
+ # callers have no observer, so their sender's close still cancels them.
508
+ control.cancel if owner.equal?(sender) && !@@observers[queue]
509
+ end
510
+ end
511
+ wake_up_select_thread
512
+ end
513
+
514
+ def retire_socket(socket)
515
+ state_mutex.synchronize { mark_socket_for_retirement(socket) }
516
+ wake_up_select_thread
517
+ end
518
+
519
+ def mark_socket_for_retirement(socket)
520
+ (@@retiring_sockets ||= Set.new) << socket
521
+ if (@@socket_hash[socket] || []).empty?
522
+ socket.close rescue nil
523
+ retire_socket_from_mutex_synchronized_block(socket)
524
+ end
525
+ end
526
+ private :mark_socket_for_retirement
527
+
528
+ def query_id_in_use?(socket, header_id)
529
+ state_mutex.synchronize do
530
+ !socket_ids_for_header(socket, header_id).empty?
531
+ end
532
+ end
533
+
534
+ # Helper callers may supply a plain array; add_to_select uses SocketQueries.
535
+ # Call with @@mutex held.
536
+ def socket_ids_for_header(socket, header_id)
537
+ ids = @@socket_hash[socket]
538
+ return [] unless ids
539
+ return ids.matching(header_id) if ids.is_a?(SocketQueries)
540
+ ids.select { |id| (settings = @@query_hash[id]) && settings.query.header.id == header_id }
541
+ end
542
+ private :socket_ids_for_header
543
+
544
+ def reusable_socket?(socket)
545
+ state_mutex.synchronize do
546
+ !socket.closed? && !(@@retiring_sockets ||= Set.new).include?(socket)
547
+ end
548
+ end
549
+
550
+ # A partial TCP frame makes the whole connection unusable. Atomically claim
551
+ # its outstanding queries, then let the selector deliver their errors.
552
+ # Sender threads must never invoke resolver callbacks themselves.
553
+ def fail_tcp_send(settings, error)
554
+ state_mutex.synchronize do
555
+ settings.socket.close rescue nil
556
+ exceptions = retire_socket_from_mutex_synchronized_block(settings.socket)
557
+ exceptions.each do |eof, socket, id|
558
+ current = @@query_hash[id]
559
+ next unless current && current.socket == socket
560
+ remove_id_from_mutex_synchronized_block(id)
561
+ @@queued_exceptions << [id, current.client_queue,
562
+ current.equal?(settings) ? error : eof, nil]
563
+ end
564
+ check_select_thread_synchronized unless @@queued_exceptions.empty?
565
+ end
566
+ wake_up_select_thread
567
+ end
568
+
569
+ # A send waiting for the pipeline writer has no new socket registration yet.
570
+ # Its original attempt may have expired or been replaced in the meantime.
571
+ def fail_query_if_current(id, queue, previous_settings, error)
572
+ state_mutex.synchronize do
573
+ return unless @@query_hash[id].equal?(previous_settings)
574
+ remove_id_from_mutex_synchronized_block(id) if previous_settings
575
+ @@queued_exceptions << [id, queue, error, nil]
576
+ check_select_thread_synchronized
577
+ end
578
+ wake_up_select_thread
579
+ end
580
+
581
+ # Cache lookup can satisfy a protocol retry before it reaches the sender.
582
+ # Claim that exact attempt before publishing a result, just like a wire reply.
583
+ def push_response_for_query(settings, msg, query, resolver)
584
+ return unless claim_query(settings)
585
+ push_response_to_select(settings.client_query_id, settings.client_queue, msg, query, resolver,
586
+ settings.send_token&.transmission_budget)
587
+ end
588
+
589
+ def claim_query(settings, response_received = false)
590
+ state_mutex.synchronize do
591
+ return false unless @@query_hash[settings.client_query_id].equal?(settings)
592
+ remove_id_from_mutex_synchronized_block(settings.client_query_id, nil, response_received)
593
+ true
594
+ end
595
+ end
596
+
597
+ def wake_up_select_thread
598
+ # One unread byte is sufficient: a full socket already guarantees that
599
+ # select will wake. Never let notification block a producer or the selector
600
+ # itself (cache hits and protocol retries also produce notifications).
601
+ @@wakeup_sockets[0].write_nonblock(".")
602
+ rescue IO::WaitWritable, IOError, SystemCallError
603
+ nil
150
604
  end
151
605
 
152
606
  def check_select_thread_synchronized
153
- if (!@@select_thread.alive?)
607
+ if !@@select_thread || !@@select_thread.alive?
154
608
  Dnsruby.log.debug{"Restarting select thread"}
155
609
  @@select_thread = Thread.new {
156
610
  do_select
@@ -160,45 +614,40 @@ module Dnsruby
160
614
 
161
615
  def select_thread_alive?
162
616
  ret=true
163
- @@mutex.synchronize{
164
- ret = @@select_thread.alive?
617
+ state_mutex.synchronize{
618
+ ret = !!(@@select_thread && @@select_thread.alive?)
165
619
  }
166
620
  return ret
167
621
  end
168
622
 
169
623
  def do_select
170
624
  unused_loop_count = 0
171
- last_tick_time = Time.now - 10
172
625
  while true do
173
- if (last_tick_time < (Time.now - 0.5))
174
- send_tick_to_observers # ONLY NEED TO SEND THIS TWICE A SECOND - NOT EVERY SELECT!!!
175
- last_tick_time = Time.now
176
- end
626
+ send_tick_to_observers
627
+ expire_idle_sockets
177
628
  send_queued_exceptions
178
629
  send_queued_responses
179
630
  send_queued_validation_responses
180
- timeout = tick_time = 0.1 # We provide a timer service to various Dnsruby classes
181
- sockets, timeouts, has_observer = @@mutex.synchronize { [@@sockets.to_a, @@timeouts.values, !@@observers.empty?] }
182
- if (timeouts.length > 0)
183
- timeouts.sort!
184
- timeout = timeouts[0] - Time.now
185
- if (timeout <= 0)
186
- process_timeouts
187
- timeout = 0
188
- next
189
- end
631
+ observer_deadline = next_observer_deadline
632
+ sockets, packet_deadline, idle_deadline = state_mutex.synchronize do
633
+ [@@sockets.to_a, @packet_deadlines&.first_deadline, @idle_deadlines&.first_deadline]
190
634
  end
191
- ready=nil
192
- if (has_observer && (timeout > tick_time))
193
- timeout = tick_time
635
+ now = TransportClock.now
636
+ if packet_deadline && packet_deadline <= now
637
+ process_timeouts
638
+ next
194
639
  end
195
- # next if (timeout < 0)
640
+ deadline = [packet_deadline, observer_deadline, idle_deadline].compact.min
641
+ timeout = deadline ? [deadline - now, 0].max : 0.1
642
+ ready=nil
196
643
  begin
197
644
  ready, _write, _errors = IO.select(sockets, nil, nil, timeout)
198
645
  rescue SelectWakeup
199
646
  # If SelectWakeup, then just restart this loop - the select call will be made with the new data
200
647
  next
201
- rescue IOError, EncodeError
648
+ rescue IOError, Errno::EBADF, EncodeError
649
+ # A pipeline writer can close a socket after this loop snapshots its
650
+ # read set, or while native select is waiting on its descriptor.
202
651
  exceptions = clean_up_closed_sockets
203
652
  exceptions.each { |exception| send_exception_to_client(*exception) }
204
653
 
@@ -206,13 +655,11 @@ module Dnsruby
206
655
  end
207
656
  if ready && ready.include?(@@wakeup_sockets[1])
208
657
  ready.delete(@@wakeup_sockets[1])
209
- wakeup_msg = "loop"
210
658
  begin
211
- while wakeup_msg && wakeup_msg.length > 0
212
- wakeup_msg = @@wakeup_sockets[1].recv_nonblock(20)
213
- end
214
- rescue
215
- # do nothing
659
+ # Bound draining so continuously arriving notifications cannot starve
660
+ # responses and timeouts. Any remainder wakes the next select.
661
+ @@wakeup_sockets[1].recv_nonblock(4096)
662
+ rescue IO::WaitReadable, IOError, SystemCallError
216
663
  end
217
664
  end
218
665
  if (ready == nil)
@@ -224,43 +671,65 @@ module Dnsruby
224
671
  unused_loop_count=0
225
672
  # process_error(errors)
226
673
  end
227
- @@mutex.synchronize do
228
- if (unused_loop_count > 10 && @@query_hash.empty? && @@observers.empty?)
674
+ state_mutex.synchronize do
675
+ if (unused_loop_count > 10 && @@query_hash.empty? && @@observers.empty? &&
676
+ @@queued_exceptions.empty? && @@queued_responses.empty? &&
677
+ @@queued_validation_responses.empty?)
229
678
  Dnsruby.log.debug("Try stop select loop")
230
679
 
680
+ # We leave the @@wakeup_sockets here, but remove everything else
231
681
  non_persistent_sockets = @@sockets.select { |s| ! @@socket_is_persistent[s] }
232
682
  non_persistent_sockets.each do |socket|
233
- socket.close rescue nil
234
- @@sockets.delete(socket)
683
+ if socket != @@wakeup_sockets[1]
684
+ socket.close rescue nil
685
+ retire_socket_from_mutex_synchronized_block(socket)
686
+ end
235
687
  end
236
688
 
237
- Dnsruby.log.debug("Deleted #{non_persistent_sockets.size} non-persistent sockets," +
238
- " #{@@sockets.count} persistent sockets remain.")
689
+ Dnsruby.log.debug("Deleted #{non_persistent_sockets.size - 1} non-persistent sockets," +
690
+ " #{@@sockets.count - 1} persistent sockets remain.")
239
691
  @@socket_hash.clear
240
692
 
241
- if @@sockets.empty?
693
+ if @@sockets.size == 1
242
694
  Dnsruby.log.debug("Stopping select loop")
695
+ # A producer acquiring the mutex after this decision must start a
696
+ # replacement, even while the old Ruby thread is still returning.
697
+ @@select_thread = nil
243
698
  return
244
699
  end
245
700
  end
246
701
  end
247
- # }
248
702
  end
249
703
  end
250
704
 
251
- # Removes closed sockets from @@sockets, and returns an array containing 1
252
- # exception for each closed socket contained in @@socket_hash.
705
+ # Detach closed sockets before notifying clients. Callbacks can register
706
+ # other queries, and must never run while holding the selector mutex.
253
707
  def clean_up_closed_sockets
254
- @@mutex.synchronize do
255
- closed_sockets_in_hash = @@sockets.select(&:closed?).select { |s| @@socket_hash[s] }
256
- @@sockets.delete_if { | socket | socket.closed? }
257
- closed_sockets_in_hash.each_with_object([]) do |socket, exceptions|
258
- @@socket_hash[socket].each do | client_id |
259
- exceptions << [SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
260
- end
708
+ state_mutex.synchronize do
709
+ @@sockets.select(&:closed?).flat_map do |socket|
710
+ retire_socket_from_mutex_synchronized_block(socket)
261
711
  end
262
712
  end
263
- exceptions
713
+ end
714
+
715
+ # Called only with @@mutex held and after the socket has been closed.
716
+ def retire_socket_from_mutex_synchronized_block(socket)
717
+ ids = @@socket_hash.delete(socket) || []
718
+ @@sockets.delete(socket)
719
+ @@tcp_buffers.delete(socket)
720
+ @@socket_remaining_queries.delete(socket)
721
+ (@@socket_idle_deadlines ||= {}).delete(socket)
722
+ @idle_deadlines&.delete(socket)
723
+ @@socket_is_persistent.delete(socket)
724
+ owner = (@@socket_owners ||= {}).delete(socket)
725
+ if (sockets = (@@sockets_by_sender ||= {}.compare_by_identity)[owner])
726
+ sockets.delete(socket)
727
+ @@sockets_by_sender.delete(owner) if sockets.empty?
728
+ end
729
+ (@@retiring_sockets ||= Set.new).delete(socket)
730
+ ids.map do |client_id|
731
+ [SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
732
+ end
264
733
  end
265
734
 
266
735
  def process_error(errors)
@@ -268,92 +737,96 @@ module Dnsruby
268
737
  # @todo@ Process errors [can we do this in single socket environment?]
269
738
  end
270
739
 
740
+ # Retained for callers of this helper; response delivery now matches only
741
+ # queries registered on the receiving socket instead of scanning every ID.
271
742
  def get_active_ids(queries, id)
272
743
  queries.keys.select { |client_query_id| client_query_id[1].header.id == id }
273
744
  end
274
745
 
275
- # @@query_hash[query_settings.client_query_id]=query_settings
276
746
  def process_ready(ready)
277
- persistent_sockets, nonpersistent_sockets = @@mutex.synchronize { ready.partition { |socket| persistent?(socket) } }
747
+ persistent_sockets, nonpersistent_sockets = state_mutex.synchronize { ready.partition { |socket| persistent?(socket) } }
278
748
 
279
749
  nonpersistent_sockets.each do |socket|
280
- query_settings = @@mutex.synchronize { @@query_hash[@@socket_hash[socket][0]] }
750
+ query_settings = state_mutex.synchronize { @@query_hash[(@@socket_hash[socket] || []).first] }
281
751
  next if !query_settings
282
752
 
283
753
  udp_packet_size = query_settings.udp_packet_size
284
754
  msg, bytes = get_incoming_data(socket, udp_packet_size)
285
755
 
286
756
  process_message(msg, bytes, socket) if msg
287
-
288
- ready.delete(socket)
289
757
  end
290
758
 
291
759
  persistent_sockets.each do |socket|
292
760
  msg, bytes = get_incoming_data(socket, 0)
293
761
  process_message(msg, bytes, socket) if msg
294
- ready.delete(socket)
295
762
  end
296
763
  end
297
764
 
298
765
  def process_message(msg, bytes, socket)
299
- @@mutex.synchronize do
300
- ids = get_active_ids(@@query_hash, msg.header.id)
301
- return if ids.empty? # should be only one
302
- end
303
-
304
- answerip = msg.answerip.downcase
305
- answerfrom = msg.answerfrom.downcase
306
- answeripaddr = IPAddr.new(answerip)
307
- dest_server = IPAddr.new("0.0.0.0")
308
-
309
- begin
310
- destserveripaddr = IPAddr.new(dest_server)
311
- rescue ArgumentError
312
- # Host name not IP address
313
- end
766
+ # Delivery looks up IDs only on this connected socket, then checks the
767
+ # question and TSIG through PacketSender#check_response.
768
+ send_response_to_client(msg, bytes, socket)
769
+ end
314
770
 
315
- if (dest_server && (dest_server != '0.0.0.0') &&
316
- (answeripaddr != destserveripaddr) &&
317
- (answerfrom != dest_server))
318
- Dnsruby.log.warn("Unsolicited response received from #{answerip} instead of #{query_settings.dest_server}")
319
- else
320
- send_response_to_client(msg, bytes, socket)
321
- end
771
+ def matching_questions?(response, query)
772
+ response.question.length == query.question.length &&
773
+ response.question.zip(query.question).all? do |answer, request|
774
+ answer.qname.labels == request.qname.labels &&
775
+ answer.qtype == request.qtype && answer.qclass == request.qclass
776
+ end
322
777
  end
778
+ private :matching_questions?
323
779
 
324
780
  def send_response_to_client(msg, bytes, socket)
325
- # Figure out which client_ids we were expecting on this socket, then see if any header ids match up
326
- # @TODO@ Can get rid of this, as we only have one query per socket.
781
+ # Match the header ID against queries outstanding on this connection.
327
782
  client_ids=[]
328
- @@mutex.synchronize{
329
- client_ids = @@socket_hash[socket].clone
783
+ state_mutex.synchronize{
784
+ client_ids = socket_ids_for_header(socket, msg.header.id)
330
785
  }
331
786
  # get the queries associated with them
332
787
  client_ids.each do |id|
333
- query_header_id=nil
334
- @@mutex.synchronize{
335
- query_header_id = @@query_hash[id].query.header.id
336
- }
788
+ settings = state_mutex.synchronize { @@query_hash[id] }
789
+ next unless settings && settings.socket == socket
790
+ query_header_id = settings.query.header.id
337
791
  if (query_header_id == msg.header.id)
792
+ # IDs can collide on a pipeline. Select the matching question before
793
+ # query-specific TSIG checks or retries, which may consume the attempt.
794
+ # Preserve raw/update responses and questionless FORMERR handling.
795
+ query = settings.query
796
+ unless query.send_raw || query.kind_of?(Update) || msg.question.empty?
797
+ next unless matching_questions?(msg, query)
798
+ end
338
799
  # process the response
339
- client_queue = nil
340
- res = nil
341
- query=nil
342
- @@mutex.synchronize{
343
- client_queue = @@query_hash[id].client_queue
344
- res = @@query_hash[id].single_resolver
345
- query = @@query_hash[id].query
346
- }
347
- tcp = tcp?(socket)
800
+ client_queue = settings.client_queue
801
+ res = settings.single_resolver
802
+ query = settings.query
803
+ begin
804
+ tcp = settings.is_persistent_socket || tcp?(socket)
805
+ rescue IOError, SystemCallError
806
+ return if socket.closed?
807
+ raise
808
+ end
348
809
  # At this point, we should check if the response is OK
349
- if (ret = res.check_response(msg, bytes, query, client_queue, id, tcp))
350
- remove_id(id)
810
+ previous_context = Thread.current[:dnsruby_response_query]
811
+ begin
812
+ # check_response may synchronously retry (FORMERR or truncation).
813
+ # Carry this attempt's identity through that unchanged public API.
814
+ Thread.current[:dnsruby_response_query] = settings
815
+ ret = res.check_response(msg, bytes, query, client_queue, id, tcp)
816
+ ensure
817
+ Thread.current[:dnsruby_response_query] = previous_context
818
+ end
819
+ if ret
820
+ # A writer failure or a retry can replace/remove settings while
821
+ # check_response runs. Only one path may claim this attempt.
822
+ return unless claim_query(settings, true)
351
823
  exception = msg.get_exception
352
824
  if (ret.kind_of?TsigError)
353
825
  exception = ret
354
826
  end
355
827
  Dnsruby.log.debug{"Pushing response to client queue"}
356
- push_to_client(id, client_queue, msg, exception, query, res)
828
+ push_to_client(id, client_queue, msg, exception, query, res,
829
+ settings.send_token&.transmission_budget)
357
830
  # client_queue.push([id, msg, exception])
358
831
  # notify_queue_observers(client_queue, id)
359
832
  else
@@ -364,7 +837,6 @@ module Dnsruby
364
837
  end
365
838
  # If not, then we have an error
366
839
  Dnsruby.log.error{"Stray packet - " + msg.inspect + "\n from " + socket.inspect}
367
- print("Stray packet - " + msg.question()[0].qname.to_s + " from " + msg.answerip.to_s + ", #{client_ids.length} client_ids\n")
368
840
  end
369
841
 
370
842
  def persistent?(socket)
@@ -372,28 +844,69 @@ module Dnsruby
372
844
  end
373
845
 
374
846
  def remove_id(id)
375
- @@mutex.synchronize do
847
+ state_mutex.synchronize do
376
848
  remove_id_from_mutex_synchronized_block(id)
377
849
  end
378
850
  end
379
851
 
380
852
  # THIS MUST BE CALLED FROM INSIDE A @@mutex SYNCHRONISED BLOCK!
381
- def remove_id_from_mutex_synchronized_block(id)
382
- socket = @@query_hash[id].socket
853
+ def remove_id_from_mutex_synchronized_block(id, preserved_token = nil, response_received = false)
854
+ settings = @@query_hash.delete(id)
855
+ return unless settings
856
+ ids = @@queries_by_queue[settings.client_queue]
857
+ if ids
858
+ ids.delete(id)
859
+ @@queries_by_queue.delete(settings.client_queue) if ids.empty?
860
+ end
861
+ if (sender_ids = (@@queries_by_sender ||= {}.compare_by_identity)[settings.single_resolver])
862
+ sender_ids.delete(id)
863
+ @@queries_by_sender.delete(settings.single_resolver) if sender_ids.empty?
864
+ end
865
+ settings.send_token.cancel unless settings.send_token.equal?(preserved_token)
383
866
  @@timeouts.delete(id)
384
- @@query_hash.delete(id)
385
- @@socket_hash[socket].delete(id)
386
-
387
- decrement_remaining_queries(socket) if persistent?(socket)
867
+ @packet_deadlines&.delete(id)
868
+ socket = settings.socket
869
+ return unless socket # A queued send may not have acquired a socket yet.
870
+ (@@socket_hash[socket] || []).delete(id)
871
+
872
+ if persistent?(socket)
873
+ decrement_remaining_queries(socket)
874
+ # An unanswered query can still produce a late reply, or leave an
875
+ # incomplete frame in the reader. Drain existing peers, but never admit
876
+ # another query on this stream after its ownership has been cancelled.
877
+ (@@retiring_sockets ||= Set.new) << socket unless response_received
878
+ end
388
879
 
389
- if !persistent?(socket) || max_attained?(socket)
390
- @@sockets.delete(socket)
391
- @@socket_hash.delete(socket)
880
+ retiring = (@@retiring_sockets ||= Set.new).include?(socket) &&
881
+ (@@socket_hash[socket] || []).empty?
882
+ if !persistent?(socket) || retiring ||
883
+ (max_attained?(socket) && (@@socket_hash[socket] || []).empty?)
392
884
  Dnsruby.log.debug("Closing socket #{socket}")
393
885
  socket.close rescue nil
886
+ retire_socket_from_mutex_synchronized_block(socket)
887
+ elsif (@@socket_hash[socket] || []).empty?
888
+ # Idle time starts after the last outstanding response, never while a
889
+ # query or pipeline writer still owns the socket.
890
+ (@@socket_idle_deadlines ||= {})[socket] =
891
+ TransportClock.now + settings.tcp_idle_timeout
892
+ (@idle_deadlines ||= DeadlineQueue.new)[socket] = @@socket_idle_deadlines[socket]
394
893
  end
395
894
  end
396
895
 
896
+ def expire_idle_sockets
897
+ state_mutex.synchronize do
898
+ now = TransportClock.now
899
+ while (entry = @idle_deadlines&.first) && entry[1] <= now
900
+ socket = entry[0]
901
+ @idle_deadlines.delete(socket)
902
+ next unless (@@socket_hash[socket] || []).empty?
903
+ socket.close rescue nil
904
+ retire_socket_from_mutex_synchronized_block(socket)
905
+ end
906
+ end
907
+ end
908
+ private :expire_idle_sockets
909
+
397
910
  def decrement_remaining_queries(socket)
398
911
  if @@socket_remaining_queries[socket]
399
912
  @@socket_remaining_queries[socket] -= 1
@@ -408,12 +921,22 @@ module Dnsruby
408
921
  end
409
922
 
410
923
  def process_timeouts
411
- # NOTE: It's @@timeouts we need to protect; after the clone we're ok
412
- timeouts = @@mutex.synchronize { @@timeouts.clone }
413
- time_now = Time.now
414
- timeouts.each do |client_id, timeout|
415
- if timeout < time_now
416
- send_exception_to_client(ResolvTimeout.new("Query timed out"), nil, client_id)
924
+ # Pop only due owners under the same lock used for registrations. Keep
925
+ # settings identities so a replacement attempt cannot be expired later.
926
+ time_now = TransportClock.now
927
+ settings_due = state_mutex.synchronize do
928
+ due = []
929
+ while (first = @packet_deadlines&.first) && first[1] <= time_now
930
+ id, = first
931
+ @packet_deadlines.delete(id)
932
+ due << @@query_hash[id] if @@query_hash[id]
933
+ end
934
+ due
935
+ end
936
+ settings_due.each do |settings|
937
+ if claim_query(settings)
938
+ deliver_queue_event(settings.client_queue, [settings.client_query_id, Resolver::EventType::ERROR,
939
+ nil, settings.authentication_error || ResolvTimeout.new("Query timed out")])
417
940
  end
418
941
  end
419
942
  end
@@ -424,58 +947,67 @@ module Dnsruby
424
947
  # then process message.
425
948
  buf = +""
426
949
  expected_length = 0
427
- @@mutex.synchronize {
428
- buf, expected_length = @@tcp_buffers[socket]
950
+ reading_length = nil
951
+ state_mutex.synchronize {
952
+ return false if socket.closed?
953
+ buf, expected_length, reading_length = @@tcp_buffers[socket]
429
954
  if (!buf)
430
955
  buf = +""
431
956
  expected_length = 2
432
- @@tcp_buffers[socket]=[buf, expected_length]
957
+ reading_length = true
958
+ @@tcp_buffers[socket]=[buf, expected_length, reading_length]
433
959
  end
434
960
  }
435
961
  if (buf.length() < expected_length)
436
962
  begin
437
963
  input, = socket.recv_nonblock(expected_length-buf.length)
438
- if (input=="")
964
+ # Depending on the Ruby version, EOF is nil or an empty string.
965
+ if input.nil? || input.empty?
439
966
  Dnsruby.log.debug("EOF from server - no bytes read - closing socket")
440
- socket.close #EOF closed by server, if we were interrupted we need to resend
441
-
442
- exceptions = @@mutex.synchronize do
443
- @@sockets.delete(socket) #remove ourselves from select, app will have to retry
444
- #maybe fire an event
445
- @@socket_hash[socket].map do | client_id |
446
- [SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
447
- end
967
+ exceptions = state_mutex.synchronize do
968
+ socket.close
969
+ retire_socket_from_mutex_synchronized_block(socket)
448
970
  end
449
-
450
971
  exceptions.each { |exception| send_exception_to_client(*exception) }
451
972
 
452
973
  return false
453
974
  end
454
975
  buf << input if input
455
- rescue
456
- # Oh well - better luck next time!
976
+ rescue IO::WaitReadable, Errno::EINTR
977
+ return false
978
+ rescue IOError, SystemCallError => error
979
+ # A reset or fatal read failure cannot become readable again. Retire
980
+ # every owner once, without invoking resolver callbacks under our lock.
981
+ exceptions = state_mutex.synchronize do
982
+ socket.close rescue nil
983
+ retire_socket_from_mutex_synchronized_block(socket)
984
+ end
985
+ exceptions.each { |exception| send_exception_to_client(*exception) }
986
+ Dnsruby.log.debug { "TCP read failed: #{error.class}: #{error.message}" }
457
987
  return false
458
988
  end
459
989
  end
460
990
  # If data is complete, then return it.
461
991
  if (buf.length == expected_length)
462
- if (expected_length == 2)
992
+ if reading_length
463
993
  # We just read the data_length field. Now we need to start reading that many bytes.
464
- @@mutex.synchronize {
994
+ state_mutex.synchronize {
995
+ return false if socket.closed?
465
996
  answersize = buf.unpack('n')[0]
466
- @@tcp_buffers[socket] = [+"", answersize]
997
+ @@tcp_buffers[socket] = [+"", answersize, false]
467
998
  }
468
999
  return tcp_read(socket)
469
1000
  else
470
1001
  # We just read the data - now return it
471
- @@mutex.synchronize {
1002
+ state_mutex.synchronize {
472
1003
  @@tcp_buffers.delete(socket)
473
1004
  }
474
1005
  return buf
475
1006
  end
476
1007
  else
477
- @@mutex.synchronize {
478
- @@tcp_buffers[socket]=[buf, expected_length]
1008
+ state_mutex.synchronize {
1009
+ return false if socket.closed?
1010
+ @@tcp_buffers[socket]=[buf, expected_length, reading_length]
479
1011
  }
480
1012
  return false
481
1013
  end
@@ -484,7 +1016,14 @@ module Dnsruby
484
1016
  def get_incoming_data(socket, packet_size)
485
1017
  answerfrom,answerip,answerport,answersize=nil
486
1018
  ans,buf = nil
487
- is_tcp = tcp?(socket)
1019
+ begin
1020
+ is_tcp = tcp?(socket)
1021
+ rescue IOError, SystemCallError
1022
+ # A concurrent pipeline writer can retire this socket after IO.select
1023
+ # returned it. It has already queued errors for its outstanding queries.
1024
+ return if socket.closed?
1025
+ raise
1026
+ end
488
1027
 
489
1028
  begin
490
1029
  if is_tcp
@@ -498,12 +1037,11 @@ module Dnsruby
498
1037
  else
499
1038
  # @TODO@ Can we get recvfrom to stop issuing PTR queries when we already
500
1039
  # know both the FQDN and the IP address?
501
- if (ret = socket.recvfrom(packet_size))
1040
+ if (ret = socket.recvfrom_nonblock(packet_size))
502
1041
  buf = ret[0]
503
1042
  answerport=ret[1][1]
504
1043
  answerfrom=ret[1][2]
505
1044
  answerip=ret[1][3]
506
- answersize=(buf.length)
507
1045
  else
508
1046
  # recvfrom failed - why?
509
1047
  Dnsruby.log.error{"Error - recvfrom failed from #{socket}"}
@@ -511,72 +1049,111 @@ module Dnsruby
511
1049
  return
512
1050
  end
513
1051
  end
1052
+ rescue IO::WaitReadable, Errno::EINTR
1053
+ # Readiness is only a hint; keep the query registered for its next event.
1054
+ return
514
1055
  rescue Exception => e
515
1056
  Dnsruby.log.error{"Error - recvfrom failed from #{socket}, exception : #{e}"}
516
1057
  handle_recvfrom_failure(socket, e)
517
1058
  return
518
1059
  end
1060
+ answersize = buf.bytesize
519
1061
  Dnsruby.log.debug{";; answer from #{answerfrom} : #{answersize} bytes\n"}
520
1062
 
521
1063
  begin
522
1064
  ans = Message.decode(buf)
523
1065
 
524
- if is_tcp
525
- @@mutex.synchronize do
526
- ids = get_active_ids(@@query_hash, ans.header.id)
527
- if ids.empty?
528
- Dnsruby.log.error("Decode error from #{answerip} but can't determine packet id")
529
- #todo add error event? The problem is we don't have a valid id so we don't
530
- #know which client queue to send the exception to
531
- end
532
- answerfrom = @@query_hash[ids[0]].dest_server
533
- answerip = answerfrom
534
- answerport = @@query_hash[ids[0]].dest_port
535
- end
536
- end
537
-
538
1066
  rescue Exception => e
539
1067
  Dnsruby.log.error("Decode error! #{e.class}, #{e}\nfor msg (length=#{buf.length}) : #{buf}")
540
- client_id=get_client_id_from_answerfrom(socket, answerip, answerport)
1068
+ # A partial Message has a newly allocated, random header if decoding
1069
+ # failed before the complete header. Only the bytes identify its ID.
1070
+ header_id = buf.unpack('n').first if buf.bytesize >= 2
1071
+ client_id=get_client_id_from_answerfrom(socket, answerip, answerport, header_id)
541
1072
  if (client_id == nil)
542
1073
  Dnsruby.log.error{"Decode error from #{answerip} but can't determine packet id"}
1074
+ return
543
1075
  end
544
- # We should check if the TC bit is set (if we can get that far)
545
- if ((DecodeError === e) && (e.partial_message.header.tc))
546
- Dnsruby.log.error{"Decode error (from {answerip})! Header shows truncation, so trying again over TCP"}
547
- # If it is, then we should retry over TCP
548
- sent = false
549
- @@mutex.synchronize{
550
- client_ids = @@socket_hash[socket]
551
- # get the queries associated with them
552
- client_ids.each do |id|
553
- query_header_id=nil
554
- query_header_id = @@query_hash[id].query.header.id
555
- if (query_header_id == e.partial_message.header.id)
556
- # process the response
557
- client_queue = @@query_hash[id].client_queue
558
- res = @@query_hash[id].single_resolver
559
- query = @@query_hash[id].query
560
- remove_id_from_mutex_synchronized_block(id)
561
-
562
- # NOW RESEND OVER TCP!
563
- Thread.new {
564
- res.send_async(query, client_queue, id, true)
565
- }
566
- sent = true
567
- end
1076
+ unless is_tcp
1077
+ matches_question = state_mutex.synchronize do
1078
+ current = @@query_hash[client_id]
1079
+ partial = e.partial_message if e.is_a?(DecodeError)
1080
+ current && current.socket == socket && (current.query.send_raw ||
1081
+ (buf.bytesize >= 12 && partial &&
1082
+ (current.query.kind_of?(Update) ||
1083
+ (!partial.question.empty? && partial.header.qdcount == partial.question.length &&
1084
+ matching_questions?(partial, current.query)))))
1085
+ end
1086
+ # A truncated answer can justify TCP fallback only after its question
1087
+ # identifies this query (RFC 5452 section 9.1). Incomplete headers or
1088
+ # questions and unrelated questions cannot end the UDP transaction.
1089
+ return unless matches_question
1090
+ end
1091
+ # Decoding may fail after a complete header. Such a packet must match
1092
+ # the response direction and opcode before it can retry or fail its
1093
+ # owner, just as a successfully decoded response must. Use wire bytes:
1094
+ # an incomplete decoded Message can still contain a default header.
1095
+ if buf.bytesize >= 12
1096
+ flags = buf.unpack('n2').last
1097
+ matches_header = state_mutex.synchronize do
1098
+ current = @@query_hash[client_id]
1099
+ current && current.socket == socket && (current.query.send_raw ||
1100
+ ((flags & 0x8000) != 0 && ((flags >> 11) & 15) == current.query.header.opcode.code))
1101
+ end
1102
+ return unless matches_header
1103
+ end
1104
+ # No undecodable packet can authenticate its TSIG. Discard it before
1105
+ # either failing the transaction or acting on an untrusted TC bit, just
1106
+ # as check_response discards unsigned or incorrectly signed replies.
1107
+ unauthenticated = state_mutex.synchronize do
1108
+ current = @@query_hash[client_id]
1109
+ if current && current.socket == socket && current.query.tsig && !current.query.send_raw
1110
+ current.authentication_error ||= TsigError.new('Cannot authenticate malformed response')
1111
+ true
1112
+ end
1113
+ end
1114
+ return if unauthenticated
1115
+ # Truncation permits a UDP-to-TCP fallback, never another TCP attempt.
1116
+ # Repeating malformed TCP replies must not renew the packet deadline.
1117
+ settings = nil
1118
+ if !is_tcp && (DecodeError === e) && e.partial_message && e.partial_message.header.tc
1119
+ settings = state_mutex.synchronize do
1120
+ current = @@query_hash[client_id]
1121
+ if current && current.socket == socket && !current.ignore_truncation && !current.no_tcp
1122
+ current
568
1123
  end
569
- }
570
- if !sent
571
- send_exception_to_client(e, socket, client_id)
572
1124
  end
573
-
1125
+ end
1126
+ if settings
1127
+ Dnsruby.log.error{"Decode error (from #{answerip})! Header shows truncation, so trying again over TCP"}
1128
+ previous_context = Thread.current[:dnsruby_response_query]
1129
+ begin
1130
+ Thread.current[:dnsruby_response_query] = settings
1131
+ settings.single_resolver.send_async(settings.query, settings.client_queue, client_id, true)
1132
+ ensure
1133
+ Thread.current[:dnsruby_response_query] = previous_context
1134
+ end
574
1135
  else
575
1136
  send_exception_to_client(e, socket, client_id)
576
1137
  end
577
1138
  return
578
1139
  end
579
1140
 
1141
+ if is_tcp
1142
+ # This supplies peer metadata only. Normal response checks below still
1143
+ # validate the question and TSIG, including sequential duplicate IDs.
1144
+ settings = state_mutex.synchronize do
1145
+ id = socket_ids_for_header(socket, ans.header.id).first
1146
+ @@query_hash[id]
1147
+ end
1148
+ unless settings
1149
+ Dnsruby.log.debug { "Ignoring TCP response with no matching query on this socket" }
1150
+ return
1151
+ end
1152
+ answerfrom = settings.dest_server
1153
+ answerip = answerfrom
1154
+ answerport = settings.dest_port
1155
+ end
1156
+
580
1157
  if (ans!= nil)
581
1158
  Dnsruby.log.debug{"#{ans}"}
582
1159
  ans.answerfrom=(answerfrom)
@@ -590,14 +1167,15 @@ module Dnsruby
590
1167
  # No way to notify the client about this error, unless there was only one connection on the socket
591
1168
  # Not a problem, as there only will ever be one connection on the socket (Kaminsky attack mitigation)
592
1169
  ids_for_socket = []
593
- @@mutex.synchronize{
594
- ids_for_socket = @@socket_hash[socket]
1170
+ state_mutex.synchronize{
1171
+ ids_for_socket = (@@socket_hash[socket] || []).to_a
595
1172
  }
1173
+ return if ids_for_socket.empty?
596
1174
  if (ids_for_socket.length == 1)
597
1175
  answerfrom=nil
598
- @@mutex.synchronize{
1176
+ state_mutex.synchronize{
599
1177
  query_settings = @@query_hash[ids_for_socket[0]]
600
- answerfrom=query_settings.dest_server
1178
+ answerfrom=query_settings.dest_server if query_settings
601
1179
  }
602
1180
  send_exception_to_client(OtherResolvError.new("recvfrom failed from #{answerfrom}; #{exception}"), socket, ids_for_socket[0])
603
1181
  else
@@ -605,16 +1183,33 @@ module Dnsruby
605
1183
  end
606
1184
  end
607
1185
 
608
- def get_client_id_from_answerfrom(socket, answerip, answerport)
609
- # @TODO@ Can get rid of this, as there is only one query per socket
1186
+ def get_client_id_from_answerfrom(socket, answerip, answerport, header_id=nil)
1187
+ # A pipelined socket can have several outstanding queries.
610
1188
  client_id=nil
611
1189
  # Figure out client id from answerfrom
612
- @@mutex.synchronize{
613
- ids = @@socket_hash[socket]
1190
+ state_mutex.synchronize{
1191
+ ids = (header_id ? socket_ids_for_header(socket, header_id) :
1192
+ (@@socket_hash[socket] || [])).select { |id| @@query_hash[id] }
1193
+ # TCP decoding can fail before the peer address has been assigned. The
1194
+ # socket and partial header identify its query, including pipeline use.
1195
+ if answerip.nil?
1196
+ if header_id
1197
+ matching = ids.select { |id| @@query_hash[id].query.header.id == header_id }
1198
+ return matching.first if matching.length == 1
1199
+ return nil
1200
+ end
1201
+ return ids.first if ids.length == 1
1202
+ return nil
1203
+ end
614
1204
  ids.each do |id|
615
1205
  # Does this id speak to this dest_server?
616
1206
  query_settings = @@query_hash[id]
617
- if (answerip == query_settings.dest_server && answerport == query_settings.dest_port)
1207
+ next if header_id && query_settings.query.header.id != header_id
1208
+ # recvfrom may spell a numeric zone as an interface name. The socket
1209
+ # is already connected to the scoped peer, so compare address bytes.
1210
+ source = IPAddr.new(answerip.split('%', 2).first)
1211
+ destination = IPAddr.new(query_settings.dest_server.to_s.split('%', 2).first)
1212
+ if (source == destination && answerport == query_settings.dest_port)
618
1213
  # We have a match
619
1214
  client_id = id
620
1215
  break
@@ -627,47 +1222,36 @@ module Dnsruby
627
1222
  def send_exception_to_client(err, socket, client_id, msg=nil)
628
1223
  # find the client response queue
629
1224
  client_queue = nil
630
- @@mutex.synchronize {
631
- client_queue = @@query_hash[client_id].client_queue
1225
+ state_mutex.synchronize {
1226
+ settings = @@query_hash[client_id]
1227
+ return unless settings && (!socket || settings.socket == socket)
1228
+ client_queue = settings.client_queue
1229
+ remove_id_from_mutex_synchronized_block(client_id)
632
1230
  }
633
- remove_id(client_id)
634
1231
  # push_to_client(client_id, client_queue, msg, err)
635
- client_queue.push([client_id, Resolver::EventType::ERROR, msg, err])
636
- notify_queue_observers(client_queue, client_id)
1232
+ deliver_queue_event(client_queue, [client_id, Resolver::EventType::ERROR, msg, err])
637
1233
  end
638
1234
 
639
1235
  def push_exception_to_select(client_id, client_queue, err, msg)
640
- @@mutex.synchronize{
1236
+ state_mutex.synchronize{
641
1237
  @@queued_exceptions.push([client_id, client_queue, err, msg])
1238
+ check_select_thread_synchronized
642
1239
  }
643
- # Make sure select loop is running!
644
- if (@@select_thread && @@select_thread.alive?)
645
- else
646
- @@select_thread = Thread.new {
647
- do_select
648
- }
649
- end
650
1240
  wake_up_select_thread
651
1241
  end
652
1242
 
653
- def push_response_to_select(client_id, client_queue, msg, query, res)
1243
+ def push_response_to_select(client_id, client_queue, msg, query, res, transmission_budget = nil)
654
1244
  # This needs to queue the response TO THE SELECT THREAD, which then needs
655
1245
  # to send it out from its normal loop.
656
1246
  Dnsruby.log.debug{"Pushing response to client queue direct from resolver or validator"}
657
- @@mutex.synchronize{
1247
+ state_mutex.synchronize{
658
1248
  err = nil
659
1249
  if (msg.rcode == RCode.NXDOMAIN)
660
1250
  err = NXDomain.new
661
1251
  end
662
- @@queued_responses.push([client_id, client_queue, msg, err, query, res])
1252
+ @@queued_responses.push([client_id, client_queue, msg, err, query, res, transmission_budget])
1253
+ check_select_thread_synchronized
663
1254
  }
664
- # Make sure select loop is running!
665
- if (@@select_thread && @@select_thread.alive?)
666
- else
667
- @@select_thread = Thread.new {
668
- do_select
669
- }
670
- end
671
1255
  wake_up_select_thread
672
1256
  end
673
1257
 
@@ -675,22 +1259,16 @@ module Dnsruby
675
1259
  # This needs to queue the response TO THE SELECT THREAD, which then needs
676
1260
  # to send it out from its normal loop.
677
1261
  Dnsruby.log.debug{"Pushing response to client queue direct from resolver or validator"}
678
- @@mutex.synchronize{
1262
+ state_mutex.synchronize{
679
1263
  @@queued_validation_responses.push([client_id, client_queue, msg, err, query, res])
1264
+ check_select_thread_synchronized
680
1265
  }
681
- # Make sure select loop is running!
682
- if (@@select_thread && @@select_thread.alive?)
683
- else
684
- @@select_thread = Thread.new {
685
- do_select
686
- }
687
- end
688
1266
  wake_up_select_thread
689
1267
  end
690
1268
 
691
1269
  def send_queued_exceptions
692
1270
  exceptions = []
693
- @@mutex.synchronize{
1271
+ state_mutex.synchronize{
694
1272
  exceptions = @@queued_exceptions
695
1273
  @@queued_exceptions = []
696
1274
  }
@@ -698,23 +1276,21 @@ module Dnsruby
698
1276
  exceptions.each do |item|
699
1277
  client_id, client_queue, err, msg = item
700
1278
  # push_to_client(client_id, client_queue, msg, err)
701
- client_queue.push([client_id, Resolver::EventType::ERROR, msg, err])
702
- notify_queue_observers(client_queue, client_id)
1279
+ deliver_queue_event(client_queue, [client_id, Resolver::EventType::ERROR, msg, err])
703
1280
  end
704
1281
  end
705
1282
 
706
1283
  def send_queued_responses
707
1284
  responses = []
708
- @@mutex.synchronize{
1285
+ state_mutex.synchronize{
709
1286
  responses = @@queued_responses
710
1287
  @@queued_responses = []
711
1288
  }
712
1289
 
713
1290
  responses.each do |item|
714
- client_id, client_queue, msg, err, query, res = item
1291
+ client_id, client_queue, msg, err, query, res, transmission_budget = item
715
1292
  # push_to_client(client_id, client_queue, msg, err)
716
- client_queue.push([client_id, Resolver::EventType::RECEIVED, msg, err])
717
- notify_queue_observers(client_queue, client_id)
1293
+ next unless deliver_queue_event(client_queue, [client_id, Resolver::EventType::RECEIVED, msg, err])
718
1294
  # Do we need to validate this? The response has come from the cache -
719
1295
  # validate it only if it has not been validated already
720
1296
  # So, if we need to validate it, send it to the validation thread
@@ -722,19 +1298,18 @@ module Dnsruby
722
1298
  if (((msg.security_level == Message::SecurityLevel.UNCHECKED) ||
723
1299
  (msg.security_level == Message::SecurityLevel.INDETERMINATE)) &&
724
1300
  (ValidatorThread.requires_validation?(query, msg, err, res)))
725
- validator = ValidatorThread.new(client_id, client_queue, msg, err, query ,self, res)
1301
+ validator = ValidatorThread.new(client_id, client_queue, msg, err, query, self, res, transmission_budget)
726
1302
  validator.run
727
1303
  else
728
1304
  PacketSender.cache(query, msg) # The validator won't cache it, so we'd better do it now
729
- client_queue.push([client_id, Resolver::EventType::VALIDATED, msg, err])
730
- notify_queue_observers(client_queue, client_id)
1305
+ deliver_queue_event(client_queue, [client_id, Resolver::EventType::VALIDATED, msg, err])
731
1306
  end
732
1307
  end
733
1308
  end
734
1309
 
735
1310
  def send_queued_validation_responses
736
1311
  responses = []
737
- @@mutex.synchronize{
1312
+ state_mutex.synchronize{
738
1313
  responses = @@queued_validation_responses
739
1314
  @@queued_validation_responses = []
740
1315
  }
@@ -742,77 +1317,197 @@ module Dnsruby
742
1317
  responses.each do |item|
743
1318
  client_id, client_queue, msg, err, _query, _res = item
744
1319
  # push_to_client(client_id, client_queue, msg, err)
745
- client_queue.push([client_id, Resolver::EventType::VALIDATED, msg, err])
746
- notify_queue_observers(client_queue, client_id)
1320
+ deliver_queue_event(client_queue, [client_id, Resolver::EventType::VALIDATED, msg, err])
747
1321
  end
748
1322
  end
749
1323
 
750
- def push_to_client(client_id, client_queue, msg, err, query, res)
751
- # @TODO@ Really need to let the client know that we have received a valid response!
752
- # Can do that by calling notify_observers here, but with an identifier which
753
- # defines the response to be a "Response received - validating. Please stop sending"
754
- # type of response.
755
- client_queue.push([client_id, Resolver::EventType::RECEIVED, msg, err])
756
- notify_queue_observers(client_queue, client_id)
1324
+ def push_to_client(client_id, client_queue, msg, err, query, res, transmission_budget = nil)
1325
+ # Notify the resolver before validation so it stops competing attempts
1326
+ # while retaining the logical query's total deadline.
1327
+ return unless deliver_queue_event(client_queue, [client_id, Resolver::EventType::RECEIVED, msg, err])
757
1328
 
758
1329
  if (!err || (err.instance_of?(NXDomain)))
759
- #
760
- # This method now needs to push the response to the validator,
761
- # which will then take responsibility for delivering it to the client.
762
- # The validator will need access to the queue observers -
763
- validator = ValidatorThread.new(client_id, client_queue, msg, err, query ,self, res)
1330
+ # The validator owns delivery of the validation result.
1331
+ validator = ValidatorThread.new(client_id, client_queue, msg, err, query, self, res, transmission_budget)
764
1332
  validator.run
765
- # @@validator.add_to_queue([client_id, client_queue, msg, err, query, self, res])
766
1333
  end
767
1334
  end
768
1335
 
769
1336
  def add_observer(client_queue, observer)
770
- @@mutex.synchronize {
771
- @@observers[client_queue]=observer
772
- check_select_thread_synchronized # Is this really necessary? The client should start the thread by sending a query, really...
773
- if (!@@tick_observers.include?observer)
774
- @@tick_observers.push(observer)
1337
+ state_mutex.synchronize {
1338
+ (@@observed_queues ||= ObjectSpace::WeakMap.new)[client_queue] = true
1339
+ previous = @@observers[client_queue]
1340
+ unless previous.equal?(observer)
1341
+ remove_observer_queue_count(previous) if previous
1342
+ @@observers[client_queue] = observer
1343
+ count = (@@observer_queue_counts[observer] || 0) + 1
1344
+ @@observer_queue_counts[observer] = count
1345
+ @@tick_observers.push(observer) if count == 1
775
1346
  end
1347
+ check_select_thread_synchronized
776
1348
  }
1349
+ # An empty resolver still has a logical deadline but no socket to wake us.
1350
+ wake_up_select_thread
777
1351
  end
778
1352
 
1353
+ # Take the observer snapshot before acquiring its Resolver lock: Resolver
1354
+ # completion acquires these locks in the opposite direction.
1355
+ def validation_deadline(client_queue, client_query_id)
1356
+ observer = state_mutex.synchronize { @@observers[client_queue] }
1357
+ observer.validation_deadline(client_queue, client_query_id) if observer.respond_to?(:validation_deadline)
1358
+ end
1359
+
1360
+ def validation_transmission_budget(client_queue, client_query_id)
1361
+ observer = state_mutex.synchronize { @@observers[client_queue] }
1362
+ if observer.respond_to?(:validation_transmission_budget)
1363
+ observer.validation_transmission_budget(client_queue, client_query_id)
1364
+ end
1365
+ end
1366
+
1367
+ def register_validation(client_queue, sender, control)
1368
+ state_mutex.synchronize do
1369
+ refresh_validation_process
1370
+ # A response may have been accepted just before close or its total
1371
+ # deadline. Remember prior observers weakly so rejected registrations
1372
+ # cannot keep completed client queues alive.
1373
+ if client_queue.closed? ||
1374
+ ((@@observed_queues ||= ObjectSpace::WeakMap.new)[client_queue] && !@@observers[client_queue])
1375
+ control.cancel
1376
+ return false
1377
+ end
1378
+ # The validation pool is small and bounded. Keep one registration list
1379
+ # rather than maintaining separate indexes for queues and senders.
1380
+ unless @@validations.any? { |queue, owner, token|
1381
+ queue.equal?(client_queue) && owner.equal?(sender) && token.equal?(control) }
1382
+ @@validations << [client_queue, sender, control]
1383
+ end
1384
+ true
1385
+ end
1386
+ end
1387
+
1388
+ def unregister_validation(client_queue, sender, control)
1389
+ state_mutex.synchronize do
1390
+ refresh_validation_process
1391
+ @@validations.delete_if do |queue, owner, token|
1392
+ queue.equal?(client_queue) && owner.equal?(sender) && token.equal?(control)
1393
+ end
1394
+ end
1395
+ end
1396
+
1397
+ # Caller holds @@mutex. Parent workers do not survive fork, so they cannot
1398
+ # unregister their inherited entries in the child. Refresh before adding a
1399
+ # child registration, independently of the validation worker pool restart.
1400
+ def refresh_validation_process
1401
+ pid = Process.pid
1402
+ return if defined?(@@validation_pid) && @@validation_pid == pid
1403
+ @@validation_pid = pid
1404
+ @@validations = []
1405
+ end
1406
+ private :refresh_validation_process
1407
+
1408
+ def cancel_validations_for_queue_synchronized(client_queue)
1409
+ refresh_validation_process
1410
+ @@validations.each do |queue, _sender, control|
1411
+ control.cancel if queue.equal?(client_queue)
1412
+ end
1413
+ end
1414
+ private :cancel_validations_for_queue_synchronized
1415
+
1416
+ # Stop packet attempts without removing the logical query's observer. A
1417
+ # received response can still need validation and its overall deadline.
1418
+ def cancel_queries_for_queue(client_queue)
1419
+ state_mutex.synchronize { cancel_queries_for_queue_synchronized(client_queue) }
1420
+ wake_up_select_thread
1421
+ end
1422
+
1423
+ def cancel_queries_for_queue_synchronized(client_queue)
1424
+ ids = @@queries_by_queue[client_queue]
1425
+ ids.to_a.each { |id| remove_id_from_mutex_synchronized_block(id) } if ids
1426
+ end
1427
+ private :cancel_queries_for_queue_synchronized
1428
+
1429
+ def remove_observer_queue_count(observer)
1430
+ count = @@observer_queue_counts[observer] - 1
1431
+ if count == 0
1432
+ @@observer_queue_counts.delete(observer)
1433
+ @@tick_observers.delete(observer)
1434
+ else
1435
+ @@observer_queue_counts[observer] = count
1436
+ end
1437
+ end
1438
+ private :remove_observer_queue_count
1439
+
779
1440
  def remove_observer(client_queue, observer)
780
- @@mutex.synchronize {
1441
+ state_mutex.synchronize {
781
1442
  if (@@observers[client_queue]==observer)
782
- # @@observers.delete(observer)
783
- @@observers.delete(client_queue)
1443
+ registered_observer = @@observers.delete(client_queue)
1444
+ # Resolver completion/close owns every packet attempt for this queue,
1445
+ # including queued sends which have not opened a socket yet.
1446
+ cancel_queries_for_queue_synchronized(client_queue)
1447
+ cancel_validations_for_queue_synchronized(client_queue)
1448
+ remove_observer_queue_count(registered_observer)
784
1449
  else
785
- if (@@observers[client_queue] == nil)
786
- end
787
1450
  Dnsruby.log.error{"remove_observer called with wrong observer for queue"}
788
1451
  raise ArgumentError.new("remove_observer called with wrong observer for queue")
789
1452
  end
790
- if (!@@observers.values.include?observer)
791
- @@tick_observers.delete(observer)
792
- end
793
1453
  }
794
1454
  end
795
1455
 
796
1456
  def notify_queue_observers(client_queue, client_query_id)
797
1457
  # If any observers are known for this query queue then notify them
798
1458
  observer=nil
799
- @@mutex.synchronize {
1459
+ state_mutex.synchronize {
800
1460
  observer = @@observers[client_queue]
801
1461
  }
802
1462
  if (observer)
803
1463
  observer.handle_queue_event(client_queue, client_query_id)
804
1464
  end
1465
+ rescue ClosedQueueError
1466
+ Dnsruby.log.debug { 'Observer could not deliver to a closed client queue' }
805
1467
  end
806
1468
 
807
- def send_tick_to_observers
808
- # If any observers are known then send them a tick
809
- tick_observers=nil
810
- @@mutex.synchronize {
811
- tick_observers = @@tick_observers
812
- }
1469
+ def deliver_queue_event(client_queue, event)
1470
+ client_queue.push(event)
1471
+ notify_queue_observers(client_queue, event.first)
1472
+ true
1473
+ rescue ClosedQueueError
1474
+ # A direct PacketSender caller can close its queue while queries are
1475
+ # pending. Cancel those queries so other clients can keep running.
1476
+ state_mutex.synchronize do
1477
+ cancel_queries_for_queue_synchronized(client_queue)
1478
+ cancel_validations_for_queue_synchronized(client_queue)
1479
+ observer = @@observers.delete(client_queue)
1480
+ remove_observer_queue_count(observer) if observer
1481
+ end
1482
+ Dnsruby.log.debug { 'Discarding event for closed client queue' }
1483
+ false
1484
+ end
1485
+ private :deliver_queue_event
1486
+
1487
+ def send_tick_to_observers(due_only = true)
1488
+ # Observer callbacks acquire Resolver locks, so never call them under the
1489
+ # selector lock. Tick only observers with a retry or query deadline due.
1490
+ tick_observers = state_mutex.synchronize { @@tick_observers.dup }
813
1491
  tick_observers.each do |observer|
814
- observer.tick
1492
+ begin
1493
+ if observer.respond_to?(:next_tick_at)
1494
+ deadline = observer.next_tick_at
1495
+ observer.tick if !due_only || (deadline && deadline <= TransportClock.now)
1496
+ else
1497
+ observer.tick
1498
+ end
1499
+ rescue ClosedQueueError
1500
+ Dnsruby.log.debug { 'Observer tick could not deliver to a closed client queue' }
1501
+ end
815
1502
  end
816
1503
  end
1504
+
1505
+ def next_observer_deadline
1506
+ observers = state_mutex.synchronize { @@tick_observers.dup }
1507
+ observers.map do |observer|
1508
+ observer.respond_to?(:next_tick_at) ? observer.next_tick_at : TransportClock.now + 0.1
1509
+ end.compact.min
1510
+ end
1511
+ private :next_observer_deadline
817
1512
  end
818
1513
  end