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
@@ -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,8 +218,11 @@ 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
 
225
+ ignore_config_resolv_errors = false;
207
226
  if arg.nil?
208
227
  elsif arg.kind_of? String
209
228
  @server = arg
@@ -212,18 +231,33 @@ module Dnsruby
212
231
  elsif arg.kind_of? Hash
213
232
  arg.keys.each do |attr|
214
233
  begin
215
- if ((attr.to_s == "src_address" || attr.to_s == "src_address6") &&
216
- (arg[attr] == nil || arg[attr] == ""))
234
+ if (attr.to_s == "ignore_config_resolv_errors")
235
+ ignore_config_resolv_errors = arg[attr]
217
236
  else
218
- send(attr.to_s + "=", arg[attr])
237
+ if ((attr.to_s == "src_address" || attr.to_s == "src_address6") &&
238
+ (arg[attr] == nil || arg[attr] == ""))
239
+ else
240
+ send(attr.to_s + "=", arg[attr])
241
+ end
219
242
  end
220
243
  rescue Exception => e
244
+ raise if attr.to_s == 'tsig' || ValidationBudget.abort?(e)
221
245
  Dnsruby.log.error { "PacketSender : Argument #{attr}, #{arg[attr]} not valid : #{e}\n" }
222
246
  end
223
247
  end
224
248
  end
225
249
  # Check server is IP
226
- @server=Config.resolve_server(@server)
250
+ # We should allow the call of PacketSender.new to flag just to log an error if this fails
251
+ # and then return null
252
+ if ignore_config_resolv_errors
253
+ begin
254
+ @server=Config.resolve_server(@server)
255
+ rescue ArgumentError
256
+ return
257
+ end
258
+ else
259
+ @server=Config.resolve_server(@server)
260
+ end
227
261
 
228
262
  check_ipv6
229
263
  # ResolverRegister::register_single_resolver(self)
@@ -236,7 +270,7 @@ module Dnsruby
236
270
  @ipv6=false
237
271
  rescue Exception
238
272
  begin
239
- IPv6.create(@server)
273
+ Config.ipv6_server_address(@server)
240
274
  # @src_address6 = '::'
241
275
  @ipv6=true
242
276
  rescue Exception
@@ -245,9 +279,61 @@ module Dnsruby
245
279
  end
246
280
  end
247
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
+
248
306
  def close
249
- # @TODO@ What about closing?
250
- # 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
251
337
  end
252
338
 
253
339
  # Asynchronously send a Message to the server. The send can be done using just
@@ -291,6 +377,7 @@ module Dnsruby
291
377
  # validation for this particular packet (assuming SingleResolver#dnssec == true)
292
378
  # Note that these options should not normally be used!
293
379
  def send_async(*args) # msg, client_queue, client_query_id, use_tcp=@use_tcp)
380
+ ensure_current_process
294
381
  # @TODO@ Need to select a good Header ID here - see forgery-resilience RFC draft for details
295
382
  msg = args[0]
296
383
  client_query_id = nil
@@ -318,31 +405,36 @@ module Dnsruby
318
405
  end
319
406
  end
320
407
  end
321
- # Need to keep track of the request mac (if using tsig) so we can validate the response (RFC2845 4.1)
322
- # #Are we using EventMachine or native Dnsruby?
323
- # if (Resolver.eventmachine?)
324
- # return send_eventmachine(query_packet, msg, client_query_id, client_queue, use_tcp)
325
- # else
408
+ # Keep the request MAC for response verification (RFC 8945 section 4.3.1).
326
409
  if (!client_query_id)
327
410
  client_query_id = Time.now + rand(10000) # is this safe?!
328
411
  end
412
+ response_settings = response_query_settings(client_query_id)
413
+ disable_edns = args[4] || (response_settings && response_settings.disable_edns)
329
414
 
330
415
  begin
331
- 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)
332
418
  rescue EncodeError => err
333
419
  Dnsruby.log.error { "#{err}" }
334
420
  st = SelectThread.instance
335
- 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
336
426
  return
337
427
  end
338
428
 
339
- if (msg.do_caching && (msg.class != Update))
429
+ if (msg.do_caching && !msg.send_raw && (msg.class != Update) && !@tsig && !msg.tsig && !msg.signed?)
340
430
  # Check the cache!!
