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
@@ -15,6 +15,7 @@
15
15
  # ++
16
16
  require 'dnsruby/select_thread'
17
17
  require 'ipaddr'
18
+ require 'securerandom'
18
19
  # require 'dnsruby/iana_ports'
19
20
  module Dnsruby
20
21
 
@@ -24,40 +25,41 @@ module Dnsruby
24
25
 
25
26
  @@authoritative_cache = Cache.new
26
27
  @@recursive_cache = Cache.new
28
+ PIPELINE_MUTEX_CREATION = Mutex.new
29
+ private_constant :PIPELINE_MUTEX_CREATION
27
30
 
28
31
 
29
- def PacketSender.cache(query, response)
30
- return if response.cached
31
- # ONLY cache the response if it is not an update response
32
+ def PacketSender.cache(query, response, scope: response.instance_variable_get(:@dnsruby_cache_scope))
33
+ return if response.cached || query.send_raw || scope == false || !query.do_caching || query.class == Update
32
34
  question = query.question()[0]
33
- if (query.do_caching && (query.class != Update) &&
34
- (question.qtype != Types::AXFR) && (question.qtype != Types::IXFR) &&
35
- (response.rcode == RCode::NOERROR) &&(!response.tsig) &&
36
- (query.class != Update) &&
37
- (response.header.ancount > 0))
38
- # # @TODO@ What about TSIG-signed responses?
39
- # Don't cache any packets with "*" in the query name! (RFC1034 sec 4.3.3)
40
- if (!question.qname.to_s.include? "*")
41
- # Now cache response RRSets
42
- if (query.header.rd)
43
- PacketSender.cache_recursive(response);
44
- else
45
- PacketSender.cache_authoritative(response);
46
- end
47
- end
35
+ return unless question && question.qtype != Types::AXFR && question.qtype != Types::IXFR &&
36
+ response.rcode == RCode::NOERROR && response.header.ancount > 0
37
+ # TSIG authentication belongs to this transaction. Verification removes
38
+ # the response's TSIG record, so its absence does not make it cacheable.
39
+ return if query.tsig || query.signed? || response.signed? || response.tsig
40
+ # Alias-bearing NODATA belongs to the scoped negative cache, even
41
+ # though its CNAME records make the answer section nonempty.
42
+ return if Cache.negative_soa(response)
43
+ # Don't cache packets with "*" in the query name (RFC 1034 section 4.3.3).
44
+ return if question.qname.to_s.include?('*')
45
+ if query.header.rd
46
+ PacketSender.cache_recursive(response, scope: scope)
47
+ else
48
+ PacketSender.cache_authoritative(response, scope: scope)
48
49
  end
49
50
  end
50
51
 
51
- def PacketSender.cache_authoritative(answer)
52
+ def PacketSender.cache_authoritative(answer, scope: nil)
52
53
  return if !answer.header.aa
53
- @@authoritative_cache.add(answer)
54
+ @@authoritative_cache.add(answer, scope: scope)
54
55
  end
55
56
 
56
- def PacketSender.cache_recursive(answer)
57
- @@recursive_cache.add(answer)
57
+ def PacketSender.cache_recursive(answer, scope: nil)
58
+ @@recursive_cache.add(answer, scope: scope)
58
59
  end
59
60
 
60
61
  def PacketSender.clear_caches
62
+ Cache.invalidate_resolutions
61
63
  @@recursive_cache.clear
62
64
  @@authoritative_cache.clear
63
65
  end
@@ -86,6 +88,18 @@ module Dnsruby
86
88
  # Limit the number of queries per pipeline
87
89
  attr_accessor :tcp_pipelining_max_queries
88
90
 
91
+ # Maximum idle time for a reusable TCP connection, in seconds.
92
+ def tcp_idle_timeout
93
+ @tcp_idle_timeout || Resolver::DefaultTcpIdleTimeout
94
+ end
95
+
96
+ def tcp_idle_timeout=(seconds)
97
+ unless seconds.is_a?(Numeric) && seconds.finite? && seconds > 0
98
+ raise ArgumentError, 'TCP idle timeout must be a positive finite number'
99
+ end
100
+ @tcp_idle_timeout = seconds
101
+ end
102
+
89
103
  # Use UDP only - don't use TCP
90
104
  # For test/debug purposes only
91
105
  # Defaults to false
@@ -109,16 +123,15 @@ module Dnsruby
109
123
  # Defaults to true
110
124
  attr_accessor :recurse
111
125
 
112
- # The max UDP packet size
113
- #
114
- # Defaults to 512
126
+ # Maximum UDP DNS payload size. Defaults to 1232 bytes; the minimum is 512.
127
+ # A caller-supplied OPT record sets the receive size for that query.
115
128
  attr_reader :udp_size
116
129
 
117
130
  # The address of the resolver to send queries to
118
131
  attr_reader :server
119
132
 
120
133
  # Use DNSSEC for this PacketSender
121
- # dnssec defaults to ON
134
+ # dnssec defaults to false
122
135
  attr_reader :dnssec
123
136
 
124
137
  # Set the source address. If the arg is nil, do nothing
@@ -138,6 +151,8 @@ module Dnsruby
138
151
  # Sets the TSIG to sign outgoing messages with.
139
152
  # Pass in either a Dnsruby::RR::TSIG, or a key_name and key (or just a key)
140
153
  # Pass in nil to stop tsig signing.
154
+ # Defaults to hmac-sha256; the algorithm must match the server's key.
155
+ # You can select hmac-md5 for legacy servers, which logs a warning.
141
156
  # It is possible for client code to sign packets prior to sending - see
142
157
  # Dnsruby::RR::TSIG#apply and Dnsruby::Message#sign
143
158
  # Note that pre-signed packets will not be signed by PacketSender.
@@ -152,8 +167,8 @@ module Dnsruby
152
167
  @dnssec=on
153
168
  if (on)
154
169
  # Set the UDP size (RFC 4035 section 4.1)
155
- if (udp_packet_size < Resolver::MinDnssecUdpSize)
156
- self.udp_size = Resolver::MinDnssecUdpSize
170
+ if (udp_packet_size < Resolver::AbsoluteMinDnssecUdpSize)
171
+ self.udp_size = Resolver::AbsoluteMinDnssecUdpSize
157
172
  end
158
173
  end
159
174
  end
@@ -176,6 +191,7 @@ module Dnsruby
176
191
  # * :use_tcp
177
192
  # * :tcp_pipelining
178
193
  # * :tcp_pipelining_max_queries
194
+ # * :tcp_idle_timeout
179
195
  # * :no_tcp
180
196
  # * :ignore_truncation
181
197
  # * :src_address
@@ -202,7 +218,9 @@ module Dnsruby
202
218
  @recurse = true
203
219
  @tcp_pipelining = false
204
220
  @tcp_pipelining_max_queries = :infinite
221
+ @tcp_idle_timeout = Resolver::DefaultTcpIdleTimeout
205
222
  @use_counts = {}
223
+ @process_pid = Process.pid
206
224
 
207
225
  ignore_config_resolv_errors = false;
208
226
  if arg.nil?
@@ -223,6 +241,7 @@ module Dnsruby
223
241
  end
224
242
  end
225
243
  rescue Exception => e
244
+ raise if attr.to_s == 'tsig' || ValidationBudget.abort?(e)
226
245
  Dnsruby.log.error { "PacketSender : Argument #{attr}, #{arg[attr]} not valid : #{e}\n" }
