dnsruby 1.74.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +302 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/demo/rubydig.rb +0 -2
- data/dnsruby.gemspec +5 -4
- data/lib/dnsruby/cache.rb +279 -47
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +75 -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 +645 -203
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +795 -339
- data/lib/dnsruby/resolver.rb +468 -186
- 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 +19 -0
- 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/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 +979 -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 +42 -20
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +21 -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 +105 -10
- 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 valid nameservers configured')])
|
|
405
|
-
}
|
|
406
|
-
end
|
|
407
|
-
client_query_id
|
|
437
|
+
engine = @single_res_mutex.synchronize { @resolver_ruby ||= ResolverRuby.new(self) }
|
|
438
|
+
engine.send_async(msg, client_queue, client_query_id)
|
|
408
439
|
end
|
|
409
440
|
|
|
410
441
|
# Close the Resolver. Unfinished queries are terminated with OtherResolvError.
|
|
411
442
|
def close
|
|
412
|
-
|
|
443
|
+
ensure_current_process
|
|
444
|
+
if @resolver_ruby
|
|
445
|
+
@resolver_ruby.close
|
|
446
|
+
else
|
|
447
|
+
@single_res_mutex.synchronize { @single_resolvers.compact.each(&:close) }
|
|
448
|
+
end
|
|
413
449
|
end
|
|
414
450
|
|
|
415
451
|
# Create a new Resolver object. If no parameters are passed in, then the default
|
|
@@ -435,12 +471,14 @@ module Dnsruby
|
|
|
435
471
|
# * :do_caching
|
|
436
472
|
# * :tcp_pipelining
|
|
437
473
|
# * :tcp_pipelining_max_queries - can be a number or :infinite symbol
|
|
474
|
+
# * :tcp_idle_timeout - positive seconds, defaults to 10
|
|
438
475
|
def initialize(*args)
|
|
439
476
|
# @TODO@ Should we allow :nameserver to be an RRSet of NS records? Would then need to randomly order them?
|
|
440
477
|
@resolver_ruby = nil
|
|
441
478
|
@src_address = nil
|
|
442
479
|
@src_address6 = nil
|
|
443
480
|
@single_res_mutex = Mutex.new
|
|
481
|
+
@process_pid = Process.pid
|
|
444
482
|
@configured = false
|
|
445
483
|
@do_caching = true
|
|
446
484
|
@config = Config.new()
|
|
@@ -462,6 +500,7 @@ module Dnsruby
|
|
|
462
500
|
send(key.to_s + '=', args[0][key])
|
|
463
501
|
end
|
|
464
502
|
rescue Exception => e
|
|
503
|
+
raise if key.to_s == 'tsig' || ValidationBudget.abort?(e)
|
|
465
504
|
Dnsruby.log.error{"Argument #{key} not valid : #{e}\n"}
|
|
466
505
|
end
|
|
467
506
|
end
|
|
@@ -478,54 +517,101 @@ module Dnsruby
|
|
|
478
517
|
end
|
|
479
518
|
|
|
480
519
|
def add_config_nameservers # :nodoc: all
|
|
481
|
-
|
|
520
|
+
ensure_current_process
|
|
521
|
+
# Queries can keep using the published configuration while a replacement
|
|
522
|
+
# resolves nameserver hostnames. First-use callers still share one setup.
|
|
523
|
+
return if @single_res_mutex.synchronize { @configured && !@configuration_error }
|
|
524
|
+
configuration_mutex.synchronize do
|
|
525
|
+
if @configured
|
|
526
|
+
raise @configuration_error if @configuration_error
|
|
527
|
+
return
|
|
528
|
+
end
|
|
482
529
|
@config.get_ready
|
|
530
|
+
configure_nameservers
|
|
483
531
|
end
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Serialize configuration separately from query state. Constructing a sender
|
|
535
|
+
# can resolve a hostname through this same selector, whose callbacks need
|
|
536
|
+
# @single_res_mutex. Only publishing the finished list takes that mutex.
|
|
537
|
+
def configure_nameservers(replace = false, nameservers = @config.nameserver)
|
|
538
|
+
new_resolvers = []
|
|
539
|
+
published = false
|
|
540
|
+
begin
|
|
541
|
+
build_sender = lambda do |ns|
|
|
489
542
|
PacketSender.new({
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
543
|
+
server: ns,
|
|
544
|
+
port: @port,
|
|
545
|
+
dnssec: @dnssec,
|
|
546
|
+
use_tcp: @use_tcp,
|
|
547
|
+
no_tcp: @no_tcp,
|
|
548
|
+
tcp_pipelining: @tcp_pipelining,
|
|
549
|
+
tcp_pipelining_max_queries: @tcp_pipelining_max_queries,
|
|
550
|
+
tcp_idle_timeout: @tcp_idle_timeout,
|
|
551
|
+
packet_timeout: @packet_timeout,
|
|
552
|
+
tsig: @tsig,
|
|
553
|
+
ignore_truncation: @ignore_truncation,
|
|
554
|
+
src_address: @src_address,
|
|
555
|
+
src_address6: @src_address6,
|
|
556
|
+
src_port: @src_port,
|
|
557
|
+
recurse: @recurse,
|
|
558
|
+
udp_size: @udp_size,
|
|
559
|
+
ignore_config_resolv_errors: true})
|
|
506
560
|
end
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
561
|
+
if QueryControl.current || SelectThread::TransmissionBudget.current
|
|
562
|
+
# DNSSEC may discover NS names without glue. Resolve those names in
|
|
563
|
+
# this thread so inherited deadlines, work and transmission limits
|
|
564
|
+
# remain shared, without creating another discovery worker pool.
|
|
565
|
+
nameservers.each do |ns|
|
|
566
|
+
QueryControl.current&.check!
|
|
567
|
+
ValidationBudget.current&.check!(1)
|
|
568
|
+
new_resolvers << build_sender.call(ns)
|
|
569
|
+
end
|
|
570
|
+
else
|
|
571
|
+
threads = nameservers.map { |ns| Thread.new { build_sender.call(ns) } }
|
|
572
|
+
new_resolvers = threads.map(&:value).compact
|
|
573
|
+
end
|
|
574
|
+
new_resolvers = new_resolvers.select do |resolver|
|
|
575
|
+
!resolver.nil? && !resolver.server.nil?
|
|
576
|
+
end
|
|
577
|
+
@single_res_mutex.synchronize do
|
|
578
|
+
# Construction may resolve a hostname and run alongside setters.
|
|
579
|
+
# Publish only after applying the settings current under this lock.
|
|
580
|
+
new_resolvers.each { |resolver| update_internal_res(resolver) }
|
|
581
|
+
complete = replace ? new_resolvers : @single_resolvers + new_resolvers
|
|
582
|
+
raise ArgumentError, "No valid nameservers found" if complete.empty?
|
|
583
|
+
@config.nameserver = nameservers if replace
|
|
584
|
+
(@single_resolvers - complete).compact.each(&:close)
|
|
585
|
+
@single_resolvers = complete
|
|
586
|
+
published = true
|
|
587
|
+
@configuration_error = nil
|
|
588
|
+
@configured = true
|
|
589
|
+
end
|
|
590
|
+
rescue Exception => error
|
|
591
|
+
new_resolvers.compact.each(&:close) unless published
|
|
592
|
+
@single_res_mutex.synchronize do
|
|
593
|
+
if ValidationBudget.abort?(error)
|
|
594
|
+
# Exhaustion belongs to the enclosing lookup, not the server
|
|
595
|
+
# configuration. A later lookup must be able to configure again.
|
|
596
|
+
@configuration_error = nil
|
|
597
|
+
@configured = !@single_resolvers.empty?
|
|
598
|
+
else
|
|
599
|
+
@configuration_error = error if @single_resolvers.empty?
|
|
600
|
+
@configured = true
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
raise
|
|
519
604
|
end
|
|
520
605
|
end
|
|
606
|
+
private :configure_nameservers
|
|
521
607
|
|
|
522
608
|
def set_config_nameserver(n)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
609
|
+
configuration_mutex.synchronize do
|
|
610
|
+
@config.get_ready unless @configured
|
|
611
|
+
nameservers = n.kind_of?(String) ? [n] : n
|
|
612
|
+
@config.check_ns(nameservers)
|
|
613
|
+
configure_nameservers(true, nameservers)
|
|
614
|
+
end
|
|
529
615
|
end
|
|
530
616
|
|
|
531
617
|
def reset_attributes # :nodoc: all
|
|
@@ -534,9 +620,8 @@ module Dnsruby
|
|
|
534
620
|
# Attributes
|
|
535
621
|
|
|
536
622
|
# do_validation tells the Resolver whether to try to validate the response
|
|
537
|
-
# with DNSSEC
|
|
538
|
-
#
|
|
539
|
-
# false. Please let me know if you require NSEC3 validation.
|
|
623
|
+
# with DNSSEC, including NSEC and NSEC3 proofs. Local validation defaults
|
|
624
|
+
# to false and is separate from requesting DNSSEC records.
|
|
540
625
|
@do_validation = false
|
|
541
626
|
@query_timeout = DefaultQueryTimeout
|
|
542
627
|
@retry_delay = DefaultRetryDelay
|
|
@@ -550,6 +635,7 @@ module Dnsruby
|
|
|
550
635
|
@no_tcp = false
|
|
551
636
|
@tcp_pipelining = false
|
|
552
637
|
@tcp_pipelining_max_queries = DefaultPipeLiningMaxQueries
|
|
638
|
+
@tcp_idle_timeout = DefaultTcpIdleTimeout
|
|
553
639
|
@tsig = nil
|
|
554
640
|
@ignore_truncation = false
|
|
555
641
|
@config = Config.new()
|
|
@@ -561,9 +647,11 @@ module Dnsruby
|
|
|
561
647
|
@single_resolvers=[]
|
|
562
648
|
}
|
|
563
649
|
@configured = false
|
|
650
|
+
@configuration_error = nil
|
|
564
651
|
end
|
|
565
652
|
|
|
566
653
|
def update # :nodoc: all
|
|
654
|
+
ensure_current_process
|
|
567
655
|
# Update any resolvers we have with the latest config
|
|
568
656
|
@single_res_mutex.synchronize do
|
|
569
657
|
@single_resolvers.delete(nil) # Just in case...
|
|
@@ -579,15 +667,20 @@ module Dnsruby
|
|
|
579
667
|
# end
|
|
580
668
|
|
|
581
669
|
def add_server(server)# :nodoc:
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
670
|
+
configuration_mutex.synchronize do
|
|
671
|
+
res = PacketSender.new(server)
|
|
672
|
+
Dnsruby.log_and_raise("Can't create server #{server}", ArgumentError) unless res
|
|
673
|
+
@single_res_mutex.synchronize do
|
|
674
|
+
update_internal_res(res)
|
|
675
|
+
@single_resolvers.push(res)
|
|
676
|
+
@configuration_error = nil
|
|
677
|
+
@configured = true
|
|
678
|
+
end
|
|
679
|
+
end
|
|
587
680
|
end
|
|
588
681
|
|
|
589
682
|
def update_internal_res(res)
|
|
590
|
-
[:port, :use_tcp, :no_tcp, :tcp_pipelining, :tcp_pipelining_max_queries, :tsig, :ignore_truncation, :packet_timeout,
|
|
683
|
+
[:port, :use_tcp, :no_tcp, :tcp_pipelining, :tcp_pipelining_max_queries, :tcp_idle_timeout, :tsig, :ignore_truncation, :packet_timeout,
|
|
591
684
|
:src_address, :src_address6, :src_port, :recurse,
|
|
592
685
|
:udp_size, :dnssec].each do |param|
|
|
593
686
|
|
|
@@ -600,10 +693,7 @@ module Dnsruby
|
|
|
600
693
|
end
|
|
601
694
|
|
|
602
695
|
def nameserver=(n)
|
|
603
|
-
@configured = true
|
|
604
|
-
@single_res_mutex.synchronize { @single_resolvers=[] }
|
|
605
696
|
set_config_nameserver(n)
|
|
606
|
-
add_config_nameservers
|
|
607
697
|
end
|
|
608
698
|
|
|
609
699
|
# --
|
|
@@ -634,8 +724,10 @@ module Dnsruby
|
|
|
634
724
|
# res.src_port=60015..60115
|
|
635
725
|
#
|
|
636
726
|
def src_port=(p)
|
|
637
|
-
|
|
638
|
-
|
|
727
|
+
ports = Resolver.get_ports_from(p)
|
|
728
|
+
raise ArgumentError, 'Source port pool must not be empty' if ports.empty?
|
|
729
|
+
if Resolver.check_port(ports)
|
|
730
|
+
@src_port = ports
|
|
639
731
|
update
|
|
640
732
|
end
|
|
641
733
|
end
|
|
@@ -653,15 +745,10 @@ module Dnsruby
|
|
|
653
745
|
# res.add_src_port(60015..60115)
|
|
654
746
|
#
|
|
655
747
|
def add_src_port(p)
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
Dnsruby.log_and_raise("src_port of 0 only allowed as only src_port value (currently #{@src_port.length} values",
|
|
661
|
-
ArgumentError)
|
|
662
|
-
end
|
|
663
|
-
@src_port.push(x)
|
|
664
|
-
end
|
|
748
|
+
additions = Resolver.get_ports_from(p)
|
|
749
|
+
ports = @src_port == [0] && !additions.empty? && !additions.include?(0) ? [] : @src_port
|
|
750
|
+
if Resolver.check_port(additions, ports)
|
|
751
|
+
@src_port = ports + additions
|
|
665
752
|
end
|
|
666
753
|
update
|
|
667
754
|
end
|
|
@@ -678,7 +765,10 @@ module Dnsruby
|
|
|
678
765
|
return true
|
|
679
766
|
end
|
|
680
767
|
if Resolver.port_in_range(p)
|
|
681
|
-
|
|
768
|
+
if (p == 0 && !src_port.empty?) || (p != 0 && src_port.include?(0))
|
|
769
|
+
raise ArgumentError, 'Source port 0 must be the only member of a source port pool'
|
|
770
|
+
end
|
|
771
|
+
return true
|
|
682
772
|
else
|
|
683
773
|
Dnsruby.log.error("Illegal port (#{p})")
|
|
684
774
|
Dnsruby.log_and_raise("Illegal port #{p}", ArgumentError)
|
|
@@ -711,6 +801,14 @@ module Dnsruby
|
|
|
711
801
|
update
|
|
712
802
|
end
|
|
713
803
|
|
|
804
|
+
def tcp_idle_timeout=(seconds)
|
|
805
|
+
unless seconds.is_a?(Numeric) && seconds.finite? && seconds > 0
|
|
806
|
+
raise ArgumentError, 'TCP idle timeout must be a positive finite number'
|
|
807
|
+
end
|
|
808
|
+
@tcp_idle_timeout = seconds
|
|
809
|
+
update
|
|
810
|
+
end
|
|
811
|
+
|
|
714
812
|
def use_tcp=(on)
|
|
715
813
|
@use_tcp = on
|
|
716
814
|
update
|
|
@@ -725,10 +823,13 @@ module Dnsruby
|
|
|
725
823
|
# Pass in either a Dnsruby::RR::TSIG, or a key_name and key (or just a key)
|
|
726
824
|
# Pass in nil to stop tsig signing.
|
|
727
825
|
# * res.tsig=(tsig_rr)
|
|
728
|
-
#
|
|
729
|
-
#
|
|
826
|
+
# Update MD5 server keys to SHA-256 when upgrading from 1.74.0, or select
|
|
827
|
+
# hmac-md5 explicitly for a legacy server. MD5 use logs a warning.
|
|
828
|
+
# * res.tsig = key_name, key # defaults to hmac-sha256
|
|
829
|
+
# * res.tsig = key_name, key, algorithm # must match the server's key
|
|
730
830
|
# * res.tsig=nil # Stop the resolver from signing
|
|
731
831
|
def tsig=(t)
|
|
832
|
+
Resolver.get_tsig([t]) unless t.nil?
|
|
732
833
|
@tsig = t
|
|
733
834
|
update
|
|
734
835
|
end
|
|
@@ -764,9 +865,11 @@ module Dnsruby
|
|
|
764
865
|
return nil
|
|
765
866
|
end
|
|
766
867
|
else
|
|
767
|
-
tsig = RR.new_from_hash(create_tsig_options(args))
|
|
868
|
+
tsig = RR.new_from_hash(create_tsig_options(*args))
|
|
768
869
|
end
|
|
769
|
-
|
|
870
|
+
raise ArgumentError, 'Expected a TSIG record or a key name, secret and optional algorithm' unless tsig
|
|
871
|
+
tsig.warn_if_legacy_algorithm
|
|
872
|
+
Dnsruby.log.info{"TSIG signing now using #{tsig.name} (#{tsig.algorithm})"}
|
|
770
873
|
tsig
|
|
771
874
|
end
|
|
772
875
|
|
|
@@ -815,8 +918,8 @@ module Dnsruby
|
|
|
815
918
|
@dnssec = d
|
|
816
919
|
if d
|
|
817
920
|
# Set the UDP size (RFC 4035 section 4.1)
|
|
818
|
-
if @udp_size <
|
|
819
|
-
self.udp_size =
|
|
921
|
+
if @udp_size < AbsoluteMinDnssecUdpSize
|
|
922
|
+
self.udp_size = AbsoluteMinDnssecUdpSize
|
|
820
923
|
end
|
|
821
924
|
end
|
|
822
925
|
update
|
|
@@ -828,37 +931,33 @@ module Dnsruby
|
|
|
828
931
|
end
|
|
829
932
|
|
|
830
933
|
def single_res_mutex # :nodoc: all
|
|
934
|
+
ensure_current_process
|
|
831
935
|
@single_res_mutex
|
|
832
936
|
end
|
|
833
937
|
|
|
834
938
|
def generate_timeouts(base=0) # :nodoc: all
|
|
835
|
-
# These should be be pegged to the single_resolver they are targetting :
|
|
836
|
-
# e.g. timeouts[timeout1]=nameserver
|
|
837
939
|
timeouts = {}
|
|
838
940
|
retry_delay = @retry_delay
|
|
839
|
-
|
|
840
|
-
@retry_times.times do |retry_count|
|
|
841
|
-
if retry_count > 0
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
941
|
+
@single_resolvers.delete(nil)
|
|
942
|
+
[@retry_times, 3].min.times do |retry_count|
|
|
943
|
+
retry_delay *= 2 if retry_count > 0
|
|
944
|
+
@single_resolvers.each_with_index do |res, i|
|
|
945
|
+
offset = i * @retry_delay.to_f / @single_resolvers.length
|
|
946
|
+
deadline = base + offset + (retry_count == 0 ? 0 : retry_delay)
|
|
947
|
+
attempt = [res, retry_count]
|
|
948
|
+
if (existing = timeouts[deadline])
|
|
949
|
+
# Keep the established single-attempt shape for distinct deadlines.
|
|
950
|
+
# Zero delays (or rounded floating-point deadlines) need a bucket.
|
|
951
|
+
existing = timeouts[deadline] = [existing] unless existing.first.is_a?(Array)
|
|
952
|
+
existing << attempt
|
|
851
953
|
else
|
|
852
|
-
|
|
853
|
-
Dnsruby.log_and_raise('Duplicate timeout key!')
|
|
854
|
-
end
|
|
855
|
-
timeouts[base + retry_delay + offset]=[res, retry_count]
|
|
954
|
+
timeouts[deadline] = attempt
|
|
856
955
|
end
|
|
857
956
|
end
|
|
858
957
|
end
|
|
859
|
-
# }
|
|
860
958
|
timeouts
|
|
861
959
|
end
|
|
960
|
+
|
|
862
961
|
end
|
|
863
962
|
|
|
864
963
|
|
|
@@ -874,7 +973,49 @@ module Dnsruby
|
|
|
874
973
|
# @mutex=Mutex.new
|
|
875
974
|
@query_list = {}
|
|
876
975
|
@timeouts = {}
|
|
877
|
-
|
|
976
|
+
@tick_deadlines = DeadlineQueue.new
|
|
977
|
+
@validation_queries = {}
|
|
978
|
+
@transmission_budgets = {}
|
|
979
|
+
@response_cache = Cache::ResolutionCache.new
|
|
980
|
+
@query_cache_scopes = {}
|
|
981
|
+
end
|
|
982
|
+
# Completed failures belong to one resolver configuration and validation
|
|
983
|
+
# policy. Snapshot them at admission, and decline insertion if it changes.
|
|
984
|
+
def response_cache_scope(query)
|
|
985
|
+
return unless query.do_caching && @parent.do_caching && !query.send_raw &&
|
|
986
|
+
query.header.opcode == OpCode.QUERY && !query.is_a?(Update) &&
|
|
987
|
+
!query.tsig && !query.signed? && !@parent.tsig && query.question.length == 1 &&
|
|
988
|
+
query.answer.empty? && query.authority.empty? &&
|
|
989
|
+
query.additional.all? { |rr| rr.type == Types::OPT }
|
|
990
|
+
return if [Types::AXFR, Types::IXFR].include?(query.question.first.qtype)
|
|
991
|
+
senders = @parent.single_resolvers
|
|
992
|
+
return if senders.empty? || senders.any?(&:tsig)
|
|
993
|
+
settings = senders.map do |sender|
|
|
994
|
+
[sender.server.to_s.dup, sender.port, sender.use_tcp, sender.no_tcp,
|
|
995
|
+
sender.dnssec, sender.packet_timeout, sender.src_address.to_s.dup,
|
|
996
|
+
sender.instance_variable_get(:@src_address6).to_s.dup,
|
|
997
|
+
sender.src_port.dup, sender.ignore_truncation]
|
|
998
|
+
end.sort_by(&:inspect)
|
|
999
|
+
[query.do_validation, query.header.rd, query.header.cd, query.header.ad,
|
|
1000
|
+
query.additional.map { |rr|
|
|
1001
|
+
encoded = MessageEncoder.new { |encoder| rr.encode_rdata(encoder) }.to_s
|
|
1002
|
+
[rr.payloadsize, rr.ttl, encoded]
|
|
1003
|
+
}, settings, @parent.query_timeout, @parent.retry_times, @parent.retry_delay,
|
|
1004
|
+
Cache.validation_scope]
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
def cached_completion(query, scope)
|
|
1008
|
+
@response_cache.find(query, scope)
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def cache_completion(client_query_id, response, error)
|
|
1012
|
+
current = @query_list[client_query_id]
|
|
1013
|
+
scope = @query_cache_scopes.delete(client_query_id)
|
|
1014
|
+
return unless current && scope && scope == response_cache_scope(current[0])
|
|
1015
|
+
@response_cache.add(current[0], scope, response, error, control: current[4])
|
|
1016
|
+
end
|
|
1017
|
+
private :response_cache_scope, :cached_completion, :cache_completion
|
|
1018
|
+
|
|
878
1019
|
def send_async(msg, client_queue, client_query_id=nil)
|
|
879
1020
|
# This is the whole point of the Resolver class.
|
|
880
1021
|
# We want to use multiple SingleResolvers to run a query.
|
|
@@ -893,58 +1034,87 @@ module Dnsruby
|
|
|
893
1034
|
unless client_queue.kind_of?(Queue)
|
|
894
1035
|
Dnsruby.log_and_raise('Wrong type for client_queue in Resolver# send_async')
|
|
895
1036
|
# @TODO@ Handle different queue tuples - push this to generic send_error method
|
|
896
|
-
client_queue.push([client_query_id, ArgumentError.new('Wrong type of client_queue passed to Dnsruby::Resolver# send_async - should have been Queue, was #{client_queue.class}')])
|
|
1037
|
+
client_queue.push([client_query_id, nil, ArgumentError.new('Wrong type of client_queue passed to Dnsruby::Resolver# send_async - should have been Queue, was #{client_queue.class}')])
|
|
897
1038
|
return
|
|
898
1039
|
end
|
|
899
1040
|
|
|
900
1041
|
unless msg.kind_of?Message
|
|
901
1042
|
Dnsruby.log.error{'Wrong type for msg in Resolver# send_async'}
|
|
902
1043
|
# @TODO@ Handle different queue tuples - push this to generic send_error method
|
|
903
|
-
client_queue.push([client_query_id, ArgumentError.new("Wrong type of msg passed to Dnsruby::Resolver# send_async - should have been Message, was #{msg.class}")])
|
|
1044
|
+
client_queue.push([client_query_id, nil, ArgumentError.new("Wrong type of msg passed to Dnsruby::Resolver# send_async - should have been Message, was #{msg.class}")])
|
|
904
1045
|
return
|
|
905
1046
|
end
|
|
906
1047
|
|
|
907
1048
|
# According to RFC 6891, a UDP payload size >512 bytes requires an
|
|
908
1049
|
# EDNS OPT RR in the message.
|
|
909
|
-
if @parent.udp_size > 512 && !msg.get_opt
|
|
1050
|
+
if @parent.udp_size > 512 && !msg.get_opt && !msg.send_raw && !msg.signed? && !msg.tsig
|
|
910
1051
|
TheLog.debug("Automatically adding EDNS OPT RR for udp_size=#{ @parent.udp_size }")
|
|
911
1052
|
msg.add_additional(Dnsruby::RR::OPT.new(@parent.udp_size))
|
|
912
1053
|
end
|
|
913
1054
|
|
|
914
1055
|
begin
|
|
915
|
-
|
|
1056
|
+
# Encoding may sign the Message, even for this size check. Set the
|
|
1057
|
+
# header and EDNS options first so the signature covers the final query.
|
|
1058
|
+
sender = @parent.single_resolvers.first
|
|
1059
|
+
wire = sender ? sender.make_query_packet(msg) : msg.encode
|
|
1060
|
+
if wire.bytesize > 65_535
|
|
1061
|
+
raise EncodeError, 'DNS message exceeds the 65535-byte wire limit'
|
|
1062
|
+
end
|
|
916
1063
|
rescue EncodeError => err
|
|
917
1064
|
Dnsruby.log.error { "Can't encode " + msg.to_s + " : #{err}" }
|
|
918
|
-
client_queue.push([client_query_id, err])
|
|
1065
|
+
client_queue.push([client_query_id, nil, err])
|
|
919
1066
|
return
|
|
920
1067
|
end
|
|
921
1068
|
|
|
922
|
-
|
|
1069
|
+
st = SelectThread.instance
|
|
923
1070
|
# add to our data structures
|
|
924
1071
|
# @mutex.synchronize{
|
|
925
1072
|
@parent.single_res_mutex.synchronize {
|
|
926
|
-
tick_needed = true if @query_list.empty?
|
|
927
1073
|
if @query_list.has_key?(client_query_id)
|
|
928
1074
|
Dnsruby.log.error("Duplicate query id requested (#{client_query_id}")
|
|
929
1075
|
# @TODO@ Handle different queue tuples - push this to generic send_error method
|
|
930
|
-
client_queue.push([client_query_id, ArgumentError.new('Client query ID already in use')])
|
|
1076
|
+
client_queue.push([client_query_id, nil, ArgumentError.new('Client query ID already in use')])
|
|
931
1077
|
return
|
|
932
1078
|
end
|
|
1079
|
+
scope = response_cache_scope(msg)
|
|
1080
|
+
control = QueryControl.current
|
|
1081
|
+
control&.check!
|
|
1082
|
+
if (cached = cached_completion(msg, scope))
|
|
1083
|
+
client_queue.push([client_query_id, *cached])
|
|
1084
|
+
return client_query_id
|
|
1085
|
+
end
|
|
1086
|
+
@query_cache_scopes[client_query_id] = scope
|
|
933
1087
|
outstanding = []
|
|
934
|
-
@query_list[client_query_id]=[msg, client_queue, q, outstanding]
|
|
1088
|
+
@query_list[client_query_id]=[msg, client_queue, q, outstanding, control]
|
|
1089
|
+
@transmission_budgets[client_query_id] = SelectThread::TransmissionBudget.current || SelectThread::TransmissionBudget.new
|
|
935
1090
|
|
|
936
|
-
query_timeout =
|
|
1091
|
+
query_timeout = TransportClock.now + @parent.query_timeout
|
|
937
1092
|
if @parent.query_timeout == 0
|
|
938
|
-
query_timeout =
|
|
1093
|
+
query_timeout = TransportClock.now + 31536000 # a year from now
|
|
939
1094
|
end
|
|
940
|
-
|
|
1095
|
+
retries = generate_timeouts
|
|
1096
|
+
# An explicitly empty nameserver list uses the ordinary query deadline
|
|
1097
|
+
# and completion path. No independent timer can publish a second result.
|
|
1098
|
+
no_nameservers = @parent.single_resolvers.empty?
|
|
1099
|
+
if no_nameservers && @parent.query_timeout == 0
|
|
1100
|
+
query_timeout = TransportClock.now + 1
|
|
1101
|
+
elsif retries.empty? && !no_nameservers
|
|
1102
|
+
# A caller may disable all attempts with retry_times=0. Complete
|
|
1103
|
+
# through the normal timeout path instead of leaving an orphan.
|
|
1104
|
+
query_timeout = TransportClock.now
|
|
1105
|
+
end
|
|
1106
|
+
query_timeout = [query_timeout, control&.deadline].compact.min
|
|
1107
|
+
@timeouts[client_query_id] = [query_timeout, retries, no_nameservers]
|
|
1108
|
+
update_tick_deadline(client_query_id)
|
|
1109
|
+
# Publish the observer together with its logical query. close must never
|
|
1110
|
+
# see a query whose selector ownership has not yet been registered.
|
|
1111
|
+
st.add_observer(q, self)
|
|
941
1112
|
}
|
|
942
1113
|
|
|
943
|
-
#
|
|
944
|
-
#
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
tick if tick_needed
|
|
1114
|
+
# Dispatch this query immediately, even while earlier queries are pending.
|
|
1115
|
+
# The same locked retry bookkeeping lets a concurrent selector tick win
|
|
1116
|
+
# safely, while socket I/O still happens after releasing the query lock.
|
|
1117
|
+
tick(client_query_id)
|
|
948
1118
|
client_query_id
|
|
949
1119
|
end
|
|
950
1120
|
|
|
@@ -953,8 +1123,8 @@ module Dnsruby
|
|
|
953
1123
|
# These are created at the same time in case the parameters change during the life of the query.
|
|
954
1124
|
#
|
|
955
1125
|
# These should be absolute, rather than relative
|
|
956
|
-
#
|
|
957
|
-
@parent.generate_timeouts(
|
|
1126
|
+
# All retry and query deadlines use the same monotonic clock.
|
|
1127
|
+
@parent.generate_timeouts(TransportClock.now).sort.to_h
|
|
958
1128
|
end
|
|
959
1129
|
|
|
960
1130
|
# Close the Resolver. Unfinished queries are terminated with OtherResolvError.
|
|
@@ -966,15 +1136,30 @@ module Dnsruby
|
|
|
966
1136
|
send_result_and_stop_querying(client_queue, client_query_id, q, nil,
|
|
967
1137
|
OtherResolvError.new('Resolver closing!'))
|
|
968
1138
|
end
|
|
1139
|
+
@parent.single_resolvers.compact.each(&:close)
|
|
969
1140
|
}
|
|
970
1141
|
end
|
|
971
1142
|
|
|
1143
|
+
# Cancel only the nested discovery query owned by this synchronous waiter;
|
|
1144
|
+
# never close a shared resolver or disturb unrelated callers.
|
|
1145
|
+
def cancel_query(client_query_id, client_queue)
|
|
1146
|
+
@parent.single_res_mutex.synchronize do
|
|
1147
|
+
current = @query_list[client_query_id]
|
|
1148
|
+
if current && current[1].equal?(client_queue)
|
|
1149
|
+
stop_querying(client_query_id)
|
|
1150
|
+
@validation_queries.delete(client_query_id)
|
|
1151
|
+
@query_cache_scopes.delete(client_query_id)
|
|
1152
|
+
SelectThread.instance.remove_observer(current[2], self)
|
|
1153
|
+
@query_list.delete(client_query_id)
|
|
1154
|
+
end
|
|
1155
|
+
end
|
|
1156
|
+
end
|
|
1157
|
+
|
|
972
1158
|
# MUST BE CALLED IN A SYNCHRONIZED BLOCK!
|
|
973
1159
|
#
|
|
974
1160
|
# Send the result back to the client, and close the socket for that query by removing
|
|
975
1161
|
# the query from the select thread.
|
|
976
1162
|
def send_result_and_stop_querying(client_queue, client_query_id, select_queue, msg, error) # :nodoc: all
|
|
977
|
-
stop_querying(client_query_id)
|
|
978
1163
|
send_result(client_queue, client_query_id, select_queue, msg, error)
|
|
979
1164
|
end
|
|
980
1165
|
|
|
@@ -983,13 +1168,51 @@ module Dnsruby
|
|
|
983
1168
|
# Stops send any more packets for a client-level query
|
|
984
1169
|
def stop_querying(client_query_id) # :nodoc: all
|
|
985
1170
|
@timeouts.delete(client_query_id)
|
|
1171
|
+
@tick_deadlines.delete(client_query_id)
|
|
1172
|
+
@transmission_budgets.delete(client_query_id)
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
# Accept one answer for validation, without losing the total query deadline.
|
|
1176
|
+
# The observer remains registered so ticks and this validator can finish the
|
|
1177
|
+
# logical query. Competing attempts, including queued sends, no longer own it.
|
|
1178
|
+
# MUST BE CALLED WITH THE PARENT MUTEX HELD.
|
|
1179
|
+
def begin_validation(client_query_id, query_id, select_queue)
|
|
1180
|
+
@validation_queries[client_query_id] = query_id
|
|
1181
|
+
@timeouts[client_query_id][1].clear
|
|
1182
|
+
update_tick_deadline(client_query_id)
|
|
1183
|
+
SelectThread.instance.cancel_queries_for_queue(select_queue)
|
|
1184
|
+
@query_list[client_query_id][3].clear
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
# Called outside the selector lock. A completed or replaced transaction
|
|
1188
|
+
# cannot start validation, and validation keeps the original query deadline.
|
|
1189
|
+
def validation_deadline(select_queue, query_id)
|
|
1190
|
+
client_query_id = query_id[2]
|
|
1191
|
+
@parent.single_res_mutex.synchronize do
|
|
1192
|
+
current = @query_list[client_query_id]
|
|
1193
|
+
if current && current[2].equal?(select_queue) && @validation_queries[client_query_id] == query_id
|
|
1194
|
+
@timeouts[client_query_id][0]
|
|
1195
|
+
end
|
|
1196
|
+
end
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
def validation_transmission_budget(select_queue, query_id)
|
|
1200
|
+
client_query_id = query_id[2]
|
|
1201
|
+
@parent.single_res_mutex.synchronize do
|
|
1202
|
+
current = @query_list[client_query_id]
|
|
1203
|
+
if current && current[2].equal?(select_queue) && @validation_queries[client_query_id] == query_id
|
|
1204
|
+
@transmission_budgets[client_query_id]
|
|
1205
|
+
end
|
|
1206
|
+
end
|
|
986
1207
|
end
|
|
987
1208
|
|
|
988
1209
|
# MUST BE CALLED IN A SYNCHRONIZED BLOCK!
|
|
989
1210
|
#
|
|
990
1211
|
# Sends the result to the client's queue, and removes the queue observer from the select thread
|
|
991
1212
|
def send_result(client_queue, client_query_id, select_queue, msg, error) # :nodoc: all
|
|
992
|
-
|
|
1213
|
+
cache_completion(client_query_id, msg, error)
|
|
1214
|
+
stop_querying(client_query_id)
|
|
1215
|
+
@validation_queries.delete(client_query_id)
|
|
993
1216
|
# We might still get some callbacks, which we should ignore
|
|
994
1217
|
st = SelectThread.instance
|
|
995
1218
|
st.remove_observer(select_queue, self)
|
|
@@ -999,50 +1222,78 @@ module Dnsruby
|
|
|
999
1222
|
# }
|
|
1000
1223
|
# Return the response to the client
|
|
1001
1224
|
client_queue.push([client_query_id, msg, error])
|
|
1225
|
+
rescue ClosedQueueError
|
|
1226
|
+
# The query and its observers are already removed. A closed client queue
|
|
1227
|
+
# must not stop the shared selector thread.
|
|
1228
|
+
Dnsruby.log.debug { "Discarding completion for closed client queue" }
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
# Earliest retry or total query deadline, using the same lock as admission.
|
|
1232
|
+
# The selector must call this outside its own lock.
|
|
1233
|
+
def next_tick_at # :nodoc: all
|
|
1234
|
+
@parent.single_res_mutex.synchronize { @tick_deadlines.first_deadline }
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
# Only a changed query's finite retry plan needs inspection. The selector
|
|
1238
|
+
# reads the minimum in constant time, without scanning all pending queries.
|
|
1239
|
+
# MUST BE CALLED WITH THE PARENT MUTEX HELD.
|
|
1240
|
+
def update_tick_deadline(client_query_id)
|
|
1241
|
+
if (entry = @timeouts[client_query_id])
|
|
1242
|
+
@tick_deadlines[client_query_id] = [entry[0], entry[1].each_key.first].compact.min
|
|
1243
|
+
end
|
|
1002
1244
|
end
|
|
1245
|
+
private :update_tick_deadline
|
|
1003
1246
|
|
|
1004
|
-
# This method is called
|
|
1247
|
+
# This method is called when a retry or query deadline is due.
|
|
1005
1248
|
# It should arguably be called from another worker thread... (which also handles the queue)
|
|
1006
1249
|
# Each tick, we check if any timeouts have occurred. If so, we take the appropriate action :
|
|
1007
1250
|
# Return a timeout to the client, or send a new query
|
|
1008
|
-
def tick # :nodoc: all
|
|
1251
|
+
def tick(client_query_id = nil) # :nodoc: all
|
|
1009
1252
|
# Handle the tick
|
|
1010
1253
|
# Do we have any retries due to be sent yet?
|
|
1011
1254
|
# @mutex.synchronize{
|
|
1255
|
+
sends = []
|
|
1012
1256
|
@parent.single_res_mutex.synchronize {
|
|
1013
|
-
time_now =
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1257
|
+
time_now = TransportClock.now
|
|
1258
|
+
query_ids = client_query_id.nil? ? [] : [client_query_id]
|
|
1259
|
+
if client_query_id.nil?
|
|
1260
|
+
while (first = @tick_deadlines.first) && first[1] <= time_now
|
|
1261
|
+
query_ids << first[0]
|
|
1262
|
+
@tick_deadlines.delete(first[0])
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
query_ids.each do |client_query_id|
|
|
1266
|
+
next unless @timeouts.key?(client_query_id)
|
|
1267
|
+
msg, client_queue, select_queue, outstanding, control = @query_list[client_query_id]
|
|
1268
|
+
query_timeout, timeouts, no_nameservers = @timeouts[client_query_id]
|
|
1269
|
+
if query_timeout <= time_now
|
|
1270
|
+
# Time the query out through the same exactly-once completion path.
|
|
1271
|
+
reason = no_nameservers ? 'Query timed out - no valid nameservers configured' : 'Query timed out'
|
|
1019
1272
|
send_result_and_stop_querying(client_queue, client_query_id, select_queue, nil,
|
|
1020
|
-
ResolvTimeout.new(
|
|
1273
|
+
ResolvTimeout.new(reason))
|
|
1021
1274
|
next
|
|
1022
1275
|
end
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1276
|
+
# The finite retry plan is ordered once when admitted. Hash deletion
|
|
1277
|
+
# retains that order, so no unrelated retry plan needs to be sorted.
|
|
1278
|
+
while (timeout = timeouts.each_key.first) && timeout <= time_now
|
|
1279
|
+
attempts = timeouts.delete(timeout)
|
|
1280
|
+
attempts = [attempts] unless attempts.first.is_a?(Array)
|
|
1281
|
+
attempts.each do |res, retry_count|
|
|
1028
1282
|
id = [res, msg, client_query_id, retry_count]
|
|
1029
1283
|
Dnsruby.log.debug("Sending msg to #{res.server}")
|
|
1030
|
-
# We should keep a list of the queries which are outstanding
|
|
1031
1284
|
outstanding.push(id)
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
res.send_async(msg, select_queue, id)
|
|
1039
|
-
else
|
|
1040
|
-
break
|
|
1285
|
+
# A query keeps its original retry plan after reconfiguration.
|
|
1286
|
+
# Retired senders must not create unreachable reusable sockets.
|
|
1287
|
+
retired = !@parent.single_resolvers.include?(res)
|
|
1288
|
+
operation = res.prepare_send_async(msg, select_queue, id, retired, control,
|
|
1289
|
+
(@transmission_budgets[client_query_id] ||= SelectThread::TransmissionBudget.new))
|
|
1290
|
+
sends << operation if operation
|
|
1041
1291
|
end
|
|
1042
1292
|
end
|
|
1043
|
-
|
|
1293
|
+
update_tick_deadline(client_query_id)
|
|
1044
1294
|
end
|
|
1045
1295
|
}
|
|
1296
|
+
sends.each(&:call)
|
|
1046
1297
|
end
|
|
1047
1298
|
|
|
1048
1299
|
# This method is called by the SelectThread (in the select thread) when the queue has a new item on it.
|
|
@@ -1070,9 +1321,6 @@ module Dnsruby
|
|
|
1070
1321
|
# can leave handleError alone
|
|
1071
1322
|
# but need to change handleResponse to stop sending, rather than send_result_and_stop_querying.
|
|
1072
1323
|
#
|
|
1073
|
-
# @TODO@ Also, we could really do with a MaxValidationTimeout - if validation not OK within
|
|
1074
|
-
# this time, then raise Timeout (and stop validation)?
|
|
1075
|
-
#
|
|
1076
1324
|
# @TODO@ Also, should there be some facility to stop validator following same chain
|
|
1077
1325
|
# concurrently?
|
|
1078
1326
|
#
|
|
@@ -1089,12 +1337,29 @@ module Dnsruby
|
|
|
1089
1337
|
end
|
|
1090
1338
|
# @mutex.synchronize{
|
|
1091
1339
|
@parent.single_res_mutex.synchronize {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1340
|
+
current = @query_list[client_query_id]
|
|
1341
|
+
# An observer selected before completion can arrive after the caller
|
|
1342
|
+
# reuses its ID. Only the original logical query owns this queue.
|
|
1343
|
+
unless current && current[2].equal?(queue)
|
|
1344
|
+
Dnsruby.log.debug{'Ignoring response for completed query'}
|
|
1345
|
+
return
|
|
1346
|
+
end
|
|
1347
|
+
deadline, _retries, no_nameservers = @timeouts[client_query_id]
|
|
1348
|
+
if deadline && deadline <= TransportClock.now
|
|
1349
|
+
reason = no_nameservers ? 'Query timed out - no valid nameservers configured' : 'Query timed out'
|
|
1350
|
+
send_result_and_stop_querying(current[1], client_query_id, queue, nil,
|
|
1351
|
+
ResolvTimeout.new(reason))
|
|
1352
|
+
return
|
|
1353
|
+
end
|
|
1354
|
+
validation_query = @validation_queries[client_query_id]
|
|
1355
|
+
if validation_query
|
|
1356
|
+
# A response already accepted for validation owns completion. Errors
|
|
1357
|
+
# and already-queued answers from competing attempts cannot replace it.
|
|
1358
|
+
return unless event_type == Resolver::EventType::VALIDATED && validation_query == event_id
|
|
1359
|
+
elsif event_type == Resolver::EventType::VALIDATED
|
|
1095
1360
|
return
|
|
1096
1361
|
end
|
|
1097
|
-
_msg, _client_queue, _select_queue, outstanding =
|
|
1362
|
+
_msg, _client_queue, _select_queue, outstanding = current
|
|
1098
1363
|
if event_type == Resolver::EventType::RECEIVED ||
|
|
1099
1364
|
event_type == Resolver::EventType::ERROR
|
|
1100
1365
|
unless outstanding.include?(id)
|
|
@@ -1134,7 +1399,20 @@ module Dnsruby
|
|
|
1134
1399
|
# Check what sort of error it was :
|
|
1135
1400
|
resolver, _msg, client_query_id, _retry_count = query_id
|
|
1136
1401
|
_msg, client_queue, select_queue, outstanding = @query_list[client_query_id]
|
|
1137
|
-
if error.kind_of?(
|
|
1402
|
+
if error.kind_of?(SocketEofResolvError) && _msg.header.opcode != OpCode.QUERY
|
|
1403
|
+
# An UPDATE or other operation may already have taken effect. An
|
|
1404
|
+
# ambiguous EOF ends the whole logical query, including other servers'
|
|
1405
|
+
# scheduled and outstanding attempts; do not replay it elsewhere.
|
|
1406
|
+
send_result_and_stop_querying(client_queue, client_query_id, select_queue, response, error)
|
|
1407
|
+
return
|
|
1408
|
+
end
|
|
1409
|
+
retryable_eof = error.kind_of?(SocketEofResolvError)
|
|
1410
|
+
if error.kind_of?(ResolvTimeout) || retryable_eof
|
|
1411
|
+
# A server can close a TCP connection before answering (RFC 7766
|
|
1412
|
+
# section 6.2.4). Keep the finite retry plan and original total deadline,
|
|
1413
|
+
# just as for a lost UDP packet; reconnecting must not add new retries.
|
|
1414
|
+
# An UPDATE may already have changed the server and must not be replayed
|
|
1415
|
+
# merely because its reply was lost.
|
|
1138
1416
|
# - if it was a timeout, then check which number it was, and how many retries are expected on that server
|
|
1139
1417
|
# - if it was the last retry, on the last server, then return a timeout to the client (and clean up)
|
|
1140
1418
|
# - otherwise, continue
|
|
@@ -1150,10 +1428,10 @@ module Dnsruby
|
|
|
1150
1428
|
# - if it was an NXDomain, then return that to the client, and stop all new queries (and clean up)
|
|
1151
1429
|
# send_result_and_stop_querying(client_queue, client_query_id, select_queue, response, error)
|
|
1152
1430
|
increment_resolver_priority(resolver) unless response.cached
|
|
1153
|
-
|
|
1431
|
+
begin_validation(client_query_id, query_id, select_queue)
|
|
1154
1432
|
# @TODO@ Does the client want notified at this point?
|
|
1155
|
-
elsif error.kind_of?(EncodeError)
|
|
1156
|
-
Dnsruby.log.debug{'
|
|
1433
|
+
elsif error.kind_of?(EncodeError) || error.kind_of?(RecursionLimitError)
|
|
1434
|
+
Dnsruby.log.debug{'Query cannot continue - sending error to client'}
|
|
1157
1435
|
send_result_and_stop_querying(client_queue, client_query_id, select_queue, response, error)
|
|
1158
1436
|
else
|
|
1159
1437
|
# - if it was any other error, then remove that server from the list for that query
|
|
@@ -1163,11 +1441,14 @@ module Dnsruby
|
|
|
1163
1441
|
Dnsruby.log.debug{"Removing #{resolver.server} from resolver list for this query"}
|
|
1164
1442
|
if timeouts
|
|
1165
1443
|
timeouts[1].each do |key, value|
|
|
1166
|
-
|
|
1167
|
-
|
|
1444
|
+
if value.first.is_a?(Array)
|
|
1445
|
+
value.reject! { |res, _retry| res == resolver }
|
|
1446
|
+
timeouts[1].delete(key) if value.empty?
|
|
1447
|
+
elsif value.first == resolver
|
|
1168
1448
|
timeouts[1].delete(key)
|
|
1169
1449
|
end
|
|
1170
1450
|
end
|
|
1451
|
+
update_tick_deadline(client_query_id)
|
|
1171
1452
|
end
|
|
1172
1453
|
# Also stick it to the back of the list for future queries
|
|
1173
1454
|
demote_resolver(resolver)
|
|
@@ -1191,7 +1472,7 @@ module Dnsruby
|
|
|
1191
1472
|
TheLog.debug("Incrementing resolver priority for #{res.server}\n")
|
|
1192
1473
|
# @parent.single_res_mutex.synchronize {
|
|
1193
1474
|
index = @parent.single_resolvers.index(res)
|
|
1194
|
-
if index > 0
|
|
1475
|
+
if index && index > 0
|
|
1195
1476
|
@parent.single_resolvers.delete(res)
|
|
1196
1477
|
@parent.single_resolvers.insert(index-1,res)
|
|
1197
1478
|
end
|
|
@@ -1203,7 +1484,7 @@ module Dnsruby
|
|
|
1203
1484
|
TheLog.debug("Decrementing resolver priority for #{res.server}\n")
|
|
1204
1485
|
# @parent.single_res_mutex.synchronize {
|
|
1205
1486
|
index = @parent.single_resolvers.index(res)
|
|
1206
|
-
if index < @parent.single_resolvers.length
|
|
1487
|
+
if index && index < @parent.single_resolvers.length - 1
|
|
1207
1488
|
@parent.single_resolvers.delete(res)
|
|
1208
1489
|
@parent.single_resolvers.insert(index+1,res)
|
|
1209
1490
|
end
|
|
@@ -1214,8 +1495,9 @@ module Dnsruby
|
|
|
1214
1495
|
def demote_resolver(res)
|
|
1215
1496
|
TheLog.debug("Demoting resolver priority for #{res.server} to bottom\n")
|
|
1216
1497
|
# @parent.single_res_mutex.synchronize {
|
|
1217
|
-
@parent.single_resolvers.delete(res)
|
|
1218
|
-
|
|
1498
|
+
if @parent.single_resolvers.delete(res)
|
|
1499
|
+
@parent.single_resolvers.push(res)
|
|
1500
|
+
end
|
|
1219
1501
|
# }
|
|
1220
1502
|
end
|
|
1221
1503
|
|
|
@@ -1230,7 +1512,7 @@ module Dnsruby
|
|
|
1230
1512
|
if s_queue != select_queue
|
|
1231
1513
|
Dnsruby.log_and_raise("Serious internal error : expected select queue #{s_queue}, got #{select_queue}")
|
|
1232
1514
|
end
|
|
1233
|
-
|
|
1515
|
+
begin_validation(client_query_id, query_id, select_queue)
|
|
1234
1516
|
# @TODO@ Does the client want notified at this point?
|
|
1235
1517
|
# client_queue.push([client_query_id, Resolver::EventType::RECEIVED, msg, nil])
|
|
1236
1518
|
# }
|