341
431
  cachedanswer = nil
342
432
  if (msg.header.rd)
343
- 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))
344
435
  else
345
- 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))
346
438
  end
347
439
  if (cachedanswer)
348
440
  TheLog.debug("Sending cached answer to client\n")
@@ -351,7 +443,11 @@ module Dnsruby
351
443
  # If we can find the answer, send it to the client straight away
352
444
  # Post the result to the client using SelectThread
353
445
  st = SelectThread.instance
354
- 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
355
451
  return client_query_id
356
452
  end
357
453
  end
@@ -361,13 +457,18 @@ module Dnsruby
361
457
  # Can't send the message - abort!
362
458
  err=IOError.new("Can't send message - too big for UDP and no_tcp=true")
363
459
  Dnsruby.log.error { "#{err}" }
364
- 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
365
466
  return
366
467
  end
367
468
  Dnsruby.log.debug { "Query packet length exceeds max UDP packet size - using TCP" }
368
469
  use_tcp = true
369
470
  end
370
- 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)
371
472
  return client_query_id
372
473
  # end
373
474
  end
@@ -381,15 +482,26 @@ module Dnsruby
381
482
  # In general, every subsequent call the function will either return the current tcp
382
483
  # pipeline socket or a new connected socket if the current one was closed by the server
383
484
  def tcp_pipeline_socket(src_port)
485
+ ensure_current_process
384
486
  Dnsruby.log.debug("Using tcp_pipeline_socket")
385
- 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])
386
493
 
387
494
  reuse_pipeline_socket = -> do
388
495
  begin
389
- max = @tcp_pipelining_max_queries
496
+ max = @pipeline_socket_max_queries
390
497
  use_count = @use_counts[@pipeline_socket]
391
- if use_count && max != :infinite && use_count >= max
392
- #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)
393
505
  @use_counts.delete(@pipeline_socket)
394
506
  @pipeline_socket = nil
395
507
  Dnsruby.log.debug("Max queries per connection attained - creating new socket")
@@ -399,6 +511,7 @@ module Dnsruby
399
511
  rescue Errno::EISCONN
400
512
  #already connected, do nothing and reuse!
401
513
  rescue IOError, Errno::ECONNRESET #close by remote host, reconnect
514
+ @use_counts.delete(@pipeline_socket)
402
515
  @pipeline_socket = nil
403
516
  Dnsruby.log.debug("Connection closed - recreating socket")
404
517
  end
@@ -406,14 +519,20 @@ module Dnsruby
406
519
 
407
520
  create_pipeline_socket = -> do
408
521
  @tcp_pipeline_local_port = src_port
409
- src_address = @ipv6 ? @src_address6 : @src_address
522
+ src_address = configuration[2]
410
523
  begin
411
- @pipeline_socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
412
- @pipeline_socket.bind(Addrinfo.tcp(src_address, src_port))
413
- @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)
414
530
  Dnsruby.log.debug("Creating socket #{src_address}:#{src_port}")
415
531
  @use_counts[@pipeline_socket] = 0
416
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
417
536
  @pipeline_socket = nil
418
537
  raise e
419
538
  end
@@ -431,35 +550,254 @@ module Dnsruby
431
550
  end
432
551
 
433
552
  # This method sends the packet using the built-in pure Ruby event loop, with no dependencies.
434
- def send_dnsruby(query_bytes, query, client_query_id, client_queue, use_tcp) #:nodoc: all
435
- 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)
436
735
  # First send the query (synchronously)
437
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
438
748
  socket = nil
439
749
  runnextportloop = true
440
750
  numtries = 0
441
- src_address = @src_address
442
- if (@ipv6)
443
- src_address = @src_address6
444
- 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]
445
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?
446
761
  begin
762
+ if TransportClock.now >= endtime
763
+ report_error.call(ResolvTimeout.new("Query timed out before connect"))
764
+ return
765
+ end
447
766
  numtries += 1
448
- 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
449
773
  if (use_tcp)
450
774
  begin
451
- if (@tcp_pipelining)
452
- 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
453
790
  src_port = @tcp_pipeline_local_port
454
791
  else