227
246
  end
228
247
  end
@@ -251,7 +270,7 @@ module Dnsruby
251
270
  @ipv6=false
252
271
  rescue Exception
253
272
  begin
254
- IPv6.create(@server)
273
+ Config.ipv6_server_address(@server)
255
274
  # @src_address6 = '::'
256
275
  @ipv6=true
257
276
  rescue Exception
@@ -260,9 +279,61 @@ module Dnsruby
260
279
  end
261
280
  end
262
281
 
282
+ # A child must never write to a stream shared with its parent, including
283
+ # connections that have not yet been registered with SelectThread.
284
+ def ensure_current_process
285
+ return if @process_pid == Process.pid
286
+ selector = @selector
287
+ selector.send(:ensure_current_process) if selector
288
+ PIPELINE_MUTEX_CREATION.synchronize do
289
+ if @process_pid != Process.pid
290
+ ((@use_counts || {}).keys + [@pipeline_socket]).compact.uniq.each do |socket|
291
+ begin
292
+ socket.close unless socket.closed?
293
+ rescue IOError, SystemCallError
294
+ end
295
+ end
296
+ @use_counts = {}
297
+ @pipeline_socket = @pipeline_socket_configuration = @pipeline_socket_max_queries = nil
298
+ @pipeline_mutex = @pipeline_connect_context = @tcp_pipeline_local_port = nil
299
+ @selector = nil
300
+ @process_pid = Process.pid
301
+ end
302
+ end
303
+ end
304
+ private :ensure_current_process
305
+
263
306
  def close
264
- # @TODO@ What about closing?
265
- # Any queries to complete? Sockets to close?
307
+ ensure_current_process
308
+ # Idle streams close immediately; streams still serving another Resolver
309
+ # drain before closing. Do not wait for a writer blocked in connect.
310
+ @selector.retire_sender(self) if @selector
311
+ end
312
+
313
+ # Reserve ownership while Resolver still holds its query lock, then perform
314
+ # the synchronous public send after releasing that lock. close/timeout can
315
+ # cancel this reservation even before send_async has started.
316
+ def prepare_send_async(message, queue, id, retire_connection = false, control = QueryControl.current, transmission_budget = nil) #:nodoc:
317
+ ensure_current_process
318
+ selector = @selector = SelectThread.instance
319
+ pending = SelectThread::QuerySettings.new(nil, message, @ignore_truncation,
320
+ queue, id, nil, @server, @port, TransportClock.now + @packet_timeout, udp_packet_size, self)
321
+ pending.send_token = SelectThread::SendToken.new(control, transmission_budget)
322
+ pending.endtime = [pending.endtime, control&.deadline].compact.min
323
+ pending.retire_connection = retire_connection
324
+ return unless selector.reserve_send(pending)
325
+ -> do
326
+ next unless selector.query_settings_for_id(id).equal?(pending)
327
+ previous_context = Thread.current[:dnsruby_response_query]
328
+ begin
329
+ Thread.current[:dnsruby_response_query] = pending
330
+ send_async(message, queue, id)
331
+ rescue StandardError => error
332
+ selector.fail_query_if_current(id, queue, pending, error)
333
+ ensure
334
+ Thread.current[:dnsruby_response_query] = previous_context
335
+ end
336
+ end
266
337
  end
267
338
 
268
339
  # Asynchronously send a Message to the server. The send can be done using just
@@ -306,6 +377,7 @@ module Dnsruby
306
377
  # validation for this particular packet (assuming SingleResolver#dnssec == true)
307
378
  # Note that these options should not normally be used!
308
379
  def send_async(*args) # msg, client_queue, client_query_id, use_tcp=@use_tcp)
380
+ ensure_current_process
309
381
  # @TODO@ Need to select a good Header ID here - see forgery-resilience RFC draft for details
310
382
  msg = args[0]
311
383
  client_query_id = nil
@@ -333,31 +405,36 @@ module Dnsruby
333
405
  end
334
406
  end
335
407
  end
336
- # Need to keep track of the request mac (if using tsig) so we can validate the response (RFC2845 4.1)
337
- # #Are we using EventMachine or native Dnsruby?
338
- # if (Resolver.eventmachine?)
339
- # return send_eventmachine(query_packet, msg, client_query_id, client_queue, use_tcp)
340
- # else
408
+ # Keep the request MAC for response verification (RFC 8945 section 4.3.1).
341
409
  if (!client_query_id)
342
410
  client_query_id = Time.now + rand(10000) # is this safe?!
343
411
  end
412
+ response_settings = response_query_settings(client_query_id)
413
+ disable_edns = args[4] || (response_settings && response_settings.disable_edns)
344
414
 
345
415
  begin
346
- query_packet = make_query_packet(msg, use_tcp)
416
+ query_packet = make_query_packet(msg, use_tcp, disable_edns)
417
+ check_message_size(query_packet)
347
418
  rescue EncodeError => err
348
419
  Dnsruby.log.error { "#{err}" }
349
420
  st = SelectThread.instance
350
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
421
+ if response_settings
422
+ st.fail_query_if_current(client_query_id, client_queue, response_settings, err)
423
+ else
424
+ st.push_exception_to_select(client_query_id, client_queue, err, nil)
425
+ end
351
426
  return
352
427
  end
353
428
 
354
- if (msg.do_caching && (msg.class != Update))
429
+ if (msg.do_caching && !msg.send_raw && (msg.class != Update) && !@tsig && !msg.tsig && !msg.signed?)
355
430
  # Check the cache!!
356
431
  cachedanswer = nil
357
432
  if (msg.header.rd)
358
- cachedanswer = @@recursive_cache.find(msg.question()[0].qname, msg.question()[0].type)
433
+ cachedanswer = @@recursive_cache.find(msg.question()[0].qname, msg.question()[0].type, msg.question()[0].qclass,
434
+ scope: positive_cache_scope(msg, query_packet))
359
435
  else
360
- cachedanswer = @@authoritative_cache.find(msg.question()[0].qname, msg.question()[0].type)
436
+ cachedanswer = @@authoritative_cache.find(msg.question()[0].qname, msg.question()[0].type, msg.question()[0].qclass,
437
+ scope: positive_cache_scope(msg, query_packet))
361
438
  end
362
439
  if (cachedanswer)
363
440
  TheLog.debug("Sending cached answer to client\n")
@@ -366,7 +443,11 @@ module Dnsruby
366
443
  # If we can find the answer, send it to the client straight away
367
444
  # Post the result to the client using SelectThread
368
445
  st = SelectThread.instance
369
- st.push_response_to_select(client_query_id, client_queue, cachedanswer, msg, self)
446
+ if response_settings
447
+ st.push_response_for_query(response_settings, cachedanswer, msg, self)
448
+ else
449
+ st.push_response_to_select(client_query_id, client_queue, cachedanswer, msg, self)
450
+ end
370
451
  return client_query_id
371
452
  end
372
453
  end
@@ -376,13 +457,18 @@ module Dnsruby
376
457
  # Can't send the message - abort!
377
458
  err=IOError.new("Can't send message - too big for UDP and no_tcp=true")
378
459
  Dnsruby.log.error { "#{err}" }
379
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
460
+ st = SelectThread.instance
461
+ if response_settings
462
+ st.fail_query_if_current(client_query_id, client_queue, response_settings, err)
463
+ else
464
+ st.push_exception_to_select(client_query_id, client_queue, err, nil)
465
+ end
380
466
  return
