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