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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
|
|
4
|
+
class RecursorAddressFallbackTest < Minitest::Test
|
|
5
|
+
def test_failed_family_keeps_the_other_address_family
|
|
6
|
+
with_address_lookup(['ns.example.']) do |run, calls, _recursor|
|
|
7
|
+
result = run.call do |name, type|
|
|
8
|
+
raise Dnsruby::ServFail, 'AAAA unavailable' if type == 'AAAA'
|
|
9
|
+
address_answer(name, type)
|
|
10
|
+
end
|
|
11
|
+
assert_equal(['192.0.2.53'], result)
|
|
12
|
+
assert_equal(%w[A AAAA], drain(calls).map(&:last).sort)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_failed_nameserver_does_not_hide_another_nameserver
|
|
17
|
+
with_address_lookup(%w[bad.example. ns.example.]) do |run, calls, _recursor|
|
|
18
|
+
result = run.call do |name, type|
|
|
19
|
+
raise Dnsruby::NXDomain, 'name unavailable' if name == 'bad.example.'
|
|
20
|
+
address_answer(name, type)
|
|
21
|
+
end
|
|
22
|
+
assert_equal(['192.0.2.53', '2001:db8::53'], result)
|
|
23
|
+
assert_equal(4, drain(calls).length)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_ordinary_lookup_failures_return_no_authority
|
|
28
|
+
with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
|
|
29
|
+
assert_nil(run.call { |_name, _type| raise Dnsruby::Refused, 'lookup refused' })
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_validation_limit_is_preserved_and_cancels_a_waiting_sibling
|
|
34
|
+
with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
|
|
35
|
+
waiting = Queue.new
|
|
36
|
+
finished = Queue.new
|
|
37
|
+
limit = Dnsruby::ValidationLimitError.new('validation budget exhausted')
|
|
38
|
+
error = assert_raises(Dnsruby::ValidationLimitError) do
|
|
39
|
+
run.call do |_name, type|
|
|
40
|
+
if type == 'A'
|
|
41
|
+
waiting.pop
|
|
42
|
+
raise limit
|
|
43
|
+
end
|
|
44
|
+
begin
|
|
45
|
+
waiting << true
|
|
46
|
+
Dnsruby::QueryControl.current.pop(Dnsruby::QueryControl::Queue.new)
|
|
47
|
+
ensure
|
|
48
|
+
finished << true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
assert_same(limit, error)
|
|
53
|
+
assert_equal(true, finished.pop(true))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_parent_cancellation_stops_all_address_workers
|
|
58
|
+
with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
|
|
59
|
+
parent = Dnsruby::QueryControl.new
|
|
60
|
+
started = Queue.new
|
|
61
|
+
finished = Queue.new
|
|
62
|
+
canceller = Thread.new { 2.times { started.pop }; parent.cancel }
|
|
63
|
+
assert_raises(Dnsruby::QueryControl::Cancelled) do
|
|
64
|
+
Dnsruby::QueryControl.with(parent) do
|
|
65
|
+
run.call do |_name, _type|
|
|
66
|
+
begin
|
|
67
|
+
started << true
|
|
68
|
+
Dnsruby::QueryControl.current.pop(Dnsruby::QueryControl::Queue.new)
|
|
69
|
+
ensure
|
|
70
|
+
finished << true
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
assert_equal([true, true], 2.times.map { finished.pop(true) })
|
|
76
|
+
ensure
|
|
77
|
+
canceller&.kill
|
|
78
|
+
canceller&.join
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_one_worker_runs_the_other_family_inline_without_deadlock
|
|
83
|
+
with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
|
|
84
|
+
parent_thread = Thread.current
|
|
85
|
+
family_threads = Queue.new
|
|
86
|
+
release_worker = Queue.new
|
|
87
|
+
limits = Dnsruby::Recursor::DEFAULT_RECURSION_LIMITS.merge(max_workers: 1)
|
|
88
|
+
Dnsruby::Recursor.send(:with_limits, nil, limits) do |budget|
|
|
89
|
+
result = run.call do |name, type|
|
|
90
|
+
family_threads << [type, Thread.current]
|
|
91
|
+
type == 'AAAA' ? release_worker.pop : release_worker.push(true)
|
|
92
|
+
address_answer(name, type)
|
|
93
|
+
end
|
|
94
|
+
assert_equal(['192.0.2.53', '2001:db8::53'], result)
|
|
95
|
+
threads = drain(family_threads).to_h
|
|
96
|
+
assert_same(parent_thread, threads['A'])
|
|
97
|
+
refute_same(parent_thread, threads['AAAA'])
|
|
98
|
+
assert(budget.reserve_worker, 'Completed discovery must release its worker slot')
|
|
99
|
+
budget.release_worker
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_worker_creation_failure_releases_its_slot
|
|
105
|
+
with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
|
|
106
|
+
limits = Dnsruby::Recursor::DEFAULT_RECURSION_LIMITS.merge(max_workers: 1)
|
|
107
|
+
Dnsruby::Recursor.send(:with_limits, nil, limits) do |budget|
|
|
108
|
+
Thread.stub(:new, ->(*) { raise ThreadError, 'Worker unavailable' }) do
|
|
109
|
+
assert_raises(ThreadError) { run.call { |name, type| address_answer(name, type) } }
|
|
110
|
+
end
|
|
111
|
+
assert(budget.reserve_worker)
|
|
112
|
+
budget.release_worker
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_recursion_limit_does_not_try_another_nameserver
|
|
118
|
+
with_address_lookup(%w[first.example. second.example.]) do |run, calls, _recursor|
|
|
119
|
+
error = Dnsruby::RecursionLimitError.new('Lookup work limit reached')
|
|
120
|
+
actual = assert_raises(Dnsruby::RecursionLimitError) { run.call { |*| raise error } }
|
|
121
|
+
assert_same(error, actual)
|
|
122
|
+
assert_equal(['first.example.'], drain(calls).map(&:first).uniq)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def address_answer(name, type)
|
|
129
|
+
answer = Dnsruby::Message.new(name, type)
|
|
130
|
+
address = type == 'AAAA' ? '2001:db8::53' : '192.0.2.53'
|
|
131
|
+
answer.add_answer(Dnsruby::RR.create("#{name} 300 IN #{type} #{address}"))
|
|
132
|
+
answer
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def drain(queue)
|
|
136
|
+
Array.new(queue.length) { queue.pop(true) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def with_address_lookup(names)
|
|
140
|
+
klass = Dnsruby::Recursor
|
|
141
|
+
original_hints = klass.class_variable_get(:@@hints)
|
|
142
|
+
original_cache = klass.class_variable_get(:@@authority_cache)
|
|
143
|
+
klass.class_variable_set(:@@hints, {})
|
|
144
|
+
klass.class_variable_set(:@@authority_cache, {})
|
|
145
|
+
recursor = klass.allocate
|
|
146
|
+
recursor.ipv6_ok = true
|
|
147
|
+
original_recursion = recursor.method(:_dorecursion)
|
|
148
|
+
authorities = names.to_h { |name| [name, klass::AddressCache.new] }
|
|
149
|
+
calls = Queue.new
|
|
150
|
+
run = lambda do |&lookup|
|
|
151
|
+
recursor.define_singleton_method(:_dorecursion) do |name, type, *_args|
|
|
152
|
+
if name == 'www.example.'
|
|
153
|
+
authorities.values.flat_map(&:values).map(&:downcase).sort
|
|
154
|
+
else
|
|
155
|
+
calls << [name, type]
|
|
156
|
+
lookup.call(name, type)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
Timeout.timeout(3) do
|
|
160
|
+
original_recursion.call('www.example.', 'A', Dnsruby::Classes::IN,
|
|
161
|
+
'example.', authorities, 0, true)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
yield run, calls, recursor
|
|
165
|
+
ensure
|
|
166
|
+
klass.class_variable_set(:@@hints, original_hints)
|
|
167
|
+
klass.class_variable_set(:@@authority_cache, original_cache)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
|
|
3
|
+
class RecursorLimitsTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', query_timeout: 0)
|
|
6
|
+
@recursor = Dnsruby::Recursor.new(@resolver)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def teardown
|
|
10
|
+
@resolver.close
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_defaults_are_finite_and_invalid_replacements_are_atomic
|
|
14
|
+
original = @recursor.recursion_limits
|
|
15
|
+
assert_equal(30.0, original[:timeout])
|
|
16
|
+
assert(original.frozen?)
|
|
17
|
+
[{timeout: 0}, {max_queries: -1}, {max_workers: 0}, {unknown: 1}].each do |settings|
|
|
18
|
+
assert_raises(ArgumentError) { @recursor.recursion_limits = settings }
|
|
19
|
+
assert_same(original, @recursor.recursion_limits)
|
|
20
|
+
end
|
|
21
|
+
@recursor.recursion_limits = {timeout: 10, max_queries: 20}
|
|
22
|
+
assert_equal(20, @recursor.recursion_limits[:max_queries])
|
|
23
|
+
assert_equal(8, @recursor.recursion_limits[:max_workers])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_default_zero_resolver_timeout_still_has_a_recursion_deadline
|
|
27
|
+
@recursor.define_singleton_method(:query_internal) do |*|
|
|
28
|
+
Dnsruby::QueryControl.current.deadline - Dnsruby::TransportClock.now
|
|
29
|
+
end
|
|
30
|
+
remaining = @recursor.query('example.', 'A', 'IN', true)
|
|
31
|
+
assert_operator(remaining, :>, 29)
|
|
32
|
+
assert_operator(remaining, :<=, 30)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_recursion_deadline_stops_a_wait_and_restores_thread_context
|
|
36
|
+
@recursor.recursion_limits = {timeout: 0.02}
|
|
37
|
+
@recursor.define_singleton_method(:query_internal) do |*|
|
|
38
|
+
Dnsruby::QueryControl.current.pop(Dnsruby::QueryControl::Queue.new)
|
|
39
|
+
end
|
|
40
|
+
assert_raises(Dnsruby::ResolvTimeout) { @recursor.query('example.', 'A', 'IN', true) }
|
|
41
|
+
assert_nil(Dnsruby::QueryControl.current)
|
|
42
|
+
assert_nil(Dnsruby::SelectThread::TransmissionBudget.current)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_shorter_parent_deadline_keeps_parent_error
|
|
46
|
+
parent = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + 0.02)
|
|
47
|
+
@recursor.define_singleton_method(:query_internal) do |*|
|
|
48
|
+
Dnsruby::QueryControl.current.pop(Dnsruby::QueryControl::Queue.new)
|
|
49
|
+
end
|
|
50
|
+
assert_raises(Dnsruby::QueryControl::DeadlineExceeded) do
|
|
51
|
+
Dnsruby::QueryControl.with(parent) { @recursor.query('example.', 'A', 'IN', true) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_supporting_queries_share_transmission_and_work_limits
|
|
56
|
+
@recursor.recursion_limits = {max_queries: 2, max_steps: 2}
|
|
57
|
+
Dnsruby::Recursor.send(:with_limits, @resolver, @recursor.recursion_limits) do |budget|
|
|
58
|
+
transmissions = Dnsruby::SelectThread::TransmissionBudget.current
|
|
59
|
+
2.times do |i|
|
|
60
|
+
Dnsruby::Recursor.send(:with_limits, @resolver) do |nested|
|
|
61
|
+
assert_same(budget, nested)
|
|
62
|
+
nested.step!
|
|
63
|
+
assert(transmissions.admit('192.0.2.53', 53, false, Dnsruby::Question.new("host#{i}.example.", 'A', 'IN')))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
assert_raises(Dnsruby::RecursionLimitError) { budget.step! }
|
|
67
|
+
assert_raises(Dnsruby::RecursionLimitError) do
|
|
68
|
+
transmissions.admit('192.0.2.54', 53, false, Dnsruby::Question.new('another.example.', 'A', 'IN'))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_worker_slots_are_released_and_dependency_paths_restore
|
|
74
|
+
Dnsruby::Recursor.send(:with_limits, @resolver, @recursor.recursion_limits.merge(max_workers: 1)) do |budget|
|
|
75
|
+
assert(budget.reserve_worker)
|
|
76
|
+
refute(budget.reserve_worker)
|
|
77
|
+
budget.release_worker
|
|
78
|
+
assert(budget.reserve_worker)
|
|
79
|
+
budget.release_worker
|
|
80
|
+
assert_equal(:done, budget.with_dependency('ns.example.', 'A', 'IN') { :done })
|
|
81
|
+
budget.with_dependency('ns.example.', 'A', 'IN') do
|
|
82
|
+
refute(budget.with_dependency('NS.EXAMPLE.', 'A', 'IN') { flunk 'Repeated dependency ran' })
|
|
83
|
+
assert_equal(:other_family, budget.with_dependency('ns.example.', 'AAAA', 'IN') { :other_family })
|
|
84
|
+
end
|
|
85
|
+
assert_empty(Thread.current[:dnsruby_recursion_dependencies])
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_transmission_limit_ends_the_resolvers_remaining_retry_plan
|
|
90
|
+
engine = Dnsruby::ResolverRuby.allocate
|
|
91
|
+
message = Dnsruby::Message.new('example.')
|
|
92
|
+
client_queue, select_queue = Queue.new, Queue.new
|
|
93
|
+
sender = Object.new
|
|
94
|
+
engine.instance_variable_set(:@query_list, {request: [message, client_queue, select_queue, [:another_attempt]]})
|
|
95
|
+
completion = nil
|
|
96
|
+
engine.define_singleton_method(:send_result_and_stop_querying) { |*args| completion = args }
|
|
97
|
+
error = Dnsruby::RecursionLimitError.new('Transmission limit reached')
|
|
98
|
+
engine.handle_error_response(select_queue, [sender, message, :request, 0], error, nil)
|
|
99
|
+
assert_equal([client_queue, :request, select_queue, nil, error], completion)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_root_bootstrap_batches_share_the_enclosing_transmission_limit
|
|
103
|
+
klass = Dnsruby::Recursor
|
|
104
|
+
saved = %i[@@hints @@stale_hints @@hints_generation @@nameservers @@zones_cache
|
|
105
|
+
@@zone_expirations @@authority_cache].to_h { |key| [key, klass.class_variable_get(key)] }
|
|
106
|
+
root = Dnsruby::Message.new('.', 'NS')
|
|
107
|
+
root.add_answer(Dnsruby::RR.create('. 300 IN NS ns.example.'))
|
|
108
|
+
budgets, queued = [], []
|
|
109
|
+
@resolver.define_singleton_method(:query_no_validation_or_recursion) do |*|
|
|
110
|
+
transmission = Dnsruby::SelectThread::TransmissionBudget.current
|
|
111
|
+
budgets << transmission
|
|
112
|
+
transmission.admit('192.0.2.53', 53, false, root.question.first)
|
|
113
|
+
root
|
|
114
|
+
end
|
|
115
|
+
@resolver.define_singleton_method(:send_async) do |message, queue|
|
|
116
|
+
queued << queue.length
|
|
117
|
+
transmission = Dnsruby::SelectThread::TransmissionBudget.current
|
|
118
|
+
budgets << transmission
|
|
119
|
+
transmission.admit('192.0.2.53', 53, false, message.question.first)
|
|
120
|
+
answer = Dnsruby::Message.new
|
|
121
|
+
type = message.question.first.qtype.to_s
|
|
122
|
+
address = type == 'A' ? '192.0.2.53' : '2001:db8::53'
|
|
123
|
+
answer.add_answer(Dnsruby::RR.create("ns.example. 300 IN #{type} #{address}"))
|
|
124
|
+
queue << [type, answer, nil]
|
|
125
|
+
type
|
|
126
|
+
end
|
|
127
|
+
limits = @recursor.recursion_limits.merge(max_queries: 3, max_workers: 1)
|
|
128
|
+
klass.send(:with_limits, @resolver, limits) do
|
|
129
|
+
klass.set_hints([], @resolver)
|
|
130
|
+
assert_equal([0, 0], queued, 'Each address batch must finish before the next begins')
|
|
131
|
+
assert_equal(1, budgets.map(&:object_id).uniq.length)
|
|
132
|
+
assert_raises(Dnsruby::RecursionLimitError) do
|
|
133
|
+
budgets.first.admit('192.0.2.53', 53, false, Dnsruby::Question.new('example.', 'A', 'IN'))
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
ensure
|
|
137
|
+
saved&.each { |key, value| klass.class_variable_set(key, value) }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
require 'timeout'
|
|
4
|
+
|
|
5
|
+
class ReleaseAuxiliaryFixesTest < Minitest::Test
|
|
6
|
+
def test_controlled_queue_wakes_for_each_producer_alias_and_preserves_nil
|
|
7
|
+
[:push, :<<, :enq].each do |method|
|
|
8
|
+
queue = Dnsruby::QueryControl::Queue.new
|
|
9
|
+
control = Dnsruby::QueryControl.new
|
|
10
|
+
started = Queue.new
|
|
11
|
+
waiter = Thread.new { started << true; control.pop(queue) }
|
|
12
|
+
started.pop
|
|
13
|
+
queue.public_send(method, nil)
|
|
14
|
+
assert_nil(Timeout.timeout(2) { waiter.value })
|
|
15
|
+
ensure
|
|
16
|
+
waiter&.kill
|
|
17
|
+
waiter&.join
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_parent_cancellation_wakes_an_event_queue_without_consuming_a_result
|
|
22
|
+
queue = Dnsruby::QueryControl::Queue.new
|
|
23
|
+
parent = Dnsruby::QueryControl.new
|
|
24
|
+
child = Dnsruby::QueryControl.new(parent: parent)
|
|
25
|
+
ready = Queue.new
|
|
26
|
+
original_pop = queue.method(:pop)
|
|
27
|
+
queue.define_singleton_method(:pop) do |*args|
|
|
28
|
+
ready << true
|
|
29
|
+
original_pop.call(*args)
|
|
30
|
+
end
|
|
31
|
+
waiter = Thread.new do
|
|
32
|
+
child.pop(queue)
|
|
33
|
+
rescue Dnsruby::QueryControl::Cancelled => error
|
|
34
|
+
error
|
|
35
|
+
end
|
|
36
|
+
ready.pop
|
|
37
|
+
parent.cancel
|
|
38
|
+
assert_instance_of(Dnsruby::QueryControl::Cancelled, Timeout.timeout(2) { waiter.value })
|
|
39
|
+
queue << :later
|
|
40
|
+
assert_equal(:later, queue.pop)
|
|
41
|
+
ensure
|
|
42
|
+
waiter&.kill
|
|
43
|
+
waiter&.join
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_event_queue_obeys_deadlines_and_closed_queues
|
|
47
|
+
queue = Dnsruby::QueryControl::Queue.new
|
|
48
|
+
control = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + 0.01)
|
|
49
|
+
assert_raises(Dnsruby::QueryControl::DeadlineExceeded) do
|
|
50
|
+
Timeout.timeout(2) { control.pop(queue) }
|
|
51
|
+
end
|
|
52
|
+
queue.close
|
|
53
|
+
assert_nil(Dnsruby::QueryControl.new.pop(queue))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_compression_table_stops_recording_unrepresentable_offsets
|
|
57
|
+
name = Dnsruby::Name.create('late.example.')
|
|
58
|
+
encoder = Dnsruby::MessageEncoder.new
|
|
59
|
+
# Exercise the offset decision directly, without constructing an oversized
|
|
60
|
+
# or malformed DNS packet.
|
|
61
|
+
encoder.to_s.stub(:bytesize, 0x4000) { encoder.put_name(name) }
|
|
62
|
+
literal = encoder.to_s.dup
|
|
63
|
+
encoder.to_s.stub(:bytesize, 0x4000) { encoder.put_name(name) }
|
|
64
|
+
assert_equal(literal + literal, encoder.to_s)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_late_names_can_still_compress_to_an_early_suffix
|
|
68
|
+
encoder = Dnsruby::MessageEncoder.new
|
|
69
|
+
encoder.put_name(Dnsruby::Name.create('example.'))
|
|
70
|
+
offset = encoder.to_s.bytesize
|
|
71
|
+
encoder.to_s.stub(:bytesize, 0x4000) do
|
|
72
|
+
encoder.put_name(Dnsruby::Name.create('late.example.'))
|
|
73
|
+
end
|
|
74
|
+
assert_equal("\x04late\xc0\x00".b, encoder.to_s.byteslice(offset..-1))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_zone_transfer_reuse_returns_current_ixfr_deltas_after_axfr
|
|
78
|
+
soa1 = Dnsruby::RR.create('example. 300 IN SOA ns.example. hostmaster.example. 1 60 60 3600 60')
|
|
79
|
+
soa2 = Dnsruby::RR.create('example. 300 IN SOA ns.example. hostmaster.example. 2 60 60 3600 60')
|
|
80
|
+
old_address = Dnsruby::RR.create('www.example. 300 IN A 192.0.2.1')
|
|
81
|
+
new_address = Dnsruby::RR.create('www.example. 300 IN A 192.0.2.2')
|
|
82
|
+
responses = [Dnsruby::Message.new('example.', 'AXFR'), Dnsruby::Message.new('example.', 'IXFR')]
|
|
83
|
+
[soa1, old_address, soa1].each { |rr| responses[0].add_answer!(rr) }
|
|
84
|
+
[soa2, soa1, old_address, soa2, new_address, soa2].each { |rr| responses[1].add_answer!(rr) }
|
|
85
|
+
transfer = Dnsruby::ZoneTransfer.new
|
|
86
|
+
transfer.define_singleton_method(:send_message) do |_, request|
|
|
87
|
+
responses.first.header.id = request.header.id
|
|
88
|
+
responses.first.header.qr = true
|
|
89
|
+
end
|
|
90
|
+
transfer.define_singleton_method(:receive_message) { |*| responses.shift }
|
|
91
|
+
socket = Object.new
|
|
92
|
+
socket.define_singleton_method(:close) { }
|
|
93
|
+
socket.define_singleton_method(:closed?) { false }
|
|
94
|
+
Socket.stub(:tcp, socket) do
|
|
95
|
+
assert_equal([soa1, old_address], transfer.do_transfer('example.', '192.0.2.53'))
|
|
96
|
+
transfer.transfer_type = Dnsruby::Types::IXFR
|
|
97
|
+
transfer.serial = 1
|
|
98
|
+
deltas = transfer.do_transfer('example.', '192.0.2.53')
|
|
99
|
+
assert_equal(1, deltas.length)
|
|
100
|
+
assert_instance_of(Dnsruby::ZoneTransfer::Delta, deltas.first)
|
|
101
|
+
assert_equal([1, 2], [deltas.first.start, deltas.first.end])
|
|
102
|
+
assert_includes(deltas.first.adds, new_address)
|
|
103
|
+
assert_includes(deltas.first.deletes, old_address)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_tsig_stream_copies_own_their_accumulation_buffer
|
|
108
|
+
signer = Dnsruby::RR.create(name: 'key.example.', type: 'TSIG', klass: 'ANY',
|
|
109
|
+
key: ['secret'].pack('m0'))
|
|
110
|
+
query = Dnsruby::Message.new('example.', 'A')
|
|
111
|
+
signer.apply(query)
|
|
112
|
+
reply = Dnsruby::Message.new('example.', 'A')
|
|
113
|
+
reply.header.qr = true
|
|
114
|
+
signer.dup.apply(reply, query)
|
|
115
|
+
wire = reply.encode.freeze
|
|
116
|
+
assert(signer.verify_envelope(Dnsruby::Message.decode(wire), wire))
|
|
117
|
+
copied_signer = signer.dup
|
|
118
|
+
prior = signer.instance_variable_get(:@buf).dup
|
|
119
|
+
intermediate = Dnsruby::Message.new('example.', 'A').encode.freeze
|
|
120
|
+
assert(copied_signer.verify_envelope(Dnsruby::Message.decode(intermediate), intermediate))
|
|
121
|
+
assert_equal(prior, signer.instance_variable_get(:@buf))
|
|
122
|
+
assert_equal(prior + intermediate, copied_signer.instance_variable_get(:@buf))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_missing_nameserver_addresses_preserve_policy_and_release_cache_mutex
|
|
126
|
+
recursor_class = Dnsruby::Recursor
|
|
127
|
+
mutex = recursor_class.class_variable_get(:@@mutex)
|
|
128
|
+
original_hints = recursor_class.class_variable_get(:@@hints)
|
|
129
|
+
original_cache = recursor_class.class_variable_get(:@@authority_cache)
|
|
130
|
+
recursor_class.class_variable_set(:@@hints, {})
|
|
131
|
+
recursor_class.class_variable_set(:@@authority_cache, {})
|
|
132
|
+
recursor = recursor_class.allocate
|
|
133
|
+
recursor.ipv6_ok = true
|
|
134
|
+
original_recursion = recursor.method(:_dorecursion)
|
|
135
|
+
calls = Queue.new
|
|
136
|
+
recursor.define_singleton_method(:_dorecursion) do |name, type, klass, zone, authorities, depth, no_validation|
|
|
137
|
+
if name == 'www.example.'
|
|
138
|
+
calls << [:retry, no_validation, authorities.fetch('ns.example.').values.map(&:downcase).sort]
|
|
139
|
+
:finished
|
|
140
|
+
else
|
|
141
|
+
mutex.synchronize do
|
|
142
|
+
calls << [type, no_validation, Dnsruby::QueryControl.current.deadline]
|
|
143
|
+
end
|
|
144
|
+
answer = Dnsruby::Message.new(name, type, klass)
|
|
145
|
+
address = type == 'AAAA' ? '2001:db8::53' : '192.0.2.53'
|
|
146
|
+
answer.add_answer(Dnsruby::RR.create("#{name} 300 IN #{type} #{address}"))
|
|
147
|
+
answer
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
authorities = {'ns.example.' => recursor_class::AddressCache.new}
|
|
151
|
+
parent = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + 2)
|
|
152
|
+
result = Dnsruby::QueryControl.with(parent) do
|
|
153
|
+
Timeout.timeout(3) do
|
|
154
|
+
original_recursion.call('www.example.', 'A', Dnsruby::Classes::IN, 'example.', authorities, 0, true)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
assert_equal(:finished, result)
|
|
158
|
+
events = 3.times.map { calls.pop }
|
|
159
|
+
assert_equal(%w[A AAAA], events.take(2).map(&:first).sort)
|
|
160
|
+
events.take(2).each do |event|
|
|
161
|
+
assert_equal(true, event[1])
|
|
162
|
+
assert_equal(parent.deadline, event[2])
|
|
163
|
+
end
|
|
164
|
+
assert_equal([:retry, true, ['192.0.2.53', '2001:db8::53']], events.last)
|
|
165
|
+
ensure
|
|
166
|
+
recursor_class.class_variable_set(:@@hints, original_hints) if recursor_class
|
|
167
|
+
recursor_class.class_variable_set(:@@authority_cache, original_cache) if recursor_class
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_ds_queries_use_cached_parent_authorities_and_preserve_the_question
|
|
171
|
+
with_cached_recursor_authorities do |recursor, zones, calls|
|
|
172
|
+
['child.example', 'child.example.', Dnsruby::Name.create('child.example'),
|
|
173
|
+
Dnsruby::Name.create('child.example.')].each do |name|
|
|
174
|
+
['DS', 43, Dnsruby::Types.new('DS')].each do |type|
|
|
175
|
+
was_absolute = name.absolute? if name.is_a?(Dnsruby::Name)
|
|
176
|
+
response = recursor.query(name, type, Dnsruby::Classes::IN, true)
|
|
177
|
+
args = calls.last
|
|
178
|
+
expected_name = Dnsruby::Name.create(name)
|
|
179
|
+
expected_name.absolute = true
|
|
180
|
+
assert_equal(expected_name, response.question.first.qname)
|
|
181
|
+
assert_equal(was_absolute, name.absolute?) if name.is_a?(Dnsruby::Name)
|
|
182
|
+
assert_equal(expected_name.to_s(true), args[0])
|
|
183
|
+
assert_same(type, args[1])
|
|
184
|
+
assert_equal('example.', args[3])
|
|
185
|
+
assert_authority_snapshot(zones.fetch('example.'), args[4])
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_ds_queries_for_top_level_names_and_root_use_root_authorities
|
|
192
|
+
with_cached_recursor_authorities do |recursor, zones, calls|
|
|
193
|
+
['example', 'example.', '.', Dnsruby::Name.create('.')].each do |name|
|
|
194
|
+
response = recursor.query(name, 'DS', Dnsruby::Classes::IN, true)
|
|
195
|
+
args = calls.last
|
|
196
|
+
expected_name = Dnsruby::Name.create(name)
|
|
197
|
+
expected_name.absolute = true
|
|
198
|
+
assert_equal(expected_name, response.question.first.qname)
|
|
199
|
+
assert_equal(expected_name.to_s(true), args[0])
|
|
200
|
+
assert_equal('.', args[3])
|
|
201
|
+
assert_authority_snapshot(zones.fetch('.'), args[4])
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def test_other_record_types_still_use_cached_child_authorities
|
|
207
|
+
with_cached_recursor_authorities do |recursor, zones, calls|
|
|
208
|
+
['DNSKEY', 'A', Dnsruby::Types.new('NS')].each do |type|
|
|
209
|
+
response = recursor.query('child.example.', type, Dnsruby::Classes::IN, true)
|
|
210
|
+
args = calls.last
|
|
211
|
+
assert_equal(Dnsruby::Types.new(type), response.question.first.qtype)
|
|
212
|
+
assert_equal('child.example.', args[3])
|
|
213
|
+
assert_authority_snapshot(zones.fetch('child.example.'), args[4])
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def test_recursion_requests_dnssec_data_whenever_it_will_validate
|
|
219
|
+
recursor_class = Dnsruby::Recursor
|
|
220
|
+
original_cache = recursor_class.class_variable_get(:@@authority_cache)
|
|
221
|
+
recursor_class.class_variable_set(:@@authority_cache, {})
|
|
222
|
+
addresses = recursor_class::AddressCache.new
|
|
223
|
+
addresses.push(['192.0.2.53', 300])
|
|
224
|
+
authorities = {'ns.example.' => addresses}
|
|
225
|
+
transport = Dnsruby::Resolver.new(nameserver: ['192.0.2.53'])
|
|
226
|
+
recursor = recursor_class.new(transport)
|
|
227
|
+
wire = nil
|
|
228
|
+
validates = nil
|
|
229
|
+
transport.define_singleton_method(:send_message) do |query|
|
|
230
|
+
validates = query.do_validation
|
|
231
|
+
wire = single_resolvers.first.make_query_packet(query)
|
|
232
|
+
response = Dnsruby::Message.new('child.example.', 'DNSKEY')
|
|
233
|
+
response.header.aa = true
|
|
234
|
+
response
|
|
235
|
+
end
|
|
236
|
+
Dnsruby::Resolver.stub(:new, transport) do
|
|
237
|
+
[nil, false, true].product([false, true]).each do |dnssec, no_validation|
|
|
238
|
+
recursor.dnssec = dnssec
|
|
239
|
+
recursor._dorecursion('child.example.', 'DNSKEY', Dnsruby::Classes::IN,
|
|
240
|
+
'child.example.', authorities, 0, no_validation)
|
|
241
|
+
query = Dnsruby::Message.decode(wire)
|
|
242
|
+
assert_equal(!!dnssec || !no_validation, query.get_opt.dnssec_ok,
|
|
243
|
+
"dnssec=#{dnssec.inspect}, no_validation=#{no_validation}")
|
|
244
|
+
assert_equal(!no_validation, validates)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
ensure
|
|
248
|
+
transport&.close
|
|
249
|
+
recursor_class.class_variable_set(:@@authority_cache, original_cache) if recursor_class
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
def assert_authority_snapshot(original, copy)
|
|
255
|
+
assert_equal(original.keys, copy.keys)
|
|
256
|
+
refute_same(original, copy)
|
|
257
|
+
original.each do |name, addresses|
|
|
258
|
+
assert_equal(addresses.values, copy.fetch(name).values)
|
|
259
|
+
refute_same(addresses, copy.fetch(name))
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def with_cached_recursor_authorities
|
|
264
|
+
recursor_class = Dnsruby::Recursor
|
|
265
|
+
original_hints = recursor_class.class_variable_get(:@@hints)
|
|
266
|
+
original_zones = recursor_class.class_variable_get(:@@zones_cache)
|
|
267
|
+
zones = ['.', 'example.', 'child.example.'].to_h do |zone|
|
|
268
|
+
[zone, { "ns.#{zone}" => recursor_class::AddressCache.new }]
|
|
269
|
+
end
|
|
270
|
+
zones.fetch('.').values.first.push(['192.0.2.53', 300])
|
|
271
|
+
recursor_class.class_variable_set(:@@hints, zones.fetch('.'))
|
|
272
|
+
recursor_class.class_variable_set(:@@zones_cache, zones)
|
|
273
|
+
recursor = recursor_class.allocate
|
|
274
|
+
recursor.instance_variable_set(:@resolver, Struct.new(:recurse).new)
|
|
275
|
+
calls = []
|
|
276
|
+
recursor.define_singleton_method(:_dorecursion) do |*args|
|
|
277
|
+
calls << args
|
|
278
|
+
Dnsruby::Message.new(*args.first(3)).tap do |response|
|
|
279
|
+
response.header.qr = response.header.aa = true
|
|
280
|
+
response.add_authority(Dnsruby::RR.create("#{args[3]} 60 IN SOA ns.example. hostmaster.example. 1 60 60 60 30"))
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
yield recursor, zones, calls
|
|
284
|
+
ensure
|
|
285
|
+
recursor_class.class_variable_set(:@@hints, original_hints) if recursor_class
|
|
286
|
+
recursor_class.class_variable_set(:@@zones_cache, original_zones) if recursor_class
|
|
287
|
+
end
|
|
288
|
+
end
|