381
467
  end
382
468
  Dnsruby.log.debug { "Query packet length exceeds max UDP packet size - using TCP" }
383
469
  use_tcp = true
384
470
  end
385
- send_dnsruby(query_packet, msg, client_query_id, client_queue, use_tcp)
471
+ send_dnsruby(query_packet, msg, client_query_id, client_queue, use_tcp, disable_edns)
386
472
  return client_query_id
387
473
  # end
388
474
  end
@@ -396,15 +482,26 @@ module Dnsruby
396
482
  # In general, every subsequent call the function will either return the current tcp
397
483
  # pipeline socket or a new connected socket if the current one was closed by the server
398
484
  def tcp_pipeline_socket(src_port)
485
+ ensure_current_process
399
486
  Dnsruby.log.debug("Using tcp_pipeline_socket")
400
- sockaddr = Socket.sockaddr_in(@port, @server)
487
+ # A live connection retains its endpoint and source binding. Compare the
488
+ # configured source-port pool, not each randomly selected candidate port.
489
+ attempt = @pipeline_connect_context&.[](2) || transport_configuration
490
+ configuration = [attempt[:server], attempt[:port],
491
+ attempt[:ipv6] ? attempt[:src_address6] : attempt[:src_address], attempt[:src_port], attempt[:ipv6]]
492
+ sockaddr = Socket.sockaddr_in(configuration[1], configuration[0])
401
493
 
402
494
  reuse_pipeline_socket = -> do
403
495
  begin
404
- max = @tcp_pipelining_max_queries
496
+ max = @pipeline_socket_max_queries
405
497
  use_count = @use_counts[@pipeline_socket]
406
- if use_count && max != :infinite && use_count >= max
407
- #we can't reuse the socket since max is reached
498
+ if !SelectThread.instance.reusable_socket?(@pipeline_socket) ||
499
+ max != attempt[:tcp_pipelining_max_queries] ||
500
+ @pipeline_socket_configuration != configuration ||
501
+ (use_count && max != :infinite && use_count >= max)
502
+ # Capacity and endpoint belong to a connection. Outstanding peers
503
+ # keep their old stream and drain before it closes.
504
+ SelectThread.instance.retire_socket(@pipeline_socket)
408
505
  @use_counts.delete(@pipeline_socket)
409
506
  @pipeline_socket = nil
410
507
  Dnsruby.log.debug("Max queries per connection attained - creating new socket")
@@ -414,6 +511,7 @@ module Dnsruby
414
511
  rescue Errno::EISCONN
415
512
  #already connected, do nothing and reuse!
416
513
  rescue IOError, Errno::ECONNRESET #close by remote host, reconnect
514
+ @use_counts.delete(@pipeline_socket)
417
515
  @pipeline_socket = nil
418
516
  Dnsruby.log.debug("Connection closed - recreating socket")
419
517
  end
@@ -421,14 +519,20 @@ module Dnsruby
421
519
 
422
520
  create_pipeline_socket = -> do
423
521
  @tcp_pipeline_local_port = src_port
424
- src_address = @ipv6 ? @src_address6 : @src_address
522
+ src_address = configuration[2]
425
523
  begin
426
- @pipeline_socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
427
- @pipeline_socket.bind(Addrinfo.tcp(src_address, src_port))
428
- @pipeline_socket.connect(sockaddr)
524
+ @pipeline_socket_max_queries = attempt[:tcp_pipelining_max_queries]
525
+ @pipeline_socket_configuration = configuration
526
+ deadline, token = @pipeline_connect_context ||
527
+ [TransportClock.now + attempt[:packet_timeout], SelectThread::SendToken.new]
528
+ @pipeline_socket = connect_tcp(configuration[0], configuration[1], src_address, src_port,
529
+ deadline, token)
429
530
  Dnsruby.log.debug("Creating socket #{src_address}:#{src_port}")
430
531
  @use_counts[@pipeline_socket] = 0
431
532
  rescue Exception => e
533
+ # A failed bind/connect never reaches the select thread, which would
534
+ # otherwise own closing the socket (for example, an unusable zone).
535
+ @pipeline_socket.close rescue nil
432
536
  @pipeline_socket = nil
433
537
  raise e
434
538
  end
@@ -446,35 +550,254 @@ module Dnsruby
446
550
  end
447
551
 
448
552
  # This method sends the packet using the built-in pure Ruby event loop, with no dependencies.