455
- 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)
456
794
  new_socket = true
457
795
  end
458
796
  rescue Errno::EBADF, Errno::ENETUNREACH => e
459
797
  # Can't create a connection
460
- 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}")
461
799
  Dnsruby.log.error { "#{err}" }
462
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
800
+ report_error.call(err)
463
801
  return
464
802
  end
465
803
  else
@@ -469,81 +807,150 @@ module Dnsruby
469
807
  socket = UDPSocket.new()
470
808
  else
471
809
  # ipv6 = @src_address =~ /:/
472
- socket = UDPSocket.new(@ipv6 ? Socket::AF_INET6 : Socket::AF_INET)
810
+ socket = UDPSocket.new(configuration[:ipv6] ? Socket::AF_INET6 : Socket::AF_INET)
473
811
  end
812
+ st.track_socket(socket)
474
813
  new_socket = true
475
814
  socket.bind(src_address, src_port)
476
- socket.connect(@server, @port)
815
+ socket.connect(server, port)
477
816
  end
478
- runnextportloop = false
817
+ rescue SendCancelled
818
+ return
479
819
  rescue Exception => e
480
820
  if (socket!=nil)
481
821
  begin
482
822
  #let the select thread close the socket if tcp_pipeli
483
- socket.close unless @tcp_pipelining && !new_socket
823
+ socket.close unless pipeline && !new_socket
484
824
  rescue Exception
485
825
  end
486
826
  end
487
- # Try again if the error was EADDRINUSE and a random source port is used
488
- # Maybe try a max number of times?
489
- if ((e.class != Errno::EADDRINUSE) || (numtries > 50) ||
490
- ((e.class == Errno::EADDRINUSE) && (src_port == @src_port[0])))
491
- 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}"
492
835
  err=IOError.new(err_msg)
493
836
  Dnsruby.log.error( "#{err}")
494
837
  Dnsruby.log.error(e.backtrace)
495
- if @tcp_pipelining
496
- 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))
497
842
  else
498
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
843
+ report_error.call(err)
499
844
  end
500
845
  return
501
846
  end
847
+ next # Retry a random source-port collision before registration.
502
848
  end
503
- end
504
- if (socket==nil)
505
- err=IOError.new("dnsruby can't connect to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}")
506
- Dnsruby.log.error { "#{err}" }
507
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
508
- return
509
- end
510
- 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}" }
511
- # print "#{Time.now} : Sending packet to #{@server} : #{query.question()[0].qname}, #{query.question()[0].qtype}\n"
512
- # Listen for the response before we send the packet (to avoid any race conditions)
513
- query_settings = SelectThread::QuerySettings.new(query_bytes, query, @ignore_truncation, client_queue, client_query_id, socket, @server, @port, endtime, udp_packet_size, self)
514
- query_settings.is_persistent_socket = @tcp_pipelining if use_tcp
515
- query_settings.tcp_pipelining_max_queries = @tcp_pipelining_max_queries if @tcp_pipelining
516
- begin
517
- if (use_tcp)
518
- lenmsg = [query_bytes.length].pack('n')
519
- socket.send(lenmsg, 0)
520
- end
521
- socket.send(query_bytes, 0)
522
-
523
- # The select thread will now wait for the response and send that or a
524
- # timeout back to the client_queue.
525
- st.add_to_select(query_settings)
526
- rescue Exception => e
527
- err_msg = "Send failed to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception : #{e}"
528
- err=IOError.new(err_msg)
529
- Dnsruby.log.error { "#{err}" }
530
- Dnsruby.log.error(e.backtrace)
531
- if @tcp_pipelining
532
- st.push_exception_to_select(client_query_id, client_queue, SocketEofResolvError.new(err_msg), nil) if new_socket
533
- else
534
- 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
535
854
  end
536
- begin
537
- #we let the select_thread close the socket when doing tcp
538
- #pipelining
539
- socket.close unless @tcp_pipelining && !new_socket
540
- 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
541
890
  end
542
- return
891
+ runnextportloop = false
543
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
544
943
 
545
- 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}" }
546
- # 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?
547
954
  end
548
955
 
549
956
  # The source port to send queries from
@@ -552,10 +959,8 @@ module Dnsruby
552
959
  #
