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,212 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
|
|
4
|
+
class ReleaseRecursorCacheTest < Minitest::Test
|
|
5
|
+
def setup
|
|
6
|
+
@klass = Dnsruby::Recursor
|
|
7
|
+
@original = %i[@@zones_cache @@zone_expirations @@authority_cache @@hints].to_h do |name|
|
|
8
|
+
[name, @klass.class_variable_get(name)]
|
|
9
|
+
end
|
|
10
|
+
@root = {'ns.root.' => @klass::AddressCache.new}
|
|
11
|
+
@root.fetch('ns.root.').push(['192.0.2.53', 300])
|
|
12
|
+
@klass.class_variable_set(:@@zones_cache, {'.' => @root})
|
|
13
|
+
@klass.class_variable_set(:@@hints, @root)
|
|
14
|
+
@klass.class_variable_set(:@@zone_expirations, {})
|
|
15
|
+
@klass.class_variable_set(:@@authority_cache, {})
|
|
16
|
+
@recursor = @klass.allocate
|
|
17
|
+
@now = Time.utc(2026, 11, 14, 12)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def teardown
|
|
21
|
+
@original.each { |name, value| @klass.class_variable_set(name, value) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_delegation_expires_before_longer_lived_glue
|
|
25
|
+
addresses = @klass::AddressCache.new
|
|
26
|
+
Time.stub(:now, @now) do
|
|
27
|
+
addresses.push(['192.0.2.53', 300])
|
|
28
|
+
@klass.cache_zone('example.', {'ns.example.' => addresses}, 30)
|
|
29
|
+
assert_equal('example.', @recursor.get_closest_known_zone_for('WWW.EXAMPLE.'))
|
|
30
|
+
end
|
|
31
|
+
Time.stub(:now, @now + 30) do
|
|
32
|
+
assert_equal(['192.0.2.53'], addresses.values)
|
|
33
|
+
assert_equal('.', @recursor.get_closest_known_zone_for('www.example.'))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_working_authority_map_is_separate_from_shared_mapping
|
|
38
|
+
@klass.cache_zone('example.', {'ns.example.' => @klass::AddressCache.new}, 30)
|
|
39
|
+
_zone, working = @recursor.get_closest_known_zone_authorities_for('www.example.')
|
|
40
|
+
working.delete('ns.example.')
|
|
41
|
+
assert_equal(['ns.example.'], @klass.class_variable_get(:@@zones_cache).fetch('example.').keys)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_empty_working_map_cannot_evict_a_newer_delegation
|
|
45
|
+
replacement = {'ns.example.' => @klass::AddressCache.new}
|
|
46
|
+
replacement.fetch('ns.example.').push(['192.0.2.54', 60])
|
|
47
|
+
working = {}
|
|
48
|
+
klass = @klass
|
|
49
|
+
working.define_singleton_method(:values) do
|
|
50
|
+
klass.cache_zone('example.', replacement, 60)
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
@klass.cache_zone('example.', {}, 30)
|
|
54
|
+
published = @klass.class_variable_get(:@@zones_cache).fetch('example.')
|
|
55
|
+
published.define_singleton_method(:dup) { working }
|
|
56
|
+
|
|
57
|
+
zone, authorities = @recursor.get_closest_known_zone_authorities_for('www.example.')
|
|
58
|
+
assert_equal('example.', zone)
|
|
59
|
+
assert_equal(replacement.keys, authorities.keys)
|
|
60
|
+
assert_equal(['192.0.2.54'], authorities.fetch('ns.example.').values)
|
|
61
|
+
latest = @klass.class_variable_get(:@@zones_cache).fetch('example.')
|
|
62
|
+
refute_same(replacement, latest)
|
|
63
|
+
refute_same(latest, authorities)
|
|
64
|
+
assert_same(latest, @klass.class_variable_get(:@@zone_expirations)['example.'].first)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_zero_ttl_delegation_is_not_retained
|
|
68
|
+
@klass.cache_zone('example.', {'ns.example.' => @klass::AddressCache.new}, 0)
|
|
69
|
+
assert_equal('.', @recursor.get_closest_known_zone_for('www.example.'))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_address_cache_length_excludes_expired_addresses
|
|
73
|
+
addresses = @klass::AddressCache.new
|
|
74
|
+
Time.stub(:now, @now) { addresses.push(['192.0.2.53', 5]) }
|
|
75
|
+
Time.stub(:now, @now + 5) { assert_equal(0, addresses.length) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_fresh_zero_ttl_delegation_invalidates_an_older_mapping
|
|
79
|
+
@klass.cache_zone('example.', {'ns.example.' => @klass::AddressCache.new}, 60)
|
|
80
|
+
@klass.cache_zone('example.', {'other.example.' => @klass::AddressCache.new}, 0)
|
|
81
|
+
assert_equal('.', @recursor.get_closest_known_zone_for('www.example.'))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_top_level_recursor_caches_nil_failure_and_honors_do_caching
|
|
85
|
+
resolver = Dnsruby::Resolver.new(nameserver: ['192.0.2.53'])
|
|
86
|
+
recursor = @klass.new(resolver)
|
|
87
|
+
count = 0
|
|
88
|
+
recursor.define_singleton_method(:_dorecursion) { |*_args| count += 1; nil }
|
|
89
|
+
2.times { assert_nil(recursor.query('missing.example.', 'A', 'IN', true)) }
|
|
90
|
+
assert_equal(1, count)
|
|
91
|
+
resolver.do_caching = false
|
|
92
|
+
assert_nil(recursor.query('missing.example.', 'A', 'IN', true))
|
|
93
|
+
assert_equal(2, count)
|
|
94
|
+
ensure
|
|
95
|
+
resolver&.close
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_increasing_recursion_limits_does_not_reuse_a_smaller_limits_failure
|
|
99
|
+
resolver = Dnsruby::Resolver.new(nameserver: ['192.0.2.53'])
|
|
100
|
+
recursor = @klass.new(resolver)
|
|
101
|
+
count = 0
|
|
102
|
+
recursor.define_singleton_method(:_dorecursion) do |*_args|
|
|
103
|
+
count += 1
|
|
104
|
+
2.times { Dnsruby::Recursor.send(:current_budget).step! }
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
recursor.recursion_limits = {max_steps: 1}
|
|
108
|
+
2.times { assert_raises(Dnsruby::RecursionLimitError) { recursor.query('example.', 'A', 'IN', true) } }
|
|
109
|
+
assert_equal(2, count, 'Shared recursion budget exhaustion must not cache a question failure')
|
|
110
|
+
recursor.recursion_limits = {max_steps: 2}
|
|
111
|
+
assert_nil(recursor.query('example.', 'A', 'IN', true))
|
|
112
|
+
assert_equal(3, count)
|
|
113
|
+
ensure
|
|
114
|
+
resolver&.close
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_cached_completion_honors_cancellation_and_deadlines
|
|
118
|
+
resolver = Dnsruby::Resolver.new(nameserver: ['192.0.2.53'])
|
|
119
|
+
recursor = @klass.new(resolver)
|
|
120
|
+
count = 0
|
|
121
|
+
recursor.define_singleton_method(:_dorecursion) { |*_args| count += 1; nil }
|
|
122
|
+
recursor.query('missing.example.', 'A', 'IN', true)
|
|
123
|
+
control = Dnsruby::QueryControl.new
|
|
124
|
+
control.cancel
|
|
125
|
+
Dnsruby::QueryControl.stub(:current, control) do
|
|
126
|
+
assert_raises(Dnsruby::QueryControl::Cancelled) do
|
|
127
|
+
recursor.query('missing.example.', 'A', 'IN', true)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
expired = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now - 1)
|
|
131
|
+
Dnsruby::QueryControl.stub(:current, expired) do
|
|
132
|
+
assert_raises(Dnsruby::QueryControl::DeadlineExceeded) do
|
|
133
|
+
recursor.query('missing.example.', 'A', 'IN', true)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
assert_equal(1, count)
|
|
137
|
+
ensure
|
|
138
|
+
resolver&.close
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_top_level_recursor_caches_negative_answers_and_scopes_validation_policy
|
|
142
|
+
resolver = Dnsruby::Resolver.new(nameserver: ['192.0.2.53'])
|
|
143
|
+
recursor = @klass.new(resolver)
|
|
144
|
+
response = Dnsruby::Message.new('missing.example.', 'A')
|
|
145
|
+
response.header.rcode = Dnsruby::RCode::NXDOMAIN
|
|
146
|
+
response.add_authority(Dnsruby::RR.create('example. 60 IN SOA ns.example. hostmaster.example. 1 60 60 60 30'))
|
|
147
|
+
count = 0
|
|
148
|
+
recursor.define_singleton_method(:_dorecursion) { |*_args| count += 1; response }
|
|
149
|
+
refute(recursor.query('missing.example.', 'A', 'IN', true).cached)
|
|
150
|
+
assert(recursor.query('missing.example.', 'A', 'IN', true).cached)
|
|
151
|
+
assert_equal(1, count)
|
|
152
|
+
recursor.dnssec = true
|
|
153
|
+
recursor.query('missing.example.', 'A', 'IN', true)
|
|
154
|
+
assert_equal(2, count)
|
|
155
|
+
ensure
|
|
156
|
+
resolver&.close
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def test_top_level_nil_failure_does_not_attempt_dnssec_validation
|
|
160
|
+
resolver = Dnsruby::Resolver.new(nameserver: ['192.0.2.53'])
|
|
161
|
+
recursor = @klass.new(resolver)
|
|
162
|
+
count = 0
|
|
163
|
+
recursor.define_singleton_method(:_dorecursion) { |*_args| count += 1; nil }
|
|
164
|
+
Dnsruby::Dnssec.stub(:validate, ->(*) { flunk('There is no response to validate') }) do
|
|
165
|
+
2.times { assert_nil(recursor.query('missing.example.')) }
|
|
166
|
+
end
|
|
167
|
+
assert_equal(1, count)
|
|
168
|
+
ensure
|
|
169
|
+
resolver&.close
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_top_level_name_error_exception_keeps_its_exception_convention
|
|
173
|
+
resolver = Dnsruby::Resolver.new(nameserver: ['192.0.2.53'])
|
|
174
|
+
recursor = @klass.new(resolver)
|
|
175
|
+
response = Dnsruby::Message.new('missing.example.', 'A')
|
|
176
|
+
response.header.rcode = Dnsruby::RCode::NXDOMAIN
|
|
177
|
+
response.add_authority(Dnsruby::RR.create('example. 60 IN SOA ns.example. hostmaster.example. 1 60 60 60 30'))
|
|
178
|
+
count = 0
|
|
179
|
+
recursor.define_singleton_method(:_dorecursion) do |*_args|
|
|
180
|
+
count += 1
|
|
181
|
+
error = Dnsruby::NXDomain.new('missing')
|
|
182
|
+
error.response = response
|
|
183
|
+
raise error
|
|
184
|
+
end
|
|
185
|
+
2.times do
|
|
186
|
+
error = assert_raises(Dnsruby::NXDomain) { recursor.query('missing.example.', 'A', 'IN', true) }
|
|
187
|
+
assert_equal(Dnsruby::RCode::NXDOMAIN, error.response.rcode)
|
|
188
|
+
end
|
|
189
|
+
assert_equal(1, count)
|
|
190
|
+
ensure
|
|
191
|
+
resolver&.close
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_outer_maps_and_address_lists_have_capacity_bounds
|
|
195
|
+
# Exercise bounded insertion with a small temporary capacity, not a load test.
|
|
196
|
+
old_limit = @klass::CACHE_LIMIT
|
|
197
|
+
@klass.send(:remove_const, :CACHE_LIMIT)
|
|
198
|
+
@klass.const_set(:CACHE_LIMIT, 3)
|
|
199
|
+
4.times do |index|
|
|
200
|
+
addresses = @klass::AddressCache.new
|
|
201
|
+
@klass.cache_authority("ns#{index}.example.", addresses)
|
|
202
|
+
@klass.cache_zone("zone#{index}.example.", {"ns#{index}.example." => addresses}, 30)
|
|
203
|
+
end
|
|
204
|
+
assert_equal(3, @klass.class_variable_get(:@@authority_cache).length)
|
|
205
|
+
zones = @klass.class_variable_get(:@@zones_cache)
|
|
206
|
+
assert_equal(3, zones.length)
|
|
207
|
+
assert_same(@root, zones['.'])
|
|
208
|
+
ensure
|
|
209
|
+
@klass.send(:remove_const, :CACHE_LIMIT)
|
|
210
|
+
@klass.const_set(:CACHE_LIMIT, old_limit)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
require_relative 'tc_pipeline_send'
|
|
2
|
+
|
|
3
|
+
class ReleaseReviewTransportTest < Minitest::Test
|
|
4
|
+
def test_malformed_tsig_responses_preserve_the_attempt_until_an_authenticated_answer
|
|
5
|
+
isolated(<<~'RUBY')
|
|
6
|
+
[false, true].product([:short_header, :bad_rdata, :truncated]).each do |tcp, malformed|
|
|
7
|
+
socket = FrameSocket.new
|
|
8
|
+
unless tcp
|
|
9
|
+
socket.define_singleton_method(:getsockopt) { |*| Struct.new(:data).new([Socket::SOCK_DGRAM].pack('i')) }
|
|
10
|
+
socket.define_singleton_method(:recvfrom_nonblock) do |*|
|
|
11
|
+
[incoming, ['AF_INET', 53, '127.0.0.1', '127.0.0.1']]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
writer = sender(socket)
|
|
15
|
+
writer.define_singleton_method(:send_async) { |*| raise 'unauthenticated TC triggered a retry' }
|
|
16
|
+
request = query
|
|
17
|
+
key = Dnsruby::RR.create(name: 'key.example.', type: 'TSIG', klass: 'ANY',
|
|
18
|
+
key: 'c2VjcmV0', algorithm: 'hmac-sha256')
|
|
19
|
+
key.apply(request)
|
|
20
|
+
live = settings(socket, [tcp, malformed], request, writer)
|
|
21
|
+
live.is_persistent_socket = tcp
|
|
22
|
+
SELECTOR.add_to_select(live)
|
|
23
|
+
original_deadline = live.endtime
|
|
24
|
+
reply = query
|
|
25
|
+
reply.header.id, reply.header.qr = request.header.id, true
|
|
26
|
+
reply.header.tc = true if malformed == :truncated
|
|
27
|
+
bytes = if malformed == :short_header
|
|
28
|
+
[request.header.id].pack('n')
|
|
29
|
+
else
|
|
30
|
+
reply.encode.tap { |wire| wire.setbyte(7, 1) } # Claims a missing answer RR.
|
|
31
|
+
end
|
|
32
|
+
socket.incoming = tcp ? [bytes.bytesize].pack('n') + bytes : bytes
|
|
33
|
+
raise 'malformed response was returned' if SELECTOR.get_incoming_data(socket, 512)
|
|
34
|
+
raise 'malformed reply consumed the query' unless SELECTOR.query_settings_for_id(live.client_query_id).equal?(live)
|
|
35
|
+
raise 'malformed reply completed the query' unless live.client_queue.empty?
|
|
36
|
+
raise 'malformed reply extended the deadline' unless live.endtime == original_deadline
|
|
37
|
+
reply.header.tc = false
|
|
38
|
+
key.apply(reply, request)
|
|
39
|
+
bytes = reply.encode
|
|
40
|
+
socket.incoming = tcp ? [bytes.bytesize].pack('n') + bytes : bytes
|
|
41
|
+
decoded = SELECTOR.get_incoming_data(socket, 512)
|
|
42
|
+
SELECTOR.process_message(*decoded, socket)
|
|
43
|
+
SELECTOR.send_queued_validation_responses
|
|
44
|
+
raise 'valid signed reply lost' unless live.client_queue.size == 2 &&
|
|
45
|
+
2.times.map { live.client_queue.pop }.all? { |event| event.last.nil? && event[2].verified? }
|
|
46
|
+
SELECTOR.retire_sender(writer)
|
|
47
|
+
assert_clean(socket)
|
|
48
|
+
end
|
|
49
|
+
RUBY
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_tcp_eof_keeps_only_configured_retries_and_the_original_query_deadline
|
|
53
|
+
isolated(<<~'RUBY')
|
|
54
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
55
|
+
[false, true].product([:success, :failure, :deadline, :update]).each do |pipeline, outcome|
|
|
56
|
+
sockets = []
|
|
57
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*|
|
|
58
|
+
FrameSocket.new.tap do |socket|
|
|
59
|
+
socket.define_singleton_method(:recv_nonblock) { |*| ''.b }
|
|
60
|
+
sockets << socket
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
|
|
64
|
+
use_tcp: true, tcp_pipelining: pipeline, packet_timeout: 30,
|
|
65
|
+
query_timeout: 30, retry_times: 2, retry_delay: 10)
|
|
66
|
+
request, completion = query, Queue.new
|
|
67
|
+
if outcome == :update
|
|
68
|
+
request = Dnsruby::Update.new('example.')
|
|
69
|
+
request.do_validation = request.do_caching = false
|
|
70
|
+
end
|
|
71
|
+
resolver.send_async(request, completion, :eof)
|
|
72
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
73
|
+
plan = engine.instance_variable_get(:@timeouts)[:eof]
|
|
74
|
+
deadline = plan.first
|
|
75
|
+
raise 'initial attempt missing' unless sockets.size == 1
|
|
76
|
+
SELECTOR.get_incoming_data(sockets.first, 0)
|
|
77
|
+
if outcome == :update
|
|
78
|
+
raise 'UPDATE did not report ambiguous EOF' unless completion.size == 1 &&
|
|
79
|
+
completion.pop.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
80
|
+
engine.tick
|
|
81
|
+
raise 'UPDATE was replayed' unless sockets.size == 1 && engine.instance_variable_get(:@timeouts).empty?
|
|
82
|
+
resolver.close
|
|
83
|
+
assert_clean(sockets.first)
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
raise 'EOF completed a retryable query' unless completion.empty? && plan[1].size == 1
|
|
87
|
+
raise 'EOF changed the deadline' unless plan.first == deadline
|
|
88
|
+
if outcome == :deadline
|
|
89
|
+
plan[0] = Dnsruby::TransportClock.now - 1
|
|
90
|
+
engine.send(:update_tick_deadline, :eof)
|
|
91
|
+
engine.tick
|
|
92
|
+
raise 'expired query reconnected' unless sockets.size == 1
|
|
93
|
+
raise 'wrong deadline completion' unless completion.size == 1 && completion.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
94
|
+
else
|
|
95
|
+
retry_value = plan[1].values.first
|
|
96
|
+
plan[1].clear
|
|
97
|
+
plan[1][Dnsruby::TransportClock.now - 1] = retry_value
|
|
98
|
+
engine.send(:update_tick_deadline, :eof)
|
|
99
|
+
engine.tick
|
|
100
|
+
raise 'retry did not reconnect' unless sockets.size == 2 && !sockets.last.equal?(sockets.first)
|
|
101
|
+
raise 'retry changed logical deadline' unless plan.first == deadline
|
|
102
|
+
if outcome == :success
|
|
103
|
+
reply = request.clone
|
|
104
|
+
reply.header.qr = true
|
|
105
|
+
SELECTOR.process_message(reply, reply.encode, sockets.last)
|
|
106
|
+
SELECTOR.send_queued_validation_responses
|
|
107
|
+
raise 'retry answer lost' unless completion.size == 1 && completion.pop.last.nil?
|
|
108
|
+
else
|
|
109
|
+
SELECTOR.get_incoming_data(sockets.last, 0)
|
|
110
|
+
raise 'exhausted EOF query did not finish' unless completion.size == 1 &&
|
|
111
|
+
completion.pop.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
engine.tick
|
|
115
|
+
resolver.close
|
|
116
|
+
raise 'retry plan retained after completion' unless engine.instance_variable_get(:@timeouts).empty?
|
|
117
|
+
raise 'completion repeated' unless completion.empty?
|
|
118
|
+
sockets.each { |socket| assert_clean(socket) }
|
|
119
|
+
end
|
|
120
|
+
RUBY
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_ambiguous_update_eof_cancels_other_servers_and_outstanding_attempts
|
|
124
|
+
isolated(<<~'RUBY')
|
|
125
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
126
|
+
[false, true].product([false, true]).each do |pipeline, other_already_sent|
|
|
127
|
+
sockets = []
|
|
128
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*|
|
|
129
|
+
FrameSocket.new.tap do |socket|
|
|
130
|
+
socket.define_singleton_method(:recv_nonblock) { |*| ''.b }
|
|
131
|
+
sockets << [server, socket]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
resolver = Dnsruby::Resolver.new(nameserver: ['127.0.0.1', '127.0.0.2'],
|
|
135
|
+
dnssec: false, use_tcp: true, tcp_pipelining: pipeline,
|
|
136
|
+
packet_timeout: 30, query_timeout: 30, retry_times: 2, retry_delay: 10)
|
|
137
|
+
request, completion = Dnsruby::Update.new('example.'), Queue.new
|
|
138
|
+
request.do_validation = request.do_caching = false
|
|
139
|
+
resolver.send_async(request, completion, :update)
|
|
140
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
141
|
+
plan = engine.instance_variable_get(:@timeouts)[:update]
|
|
142
|
+
raise 'initial UPDATE missing' unless sockets.map(&:first) == ['127.0.0.1']
|
|
143
|
+
if other_already_sent
|
|
144
|
+
scheduled = plan[1].keys.min
|
|
145
|
+
next_attempt = plan[1].delete(scheduled)
|
|
146
|
+
raise 'unexpected retry ordering' unless next_attempt.first.server == '127.0.0.2'
|
|
147
|
+
plan[1][Dnsruby::TransportClock.now - 1] = next_attempt
|
|
148
|
+
plan[1].replace(plan[1].sort.to_h)
|
|
149
|
+
engine.send(:update_tick_deadline, :update)
|
|
150
|
+
engine.tick
|
|
151
|
+
raise 'second attempt missing' unless sockets.map(&:first) == ['127.0.0.1', '127.0.0.2']
|
|
152
|
+
end
|
|
153
|
+
SELECTOR.get_incoming_data(sockets.first.last, 0)
|
|
154
|
+
raise 'ambiguous UPDATE not terminal' unless completion.size == 1 &&
|
|
155
|
+
completion.pop.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
156
|
+
raise 'UPDATE retries retained' unless engine.instance_variable_get(:@timeouts).empty?
|
|
157
|
+
raise 'UPDATE outstanding ownership retained' unless K.class_variable_get(:@@query_hash).empty?
|
|
158
|
+
engine.tick
|
|
159
|
+
expected_sends = other_already_sent ? 2 : 1
|
|
160
|
+
raise 'UPDATE replayed after ambiguous EOF' unless sockets.length == expected_sends
|
|
161
|
+
resolver.close
|
|
162
|
+
raise 'duplicate UPDATE completion' unless completion.empty?
|
|
163
|
+
sockets.each { |_, socket| assert_clean(socket) }
|
|
164
|
+
end
|
|
165
|
+
RUBY
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_sender_retirement_uses_indexes_and_cleans_pending_active_and_idle_ownership
|
|
169
|
+
isolated(<<~'RUBY')
|
|
170
|
+
senders, attempts, sockets = [], [], []
|
|
171
|
+
12.times do |index|
|
|
172
|
+
active, idle = FrameSocket.new, FrameSocket.new
|
|
173
|
+
writer = sender(active)
|
|
174
|
+
senders << writer
|
|
175
|
+
sockets.concat([active, idle])
|
|
176
|
+
live = settings(active, [index, :active], query, writer)
|
|
177
|
+
unused = settings(idle, [index, :idle], query, writer)
|
|
178
|
+
pending = settings(nil, [index, :pending], query, writer)
|
|
179
|
+
SELECTOR.add_to_select(live)
|
|
180
|
+
SELECTOR.add_to_select(unused)
|
|
181
|
+
SELECTOR.claim_query(unused, true)
|
|
182
|
+
SELECTOR.reserve_send(pending)
|
|
183
|
+
attempts << [live, pending]
|
|
184
|
+
end
|
|
185
|
+
# A sender close must never enumerate other senders' global tables.
|
|
186
|
+
[K.class_variable_get(:@@query_hash), K.class_variable_get(:@@socket_owners)].each do |table|
|
|
187
|
+
[:each, :each_value, :select].each do |method|
|
|
188
|
+
table.define_singleton_method(method) { |*| raise 'retirement scanned a global index' }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
SELECTOR.retire_sender(senders.first)
|
|
192
|
+
raise 'active or pending reservation not retired' unless attempts.first.all?(&:retire_connection)
|
|
193
|
+
raise 'another sender was retired' unless attempts.drop(1).flatten.none?(&:retire_connection)
|
|
194
|
+
raise 'idle stream retained' unless sockets[1].closed?
|
|
195
|
+
raise 'active stream closed before draining' if sockets[0].closed?
|
|
196
|
+
attempts.first.each { |attempt| SELECTOR.remove_id(attempt.client_query_id) }
|
|
197
|
+
[:queries_by_sender, :sockets_by_sender].each do |name|
|
|
198
|
+
raise 'retired sender retained' if K.class_variable_get("@@#{name}").key?(senders.first)
|
|
199
|
+
end
|
|
200
|
+
senders.drop(1).each { |writer| SELECTOR.retire_sender(writer) }
|
|
201
|
+
attempts.drop(1).flatten.each { |attempt| SELECTOR.remove_id(attempt.client_query_id) }
|
|
202
|
+
[:queries_by_sender, :sockets_by_sender].each do |name|
|
|
203
|
+
raise 'sender index leaked' unless K.class_variable_get("@@#{name}").empty?
|
|
204
|
+
end
|
|
205
|
+
sockets.each { |socket| assert_clean(socket) }
|
|
206
|
+
|
|
207
|
+
# Re-registering a reusable stream under a different helper sender must
|
|
208
|
+
# replace its owner instead of leaving a stale retirement entry.
|
|
209
|
+
shared = FrameSocket.new
|
|
210
|
+
first, second = sender(shared), sender(shared)
|
|
211
|
+
old = settings(shared, :old_owner, query, first)
|
|
212
|
+
SELECTOR.add_to_select(old)
|
|
213
|
+
SELECTOR.claim_query(old, true)
|
|
214
|
+
current = settings(shared, :new_owner, query, second)
|
|
215
|
+
SELECTOR.add_to_select(current)
|
|
216
|
+
raise 'old socket owner retained' if K.class_variable_get(:@@sockets_by_sender).key?(first)
|
|
217
|
+
SELECTOR.retire_sender(first)
|
|
218
|
+
raise 'old owner retired reassigned socket' if K.class_variable_get(:@@retiring_sockets).include?(shared)
|
|
219
|
+
SELECTOR.retire_sender(second)
|
|
220
|
+
SELECTOR.remove_id(:new_owner)
|
|
221
|
+
assert_clean(shared)
|
|
222
|
+
raise 'reassigned sender retained' unless K.class_variable_get(:@@sockets_by_sender).empty?
|
|
223
|
+
RUBY
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def test_validation_registration_obeys_queue_and_sender_cancellation_without_leaking
|
|
227
|
+
isolated(<<~'RUBY')
|
|
228
|
+
budget_class = Class.new do
|
|
229
|
+
attr_accessor :cancelled
|
|
230
|
+
def initialize(cancelled); @cancelled = cancelled; end
|
|
231
|
+
def cancel; @cancelled = true; end
|
|
232
|
+
end
|
|
233
|
+
queue, observer, writer = Queue.new, Object.new, sender(FrameSocket.new)
|
|
234
|
+
other_queue, other_writer = Queue.new, sender(FrameSocket.new)
|
|
235
|
+
other_budget = budget_class.new(false)
|
|
236
|
+
raise 'unrelated validation rejected' unless SELECTOR.register_validation(other_queue, other_writer, other_budget)
|
|
237
|
+
SELECTOR.add_observer(queue, observer)
|
|
238
|
+
active = budget_class.new(false)
|
|
239
|
+
raise 'active validation rejected' unless SELECTOR.register_validation(queue, writer, active)
|
|
240
|
+
SELECTOR.register_validation(queue, writer, active)
|
|
241
|
+
raise 'duplicate registration retained' unless K.class_variable_get(:@@validations).length == 2
|
|
242
|
+
SELECTOR.remove_observer(queue, observer)
|
|
243
|
+
raise 'finished query did not cancel work' unless active.cancelled
|
|
244
|
+
raise 'queue completion cancelled unrelated work' if other_budget.cancelled
|
|
245
|
+
late = budget_class.new(false)
|
|
246
|
+
raise 'completed queue started late validation' if SELECTOR.register_validation(queue, writer, late)
|
|
247
|
+
raise 'late work not cancelled' unless late.cancelled
|
|
248
|
+
SELECTOR.unregister_validation(queue, writer, active)
|
|
249
|
+
|
|
250
|
+
# Direct PacketSender queues have no observer and remain supported. A
|
|
251
|
+
# shared budget may be registered with more than one nested query.
|
|
252
|
+
first, second, shared = Queue.new, Queue.new, budget_class.new(false)
|
|
253
|
+
[first, second].each do |direct|
|
|
254
|
+
raise 'direct validation rejected' unless SELECTOR.register_validation(direct, writer, shared)
|
|
255
|
+
end
|
|
256
|
+
SELECTOR.unregister_validation(first, writer, shared)
|
|
257
|
+
SELECTOR.retire_sender(writer)
|
|
258
|
+
raise 'shared remaining validation escaped retirement' unless shared.cancelled
|
|
259
|
+
raise 'sender retirement cancelled unrelated work' if other_budget.cancelled
|
|
260
|
+
SELECTOR.unregister_validation(second, writer, shared)
|
|
261
|
+
SELECTOR.retire_sender(other_writer)
|
|
262
|
+
raise 'unrelated sender lost its registration' unless other_budget.cancelled
|
|
263
|
+
SELECTOR.unregister_validation(other_queue, other_writer, other_budget)
|
|
264
|
+
raise 'validation registration leaked' unless K.class_variable_get(:@@validations).empty?
|
|
265
|
+
RUBY
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def test_sender_retirement_preserves_resolver_validation_but_owner_close_cancels_it
|
|
269
|
+
isolated(<<~'RUBY')
|
|
270
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
271
|
+
[:reconfigure, :other_close, :owner_close].each do |action|
|
|
272
|
+
socket = FrameSocket.new
|
|
273
|
+
writer = sender(socket)
|
|
274
|
+
writer.use_tcp = writer.dnssec = true
|
|
275
|
+
first, second = 2.times.map do
|
|
276
|
+
Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: true, use_tcp: true, do_validation: true)
|
|
277
|
+
end
|
|
278
|
+
[first, second].each { |resolver| resolver.single_resolvers = [writer] }
|
|
279
|
+
entered, release = Queue.new, Queue.new
|
|
280
|
+
Dnsruby::Dnssec.define_singleton_method(:validate_with_query) do |_query, answer|
|
|
281
|
+
entered << true
|
|
282
|
+
release.pop
|
|
283
|
+
answer.security_level = Dnsruby::Message::SecurityLevel::SECURE
|
|
284
|
+
end
|
|
285
|
+
request = query('validating.example.')
|
|
286
|
+
request.do_validation = true
|
|
287
|
+
results = Queue.new
|
|
288
|
+
second.send_async(request, results, :client)
|
|
289
|
+
answer = request.clone
|
|
290
|
+
answer.header.qr = true
|
|
291
|
+
SELECTOR.process_message(answer, answer.encode, socket)
|
|
292
|
+
entered.pop
|
|
293
|
+
case action
|
|
294
|
+
when :reconfigure then second.nameserver = '127.0.0.2'
|
|
295
|
+
when :other_close then first.close
|
|
296
|
+
when :owner_close then second.close
|
|
297
|
+
end
|
|
298
|
+
release << true
|
|
299
|
+
sleep 0.001 until K.class_variable_get(:@@validations).empty?
|
|
300
|
+
SELECTOR.send_queued_validation_responses
|
|
301
|
+
raise "#{action}: missing or duplicate completion" unless results.size == 1
|
|
302
|
+
_id, response, error = results.pop
|
|
303
|
+
if action == :owner_close
|
|
304
|
+
raise 'owner close did not cancel its query' unless error.is_a?(Dnsruby::OtherResolvError)
|
|
305
|
+
else
|
|
306
|
+
raise "#{action}: accepted validation was cancelled: #{error.inspect}" unless
|
|
307
|
+
error.nil? && response.security_level == Dnsruby::Message::SecurityLevel::SECURE
|
|
308
|
+
end
|
|
309
|
+
first.close
|
|
310
|
+
second.close
|
|
311
|
+
SELECTOR.send_queued_validation_responses
|
|
312
|
+
raise 'late validation completed twice' unless results.empty?
|
|
313
|
+
raise 'validation registration leaked' unless K.class_variable_get(:@@validations).empty?
|
|
314
|
+
end
|
|
315
|
+
RUBY
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def test_fork_discards_parent_validation_registrations_without_losing_child_work
|
|
319
|
+
skip 'fork unavailable' unless Process.respond_to?(:fork)
|
|
320
|
+
isolated(<<~'RUBY')
|
|
321
|
+
entered, release = Queue.new, Queue.new
|
|
322
|
+
request = query
|
|
323
|
+
request.do_validation = true
|
|
324
|
+
response = request.clone
|
|
325
|
+
response.header.qr = true
|
|
326
|
+
writer = sender(FrameSocket.new)
|
|
327
|
+
writer.dnssec = true
|
|
328
|
+
parent_queue = Queue.new
|
|
329
|
+
parent = Dnsruby::ValidatorThread.new(:parent, parent_queue, response, nil, request, SELECTOR, writer)
|
|
330
|
+
parent.define_singleton_method(:validate) { |*| entered << true; release.pop; true }
|
|
331
|
+
parent.run
|
|
332
|
+
Timeout.timeout(3) { entered.pop }
|
|
333
|
+
[false, true].each do |retire_first|
|
|
334
|
+
child_status = ForkTestHelper.run do
|
|
335
|
+
SELECTOR.retire_sender(writer) if retire_first
|
|
336
|
+
child_entered, child_release = Queue.new, Queue.new
|
|
337
|
+
child_queue = Queue.new
|
|
338
|
+
child = Dnsruby::ValidatorThread.new(:child, child_queue, response.clone, nil, request, SELECTOR, writer)
|
|
339
|
+
child.define_singleton_method(:validate) { |*| child_entered << true; child_release.pop; true }
|
|
340
|
+
child.run
|
|
341
|
+
Timeout.timeout(3) { child_entered.pop }
|
|
342
|
+
registrations = K.class_variable_get(:@@validations)
|
|
343
|
+
raise 'parent validation retained after fork' if registrations.any? { |queue, _, _| queue.equal?(parent_queue) }
|
|
344
|
+
raise 'child registration lost during pool restart' unless registrations.length == 1 &&
|
|
345
|
+
registrations.first[0].equal?(child_queue) && registrations.first[1].equal?(writer)
|
|
346
|
+
child_release << true
|
|
347
|
+
Timeout.timeout(3) do
|
|
348
|
+
sleep(0.001) until K.class_variable_get(:@@validations).empty?
|
|
349
|
+
end
|
|
350
|
+
exit! 0
|
|
351
|
+
end
|
|
352
|
+
raise 'child registration lifecycle failed' unless child_status.success?
|
|
353
|
+
raise 'child cleared parent registration' unless K.class_variable_get(:@@validations).any? { |queue, _, _| queue.equal?(parent_queue) }
|
|
354
|
+
end
|
|
355
|
+
release << true
|
|
356
|
+
Timeout.timeout(3) do
|
|
357
|
+
sleep(0.001) until K.class_variable_get(:@@validations).empty?
|
|
358
|
+
end
|
|
359
|
+
RUBY
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
private
|
|
363
|
+
|
|
364
|
+
def isolated(body)
|
|
365
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
366
|
+
source = "require #{File.expand_path('fork_test_helper', __dir__).inspect}\n#{TestPipelineSend::FIXTURE}\nTimeout.timeout(12) do\n#{body}\nend"
|
|
367
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
368
|
+
File.expand_path('../lib', __dir__), '-e', source)
|
|
369
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
370
|
+
assert_empty(stderr)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require_relative 'tc_review_transport_native'
|
|
2
|
+
|
|
3
|
+
class ReviewTransportNativeTest < Minitest::Test
|
|
4
|
+
def test_malformed_unsigned_tsig_reply_cannot_preempt_a_valid_signed_answer
|
|
5
|
+
key = tsig_key
|
|
6
|
+
port, requests = udp_server do |request|
|
|
7
|
+
malformed = response(request, 'untrusted').encode
|
|
8
|
+
malformed[10, 2] = [1].pack('n') # Advertise an absent additional RR.
|
|
9
|
+
truncated = malformed.dup
|
|
10
|
+
truncated.setbyte(2, truncated.getbyte(2) | 2)
|
|
11
|
+
signed = response(request, 'authenticated')
|
|
12
|
+
key.apply(signed, request)
|
|
13
|
+
[[request.header.id].pack('n'), malformed, truncated, signed]
|
|
14
|
+
end
|
|
15
|
+
resolver = resolver_for(port, do_caching: false)
|
|
16
|
+
resolver.tsig = key
|
|
17
|
+
reply = query(resolver)
|
|
18
|
+
assert(reply.verified?)
|
|
19
|
+
assert_equal(['authenticated'], reply.answer.first.strings)
|
|
20
|
+
assert_equal(1, requests.size)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_tcp_connection_loss_reconnects_within_the_configured_retry_plan
|
|
24
|
+
[false, true].product([:success, :exhausted, :deadline]).each do |pipeline, outcome|
|
|
25
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
26
|
+
@sockets << server
|
|
27
|
+
requests = Queue.new
|
|
28
|
+
worker do
|
|
29
|
+
count = 0
|
|
30
|
+
loop do
|
|
31
|
+
client = server.accept
|
|
32
|
+
begin
|
|
33
|
+
request = read_frame(client)
|
|
34
|
+
requests << request
|
|
35
|
+
count += 1
|
|
36
|
+
write_frame(client, response(request, 'retried')) if count == 2 && outcome == :success
|
|
37
|
+
ensure
|
|
38
|
+
client.close
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
resolver = resolver_for(server.addr[1], use_tcp: true, tcp_pipelining: pipeline,
|
|
43
|
+
do_caching: false, retry_times: 2, retry_delay: outcome == :deadline ? 10 : 0.01,
|
|
44
|
+
query_timeout: outcome == :deadline ? 0.15 : 3)
|
|
45
|
+
if outcome == :success
|
|
46
|
+
assert_equal(['retried'], query(resolver).answer.first.strings)
|
|
47
|
+
elsif outcome == :exhausted
|
|
48
|
+
assert_raises(Dnsruby::SocketEofResolvError) { query(resolver) }
|
|
49
|
+
else
|
|
50
|
+
assert_raises(Dnsruby::ResolvTimeout) { query(resolver) }
|
|
51
|
+
end
|
|
52
|
+
assert_equal(outcome == :deadline ? 1 : 2, requests.size)
|
|
53
|
+
resolver.close
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|