449
- def send_dnsruby(query_bytes, query, client_query_id, client_queue, use_tcp) #:nodoc: all
450
- endtime = Time.now + @packet_timeout
553
+ def send_dnsruby(query_bytes, query, client_query_id, client_queue, use_tcp, disable_edns = false) #:nodoc: all
554
+ ensure_current_process
555
+ st = @selector = SelectThread.instance
556
+ response_settings = response_query_settings(client_query_id)
557
+ # A configuration change applies to the next attempt, never half of this one.
558
+ configuration = transport_configuration
559
+ pipeline_mode = use_tcp && configuration[:tcp_pipelining]
560
+ begin
561
+ check_message_size(query_bytes)
562
+ rescue EncodeError => error
563
+ if response_settings
564
+ st.fail_query_if_current(client_query_id, client_queue, response_settings, error)
565
+ else
566
+ st.push_exception_to_select(client_query_id, client_queue, error, nil)
567
+ end
568
+ return
569
+ end
570
+ # A Resolver reservation already started this attempt's packet deadline.
571
+ # A protocol retry (which has bytes) receives a fresh packet deadline.
572
+ endtime = response_settings && response_settings.query_bytes.nil? ?
573
+ response_settings.endtime : TransportClock.now + configuration[:packet_timeout]
574
+ previous_settings = response_settings ||
575
+ (st.query_settings_for_id(client_query_id) if pipeline_mode)
576
+ pending = SelectThread::QuerySettings.new(query_bytes, query,
577
+ configuration[:ignore_truncation], client_queue, client_query_id, nil,
578
+ configuration[:server], configuration[:port],
579
+ endtime, udp_receive_size(query, configuration), self)
580
+ pending.transport_configuration = configuration
581
+ pending.send_token = previous_settings.send_token if previous_settings
582
+ # Carry the controlling lookup through selector retries and workers, whose
583
+ # thread-local control may differ from the thread which admitted it.
584
+ endtime = pending.endtime = [endtime, pending.send_token.control&.deadline].compact.min
585
+ pending.disable_edns = disable_edns || (previous_settings && previous_settings.disable_edns)
586
+ pending.cache_scope = positive_cache_scope(query, query_bytes, configuration)
587
+ # A protocol retry starts its own packet deadline at dispatch, including
588
+ # time queued for a worker. Retire the old socket membership atomically.
589
+ return unless st.reserve_send(pending, previous_settings)
590
+ previous_settings = pending
591
+ operation = -> do
592
+ send_dnsruby_with_socket(query_bytes, query, client_query_id, client_queue,
593
+ use_tcp, endtime, previous_settings, pipeline_mode)
594
+ end
595
+ send_operation = operation
596
+ if pipeline_mode
597
+ # Configuration-only senders remain Marshal-compatible. Allocate the
598
+ # writer mutex only on first use, including senders loaded from storage.
599
+ pipeline_mutex = PIPELINE_MUTEX_CREATION.synchronize { @pipeline_mutex ||= Mutex.new }
600
+ send_operation = -> do
601
+ locked = false
602
+ begin
603
+ until (locked = pipeline_mutex.try_lock)
604
+ break if previous_settings.send_token.cancelled? || TransportClock.now >= endtime ||
605
+ !st.query_settings_for_id(client_query_id).equal?(previous_settings)
606
+ sleep 0.005
607
+ end
608
+ if locked && !previous_settings.send_token.cancelled? && TransportClock.now < endtime
609
+ operation.call
610
+ else
611
+ st.fail_query_if_current(client_query_id, client_queue, previous_settings,
612
+ ResolvTimeout.new("Query timed out waiting for TCP pipeline"))
613
+ end
614
+ ensure
615
+ pipeline_mutex.unlock if locked
616
+ end
617
+ end
618
+ end
619
+ guarded_operation = -> do
620
+ send_operation.call
621
+ rescue StandardError => error
622
+ # Deferred callers do not read SendTask#value. Failures before socket
623
+ # registration still belong to this exact reserved attempt.
624
+ st.fail_query_if_current(client_query_id, client_queue, pending, error)
625
+ end
626
+ if use_tcp && st.selector_thread?
627
+ begin
628
+ st.defer_send(pipeline_mode ? self : nil, &guarded_operation)
629
+ rescue ThreadError => error
630
+ st.fail_query_if_current(client_query_id, client_queue, previous_settings,
631
+ ResolvTimeout.new("Cannot queue TCP send: #{error.message}"))
632
+ end
633
+ else
634
+ # Only the selector delegates I/O. Public caller sends remain synchronous.
635
+ guarded_operation.call
636
+ end
637
+ end
638
+
639
+ def check_message_size(query_bytes)
640
+ if query_bytes.bytesize > 65_535
641
+ raise EncodeError, "DNS message exceeds the 65535-byte wire limit (#{query_bytes.bytesize} bytes)"
642
+ end
643
+ end
644
+ private :check_message_size
645
+
646
+ class SendCancelled < StandardError; end
647
+ private_constant :SendCancelled
648
+
649
+ # Config has already resolved the server address. Check cancellation while
650
+ # connecting so an unresponsive peer cannot hold up a sender worker.
651
+ def connect_tcp(server, port, source, source_port, deadline, token)
652
+ socket = nil
653
+ connected = false
654
+ begin
655
+ raise SendCancelled if token.cancelled?
656
+ raise ResolvTimeout, 'Query timed out before TCP connect' if TransportClock.now >= deadline
657
+ socket = SelectThread.instance.track_socket(
658
+ Socket.new(server.include?(':') ? Socket::AF_INET6 : Socket::AF_INET, Socket::SOCK_STREAM, 0))
659
+ socket.bind(Socket.sockaddr_in(source_port, source))
660
+ result = socket.connect_nonblock(Socket.sockaddr_in(port, server), exception: false)
661
+ while result == :wait_writable || result == :wait_readable
662
+ raise SendCancelled if token.cancelled?
663
+ remaining = deadline - TransportClock.now
664
+ raise ResolvTimeout, 'Query timed out connecting TCP' if remaining <= 0
665
+ readers, writers = result == :wait_readable ? [[socket], nil] : [nil, [socket]]
666
+ next unless IO.select(readers, writers, nil, [remaining, 0.05].min)
667
+ error = socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR).int
668
+ raise SystemCallError.new('TCP connect', error) unless error.zero?
669
+ result = 0
670
+ end
671
+ raise SendCancelled if token.cancelled?
672
+ raise ResolvTimeout, 'Query timed out connecting TCP' if TransportClock.now >= deadline
673
+ connected = true
674
+ socket
675
+ ensure
676
+ socket.close if socket && !connected && !socket.closed?
677
+ end
678
+ end
679
+ private :connect_tcp
680
+
681
+ # Socket#send may write only a prefix of a TCP frame. Nonblocking writes keep
682
+ # a stalled peer from holding the pipeline writer past the packet deadline.
683
+ def write_tcp_frame(socket, query_bytes, endtime, send_token)
684
+ check_message_size(query_bytes)
685
+ frame = [query_bytes.bytesize].pack('n') + query_bytes
686
+ offset = 0
687
+ while offset < frame.bytesize
688
+ remaining = endtime - TransportClock.now
689
+ begin
690
+ written = send_token.write do
691
+ raise ResolvTimeout, "Query timed out writing TCP frame" if remaining <= 0
692
+ socket.write_nonblock(frame.byteslice(offset, frame.bytesize - offset))
693
+ end
694
+ return offset if written == false # Cancelled between partial writes.
695
+ raise IOError, "TCP write made no progress" unless written && written > 0
696
+ offset += written
697
+ rescue IO::WaitWritable
698
+ # A canceled pipeline may still have peers and remain open. Bound the
699
+ # wait so cancellation releases the worker even if it never becomes
700
+ # writable; the next iteration retires any incomplete frame.
701
+ IO.select(nil, [socket], nil, [remaining, 0.05].min)
702
+ end
703
+ end
704
+ true
705
+ end
706
+
707
+ # A datagram is sent atomically. Order the actual write with cancellation,
708
+ # as for TCP, without holding the token while waiting for a writable socket.
709
+ def write_udp_packet(socket, query_bytes, endtime, send_token, wait_for_writable)
710
+ loop do
711
+ remaining = endtime - TransportClock.now
712
+ begin
713
+ written = send_token.write do
714
+ raise ResolvTimeout, "Query timed out writing UDP packet" if remaining <= 0
715
+ socket.write_nonblock(query_bytes)
716
+ end
717
+ return false if written == false
718
+ raise IOError, "Incomplete UDP datagram write" unless written == query_bytes.bytesize
719
+ return true
720
+ rescue IO::WaitWritable
721
+ raise unless wait_for_writable
722
+ IO.select(nil, [socket], nil, [remaining, 0.05].min)
723
+ end
724
+ end
725
+ end
726
+
727
+ def response_query_settings(client_query_id)
728
+ settings = Thread.current[:dnsruby_response_query]
729
+ settings if settings && settings.client_query_id == client_query_id &&
730
+ settings.single_resolver.equal?(self)
731
+ end
732
+
733
+ def send_dnsruby_with_socket(query_bytes, query, client_query_id, client_queue,
734
+ use_tcp, endtime, previous_settings, pipeline_mode = use_tcp && @tcp_pipelining)
451
735
  # First send the query (synchronously)
452
736
  st = SelectThread.instance
737
+ report_error = ->(error) do
738
+ st.fail_query_if_current(client_query_id, client_queue, previous_settings, error)
739
+ end
740
+ return unless st.query_settings_for_id(client_query_id).equal?(previous_settings)
741
+ return if previous_settings.send_token.cancelled?
742
+ configuration = previous_settings.transport_configuration || transport_configuration
743
+ server, port = configuration.values_at(:server, :port)
744
+ unless previous_settings.send_token.transmission_budget.admit(server, port, use_tcp, query.question.first)
745
+ report_error.call(ResolvTimeout.new("DNS retry limit reached for server and transport"))
746
+ return
747
+ end
453
748
  socket = nil
454
749
  runnextportloop = true
455
750
  numtries = 0
456
- src_address = @src_address
457
- if (@ipv6)
458
- src_address = @src_address6
459
- end
751
+ source_port_configuration = configuration[:src_port]
752
+ source_ports = source_port_configuration.uniq
753
+ random_source_port = source_ports.first == 0
754
+ src_address = configuration[configuration[:ipv6] ? :src_address6 : :src_address]
460
755
  while (runnextportloop) do