553
960
  # Defaults to 0 - random port
554
961
  def src_port
555
- if (@src_port.length == 1)
556
- return @src_port[0]
557
- end
558
- return @src_port
962
+ ports = @src_port
963
+ ports.length == 1 ? ports[0] : ports
559
964
  end
560
965
 
561
966
  # Can be a single Integer or a Range or an Array
@@ -567,8 +972,9 @@ module Dnsruby
567
972
  # res.src_port=60015..60115
568
973
  #
569
974
  def src_port=(p)
570
- @src_port=[]
571
- 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)
572
978
  end
573
979
 
574
980
  # Can be a single Integer or a Range or an Array
@@ -584,57 +990,81 @@ module Dnsruby
584
990
  # res.add_src_port(60015..60115)
585
991
  #
586
992
  def add_src_port(p)
587
- if (Resolver.check_port(p, @src_port))
588
- a = Resolver.get_ports_from(p)
589
- a.each do |x|
590
- if ((@src_port.length > 0) && (x == 0))
591
- raise ArgumentError.new("src_port of 0 only allowed as only src_port value (currently #{@src_port.length} values")
592
- end
593
- @src_port.push(x)
594
- 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
595
999
  end
596
1000
  end
597
1001
 
598
1002
 
599
1003
  def get_next_src_port
600
- # Different OSes have different interpretations of "random port" here.
601
- # Apparently, Linux will just give you the same port as last time, unless it is still
602
- # open, in which case you get n+1.
603
- # We need to determine an actual (random) number here, then ask the OS for it, and
604
- # continue until we get one.
605
- if (@src_port[0] == 0)
606
- candidate = -1
607
- # # better to construct an array of all the ports we *can* use, and then just pick one at random!
608
- # candidate = Iana::UNRESERVED_PORTS[rand(Iana::UNRESERVED_PORTS.length())]
609
- # # while (!(Resolver.port_in_range(candidate)))
610
- # # candidate = (rand(65535-1024) + 1024)
611
- # # end
612
- # @TODO@ Should probably construct a bitmap of the IANA ports...
613
- candidate = 50000 + (rand(15535)) # pick one over 50000
614
- return candidate
615
- end
616
- pos = rand(@src_port.length)
617
- 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)]
618
1011
  end
619
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
1035
+ end
1036
+ private :transport_configuration
1037
+
620
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
621
1044
  # @TODO@ Should send_raw avoid this?
622
1045
  if (!query.send_raw)
623
- sig_value = check_tsig(query, response, response_bytes)
624
- if (sig_value != :okay)
625
- # Should send error back up to Resolver here, and then NOT QUERY AGAIN!!!
626
- 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
627
1051
  end
628
- if ((response.header.get_header_rcode == RCode.FORMERR) &&
629
- (query.header.arcount == 0))
630
- # Raise an error
631
- 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
632
1062
  end
633
- # Should check that question section is same as question that was sent! RFC 5452
634
- # 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).
635
1065
  if (query.class == Update)
636
- # @TODO@!!
637
- else
1066
+ # TODO: Validate the zone section for UPDATE replies.
1067
+ elsif !(tcp && response.question.empty? && response.header.get_header_rcode != RCode.NOERROR)
638
1068
  if ((response.question.size == 0) ||
639
1069
  (response.question[0].qname.labels != query.question[0].qname.labels) ||
640
1070
  (response.question[0].qtype != query.question[0].qtype) ||
@@ -645,19 +1075,29 @@ module Dnsruby
645
1075
  return false
646
1076
  end
647
1077
  end
648
- # end
649
- # IF WE GET FORMERR BACK HERE (and we have EDNS0 on) THEN
650
- # 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.
651
1081
  if ((response.header.get_header_rcode == RCode.FORMERR) &&
652
- (query.header.arcount > 0))
653
- # try resending the message with no OPT record
654
- query.remove_additional
655
- query.send_raw = true
656
- 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)
657
1097
  return false
658
1098
  end
659
- if (response.header.tc && !tcp && !@ignore_truncation)
660
- if (@no_tcp)
1099
+ if (response.header.tc && !tcp && !ignore_truncation)
1100
+ if (no_tcp)
661
1101
  Dnsruby.log.debug { "Truncated response - not resending over TCP as no_tcp==true" }
