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/recursor.rb
CHANGED
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
# ++
|
|
16
|
+
require 'dnsruby/query_control'
|
|
17
|
+
|
|
16
18
|
module Dnsruby
|
|
17
19
|
# Dnsruby::Recursor - Perform recursive dns lookups
|
|
18
20
|
#
|
|
19
21
|
# require 'Dnsruby'
|
|
20
22
|
# rec = Dnsruby::Recursor.new()
|
|
21
|
-
# answer = rec.
|
|
23
|
+
# answer = rec.query("rob.com.au")
|
|
22
24
|
#
|
|
23
25
|
# This module uses a Dnsruby::Resolver to perform recursive queries.
|
|
24
26
|
#
|
|
@@ -111,40 +113,161 @@ module Dnsruby
|
|
|
111
113
|
# ;;; FINALLY, THE ANSWER!
|
|
112
114
|
# Now,DNSSEC validation is performed (unless disabled).
|
|
113
115
|
class Recursor
|
|
116
|
+
CACHE_LIMIT = 16_384
|
|
117
|
+
ADDRESS_CACHE_LIMIT = 256
|
|
118
|
+
DEFAULT_RECURSION_LIMITS = {
|
|
119
|
+
timeout: 30.0, max_queries: 128, max_steps: 256, max_workers: 8
|
|
120
|
+
}.freeze
|
|
121
|
+
|
|
122
|
+
# TransmissionBudget carries this across address and validation workers.
|
|
123
|
+
class Budget # :nodoc:
|
|
124
|
+
attr_reader :limits, :deadline
|
|
125
|
+
|
|
126
|
+
def initialize(limits)
|
|
127
|
+
@limits = limits
|
|
128
|
+
@deadline = TransportClock.now + limits[:timeout]
|
|
129
|
+
@steps = @workers = 0
|
|
130
|
+
@mutex = Mutex.new
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def step!
|
|
134
|
+
QueryControl.current&.check!
|
|
135
|
+
@mutex.synchronize do
|
|
136
|
+
raise RecursionLimitError, 'Recursive lookup work limit exceeded' if @steps >= @limits[:max_steps]
|
|
137
|
+
@steps += 1
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def reserve_worker
|
|
142
|
+
@mutex.synchronize do
|
|
143
|
+
return false if @workers >= @limits[:max_workers]
|
|
144
|
+
@workers += 1
|
|
145
|
+
true
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def release_worker
|
|
150
|
+
@mutex.synchronize { @workers -= 1 }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Zero-TTL priming data remains usable by the lookup that received it,
|
|
154
|
+
# including its address workers, but never by an independent lookup.
|
|
155
|
+
def retain_hints(generation, hints)
|
|
156
|
+
@mutex.synchronize { @hints = [generation, hints] }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def hints_for(generation)
|
|
160
|
+
@mutex.synchronize do
|
|
161
|
+
if @hints && @hints.first == generation &&
|
|
162
|
+
@hints.last.any? { |_name, addresses| !addresses.current_values.empty? }
|
|
163
|
+
@hints.last
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def with_dependency(name, type, klass)
|
|
169
|
+
previous = Thread.current[:dnsruby_recursion_dependencies] || []
|
|
170
|
+
identity = [Name.create(name).canonical, Types.new(type).code, Classes.new(klass).code]
|
|
171
|
+
return nil if previous.include?(identity)
|
|
172
|
+
Thread.current[:dnsruby_recursion_dependencies] = previous + [identity]
|
|
173
|
+
yield
|
|
174
|
+
ensure
|
|
175
|
+
Thread.current[:dnsruby_recursion_dependencies] = previous
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
private_constant :Budget
|
|
179
|
+
|
|
180
|
+
def recursion_limits
|
|
181
|
+
@recursion_limits || DEFAULT_RECURSION_LIMITS
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def recursion_limits=(settings)
|
|
185
|
+
unless settings.is_a?(Hash) && (settings.keys - DEFAULT_RECURSION_LIMITS.keys).empty?
|
|
186
|
+
raise ArgumentError, 'Unknown recursive lookup limit'
|
|
187
|
+
end
|
|
188
|
+
limits = DEFAULT_RECURSION_LIMITS.merge(settings)
|
|
189
|
+
limits.each do |key, value|
|
|
190
|
+
valid = if key == :timeout
|
|
191
|
+
value.is_a?(Numeric) && value.finite? && value > 0
|
|
192
|
+
else
|
|
193
|
+
value.is_a?(Integer) && value > 0
|
|
194
|
+
end
|
|
195
|
+
raise ArgumentError, "Invalid recursive lookup limit #{key}" unless valid
|
|
196
|
+
end
|
|
197
|
+
@recursion_limits = limits.freeze
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.current_budget
|
|
201
|
+
SelectThread::TransmissionBudget.current&.context(:recursion) { nil }
|
|
202
|
+
end
|
|
203
|
+
private_class_method :current_budget
|
|
204
|
+
|
|
205
|
+
def self.with_limits(resolver, limits = DEFAULT_RECURSION_LIMITS)
|
|
206
|
+
parent = QueryControl.current
|
|
207
|
+
parent&.check!
|
|
208
|
+
transmissions = SelectThread::TransmissionBudget.current || SelectThread::TransmissionBudget.new
|
|
209
|
+
budget = transmissions.context(:recursion) { Budget.new(limits) }
|
|
210
|
+
transmissions.limit_total!(budget.limits[:max_queries], RecursionLimitError)
|
|
211
|
+
timeout = resolver.query_timeout if resolver.respond_to?(:query_timeout)
|
|
212
|
+
deadline = budget.deadline
|
|
213
|
+
deadline = [deadline, TransportClock.now + timeout].min if timeout && timeout > 0
|
|
214
|
+
control = QueryControl.new(deadline: deadline, parent: parent)
|
|
215
|
+
SelectThread::TransmissionBudget.with(transmissions) do
|
|
216
|
+
QueryControl.with(control) { yield budget }
|
|
217
|
+
end
|
|
218
|
+
rescue QueryControl::DeadlineExceeded
|
|
219
|
+
parent&.check!
|
|
220
|
+
raise ResolvTimeout, 'Recursive query timed out'
|
|
221
|
+
ensure
|
|
222
|
+
control&.cancel
|
|
223
|
+
end
|
|
224
|
+
private_class_method :with_limits
|
|
225
|
+
|
|
114
226
|
class AddressCache # :nodoc: all
|
|
115
227
|
# Like an array, but stores the expiration of each record.
|
|
116
228
|
def initialize(*args)
|
|
117
229
|
@hash = Hash.new # stores addresses against their expiration
|
|
230
|
+
@fresh = {} # Zero-TTL addresses belong only to the current lookup.
|
|
118
231
|
@mutex = Mutex.new # This class is thread-safe
|
|
119
232
|
end
|
|
120
233
|
def push(item)
|
|
121
234
|
address, ttl = item
|
|
122
235
|
expiration = Time.now + ttl
|
|
123
236
|
@mutex.synchronize {
|
|
124
|
-
@hash
|
|
237
|
+
@hash.delete(address)
|
|
238
|
+
@fresh.delete(address)
|
|
239
|
+
if ttl <= 0
|
|
240
|
+
@fresh[address] = true
|
|
241
|
+
else
|
|
242
|
+
@hash[address] = expiration
|
|
243
|
+
end
|
|
244
|
+
@hash.shift while @hash.length > ADDRESS_CACHE_LIMIT
|
|
245
|
+
@fresh.shift while @fresh.length > ADDRESS_CACHE_LIMIT
|
|
125
246
|
}
|
|
126
247
|
end
|
|
127
248
|
def values
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
249
|
+
@mutex.synchronize { expire; @hash.keys }
|
|
250
|
+
end
|
|
251
|
+
def current_values
|
|
252
|
+
@mutex.synchronize { expire; @hash.keys | @fresh.keys }
|
|
253
|
+
end
|
|
254
|
+
# Shared caches contain only reusable addresses. Copies also keep later
|
|
255
|
+
# updates to one lookup's working map out of another lookup's view.
|
|
256
|
+
def snapshot(include_current: false)
|
|
257
|
+
copy = self.class.new
|
|
258
|
+
@mutex.synchronize do
|
|
259
|
+
expire
|
|
260
|
+
copy.instance_variable_set(:@hash, @hash.dup)
|
|
261
|
+
copy.instance_variable_set(:@fresh, @fresh.dup) if include_current
|
|
262
|
+
end
|
|
263
|
+
copy
|
|
264
|
+
end
|
|
265
|
+
private def expire
|
|
266
|
+
now = Time.now
|
|
267
|
+
@hash.delete_if { |_address, expiration| expiration <= now }
|
|
143
268
|
end
|
|
144
269
|
def length
|
|
145
|
-
|
|
146
|
-
return @hash.length
|
|
147
|
-
}
|
|
270
|
+
values.length
|
|
148
271
|
end
|
|
149
272
|
def each()
|
|
150
273
|
values.each {|v|
|
|
@@ -159,9 +282,15 @@ module Dnsruby
|
|
|
159
282
|
|
|
160
283
|
# For guarding access to shared caches.
|
|
161
284
|
@@mutex = Mutex.new # :nodoc: all
|
|
285
|
+
@@hints_mutex = Mutex.new
|
|
286
|
+
@@hints_generation = 0
|
|
287
|
+
@@stale_hints = nil
|
|
162
288
|
@@hints = nil
|
|
163
289
|
@@authority_cache = Hash.new
|
|
164
290
|
@@zones_cache = nil
|
|
291
|
+
# Keep delegation lifetimes independent of the A/AAAA glue lifetimes.
|
|
292
|
+
# The stored Hash identity prevents stale metadata from expiring a replacement.
|
|
293
|
+
@@zone_expirations = {}
|
|
165
294
|
@@nameservers = nil
|
|
166
295
|
|
|
167
296
|
def dnssec=(dnssec_on)
|
|
@@ -173,14 +302,22 @@ module Dnsruby
|
|
|
173
302
|
if (res)
|
|
174
303
|
@resolver = res
|
|
175
304
|
else
|
|
176
|
-
|
|
177
|
-
|
|
305
|
+
nameservers = @@mutex.synchronize do
|
|
306
|
+
# Hints already contain addresses. Re-resolving their owner names
|
|
307
|
+
# would start separate bootstrap queries outside this lookup.
|
|
308
|
+
if @@hints && !@@hints.equal?(@@stale_hints)
|
|
309
|
+
@@hints.values.flat_map(&:values).uniq
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
if nameservers && !nameservers.empty?
|
|
313
|
+
@resolver = Resolver.new({:nameserver => nameservers})
|
|
178
314
|
else
|
|
179
315
|
@resolver = Resolver.new
|
|
180
316
|
end
|
|
181
317
|
end
|
|
182
318
|
@resolver.dnssec = @dnssec
|
|
183
319
|
@ipv6_ok = false
|
|
320
|
+
@response_cache = Cache::ResolutionCache.new
|
|
184
321
|
end
|
|
185
322
|
# Initialize the hint servers. Recursive queries need a starting name
|
|
186
323
|
# server to work off of. This method takes a list of IP addresses to use
|
|
@@ -195,41 +332,95 @@ module Dnsruby
|
|
|
195
332
|
# ftp://ftp.internic.net/domain/named.root
|
|
196
333
|
#
|
|
197
334
|
def hints=(hints)
|
|
198
|
-
Recursor.set_hints(hints, @resolver)
|
|
335
|
+
Recursor.send(:with_limits, @resolver, recursion_limits) { Recursor.set_hints(hints, @resolver) }
|
|
199
336
|
end
|
|
200
|
-
def
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
337
|
+
def self.with_hints_configuration
|
|
338
|
+
# Bootstrap can wait on DNS. Keep it outside the cache mutex, and allow
|
|
339
|
+
# a query waiting for another bootstrap to honour its own deadline.
|
|
340
|
+
until @@hints_mutex.try_lock
|
|
341
|
+
QueryControl.current&.check!
|
|
342
|
+
sleep 0.01
|
|
343
|
+
end
|
|
344
|
+
begin
|
|
345
|
+
QueryControl.current&.check!
|
|
346
|
+
yield
|
|
347
|
+
ensure
|
|
348
|
+
@@hints_mutex.unlock
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
private_class_method :with_hints_configuration
|
|
352
|
+
|
|
353
|
+
def self.reset_hints
|
|
354
|
+
with_hints_configuration do
|
|
355
|
+
@@mutex.synchronize do
|
|
356
|
+
# Existing queries may still hold the previous root bootstrap.
|
|
357
|
+
# Keep it readable until a replacement is ready, but require the
|
|
358
|
+
# next query to refresh it and prevent old work from seeding caches.
|
|
359
|
+
@@stale_hints = @@hints
|
|
360
|
+
@@zones_cache = @@hints && {'.' => @@hints}
|
|
361
|
+
@@nameservers = nil
|
|
362
|
+
@@zone_expirations = {}
|
|
363
|
+
@@authority_cache = {}
|
|
364
|
+
@@hints_generation += 1
|
|
365
|
+
Cache.invalidate_resolutions
|
|
211
366
|
end
|
|
212
|
-
hints.each {|hint|
|
|
213
|
-
@@hints = Hash.new
|
|
214
|
-
@@hints[hint]=hint
|
|
215
|
-
}
|
|
216
367
|
end
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def self.ensure_hints(resolver)
|
|
371
|
+
generation = @@mutex.synchronize do
|
|
372
|
+
if hints_ready?
|
|
373
|
+
current_budget&.retain_hints(@@hints_generation, @@hints)
|
|
374
|
+
@@hints_generation
|
|
375
|
+
elsif current_budget&.hints_for(@@hints_generation)
|
|
376
|
+
@@hints_generation
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
return generation if generation
|
|
380
|
+
with_hints_configuration do
|
|
381
|
+
configured = @@mutex.synchronize { hints_ready? || current_budget&.hints_for(@@hints_generation) }
|
|
382
|
+
configure_hints({}, resolver) unless configured
|
|
383
|
+
@@mutex.synchronize { @@hints_generation }
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
private_class_method :ensure_hints
|
|
387
|
+
|
|
388
|
+
# Called with the cache mutex held. A retained bootstrap is usable only
|
|
389
|
+
# while at least one root address is live and no reset has superseded it.
|
|
390
|
+
def self.hints_ready?
|
|
391
|
+
lifetime = @@zone_expirations['.']
|
|
392
|
+
return false if lifetime && lifetime.first.equal?(@@hints) && lifetime[1] <= Time.now
|
|
393
|
+
@@hints && @@zones_cache && !@@hints.equal?(@@stale_hints) &&
|
|
394
|
+
@@hints.any? { |_name, addresses| !addresses.values.empty? }
|
|
395
|
+
end
|
|
396
|
+
private_class_method :hints_ready?
|
|
397
|
+
|
|
398
|
+
# Caller holds the cache mutex. Fresh hints belong to this query's budget.
|
|
399
|
+
def self.query_hints
|
|
400
|
+
current_budget&.hints_for(@@hints_generation) || @@hints
|
|
401
|
+
end
|
|
402
|
+
private_class_method :query_hints
|
|
403
|
+
|
|
404
|
+
def self.set_hints(hints, resolver)
|
|
405
|
+
with_limits(resolver) do
|
|
406
|
+
with_hints_configuration { configure_hints(hints, resolver) }
|
|
222
407
|
end
|
|
223
|
-
|
|
224
|
-
# bind always asks one of the hint servers
|
|
225
|
-
# for who it thinks is authoritative for
|
|
226
|
-
# the (root) zone as a sanity check.
|
|
227
|
-
# Nice idea.
|
|
408
|
+
end
|
|
228
409
|
|
|
229
|
-
|
|
410
|
+
def self.configure_hints(hints, resolver)
|
|
411
|
+
TheLog.debug(";; hints(#{hints.inspect})\n")
|
|
412
|
+
temporary_resolver = nil
|
|
413
|
+
if resolver.single_resolvers.empty?
|
|
414
|
+
resolver = temporary_resolver = Resolver.new
|
|
415
|
+
end
|
|
416
|
+
resolver.nameservers = hints if hints && !hints.empty?
|
|
230
417
|
resolver.recurse = true
|
|
418
|
+
budget = current_budget
|
|
419
|
+
budget&.step!
|
|
231
420
|
packet=resolver.query_no_validation_or_recursion(".", "NS", "IN")
|
|
421
|
+
received_at = Time.now
|
|
232
422
|
hints = Hash.new
|
|
423
|
+
root_expiration = nil
|
|
233
424
|
if (packet)
|
|
234
425
|
if (ans = packet.answer)
|
|
235
426
|
ans.each do |rr|
|
|
@@ -240,98 +431,77 @@ module Dnsruby
|
|
|
240
431
|
server.sub!(/\.$/,"")
|
|
241
432
|
TheLog.debug(";; FOUND HINT: #{server}\n")
|
|
242
433
|
hints[server] = AddressCache.new
|
|
434
|
+
receipt = rr.instance_variable_get(:@dnsruby_received_at)
|
|
435
|
+
expiration = (receipt ? Time.at(receipt) : received_at) + rr.ttl
|
|
436
|
+
root_expiration = [root_expiration, expiration].compact.min
|
|
243
437
|
end
|
|
244
438
|
end
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
#
|
|
249
|
-
#
|
|
250
|
-
|
|
251
|
-
q = Queue.new
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
[
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
msg.
|
|
439
|
+
packet.additional.each { |rr| add_to_hints(hints, rr) }
|
|
440
|
+
missing = hints.keys.select { |server| hints[server].current_values.empty? }
|
|
441
|
+
unless missing.empty?
|
|
442
|
+
# Resolve missing root addresses in bounded batches. All of the
|
|
443
|
+
# queries keep the bootstrap's deadline and transmission budget.
|
|
444
|
+
control = QueryControl.current
|
|
445
|
+
q = control ? QueryControl::Queue.new : Queue.new
|
|
446
|
+
batch_size = budget ? budget.limits[:max_workers] : DEFAULT_RECURSION_LIMITS[:max_workers]
|
|
447
|
+
missing.product(['A', 'AAAA']).each_slice(batch_size) do |batch|
|
|
448
|
+
query_ids = []
|
|
449
|
+
begin
|
|
450
|
+
batch.each do |server, type|
|
|
451
|
+
budget&.step!
|
|
452
|
+
msg = Message.new
|
|
453
|
+
msg.do_caching = @do_caching
|
|
454
|
+
msg.header.rd = false
|
|
455
|
+
msg.do_validation = false
|
|
456
|
+
msg.add_question(server, type, 'IN')
|
|
457
|
+
msg.header.cd = true if @dnssec
|
|
458
|
+
control&.check!
|
|
459
|
+
query_ids << resolver.send_async(msg, q)
|
|
460
|
+
end
|
|
461
|
+
query_ids.length.times do
|
|
462
|
+
_id, result, error = control ? control.pop(q) : q.pop
|
|
463
|
+
raise error if error.is_a?(RecursionLimitError)
|
|
464
|
+
result&.answer&.each { |rr| add_to_hints(hints, rr) }
|
|
465
|
+
end
|
|
466
|
+
ensure
|
|
467
|
+
if control && (engine = resolver.instance_variable_get(:@resolver_ruby))
|
|
468
|
+
query_ids.each { |id| engine.cancel_query(id, q) }
|
|
262
469
|
end
|
|
263
|
-
resolver.send_async(msg, q)
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
(hints.length * 2).times {
|
|
267
|
-
_id, result, _error = q.pop
|
|
268
|
-
if (result)
|
|
269
|
-
result.answer.each {|rr|
|
|
270
|
-
TheLog.debug(";; NS address: " + rr.inspect+"\n")
|
|
271
|
-
add_to_hints(hints, rr)
|
|
272
|
-
}
|
|
273
470
|
end
|
|
274
|
-
}
|
|
275
|
-
else
|
|
276
|
-
packet.additional.each do |rr|
|
|
277
|
-
TheLog.debug(";; ADDITIONAL: "+rr.inspect+"\n")
|
|
278
|
-
add_to_hints(hints, rr)
|
|
279
|
-
|
|
280
471
|
end
|
|
281
472
|
end
|
|
282
473
|
end
|
|
283
474
|
# foreach my $server (keys %hints) {
|
|
284
475
|
hints.keys.each do |server|
|
|
285
|
-
if (!hints[server] || hints[server].
|
|
476
|
+
if (!hints[server] || hints[server].current_values.empty?)
|
|
286
477
|
# Wipe the servers without lookups
|
|
287
478
|
hints.delete(server)
|
|
288
479
|
end
|
|
289
480
|
end
|
|
290
|
-
@@hints = hints
|
|
291
|
-
else
|
|
292
|
-
@@hints = {}
|
|
293
|
-
end
|
|
294
|
-
if (@@hints.size > 0)
|
|
295
|
-
TheLog.info(";; USING THE FOLLOWING HINT IPS:\n")
|
|
296
|
-
@@hints.values.each do |ips|
|
|
297
|
-
ips.each do |server|
|
|
298
|
-
TheLog.info(";; #{server}\n")
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
else
|
|
302
|
-
raise ResolvError.new( "Server ["+(@@nameservers)[0].to_s+".] did not give answers")
|
|
303
481
|
end
|
|
482
|
+
raise ResolvError, 'Nameserver did not supply usable root hints' if hints.empty?
|
|
304
483
|
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
@@hints.
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
@@
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
print "Adding hint : #{temp[count]}\n"
|
|
319
|
-
@@hints[count] = temp[count]
|
|
320
|
-
count += 1
|
|
321
|
-
}
|
|
484
|
+
# Publish a complete bootstrap, replacing delegations learned through the
|
|
485
|
+
# old hints. An unsuccessful bootstrap leaves the previous state intact.
|
|
486
|
+
@@mutex.synchronize do
|
|
487
|
+
fresh_hints = hints
|
|
488
|
+
@@hints = hints.transform_values(&:snapshot)
|
|
489
|
+
@@stale_hints = nil
|
|
490
|
+
@@hints_generation += 1
|
|
491
|
+
@@nameservers = hints.keys
|
|
492
|
+
@@zones_cache = {'.' => @@hints}
|
|
493
|
+
@@zone_expirations = {'.' => [@@hints, root_expiration]}
|
|
494
|
+
budget&.retain_hints(@@hints_generation, fresh_hints)
|
|
495
|
+
@@authority_cache = {}
|
|
496
|
+
Cache.invalidate_resolutions
|
|
322
497
|
end
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
end
|
|
328
|
-
# @@nameservers = @@hints.values
|
|
329
|
-
@@nameservers=[]
|
|
330
|
-
@@hints.each {|key, value|
|
|
331
|
-
@@nameservers.push(key)
|
|
332
|
-
}
|
|
333
|
-
return @@nameservers
|
|
498
|
+
@@nameservers
|
|
499
|
+
ensure
|
|
500
|
+
resolver.recurse = false if resolver
|
|
501
|
+
temporary_resolver&.close
|
|
334
502
|
end
|
|
503
|
+
private_class_method :configure_hints
|
|
504
|
+
|
|
335
505
|
|
|
336
506
|
def Recursor.add_to_hints(hints, rr)
|
|
337
507
|
server = rr.name.to_s.downcase
|
|
@@ -378,13 +548,64 @@ module Dnsruby
|
|
|
378
548
|
return @callback
|
|
379
549
|
end
|
|
380
550
|
|
|
381
|
-
def Recursor.clear_caches(resolver =
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
551
|
+
def Recursor.clear_caches(resolver = nil)
|
|
552
|
+
temporary_resolver = resolver.nil?
|
|
553
|
+
resolver ||= Resolver.new
|
|
554
|
+
Recursor.set_hints({}, resolver)
|
|
555
|
+
ensure
|
|
556
|
+
resolver&.close if temporary_resolver
|
|
557
|
+
end
|
|
386
558
|
|
|
387
|
-
|
|
559
|
+
# These helpers are called with the shared cache mutex held. Root hints
|
|
560
|
+
# are retained as the recursion bootstrap; learned delegations are bounded.
|
|
561
|
+
def self.cache_authority(name, addresses) # :nodoc:
|
|
562
|
+
unless @@mutex.owned?
|
|
563
|
+
return @@mutex.synchronize { cache_authority(name, addresses) }
|
|
564
|
+
end
|
|
565
|
+
generation = Thread.current[:dnsruby_recursor_generation]
|
|
566
|
+
return if generation && generation != @@hints_generation
|
|
567
|
+
@@authority_cache.delete(name)
|
|
568
|
+
@@authority_cache[name] = addresses.is_a?(AddressCache) ? addresses.snapshot : addresses.dup
|
|
569
|
+
@@authority_cache.shift while @@authority_cache.length > CACHE_LIMIT
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def self.cache_zone(name, authorities, ttl, parent: nil) # :nodoc:
|
|
573
|
+
unless @@mutex.owned?
|
|
574
|
+
return @@mutex.synchronize { cache_zone(name, authorities, ttl, parent: parent) }
|
|
575
|
+
end
|
|
576
|
+
generation = Thread.current[:dnsruby_recursor_generation]
|
|
577
|
+
return if generation && generation != @@hints_generation
|
|
578
|
+
name = Name.create(name)
|
|
579
|
+
name.absolute = true
|
|
580
|
+
name = name.to_s(true).downcase
|
|
581
|
+
if parent
|
|
582
|
+
parent = Name.create(parent)
|
|
583
|
+
parent.absolute = true
|
|
584
|
+
parent = parent.to_s(true).downcase
|
|
585
|
+
end
|
|
586
|
+
return if name == "."
|
|
587
|
+
@@zones_cache.delete(name)
|
|
588
|
+
@@zone_expirations.delete(name)
|
|
589
|
+
return if ttl.nil? || ttl <= 0
|
|
590
|
+
expiration = Time.now + ttl
|
|
591
|
+
if parent
|
|
592
|
+
lifetime = @@zone_expirations[parent]
|
|
593
|
+
# Retain the parent's deadline, not a reference to its cache entry.
|
|
594
|
+
# LRU eviction must not invalidate descendants already learned, while
|
|
595
|
+
# a descendant cannot extend its route beyond the parent admission.
|
|
596
|
+
return unless lifetime && lifetime.first.equal?(@@zones_cache[parent])
|
|
597
|
+
expiration = [expiration, lifetime[1]].min
|
|
598
|
+
return if expiration <= Time.now
|
|
599
|
+
end
|
|
600
|
+
snapshot = authorities.transform_values { |addresses| addresses.is_a?(AddressCache) ? addresses.snapshot : addresses.dup }
|
|
601
|
+
@@zones_cache[name] = snapshot
|
|
602
|
+
@@zone_expirations[name] = [snapshot, expiration]
|
|
603
|
+
while @@zones_cache.length > CACHE_LIMIT
|
|
604
|
+
oldest = @@zones_cache.each_key.find { |zone| zone != "." }
|
|
605
|
+
break unless oldest
|
|
606
|
+
@@zones_cache.delete(oldest)
|
|
607
|
+
@@zone_expirations.delete(oldest)
|
|
608
|
+
end
|
|
388
609
|
end
|
|
389
610
|
|
|
390
611
|
def query_no_validation_or_recursion(name, type=Types.A, klass=Classes.IN) # :nodoc: all
|
|
@@ -400,71 +621,231 @@ module Dnsruby
|
|
|
400
621
|
# The Recursor maintains a cache of known nameservers.
|
|
401
622
|
# DNSSEC validation is performed unless true is passed as the fourth parameter.
|
|
402
623
|
def query(name, type=Types.A, klass=Classes.IN, no_validation = false)
|
|
624
|
+
Recursor.send(:with_limits, @resolver, recursion_limits) do
|
|
625
|
+
if no_validation
|
|
626
|
+
query_internal(name, type, klass, no_validation)
|
|
627
|
+
else
|
|
628
|
+
transmissions = SelectThread::TransmissionBudget.current
|
|
629
|
+
budget = transmissions.context(:validation) { ValidationBudget.current || ValidationBudget.new }
|
|
630
|
+
ValidationBudget.with(budget) { query_internal(name, type, klass, no_validation) }
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def query_internal(name, type, klass, no_validation)
|
|
636
|
+
previous_generation = Thread.current[:dnsruby_recursor_generation]
|
|
637
|
+
QueryControl.current&.check!
|
|
638
|
+
unless SelectThread::TransmissionBudget.current
|
|
639
|
+
return SelectThread::TransmissionBudget.with(SelectThread::TransmissionBudget.new) do
|
|
640
|
+
query_internal(name, type, klass, no_validation)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
403
643
|
# @TODO@ PROVIDE AN ASYNCHRONOUS SEND WHICH RETURNS MESSAGE WITH ERROR!!!
|
|
404
644
|
|
|
405
|
-
|
|
406
|
-
@@mutex.synchronize {
|
|
407
|
-
self.hints=(Hash.new) unless @@hints
|
|
408
|
-
}
|
|
645
|
+
Thread.current[:dnsruby_recursor_generation] = Recursor.send(:ensure_hints, @resolver)
|
|
409
646
|
@resolver.recurse = false
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
647
|
+
|
|
648
|
+
query_message = Message.new(name, type, klass)
|
|
649
|
+
query_message.do_validation = !no_validation
|
|
650
|
+
scope = recursion_cache_scope(no_validation)
|
|
651
|
+
if scope && (cached = @response_cache.find(query_message, scope))
|
|
652
|
+
response, error = cached
|
|
653
|
+
error.response = response if error.is_a?(ResolvError)
|
|
654
|
+
raise error if error
|
|
655
|
+
return response
|
|
656
|
+
end
|
|
419
657
|
|
|
420
658
|
# So we have normal hashes, but the array of addresses at the end is now an AddressCache
|
|
421
659
|
# which respects the ttls of the A/AAAA records
|
|
422
660
|
|
|
423
661
|
# Now see if we already know the zone in question
|
|
424
662
|
# Otherwise, see if we know any of its parents (will know at least ".")
|
|
425
|
-
|
|
663
|
+
authority_name = name
|
|
664
|
+
if Types.new(type) == Types::DS
|
|
665
|
+
# DS records belong to the parent side of a delegation (RFC 4035
|
|
666
|
+
# section 3.1.4). A cached child authority cannot answer for them.
|
|
667
|
+
authority_name = Name.create(name)
|
|
668
|
+
authority_name = authority_name.strip_label unless authority_name.labels.empty?
|
|
669
|
+
end
|
|
670
|
+
known_zone, known_authorities = get_closest_known_zone_authorities_for(authority_name) # ".", @hints if nothing else
|
|
426
671
|
|
|
427
672
|
# Seed name servers with the closest known authority
|
|
428
673
|
# ret = _dorecursion( name, type, klass, ".", @hints, 0)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
674
|
+
begin
|
|
675
|
+
ret = follow_aliases(name, type, klass, known_zone, known_authorities, 0, no_validation)
|
|
676
|
+
Dnssec.validate(ret) if ret && !no_validation
|
|
677
|
+
if scope && scope == recursion_cache_scope(no_validation)
|
|
678
|
+
@response_cache.add(query_message, scope, ret, nil,
|
|
679
|
+
control: QueryControl.current, no_response: ret.nil?)
|
|
680
|
+
end
|
|
681
|
+
ret
|
|
682
|
+
rescue StandardError => error
|
|
683
|
+
if scope && scope == recursion_cache_scope(no_validation)
|
|
684
|
+
failed_response = ret || (error.response if error.is_a?(ResolvError))
|
|
685
|
+
@response_cache.add(query_message, scope, failed_response, error, control: QueryControl.current)
|
|
686
|
+
end
|
|
687
|
+
raise
|
|
688
|
+
end
|
|
689
|
+
ensure
|
|
690
|
+
Thread.current[:dnsruby_recursor_generation] = previous_generation
|
|
433
691
|
end
|
|
434
692
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
693
|
+
private :query_internal
|
|
694
|
+
|
|
695
|
+
# An authoritative server may finish its answer at an alias whose target
|
|
696
|
+
# belongs to another zone. Continue within this lookup's budgets, retaining
|
|
697
|
+
# the caller's question and every answer in the alias chain.
|
|
698
|
+
def follow_aliases(name, type, klass, known_zone, authorities, depth, no_validation)
|
|
699
|
+
original_name = Name.create(name)
|
|
700
|
+
original_name.absolute = true
|
|
701
|
+
qtype, qclass = Types.new(type), Classes.new(klass)
|
|
702
|
+
current_name = original_name
|
|
703
|
+
responses = []
|
|
704
|
+
seen = {}
|
|
705
|
+
loop do
|
|
706
|
+
QueryControl.current&.check!
|
|
707
|
+
begin
|
|
708
|
+
packet = _dorecursion(current_name.to_s(true), type, klass, known_zone, authorities, depth, no_validation)
|
|
709
|
+
rescue ResolvError => error
|
|
710
|
+
if error.response && !responses.empty?
|
|
711
|
+
error.response = combine_alias_responses(responses + [error.response], original_name, qtype, qclass)
|
|
712
|
+
end
|
|
713
|
+
raise
|
|
714
|
+
end
|
|
715
|
+
return nil unless packet
|
|
716
|
+
responses << packet
|
|
717
|
+
# Explicit alias queries and ANY request the records themselves.
|
|
718
|
+
if [Types::CNAME, Types::DNAME, Types::ANY].include?(qtype) ||
|
|
719
|
+
packet.rcode != RCode::NOERROR || Cache.negative_soa(packet)
|
|
720
|
+
return combine_alias_responses(responses, original_name, qtype, qclass)
|
|
721
|
+
end
|
|
722
|
+
target, complete = alias_target(packet, current_name, qtype, qclass, seen)
|
|
723
|
+
if complete || target == current_name
|
|
724
|
+
return combine_alias_responses(responses, original_name, qtype, qclass)
|
|
725
|
+
end
|
|
726
|
+
current_name = target
|
|
727
|
+
depth += 1
|
|
728
|
+
authority_name = qtype == Types::DS && !current_name.labels.empty? ? current_name.strip_label : current_name
|
|
729
|
+
known_zone, authorities = get_closest_known_zone_authorities_for(authority_name)
|
|
441
730
|
end
|
|
731
|
+
end
|
|
732
|
+
private :follow_aliases
|
|
733
|
+
|
|
734
|
+
def alias_target(packet, name, type, klass, seen)
|
|
735
|
+
loop do
|
|
736
|
+
QueryControl.current&.check!
|
|
737
|
+
records = packet.answer.select { |rr| rr.klass == klass && rr.name == name }
|
|
738
|
+
return [name, true] if records.any? { |rr| rr.type == type }
|
|
739
|
+
aliases = records.select { |rr| rr.type == Types::CNAME }
|
|
740
|
+
if aliases.empty?
|
|
741
|
+
dnames = packet.answer.select do |rr|
|
|
742
|
+
rr.type == Types::DNAME && rr.klass == klass && name.subdomain_of?(rr.name)
|
|
743
|
+
end
|
|
744
|
+
dname = dnames.max_by { |rr| rr.name.labels.length }
|
|
745
|
+
return [name, false] unless dname
|
|
746
|
+
if dnames.any? { |rr| rr.name == dname.name && rr.domainname != dname.domainname }
|
|
747
|
+
raise ResolvError, 'Conflicting DNAME targets in recursive answer'
|
|
748
|
+
end
|
|
749
|
+
begin
|
|
750
|
+
target = Name.new(name.labels.first(name.labels.length - dname.name.labels.length) +
|
|
751
|
+
dname.domainname.labels, true)
|
|
752
|
+
rescue ResolvError => error
|
|
753
|
+
raise YXDomain, "DNAME target is too long: #{error.message}"
|
|
754
|
+
end
|
|
755
|
+
cname = RR.create(name: name, type: Types::CNAME, klass: klass, ttl: dname.ttl, domainname: target)
|
|
756
|
+
packet.add_answer(cname)
|
|
757
|
+
else
|
|
758
|
+
targets = aliases.map(&:domainname).uniq
|
|
759
|
+
raise ResolvError, 'Conflicting CNAME targets in recursive answer' unless targets.length == 1
|
|
760
|
+
target = targets.first
|
|
761
|
+
end
|
|
762
|
+
Recursor.send(:current_budget)&.step!
|
|
763
|
+
raise RecursionLimitError, 'Recursive alias chain is too long' if seen.length >= 255
|
|
764
|
+
raise RecursionLimitError, 'Recursive alias loop' if seen[name.canonical]
|
|
765
|
+
seen[name.canonical] = true
|
|
766
|
+
name = target
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
private :alias_target
|
|
770
|
+
|
|
771
|
+
def combine_alias_responses(responses, name, type, klass)
|
|
772
|
+
return responses.first if responses.length == 1
|
|
773
|
+
result = responses.last.clone
|
|
774
|
+
result.question.replace([Question.new(name, type, klass)])
|
|
775
|
+
result.answer.replace(responses.flat_map(&:answer))
|
|
776
|
+
# Earlier wildcard answers can require their own denial proofs when the
|
|
777
|
+
# completed chain is verified. Keep those proofs with their signatures.
|
|
778
|
+
proofs = responses[0...-1].flat_map(&:authority).select do |rr|
|
|
779
|
+
[Types::NSEC, Types::NSEC3].include?(rr.type) ||
|
|
780
|
+
(rr.type == Types::RRSIG && [Types::NSEC, Types::NSEC3].include?(rr.type_covered))
|
|
781
|
+
end
|
|
782
|
+
result.authority.concat(proofs)
|
|
783
|
+
result.header.id = responses.first.header.id
|
|
784
|
+
# The assembled response is no longer one authority's signed message.
|
|
785
|
+
# DNSSEC is checked over the complete answer before it is returned.
|
|
786
|
+
result.header.aa = result.header.ad = false
|
|
787
|
+
expirations = responses.filter_map { |packet| packet.instance_variable_get(:@dnsruby_authenticated_expiration) }
|
|
788
|
+
result.instance_variable_set(:@dnsruby_authenticated_expiration, expirations.min) unless expirations.empty?
|
|
789
|
+
result.answerfrom = responses.last.answerfrom
|
|
790
|
+
result.answerip = responses.last.answerip
|
|
791
|
+
result.update_counts
|
|
792
|
+
result
|
|
793
|
+
end
|
|
794
|
+
private :combine_alias_responses
|
|
795
|
+
|
|
796
|
+
HOP_SETTINGS = %i[port use_tcp no_tcp tcp_pipelining tcp_pipelining_max_queries
|
|
797
|
+
tcp_idle_timeout packet_timeout query_timeout retry_times retry_delay
|
|
798
|
+
ignore_truncation udp_size src_address src_address6 src_port].freeze
|
|
799
|
+
private_constant :HOP_SETTINGS
|
|
800
|
+
|
|
801
|
+
def hop_settings
|
|
802
|
+
HOP_SETTINGS.each_with_object({}) do |field, settings|
|
|
803
|
+
next unless @resolver && @resolver.respond_to?(field)
|
|
804
|
+
value = @resolver.public_send(field)
|
|
805
|
+
settings[field] = value.is_a?(Array) || value.is_a?(String) ? value.dup : value
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
private :hop_settings
|
|
809
|
+
|
|
810
|
+
def recursion_cache_scope(no_validation)
|
|
811
|
+
return unless @resolver.respond_to?(:do_caching) && @resolver.do_caching
|
|
812
|
+
return if @resolver.tsig
|
|
813
|
+
hints = @@mutex.synchronize do
|
|
814
|
+
@@hints.map { |name, addresses| [name, addresses.values.map(&:to_s).sort] }.sort_by(&:first)
|
|
815
|
+
end
|
|
816
|
+
[no_validation, @dnssec, @ipv6_ok, hints, @resolver.object_id, hop_settings, recursion_limits,
|
|
817
|
+
@resolver.single_resolvers.map { |sender| [sender.server.to_s.dup, sender.port] }.sort,
|
|
818
|
+
Cache.validation_scope]
|
|
819
|
+
end
|
|
820
|
+
private :recursion_cache_scope
|
|
821
|
+
|
|
822
|
+
def get_closest_known_zone_for(n) # :nodoc:
|
|
442
823
|
if (n == nil)
|
|
443
824
|
TheLog.error("Name is nil")
|
|
444
825
|
raise ResolvError.new("Nameserver invalid!")
|
|
445
826
|
end
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
while (true)
|
|
452
|
-
# print "Checking for known zone : #{name}\n"
|
|
453
|
-
zone = nil
|
|
827
|
+
candidate = Name.create(n)
|
|
828
|
+
candidate.absolute = true
|
|
829
|
+
loop do
|
|
830
|
+
QueryControl.current&.check!
|
|
831
|
+
name = candidate.to_s(true).downcase
|
|
454
832
|
@@mutex.synchronize{
|
|
833
|
+
if name == '.' && Recursor.send(:current_budget)&.hints_for(@@hints_generation)
|
|
834
|
+
return name
|
|
835
|
+
end
|
|
455
836
|
zone = @@zones_cache[name]
|
|
837
|
+
lifetime = @@zone_expirations[name]
|
|
838
|
+
if lifetime && lifetime[0].equal?(zone) && lifetime[1] <= Time.now
|
|
839
|
+
@@zones_cache.delete(name)
|
|
840
|
+
@@zone_expirations.delete(name)
|
|
841
|
+
zone = nil
|
|
842
|
+
end
|
|
456
843
|
if (zone != nil)
|
|
457
844
|
return name
|
|
458
845
|
end
|
|
459
846
|
}
|
|
460
|
-
return false if
|
|
461
|
-
|
|
462
|
-
first_dot = name.index(".")
|
|
463
|
-
if (first_dot == (name.length-1))
|
|
464
|
-
name = "."
|
|
465
|
-
else
|
|
466
|
-
name = name[first_dot+1, name.length]
|
|
467
|
-
end
|
|
847
|
+
return false if candidate.labels.empty?
|
|
848
|
+
candidate = candidate.strip_label
|
|
468
849
|
end
|
|
469
850
|
end
|
|
470
851
|
|
|
@@ -472,10 +853,16 @@ module Dnsruby
|
|
|
472
853
|
done = false
|
|
473
854
|
known_authorities, known_zone = nil
|
|
474
855
|
while (!done)
|
|
856
|
+
QueryControl.current&.check!
|
|
475
857
|
known_zone = get_closest_known_zone_for(name)
|
|
476
858
|
# print "GOT KNOWN ZONE : #{known_zone}\n"
|
|
859
|
+
observed_authorities = nil
|
|
477
860
|
@@mutex.synchronize {
|
|
478
|
-
|
|
861
|
+
observed_authorities = known_zone == '.' ? Recursor.send(:query_hints) : @@zones_cache[known_zone]
|
|
862
|
+
known_authorities = observed_authorities&.dup # isolate each recursion's working map
|
|
863
|
+
known_authorities&.transform_values! do |addresses|
|
|
864
|
+
addresses.is_a?(AddressCache) ? addresses.snapshot(include_current: known_zone == '.') : addresses.dup
|
|
865
|
+
end
|
|
479
866
|
}
|
|
480
867
|
# print "Known authorities : #{known_authorities}\n"
|
|
481
868
|
|
|
@@ -485,7 +872,10 @@ module Dnsruby
|
|
|
485
872
|
done = true
|
|
486
873
|
else
|
|
487
874
|
@@mutex.synchronize{
|
|
488
|
-
@@zones_cache.
|
|
875
|
+
if @@zones_cache[known_zone].equal?(observed_authorities)
|
|
876
|
+
@@zones_cache.delete(known_zone)
|
|
877
|
+
@@zone_expirations.delete(known_zone)
|
|
878
|
+
end
|
|
489
879
|
}
|
|
490
880
|
end
|
|
491
881
|
end
|
|
@@ -493,6 +883,8 @@ module Dnsruby
|
|
|
493
883
|
end
|
|
494
884
|
|
|
495
885
|
def _dorecursion(name, type, klass, known_zone, known_authorities, depth, no_validation) # :nodoc:
|
|
886
|
+
QueryControl.current&.check!
|
|
887
|
+
Recursor.send(:current_budget)&.step!
|
|
496
888
|
|
|
497
889
|
if ( depth > 255 )
|
|
498
890
|
TheLog.debug(";; _dorecursion() Recursion too deep, aborting...\n")
|
|
@@ -502,110 +894,187 @@ module Dnsruby
|
|
|
502
894
|
|
|
503
895
|
known_zone = known_zone.sub(/\.*$/, ".")
|
|
504
896
|
|
|
505
|
-
ns = [] #
|
|
897
|
+
ns = [] # Snapshot each address list once for this hop.
|
|
898
|
+
missing_authorities = []
|
|
506
899
|
@@mutex.synchronize{
|
|
507
900
|
# Get IPs from authorities
|
|
508
901
|
known_authorities.keys.each do |ns_rec|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
902
|
+
addresses = known_authorities[ns_rec]
|
|
903
|
+
cached = @@authority_cache[ns_rec]
|
|
904
|
+
values = addresses.is_a?(AddressCache) ? addresses.current_values : Array(addresses)
|
|
905
|
+
cached_values = if values.empty?
|
|
906
|
+
cached.respond_to?(:values) ? cached.values : Array(cached)
|
|
907
|
+
end
|
|
908
|
+
if !values.empty?
|
|
909
|
+
Recursor.cache_authority(ns_rec, known_authorities[ns_rec])
|
|
910
|
+
ns.push(values)
|
|
911
|
+
elsif !cached_values.empty?
|
|
912
|
+
known_authorities[ns_rec] = cached.is_a?(AddressCache) ? cached.snapshot : cached.dup
|
|
913
|
+
ns.push(cached_values)
|
|
914
|
+
else
|
|
915
|
+
@@authority_cache.delete(ns_rec)
|
|
916
|
+
missing_authorities << ns_rec
|
|
515
917
|
end
|
|
516
918
|
end
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if known_zone == '.'
|
|
922
|
+
if ns.empty?
|
|
923
|
+
# Root addresses can expire after query admission, including while
|
|
924
|
+
# finding an address for a delegated server. Refresh the bootstrap
|
|
925
|
+
# instead of recursively looking up root servers against empty hints.
|
|
926
|
+
Recursor.send(:ensure_hints, @resolver)
|
|
927
|
+
ns = @@mutex.synchronize { Recursor.send(:query_hints).values.map(&:current_values).reject(&:empty?) }
|
|
928
|
+
raise ResolvError, 'Root hints expired before use' if ns.empty?
|
|
929
|
+
# Retain this query's original generation: a refresh or reset must
|
|
930
|
+
# not let earlier traversal work populate the replacement caches.
|
|
931
|
+
end
|
|
932
|
+
# Missing root addresses need bootstrap refresh, not hostname lookup
|
|
933
|
+
# through the same root servers after their other addresses fail.
|
|
934
|
+
missing_authorities.clear
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
if (ns.length == 0)
|
|
938
|
+
found_auth = 0
|
|
939
|
+
TheLog.debug(";; _dorecursion() Failed to extract nameserver IPs:")
|
|
940
|
+
TheLog.debug(known_authorities.inspect + @@authority_cache.inspect)
|
|
941
|
+
missing_authorities.each do |ns_rec|
|
|
942
|
+
TheLog.debug(";; _dorecursion() Manual lookup for authority [#{ns_rec}]")
|
|
517
943
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
944
|
+
# Recursive lookups must run outside the shared cache mutex.
|
|
945
|
+
# Propagate cancellation and validation policy into both workers.
|
|
946
|
+
control = QueryControl.new(parent: QueryControl.current)
|
|
947
|
+
transmission_budget = SelectThread::TransmissionBudget.current
|
|
948
|
+
hints_generation = Thread.current[:dnsruby_recursor_generation]
|
|
949
|
+
hints = @@mutex.synchronize { Recursor.send(:query_hints).dup }
|
|
950
|
+
workers = []
|
|
951
|
+
budget = Recursor.send(:current_budget)
|
|
952
|
+
dependencies = Thread.current[:dnsruby_recursion_dependencies]
|
|
953
|
+
results = QueryControl::Queue.new
|
|
954
|
+
ans = []
|
|
955
|
+
begin
|
|
956
|
+
address_types = @ipv6_ok ? ['AAAA', 'A'] : ['A']
|
|
957
|
+
address_types.each do |address_type|
|
|
958
|
+
lookup = lambda do
|
|
959
|
+
previous_dependencies = Thread.current[:dnsruby_recursion_dependencies]
|
|
960
|
+
Thread.current[:dnsruby_recursion_dependencies] = dependencies
|
|
961
|
+
begin
|
|
962
|
+
answer = SelectThread::TransmissionBudget.with(transmission_budget) do
|
|
963
|
+
QueryControl.with(control) do
|
|
964
|
+
local_hints = hints.transform_values do |addresses|
|
|
965
|
+
addresses.is_a?(AddressCache) ? addresses.snapshot(include_current: true) : addresses.dup
|
|
966
|
+
end
|
|
967
|
+
recurse = -> { follow_aliases(ns_rec, address_type, klass, '.', local_hints,
|
|
968
|
+
depth + 1, no_validation) }
|
|
969
|
+
budget ? budget.with_dependency(ns_rec, address_type, klass, &recurse) : recurse.call
|
|
970
|
+
end
|
|
971
|
+
end
|
|
972
|
+
results << [answer, nil]
|
|
973
|
+
rescue StandardError => error
|
|
974
|
+
results << [nil, error]
|
|
975
|
+
ensure
|
|
976
|
+
Thread.current[:dnsruby_recursion_dependencies] = previous_dependencies
|
|
977
|
+
end
|
|
978
|
+
end
|
|
979
|
+
reserved = !budget || budget.reserve_worker
|
|
980
|
+
if reserved
|
|
981
|
+
begin
|
|
982
|
+
workers << Thread.new(lookup) do |operation|
|
|
983
|
+
Thread.current.report_on_exception = false
|
|
984
|
+
Thread.current[:dnsruby_recursor_generation] = hints_generation
|
|
985
|
+
begin
|
|
986
|
+
operation.call
|
|
987
|
+
ensure
|
|
988
|
+
budget&.release_worker
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
rescue Exception
|
|
992
|
+
budget&.release_worker
|
|
993
|
+
raise
|
|
994
|
+
end
|
|
995
|
+
else
|
|
996
|
+
lookup.call
|
|
997
|
+
end
|
|
998
|
+
end
|
|
999
|
+
address_types.length.times do
|
|
1000
|
+
answer, error = control.pop(results)
|
|
1001
|
+
if error
|
|
1002
|
+
# A failed family or nameserver must not hide a usable
|
|
1003
|
+
# alternative. Budget failures and cancellation still stop
|
|
1004
|
+
# the entire operation, as do unexpected programming errors.
|
|
1005
|
+
raise error if error.is_a?(VerifyError) || error.is_a?(RecursionLimitError)
|
|
1006
|
+
case error
|
|
1007
|
+
when ResolvError, ResolvTimeout, IOError, SystemCallError
|
|
1008
|
+
TheLog.debug(";; _dorecursion() Address lookup failed: #{error}")
|
|
1009
|
+
else
|
|
1010
|
+
raise error
|
|
541
1011
|
end
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (
|
|
569
|
-
cname =
|
|
570
|
-
cname.sub!(/\.*$/, ".")
|
|
571
|
-
TheLog.debug(";; _dorecursion() Following CNAME ns [#{ns_rec}] -> [#{cname}]")
|
|
572
|
-
if (!(known_authorities[cname]))
|
|
573
|
-
known_authorities[cname] = AddressCache.new
|
|
574
|
-
end
|
|
575
|
-
known_authorities.delete(ns_rec)
|
|
576
|
-
next
|
|
1012
|
+
elsif answer
|
|
1013
|
+
ans << answer.answer
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
ensure
|
|
1017
|
+
control.cancel
|
|
1018
|
+
# A failed lookup must not leave its sibling running.
|
|
1019
|
+
workers.each { |worker| worker.join rescue nil }
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
if ( ans.length > 0 )
|
|
1023
|
+
TheLog.debug(";; _dorecursion() Answers found for [#{ns_rec}]")
|
|
1024
|
+
# foreach my $rr (@ans) {
|
|
1025
|
+
@@mutex.synchronize do
|
|
1026
|
+
ans.each do |rr_arr|
|
|
1027
|
+
rr_arr.each do |rr|
|
|
1028
|
+
TheLog.debug(";; RR:" + rr.inspect + "")
|
|
1029
|
+
if (rr.type == Types::CNAME)
|
|
1030
|
+
# Follow CNAME
|
|
1031
|
+
server = rr.name.to_s.downcase
|
|
1032
|
+
if (server)
|
|
1033
|
+
server.sub!(/\.*$/, ".")
|
|
1034
|
+
if (server == ns_rec)
|
|
1035
|
+
cname = rr.cname.to_s.downcase
|
|
1036
|
+
cname.sub!(/\.*$/, ".")
|
|
1037
|
+
TheLog.debug(";; _dorecursion() Following CNAME ns [#{ns_rec}] -> [#{cname}]")
|
|
1038
|
+
if (!(known_authorities[cname]))
|
|
1039
|
+
known_authorities[cname] = AddressCache.new
|
|
577
1040
|
end
|
|
1041
|
+
known_authorities.delete(ns_rec)
|
|
1042
|
+
next
|
|
578
1043
|
end
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1044
|
+
end
|
|
1045
|
+
elsif (rr.type == Types::A || rr.type == Types::AAAA )
|
|
1046
|
+
server = rr.name.to_s.downcase
|
|
1047
|
+
if (server)
|
|
1048
|
+
server.sub!(/\.*$/, ".")
|
|
1049
|
+
if known_authorities.key?(server)
|
|
1050
|
+
ip = rr.address.to_s
|
|
1051
|
+
TheLog.debug(";; _dorecursion() Found ns: #{server} IN A #{ip}")
|
|
1052
|
+
addresses = known_authorities[server]
|
|
1053
|
+
unless addresses.is_a?(AddressCache)
|
|
1054
|
+
addresses = known_authorities[server] = AddressCache.new
|
|
590
1055
|
end
|
|
1056
|
+
addresses.push([ip, rr.ttl])
|
|
1057
|
+
Recursor.cache_authority(server, addresses)
|
|
1058
|
+
found_auth+=1
|
|
1059
|
+
next
|
|
591
1060
|
end
|
|
592
1061
|
end
|
|
593
|
-
TheLog.debug(";; _dorecursion() Ignoring useless answer: " + rr.inspect + "")
|
|
594
1062
|
end
|
|
1063
|
+
TheLog.debug(";; _dorecursion() Ignoring useless answer: " + rr.inspect + "")
|
|
595
1064
|
end
|
|
596
|
-
else
|
|
597
|
-
TheLog.debug(";; _dorecursion() Could not find A records for [#{ns_rec}]")
|
|
598
1065
|
end
|
|
599
1066
|
end
|
|
1067
|
+
else
|
|
1068
|
+
TheLog.debug(";; _dorecursion() Could not find A records for [#{ns_rec}]")
|
|
600
1069
|
end
|
|
601
|
-
if (found_auth > 0)
|
|
602
|
-
TheLog.debug(";; _dorecursion() Found #{found_auth} new NS authorities...")
|
|
603
|
-
return _dorecursion( name, type, klass, known_zone, known_authorities, depth+1)
|
|
604
|
-
end
|
|
605
|
-
TheLog.debug(";; _dorecursion() No authority information could be obtained.")
|
|
606
|
-
return nil
|
|
607
1070
|
end
|
|
608
|
-
|
|
1071
|
+
if (found_auth > 0)
|
|
1072
|
+
TheLog.debug(";; _dorecursion() Found #{found_auth} new NS authorities...")
|
|
1073
|
+
return _dorecursion(name, type, klass, known_zone, known_authorities, depth + 1, no_validation)
|
|
1074
|
+
end
|
|
1075
|
+
TheLog.debug(";; _dorecursion() No authority information could be obtained.")
|
|
1076
|
+
return nil
|
|
1077
|
+
end
|
|
609
1078
|
|
|
610
1079
|
# Cut the deck of IPs in a random place.
|
|
611
1080
|
TheLog.debug(";; _dorecursion() cutting deck of (" + ns.length.to_s + ") authorities...")
|
|
@@ -617,17 +1086,17 @@ module Dnsruby
|
|
|
617
1086
|
nameservers = []
|
|
618
1087
|
ns.each do |nss|
|
|
619
1088
|
nss.each {|n|
|
|
1089
|
+
Recursor.send(:current_budget)&.step!
|
|
620
1090
|
nameservers.push(n.to_s)
|
|
621
1091
|
}
|
|
622
1092
|
end
|
|
623
|
-
|
|
624
|
-
resolver
|
|
1093
|
+
# TSIG belongs to the configured upstream, not to arbitrary authorities.
|
|
1094
|
+
resolver = Resolver.new(hop_settings.merge(nameserver: nameservers))
|
|
1095
|
+
# Validation needs signatures even when requesting DNSSEC records was not
|
|
1096
|
+
# explicitly enabled. Non-validating lookups retain the caller's setting.
|
|
1097
|
+
resolver.dnssec = @dnssec || !no_validation
|
|
625
1098
|
resolver.recurse = false
|
|
626
|
-
|
|
627
|
-
resolver.single_resolvers.each {|s|
|
|
628
|
-
servers.push(s.server)
|
|
629
|
-
}
|
|
630
|
-
resolver.retry_delay = nameservers.length
|
|
1099
|
+
authority_error = nil
|
|
631
1100
|
begin
|
|
632
1101
|
# Should construct packet ourselves and clear RD bit
|
|
633
1102
|
query = Message.new(name, type, klass)
|
|
@@ -642,7 +1111,29 @@ module Dnsruby
|
|
|
642
1111
|
rescue ResolvTimeout, IOError
|
|
643
1112
|
# TheLog.debug(";; nameserver #{levelns.to_s} didn't respond")
|
|
644
1113
|
# next
|
|
1114
|
+
QueryControl.current&.check!
|
|
645
1115
|
TheLog.debug("No response!")
|
|
1116
|
+
packet = nil
|
|
1117
|
+
rescue ServFail, Refused, NotImp, FormErr, SocketEofResolvError => error
|
|
1118
|
+
raise if missing_authorities.empty?
|
|
1119
|
+
QueryControl.current&.check!
|
|
1120
|
+
authority_error = error
|
|
1121
|
+
packet = nil
|
|
1122
|
+
ensure
|
|
1123
|
+
resolver.close
|
|
1124
|
+
end
|
|
1125
|
+
if packet.nil? && !missing_authorities.empty?
|
|
1126
|
+
# A referral may contain both glue and names that need address
|
|
1127
|
+
# discovery. Try the latter if all immediately usable servers fail.
|
|
1128
|
+
unresolved = known_authorities.select { |server, _| missing_authorities.include?(server) }
|
|
1129
|
+
begin
|
|
1130
|
+
alternative = _dorecursion(name, type, klass, known_zone, unresolved, depth + 1, no_validation)
|
|
1131
|
+
return alternative if alternative
|
|
1132
|
+
rescue ResolvTimeout, IOError, ServFail, Refused, NotImp, FormErr, SocketEofResolvError => error
|
|
1133
|
+
QueryControl.current&.check!
|
|
1134
|
+
raise(authority_error || error)
|
|
1135
|
+
end
|
|
1136
|
+
raise authority_error if authority_error
|
|
646
1137
|
return nil
|
|
647
1138
|
end
|
|
648
1139
|
if (packet) # @TODO@ Check that the packet *is* actually authoritative!!
|
|
@@ -666,80 +1157,11 @@ module Dnsruby
|
|
|
666
1157
|
TheLog.debug(";; _dorecursion() Authoritative answer found")
|
|
667
1158
|
return packet
|
|
668
1159
|
elsif (authority.length > 0)
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
server.sub!(/\.*$/, ".")
|
|
675
|
-
of = rr.name.to_s.downcase
|
|
676
|
-
of.sub!(/\.*$/, ".")
|
|
677
|
-
TheLog.debug(";; _dorecursion() Received authority [#{of}] [" + rr.type().to_s + "] [#{server}]")
|
|
678
|
-
if (of.length <= known_zone.length)
|
|
679
|
-
TheLog.debug(";; _dorecursion() Deadbeat name server did not provide new information.")
|
|
680
|
-
next
|
|
681
|
-
# elsif (of =~ /#{known_zone}/)
|
|
682
|
-
elsif (of == known_zone || known_zone.length == 1 || of.end_with?(".#{known_zone}"))
|
|
683
|
-
TheLog.debug(";; _dorecursion() FOUND closer authority for [#{of}] at [#{server}].")
|
|
684
|
-
auth[server] ||= AddressCache.new #[] @TODO@ If there is no additional record for this, then we want to use the authority!
|
|
685
|
-
if (rr.type == Types.NS)
|
|
686
|
-
if ((packet.additional.rrset(rr.nsdname, Types::A).length == 0) &&
|
|
687
|
-
(packet.additional.rrset(rr.nsdname, Types::AAAA).length == 0))
|
|
688
|
-
auth[server].push([rr.nsdname, rr.ttl])
|
|
689
|
-
end
|
|
690
|
-
end
|
|
691
|
-
else
|
|
692
|
-
TheLog.debug(";; _dorecursion() Confused name server [" + @answerfrom.to_s + "] thinks " + of.to_s + " is closer than " + known_zone.to_s + "?")
|
|
693
|
-
return nil
|
|
694
|
-
end
|
|
695
|
-
else
|
|
696
|
-
TheLog.debug(";; _dorecursion() Ignoring NON NS entry found in authority section: " + rr.inspect)
|
|
697
|
-
end
|
|
698
|
-
end
|
|
699
|
-
# foreach my $rr ($packet->additional)
|
|
700
|
-
packet.additional.each do |rr|
|
|
701
|
-
if (rr.type == Types::CNAME)
|
|
702
|
-
# Store this CNAME into %auth too
|
|
703
|
-
server = rr.name.to_s.downcase
|
|
704
|
-
if (server)
|
|
705
|
-
server.sub!(/\.*$/, ".")
|
|
706
|
-
if (auth[server]!=nil && auth[server].length > 0)
|
|
707
|
-
cname = rr.cname.to_s.downcase
|
|
708
|
-
cname.sub!(/\.*$/, ".")
|
|
709
|
-
TheLog.debug(";; _dorecursion() FOUND CNAME authority: " + rr.string)
|
|
710
|
-
auth[cname] ||= AddressCache.new # []
|
|
711
|
-
auth[server] = auth[cname]
|
|
712
|
-
next
|
|
713
|
-
end
|
|
714
|
-
|
|
715
|
-
end
|
|
716
|
-
elsif (rr.type == Types::A || rr.type == Types::AAAA)
|
|
717
|
-
server = rr.name.to_s.downcase
|
|
718
|
-
if (server)
|
|
719
|
-
server.sub!(/\.*$/, ".")
|
|
720
|
-
if (auth[server]!=nil)
|
|
721
|
-
if (rr.type == Types::A)
|
|
722
|
-
TheLog.debug(";; _dorecursion() STORING: #{server} IN A " + rr.address.to_s)
|
|
723
|
-
end
|
|
724
|
-
if (rr.type == Types::AAAA)
|
|
725
|
-
TheLog.debug(";; _dorecursion() STORING: #{server} IN AAAA " + rr.address.to_s)
|
|
726
|
-
end
|
|
727
|
-
auth[server].push([rr.address.to_s, rr.ttl])
|
|
728
|
-
next
|
|
729
|
-
end
|
|
730
|
-
end
|
|
731
|
-
end
|
|
732
|
-
TheLog.debug(";; _dorecursion() Ignoring useless: " + rr.inspect)
|
|
733
|
-
end
|
|
734
|
-
if (of =~ /#{known_zone}/)
|
|
735
|
-
# print "Adding #{of} with :\n#{auth}\nto zones_cache\n"
|
|
736
|
-
@@mutex.synchronize{
|
|
737
|
-
@@zones_cache[of]=auth
|
|
738
|
-
}
|
|
739
|
-
return _dorecursion( name, type, klass, of, auth, depth+1, no_validation)
|
|
740
|
-
else
|
|
741
|
-
return _dorecursion( name, type, klass, known_zone, known_authorities, depth+1, no_validation )
|
|
742
|
-
end
|
|
1160
|
+
referral = referral_authorities(packet, known_zone, name, klass)
|
|
1161
|
+
return nil unless referral
|
|
1162
|
+
of, auth, delegation_ttl = referral
|
|
1163
|
+
@@mutex.synchronize { Recursor.cache_zone(of, auth.dup, delegation_ttl, parent: known_zone) }
|
|
1164
|
+
return _dorecursion(name, type, klass, of, auth, depth + 1, no_validation)
|
|
743
1165
|
end
|
|
744
1166
|
end
|
|
745
1167
|
end
|
|
@@ -747,6 +1169,40 @@ module Dnsruby
|
|
|
747
1169
|
return nil
|
|
748
1170
|
end
|
|
749
1171
|
|
|
1172
|
+
# A referral advances one enclosing delegation. SOA records describe
|
|
1173
|
+
# negative answers; unrelated NS owners must not join this delegation.
|
|
1174
|
+
def referral_authorities(packet, known_zone, question_name, question_class) #:nodoc:
|
|
1175
|
+
parent = Name.create(known_zone)
|
|
1176
|
+
parent.absolute = true
|
|
1177
|
+
question = Name.create(question_name)
|
|
1178
|
+
question.absolute = true
|
|
1179
|
+
klass = Classes.new(question_class)
|
|
1180
|
+
candidates = packet.authority.select do |rr|
|
|
1181
|
+
rr.type == Types::NS && rr.klass == klass && rr.name.subdomain_of?(parent) &&
|
|
1182
|
+
(question == rr.name || question.subdomain_of?(rr.name))
|
|
1183
|
+
end
|
|
1184
|
+
delegation = candidates.max_by { |rr| rr.name.labels.length }&.name
|
|
1185
|
+
return unless delegation
|
|
1186
|
+
records = candidates.select { |rr| rr.name == delegation }
|
|
1187
|
+
authorities = {}
|
|
1188
|
+
records.each do |rr|
|
|
1189
|
+
server = rr.nsdname.to_s(true).downcase
|
|
1190
|
+
addresses = authorities[server] ||= AddressCache.new
|
|
1191
|
+
# RFC 9471 permits sibling glue. Trust addresses within the responding
|
|
1192
|
+
# parent's zone, then look up out-of-bailiwick names independently.
|
|
1193
|
+
glue = packet.additional.select do |address|
|
|
1194
|
+
[Types::A, Types::AAAA].include?(address.type) && address.klass == klass &&
|
|
1195
|
+
address.name == rr.nsdname &&
|
|
1196
|
+
(address.name == parent || address.name.subdomain_of?(parent))
|
|
1197
|
+
end
|
|
1198
|
+
# Leave missing addresses empty so _dorecursion resolves them with the
|
|
1199
|
+
# same transport settings, deadline and transmission budget. Passing a
|
|
1200
|
+
# hostname to Resolver.new would start a separate bootstrap lookup.
|
|
1201
|
+
glue.each { |address| addresses.push([address.address.to_s, address.ttl]) }
|
|
1202
|
+
end
|
|
1203
|
+
[delegation.to_s(true).downcase, authorities, records.map(&:ttl).min]
|
|
1204
|
+
end
|
|
1205
|
+
|
|
750
1206
|
def prune_rrsets_to_rfc5452(packet, zone)
|
|
751
1207
|
# Now prune the response of any unrelated rrsets (RFC5452 section6)
|
|
752
1208
|
# "One very simple way to achieve this is to only accept data if it is
|