756
+ # A pre-close reservation gets a one-use socket. Recheck on every
757
+ # admission retry without changing ownership or extending its deadline.
758
+ pipeline = pipeline_mode && !previous_settings.retire_connection
759
+ return unless st.query_settings_for_id(client_query_id).equal?(previous_settings)
760
+ return if previous_settings.send_token.cancelled?
461
761
  begin
762
+ if TransportClock.now >= endtime
763
+ report_error.call(ResolvTimeout.new("Query timed out before connect"))
764
+ return
765
+ end
462
766
  numtries += 1
463
- src_port = get_next_src_port
767
+ src_port = if random_source_port
768
+ # Keep this attempt's mode even if a caller replaces the pool.
769
+ 50_000 + SecureRandom.random_number(15_536)
770
+ else
771
+ source_ports[SecureRandom.random_number(source_ports.length)]
772
+ end
464
773
  if (use_tcp)
465
774
  begin
466
- if (@tcp_pipelining)
467
- socket, new_socket = tcp_pipeline_socket(src_port)
775
+ if pipeline
776
+ begin
777
+ # RFC 7766 requires unique outstanding IDs on a connection.
778
+ # Moving a collision to a fresh stream preserves caller IDs,
779
+ # including raw messages and bytes covered by TSIG.
780
+ if @pipeline_socket && st.query_id_in_use?(@pipeline_socket, query.header.id)
781
+ st.retire_socket(@pipeline_socket)
782
+ @use_counts.delete(@pipeline_socket)
783
+ @pipeline_socket = nil
784
+ end
785
+ @pipeline_connect_context = [endtime, previous_settings.send_token, configuration]
786
+ socket, new_socket = tcp_pipeline_socket(src_port)
787
+ ensure
788
+ remove_instance_variable(:@pipeline_connect_context)
789
+ end
468
790
  src_port = @tcp_pipeline_local_port
469
791
  else
470
- socket = Socket.tcp(@server, @port, src_address, src_port, connect_timeout: @packet_timeout)
792
+ socket = connect_tcp(server, port, src_address, src_port,
793
+ endtime, previous_settings.send_token)
471
794
  new_socket = true
472
795
  end
473
796
  rescue Errno::EBADF, Errno::ENETUNREACH => e
474
797
  # Can't create a connection
475
- err=IOError.new("TCP connection error to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception = #{e.class}, #{e}")
798
+ err=IOError.new("TCP connection error to #{server}:#{port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception = #{e.class}, #{e}")
476
799
  Dnsruby.log.error { "#{err}" }
477
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
800
+ report_error.call(err)
478
801
  return
479
802
  end
480
803
  else
@@ -484,81 +807,150 @@ module Dnsruby
484
807
  socket = UDPSocket.new()
485
808
  else
486
809
  # ipv6 = @src_address =~ /:/
487
- socket = UDPSocket.new(@ipv6 ? Socket::AF_INET6 : Socket::AF_INET)
810
+ socket = UDPSocket.new(configuration[:ipv6] ? Socket::AF_INET6 : Socket::AF_INET)
488
811
  end
812
+ st.track_socket(socket)
489
813
  new_socket = true
490
814
  socket.bind(src_address, src_port)
491
- socket.connect(@server, @port)
815
+ socket.connect(server, port)
492
816
  end
493
- runnextportloop = false
817
+ rescue SendCancelled
818
+ return
494
819
  rescue Exception => e
495
820
  if (socket!=nil)
496
821
  begin
497
822
  #let the select thread close the socket if tcp_pipeli
498
- socket.close unless @tcp_pipelining && !new_socket
823
+ socket.close unless pipeline && !new_socket
499
824
  rescue Exception
500
825
  end
501
826
  end
502
- # Try again if the error was EADDRINUSE and a random source port is used
503
- # Maybe try a max number of times?
504
- if ((e.class != Errno::EADDRINUSE) || (numtries > 50) ||
505
- ((e.class == Errno::EADDRINUSE) && (src_port == @src_port[0])))
506
- err_msg = "dnsruby can't connect to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception = #{e.class}, #{e} #{e.backtrace}"
827
+ # Only an occupied bind removes a candidate. A pipeline admission
828
+ # retry has not found the port unusable and must retain the pool.
829
+ source_ports.delete(src_port) if e.is_a?(Errno::EADDRINUSE) && !random_source_port
830
+ # Random ports keep the bounded retry policy; configured pools do
831
+ # not revisit an occupied member during this attempt.
832
+ if !e.is_a?(Errno::EADDRINUSE) || (random_source_port && numtries >= 50) ||
833
+ (!random_source_port && source_ports.empty?)
834
+ err_msg = "dnsruby can't connect to #{server}:#{port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception = #{e.class}, #{e} #{e.backtrace}"
507
835
  err=IOError.new(err_msg)
508
836
  Dnsruby.log.error( "#{err}")
509
837
  Dnsruby.log.error(e.backtrace)
510
- if @tcp_pipelining
511
- st.push_exception_to_select(client_query_id, client_queue, SocketEofResolvError.new(err_msg), nil)
838
+ if e.kind_of?(Errno::ETIMEDOUT) || e.kind_of?(ResolvTimeout)
839
+ report_error.call(ResolvTimeout.new(err_msg))
840
+ elsif pipeline
841
+ report_error.call(SocketEofResolvError.new(err_msg))
512
842
  else
513
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
843
+ report_error.call(err)
514
844
  end
515
845
  return
516
846
  end
847
+ next # Retry a random source-port collision before registration.
517
848
  end
518
- end
519
- if (socket==nil)
520
- err=IOError.new("dnsruby can't connect to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}")
521
- Dnsruby.log.error { "#{err}" }
522
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
523
- return
524
- end
525
- Dnsruby.log.debug { "Sending packet to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp} : #{query.question()[0].qname}, #{query.question()[0].qtype}" }
526
- # print "#{Time.now} : Sending packet to #{@server} : #{query.question()[0].qname}, #{query.question()[0].qtype}\n"
527
- # Listen for the response before we send the packet (to avoid any race conditions)
528
- query_settings = SelectThread::QuerySettings.new(query_bytes, query, @ignore_truncation, client_queue, client_query_id, socket, @server, @port, endtime, udp_packet_size, self)
529
- query_settings.is_persistent_socket = @tcp_pipelining if use_tcp
530
- query_settings.tcp_pipelining_max_queries = @tcp_pipelining_max_queries if @tcp_pipelining
531
- begin
532
- if (use_tcp)
533
- lenmsg = [query_bytes.length].pack('n')
534
- socket.send(lenmsg, 0)
535
- end
536
- socket.send(query_bytes, 0)
537
-
538
- # The select thread will now wait for the response and send that or a
539
- # timeout back to the client_queue.
540
- st.add_to_select(query_settings)
541
- rescue Exception => e
542
- err_msg = "Send failed to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception : #{e}"
543
- err=IOError.new(err_msg)
544
- Dnsruby.log.error { "#{err}" }
545
- Dnsruby.log.error(e.backtrace)
546
- if @tcp_pipelining
547
- st.push_exception_to_select(client_query_id, client_queue, SocketEofResolvError.new(err_msg), nil) if new_socket
548
- else
549
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
849
+ if (socket==nil)
850
+ err=IOError.new("dnsruby can't connect to #{server}:#{port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}")
851
+ Dnsruby.log.error { "#{err}" }
852
+ report_error.call(err)
853
+ return
550
854
  end