662
1102
  else
663
1103
  # Try to resend over tcp
@@ -669,21 +1109,32 @@ module Dnsruby
669
1109
  end
670
1110
  end
671
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
672
1120
  return true
673
1121
  end
674
1122
 
675
1123
  def check_tsig(query, response, response_bytes)
676
1124
  if (query.tsig)
677
- if (response.tsig)
1125
+ if (response_signature = response.tsig)
678
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
679
1129
  # Discard packet and wait for correctly signed response
680
1130
  Dnsruby.log.error { "TSIG authentication failed!" }
681
1131
  return TsigError.new
682
1132
  end
683
1133
  else
684
- # Treated as having format error and discarded (RFC2845, 4.6)
685
- # but return a different error code, because some servers fail at
686
- # 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
687
1138
  Dnsruby.log.error { "Expecting TSIG signed response, but got unsigned response - discarding" }
688
1139
  return TsigNotSignedResponseError.new
689
1140
  end
@@ -706,52 +1157,52 @@ module Dnsruby
706
1157
  end
707
1158
 
708
1159
  # Prepare the packet for sending
709
- def make_query_packet(packet, use_tcp = @use_tcp) #:nodoc: all
710
- 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
711
1166
  if (packet.header.opcode == OpCode.QUERY || @recurse)
712
1167
  packet.header.rd=@recurse
713
1168
  end
714
1169
 
715
- # Only do this if the packet has not been prepared already!
716
- if (@dnssec)
1170
+ if @dnssec && !disable_edns
717
1171
  prepare_for_dnssec(packet)
718
- elsif ((udp_packet_size > Resolver::DefaultUDPSize) && !use_tcp)
719
- # if ((udp_packet_size > Resolver::DefaultUDPSize) && !use_tcp)
720
- # @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
721
1173
  add_opt_rr(packet)
722
1174
  end
723
1175
  end
724
1176
 
725
- if (@tsig && !packet.signed?)
726
- @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
727
1183
  end
728
- return packet.encode
1184
+ encoded
729
1185
  end
730
1186
 
731
1187
  def add_opt_rr(packet)
732
1188
  Dnsruby.log.debug { ";; Adding EDNS extension with UDP packetsize #{udp_packet_size}.\n" }
733
- # RFC 3225
734
- optrr = RR::OPT.new(udp_packet_size)
735
-
736
- # Only one OPT RR allowed per packet - do we already have one?
737
- if (packet.additional.rrset(packet.question()[0].qname, Types::OPT).rrs.length == 0)
738
- packet.add_additional(optrr)
739
- 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
740
1193
  end
741
1194
 
742
1195
  def prepare_for_dnssec(packet)
743
- # RFC 4035
1196
+ # Request DNSSEC records with DO (RFC 4035 section 4.1).
744
1197
  Dnsruby.log.debug { ";; Adding EDNS extension with UDP packetsize #{udp_packet_size} and DNS OK bit set\n" }
745
- optrr = RR::OPT.new(udp_packet_size) # Decimal UDPpayload
1198
+ optrr = add_opt_rr(packet)
746
1199
  optrr.dnssec_ok=true
747
1200
 
748
- if (packet.additional.rrset(packet.question()[0].qname, Types::OPT).rrs.length == 0)
749
- packet.add_additional(optrr)
750
- end
751
-
752
- 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
753
1204
 
754
- # SHOULD SET CD HERE!!!
1205
+ # Local validation needs the original data, even if the server rejects it.
755
1206
  if (packet.do_validation)
756
1207
  packet.header.cd = true
757
1208
  end
@@ -763,10 +1214,16 @@ module Dnsruby
763
1214
 
764
1215
  # Return the packet size to use for UDP
765
1216
  def udp_packet_size
766
- # if @udp_size > DefaultUDPSize then we use EDNS and
767
- # @udp_size should be taken as the maximum packet_data length
768
- ret = (@udp_size > Resolver::DefaultUDPSize ? @udp_size : Resolver::DefaultUDPSize)
769
- 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)
770
1226
  end
1227
+ private :udp_receive_size
771
1228
  end
772
1229
  end