551
- begin
552
- #we let the select_thread close the socket when doing tcp
553
- #pipelining
554
- socket.close unless @tcp_pipelining && !new_socket
555
- rescue Exception
855
+ Dnsruby.log.debug { "Sending packet to #{server}:#{port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp} : #{query.question()[0].qname}, #{query.question()[0].qtype}" }
856
+ # A reused pipeline is already readable by the selector. Publish its new
857
+ # query before writing so an immediate answer always has an owner.
858
+ query_settings = SelectThread::QuerySettings.new(query_bytes, query, previous_settings.ignore_truncation,
859
+ client_queue, client_query_id, socket, server, port, endtime, previous_settings.udp_packet_size, self)
860
+ query_settings.transport_configuration = configuration
861
+ query_settings.send_token = previous_settings.send_token
862
+ query_settings.disable_edns = previous_settings.disable_edns
863
+ query_settings.cache_scope = previous_settings.cache_scope
864
+ query_settings.is_persistent_socket = pipeline
865
+ query_settings.tcp_pipelining_max_queries = (@pipeline_socket_max_queries || configuration[:tcp_pipelining_max_queries]) if pipeline
866
+ if use_tcp
867
+ registered = st.register_query_if_current(query_settings, previous_settings)
868
+ if registered == :retry_connection
869
+ # No bytes were written and the original reservation still owns the
870
+ # attempt. Peers on the retiring stream keep their registrations.
871
+ @use_counts.delete(socket)
872
+ @pipeline_socket = nil if @pipeline_socket == socket
873
+ next
874
+ end
875
+ unless registered
876
+ # Completion can win during connect. Release only this unused socket
877
+ # reservation; other queries may still own an existing pipeline.
878
+ @use_counts[socket] -= 1 if pipeline && @use_counts[socket]
879
+ socket.close if new_socket && !socket.closed?
880
+ if pipeline && socket.closed?
881
+ @use_counts.delete(socket)
882
+ @pipeline_socket = nil if @pipeline_socket == socket
883
+ end
884
+ return
885
+ end
886
+ previous_settings = query_settings
887
+ elsif !st.query_settings_for_id(client_query_id).equal?(previous_settings)
888
+ socket.close
889
+ return
556
890
  end
557
- return
891
+ runnextportloop = false
558
892
  end
893
+ send_packet = -> do
894
+ begin
895
+ if use_tcp
896
+ completed = write_tcp_frame(socket, query_bytes, endtime, query_settings.send_token)
897
+ # Cancellation of a partially written frame retires its whole stream;
898
+ # otherwise another frame could be appended to a corrupt prefix.
899
+ unless completed == true
900
+ if pipeline && completed > 0
901
+ st.fail_tcp_send(query_settings, SocketEofResolvError.new("TCP send cancelled"))
902
+ @use_counts.delete(socket)
903
+ @pipeline_socket = nil if @pipeline_socket == socket
904
+ elsif !pipeline
905
+ socket.close rescue nil
906
+ end
907
+ return
908
+ end
909
+ else
910
+ unless write_udp_packet(socket, query_bytes, endtime, query_settings.send_token, !st.selector_thread?)
911
+ socket.close
912
+ return
913
+ end
914
+ socket.close unless st.register_query_if_current(query_settings, previous_settings)
915
+ end
916
+ rescue IO::WaitWritable
917
+ # Only a congested UDP send reaches this branch on the selector. Keep
918
+ # ordinary UDP traffic independent of the pool's blocked TCP writers.
919
+ return st.defer_send { send_packet.call }
920
+ rescue Exception => e
921
+ err_msg = "Send failed to #{server}:#{port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception : #{e}"
922
+ err=IOError.new(err_msg)
923
+ Dnsruby.log.error { "#{err}" }
924
+ Dnsruby.log.error(e.backtrace)
925
+ if pipeline
926
+ error = e.kind_of?(ResolvTimeout) ? e : SocketEofResolvError.new(err_msg)
927
+ st.fail_tcp_send(query_settings, error)
928
+ @use_counts.delete(socket)
929
+ @pipeline_socket = nil if @pipeline_socket == socket
930
+ elsif e.kind_of?(ResolvTimeout)
931
+ report_error.call(e)
932
+ else
933
+ report_error.call(err)
934
+ end
935
+ begin
936
+ #we let the select_thread close the socket when doing tcp
937
+ #pipelining
938
+ socket.close unless pipeline && !new_socket
939
+ rescue Exception
940
+ end
941
+ return
942
+ end
559
943
 
560
- Dnsruby.log.debug { "Packet sent to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp} : #{query.question()[0].qname}, #{query.question()[0].qtype}" }
561
- # print "Packet sent to #{@server}:#{@port} from #{@src_address}:#{src_port}, use_tcp=#{use_tcp} : #{query.question()[0].qname}, #{query.question()[0].qtype}\n"
944
+ Dnsruby.log.debug { "Packet sent to #{server}:#{port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp} : #{query.question()[0].qname}, #{query.question()[0].qtype}" }
945
+ end
946
+ send_packet.call
947
+ rescue StandardError => error
948
+ # Errors outside the expected connect/write failures must not disappear in
949
+ # a worker's result queue. Do not consume a replacement protocol attempt.
950
+ st.fail_query_if_current(client_query_id, client_queue, previous_settings, error)
951
+ # A failure between connect and registration has no selector-owned socket
952
+ # yet. Release that new descriptor here; reused streams retain their peers.
953
+ socket.close if socket && new_socket && !socket.closed?
562
954
  end
563
955
 
564
956
  # The source port to send queries from
@@ -567,10 +959,8 @@ module Dnsruby
567
959
  #
568
960
  # Defaults to 0 - random port
569
961
  def src_port
570
- if (@src_port.length == 1)
571
- return @src_port[0]
572
- end
573
- return @src_port
962
+ ports = @src_port
963
+ ports.length == 1 ? ports[0] : ports
574
964
  end
575
965
 
576
966
  # Can be a single Integer or a Range or an Array
@@ -582,8 +972,9 @@ module Dnsruby
582
972
  # res.src_port=60015..60115
583
973
  #
584
974
  def src_port=(p)
585
- @src_port=[]
586
- add_src_port(p)
975
+ ports = Resolver.get_ports_from(p)
976
+ raise ArgumentError, 'Source port pool must not be empty' if ports.empty?
977
+ @src_port = ports if Resolver.check_port(ports)
587
978
  end
588
979
 
589
980
  # Can be a single Integer or a Range or an Array
@@ -599,57 +990,81 @@ module Dnsruby
599
990
  # res.add_src_port(60015..60115)
600
991
  #
601
992
  def add_src_port(p)
602
- if (Resolver.check_port(p, @src_port))
603
- a = Resolver.get_ports_from(p)
604
- a.each do |x|
605
- if ((@src_port.length > 0) && (x == 0))
606
- raise ArgumentError.new("src_port of 0 only allowed as only src_port value (currently #{@src_port.length} values")
607
- end
608
- @src_port.push(x)
609
- end
993
+ additions = Resolver.get_ports_from(p)
994
+ # Adding a concrete port starts a pool instead of retaining the default
995
+ # random-port sentinel, which would otherwise hide every added port.
996
+ ports = @src_port == [0] && !additions.empty? && !additions.include?(0) ? [] : @src_port
997
+ if Resolver.check_port(additions, ports)
998
+ @src_port = ports + additions
610
999
  end
611
1000
  end
612
1001
 
613
1002
 
614
1003
  def get_next_src_port
615
- # Different OSes have different interpretations of "random port" here.
616
- # Apparently, Linux will just give you the same port as last time, unless it is still
617
- # open, in which case you get n+1.
618
- # We need to determine an actual (random) number here, then ask the OS for it, and
619
- # continue until we get one.
620
- if (@src_port[0] == 0)
621
- candidate = -1
622
- # # better to construct an array of all the ports we *can* use, and then just pick one at random!
623
- # candidate = Iana::UNRESERVED_PORTS[rand(Iana::UNRESERVED_PORTS.length())]
624
- # # while (!(Resolver.port_in_range(candidate)))
625
- # # candidate = (rand(65535-1024) + 1024)
626
- # # end
627
- # @TODO@ Should probably construct a bitmap of the IANA ports...
628
- candidate = 50000 + (rand(15535)) # pick one over 50000
629
- return candidate
630
- end
631
- pos = rand(@src_port.length)
632
- return @src_port[pos]
1004
+ # Preserve the existing high-port range and configured pools, using
1005
+ # unpredictable choices for the source-port contribution to DNS IDs.
1006
+ ports = @src_port.dup
1007
+ if (ports[0] == 0)
1008
+ return 50_000 + SecureRandom.random_number(15_536)
1009
+ end
1010
+ ports[SecureRandom.random_number(ports.length)]
1011
+ end
1012
+
1013
+ # Retain the request actually sent, including EDNS options, but not its
1014
+ # transaction ID. Never share answers across upstream or source routes.
1015
+ def positive_cache_scope(query, query_bytes = query.encode, configuration = nil) #:nodoc:
1016
+ route = if configuration
1017
+ configuration.values_at(:server, :port, :src_address, :src_address6, :src_port)
1018
+ else
1019
+ [@server.to_s.dup.freeze, @port, @src_address.to_s.dup.freeze,
1020
+ @src_address6.to_s.dup.freeze, @src_port.dup.freeze]
1021
+ end
1022
+ (route + [query.send_raw, query_bytes.byteslice(2..-1).b.freeze]).freeze
1023
+ end
1024
+
1025
+ # Keep the endpoint, binding and response policy together while connect or
1026
+ # pipeline admission releases the caller to change resolver settings.
1027
+ def transport_configuration
1028
+ server = @server.to_s.dup.freeze
1029
+ {server: server, port: @port, src_address: @src_address.to_s.dup.freeze,
1030
+ src_address6: @src_address6.to_s.dup.freeze, src_port: @src_port.dup.freeze,
1031
+ ipv6: server.include?(':'), packet_timeout: @packet_timeout, udp_size: udp_packet_size,
1032
+ tcp_pipelining: @tcp_pipelining, tcp_pipelining_max_queries: @tcp_pipelining_max_queries,
1033
+ tcp_idle_timeout: tcp_idle_timeout, ignore_truncation: @ignore_truncation,
1034
+ no_tcp: @no_tcp, dnssec: @dnssec}.freeze
633
1035
  end
1036
+ private :transport_configuration
634
1037
 
635
1038
  def check_response(response, response_bytes, query, client_queue, client_query_id, tcp)
1039
+ settings = response_query_settings(client_query_id)
1040
+ configuration = settings&.transport_configuration
1041
+ dnssec = configuration ? configuration[:dnssec] : @dnssec
1042
+ ignore_truncation = settings ? settings.ignore_truncation : @ignore_truncation
1043
+ no_tcp = settings ? settings.no_tcp : @no_tcp
636
1044
  # @TODO@ Should send_raw avoid this?
637
1045
  if (!query.send_raw)
638
- sig_value = check_tsig(query, response, response_bytes)
639
- if (sig_value != :okay)
640
- # Should send error back up to Resolver here, and then NOT QUERY AGAIN!!!
641
- return sig_value
1046
+ # Reject unrelated packet headers before TSIG verification mutates the
1047
+ # response or an error/truncation triggers another transport attempt.
1048
+ unless response.header.qr && response.header.opcode == query.header.opcode &&
1049
+ response.header.id == query.header.id
1050
+ return false
642
1051
  end
643
- if ((response.header.get_header_rcode == RCode.FORMERR) &&
644
- (query.header.arcount == 0))
645
- # Raise an error
646
- return true
1052
+ checking_signature = query.tsig && response.tsig
1053
+ sig_value = check_tsig(query, response, response_bytes)
1054
+ authenticated_tsig_error = checking_signature && sig_value.kind_of?(TsigError) && response.verified? &&
1055
+ response.header.get_header_rcode == RCode.NOTAUTH && [18, 22].include?(response.tsigerror)
1056
+ if (sig_value != :okay) && !authenticated_tsig_error
1057
+ # Keep waiting for a signed response without extending the deadline
1058
+ # (RFC 8945 section 5.4). Save the error in case the query times out.
1059
+ settings = response_query_settings(client_query_id)
1060
+ settings.authentication_error = sig_value if settings
1061
+ return false
647
1062
  end
648
- # Should check that question section is same as question that was sent! RFC 5452
649
- # If it's not an update...
1063
+ # Match the question when one is present. TCP error replies may omit
1064
+ # it (RFC 7766 section 7); UDP replies must match (RFC 5452 section 9.1).
650
1065
  if (query.class == Update)
651
- # @TODO@!!
652
- else
1066
+ # TODO: Validate the zone section for UPDATE replies.
1067
+ elsif !(tcp && response.question.empty? && response.header.get_header_rcode != RCode.NOERROR)
653
1068
  if ((response.question.size == 0) ||
654
1069
  (response.question[0].qname.labels != query.question[0].qname.labels) ||
655
1070
  (response.question[0].qtype != query.question[0].qtype) ||
@@ -660,19 +1075,29 @@ module Dnsruby
660
1075
  return false
661
1076
  end
662
1077
  end
663
- # end
664
- # IF WE GET FORMERR BACK HERE (and we have EDNS0 on) THEN
665
- # TRY AGAIN WITH NO OPT RECORDS! (rfc2671 section 5.3)
1078
+ return sig_value if authenticated_tsig_error
1079
+ # Retry a FORMERR without optional EDNS (RFC 6891 section 6.2.2).
1080
+ # Queries requiring DNSSEC validation must keep EDNS and DO.
666
1081
  if ((response.header.get_header_rcode == RCode.FORMERR) &&
667
- (query.header.arcount > 0))
668
- # try resending the message with no OPT record
669
- query.remove_additional
670
- query.send_raw = true
671
- send_async(query, client_queue, client_query_id, false)
1082
+ query.get_opt && !(dnssec && query.do_validation))
1083
+ # EDNS negotiation belongs to this attempt, not to the caller's raw
1084
+ # mode or Message (which may also be in use by another nameserver).
1085
+ retry_query = query.dup
1086
+ retry_query.header = query.header.dup
1087
+ retry_query.remove_additional
1088
+ query.additional.each do |rr|
1089
+ retry_query.add_additional(rr) unless rr.type == Types.OPT || rr.type == Types.TSIG
1090
+ end
1091
+ if query.tsig
1092
+ return TsigError.new unless query.tsig.key
1093
+ retry_query.tsigstate = :Unsigned
1094
+ retry_query.set_tsig(query.tsig.dup)
1095
+ end
1096
+ send_async(retry_query, client_queue, client_query_id, tcp, true)
672
1097
  return false
673
1098
  end
674
- if (response.header.tc && !tcp && !@ignore_truncation)
675
- if (@no_tcp)
1099
+ if (response.header.tc && !tcp && !ignore_truncation)
1100
+ if (no_tcp)
676
1101
  Dnsruby.log.debug { "Truncated response - not resending over TCP as no_tcp==true" }
677
1102
  else
678
1103
  # Try to resend over tcp
@@ -684,21 +1109,32 @@ module Dnsruby
684
1109
  end
685
1110
  end
686
1111
  end
1112
+ settings = response_query_settings(client_query_id)
1113
+ if settings && settings.cache_scope && settings.cache_scope == positive_cache_scope(query, settings.query_bytes)
1114
+ response.instance_variable_set(:@dnsruby_cache_scope, settings.cache_scope)
1115
+ else
1116
+ # Configuration changed during this exchange, or this response has no
1117
+ # tracked transport. Do not seed a cache with an ambiguous route.
1118
+ response.instance_variable_set(:@dnsruby_cache_scope, false)
1119
+ end
687
1120
  return true
688
1121
  end
689
1122
 
690
1123
  def check_tsig(query, response, response_bytes)
691
1124
  if (query.tsig)
692
- if (response.tsig)
1125
+ if (response_signature = response.tsig)
693
1126
  if !query.tsig.verify(query, response, response_bytes)
1127
+ authenticated_error = RR::TSIG.authenticated_error(response, response_signature)
1128
+ return authenticated_error if authenticated_error
694
1129
  # Discard packet and wait for correctly signed response
695
1130
  Dnsruby.log.error { "TSIG authentication failed!" }
696
1131
  return TsigError.new
697
1132
  end
698
1133
  else
699
- # Treated as having format error and discarded (RFC2845, 4.6)
700
- # but return a different error code, because some servers fail at
701
- # this
1134
+ # A missing signature is a format error (RFC 8945 section 5.4).
1135
+ # Keep a distinct exception so callers can diagnose unsigned peers.
1136
+ response.tsigstate = :Failed
1137
+ response.tsigerror = RCode.FORMERR
702
1138
  Dnsruby.log.error { "Expecting TSIG signed response, but got unsigned response - discarding" }
703
1139
  return TsigNotSignedResponseError.new
704
1140
  end
@@ -721,52 +1157,52 @@ module Dnsruby
721
1157
  end
722
1158
 
723
1159
  # Prepare the packet for sending
724
- def make_query_packet(packet, use_tcp = @use_tcp) #:nodoc: all
725
- if (!packet.send_raw) # Don't mess with this packet!
1160
+ def make_query_packet(packet, use_tcp = @use_tcp, disable_edns = false) #:nodoc: all
1161
+ packet.tsig.warn_if_legacy_algorithm if packet.tsig
1162
+ # TSIG signs the header and every section, so leave signed messages intact.
1163
+ # Decoded messages may have a TSIG without a signing state.
1164
+ prepared = packet.signed? || packet.tsig
1165
+ if !packet.send_raw && !prepared
726
1166
  if (packet.header.opcode == OpCode.QUERY || @recurse)
727
1167
  packet.header.rd=@recurse
728
1168
  end
729
1169
 
730
- # Only do this if the packet has not been prepared already!
731
- if (@dnssec)
1170
+ if @dnssec && !disable_edns
732
1171
  prepare_for_dnssec(packet)
733
- elsif ((udp_packet_size > Resolver::DefaultUDPSize) && !use_tcp)
734
- # if ((udp_packet_size > Resolver::DefaultUDPSize) && !use_tcp)
735
- # @TODO@ What if an existing OPT RR is not big enough? Should we replace it?
1172
+ elsif !disable_edns && (udp_packet_size > 512) && !use_tcp
736
1173
  add_opt_rr(packet)
737
1174
  end
738
1175
  end
739
1176
 
740
- if (@tsig && !packet.signed?)
741
- @tsig.apply(packet)
1177
+ # Prepare the whole message before encoding, which may sign it.
1178
+ # Use the resolver's key if the Message has none.
1179
+ encoded = packet.encode
1180
+ if @tsig && !packet.signed? && !packet.tsig
1181
+ packet.set_tsig(@tsig)
1182
+ encoded = packet.encode
742
1183
  end
743
- return packet.encode
1184
+ encoded
744
1185
  end
745
1186
 
746
1187
  def add_opt_rr(packet)
747
1188
  Dnsruby.log.debug { ";; Adding EDNS extension with UDP packetsize #{udp_packet_size}.\n" }
748
- # RFC 3225
749
- optrr = RR::OPT.new(udp_packet_size)
750
-
751
- # Only one OPT RR allowed per packet - do we already have one?
752
- if (packet.additional.rrset(packet.question()[0].qname, Types::OPT).rrs.length == 0)
753
- packet.add_additional(optrr)
754
- end
1189
+ # Find OPT by type: its class field stores the payload size. Reuse it and
1190
+ # its options, since only one OPT is allowed (RFC 6891 section 6.1.1).
1191
+ packet.add_additional(RR::OPT.new(udp_packet_size)) unless packet.get_opt
1192
+ packet.get_opt
755
1193
  end
756
1194
 
757
1195
  def prepare_for_dnssec(packet)
758
- # RFC 4035
1196
+ # Request DNSSEC records with DO (RFC 4035 section 4.1).
759
1197
  Dnsruby.log.debug { ";; Adding EDNS extension with UDP packetsize #{udp_packet_size} and DNS OK bit set\n" }
760
- optrr = RR::OPT.new(udp_packet_size) # Decimal UDPpayload
1198
+ optrr = add_opt_rr(packet)
761
1199
  optrr.dnssec_ok=true
762
1200
 
763
- if (packet.additional.rrset(packet.question()[0].qname, Types::OPT).rrs.length == 0)
764
- packet.add_additional(optrr)
765
- end
766
-
767
- packet.header.ad = false # RFC 4035 section 4.6
1201
+ # We use DO to request DNSSEC records and leave AD clear
1202
+ # (RFC 6840 section 5.7).
1203
+ packet.header.ad = false
768
1204
 
769
- # SHOULD SET CD HERE!!!
1205
+ # Local validation needs the original data, even if the server rejects it.
770
1206
  if (packet.do_validation)
771
1207
  packet.header.cd = true
772
1208
  end
@@ -778,10 +1214,16 @@ module Dnsruby
778
1214
 
779
1215
  # Return the packet size to use for UDP
780
1216
  def udp_packet_size
781
- # if @udp_size > DefaultUDPSize then we use EDNS and
782
- # @udp_size should be taken as the maximum packet_data length
783
- ret = (@udp_size > Resolver::DefaultUDPSize ? @udp_size : Resolver::DefaultUDPSize)
784
- return ret
1217
+ # Values below 512 mean 512 in EDNS (RFC 6891 section 6.2.3).
1218
+ [@udp_size, 512].max
1219
+ end
1220
+
1221
+ # Match the receive buffer to this query's OPT so a valid reply isn't
1222
+ # cut short when the caller advertises more than the resolver default.
1223
+ def udp_receive_size(query, configuration = nil)
1224
+ opt = query.get_opt
1225
+ opt ? [opt.payloadsize, 512].max : (configuration ? configuration[:udp_size] : udp_packet_size)
785
1226
  end
1227
+ private :udp_receive_size
786
1228
  end
787
1229
  end