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,906 @@
|
|
|
1
|
+
require_relative 'tc_pipeline_send'
|
|
2
|
+
|
|
3
|
+
class TestReleaseTransport < Minitest::Test
|
|
4
|
+
def test_initial_connect_does_not_hold_resolver_lock_and_close_cancels_it
|
|
5
|
+
isolated(<<~'RUBY')
|
|
6
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
7
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
|
|
8
|
+
use_tcp: true, tcp_pipelining: true)
|
|
9
|
+
socket = FrameSocket.new
|
|
10
|
+
entered, release = Queue.new, Queue.new
|
|
11
|
+
resolver.single_resolvers.first.define_singleton_method(:tcp_pipeline_socket) do |_|
|
|
12
|
+
entered << true
|
|
13
|
+
release.pop
|
|
14
|
+
[socket, true]
|
|
15
|
+
end
|
|
16
|
+
queue = Queue.new
|
|
17
|
+
sending = Thread.new { resolver.send_async(query, queue, :slow) }
|
|
18
|
+
entered.pop
|
|
19
|
+
# This is the actual callback entry used by the global selector.
|
|
20
|
+
Timeout.timeout(1) { SELECTOR.send_tick_to_observers }
|
|
21
|
+
Timeout.timeout(1) { resolver.close }
|
|
22
|
+
raise 'close did not complete exactly once' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
|
|
23
|
+
release << true
|
|
24
|
+
sending.value
|
|
25
|
+
drain_errors
|
|
26
|
+
raise 'cancelled connect sent bytes' unless socket.written.empty?
|
|
27
|
+
raise 'cancelled connect retained socket' unless socket.closed?
|
|
28
|
+
raise 'cancelled connect recreated query' unless K.class_variable_get(:@@query_hash).empty?
|
|
29
|
+
raise 'duplicate completion' unless queue.empty?
|
|
30
|
+
RUBY
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_close_before_initial_dispatch_cancels_reserved_attempt
|
|
34
|
+
isolated(<<~'RUBY')
|
|
35
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
|
|
36
|
+
writer = resolver.single_resolvers.first
|
|
37
|
+
original = writer.method(:prepare_send_async)
|
|
38
|
+
entered, release = Queue.new, Queue.new
|
|
39
|
+
writer.define_singleton_method(:prepare_send_async) do |*args|
|
|
40
|
+
operation = original.call(*args)
|
|
41
|
+
-> { entered << true; release.pop; operation.call }
|
|
42
|
+
end
|
|
43
|
+
writer.define_singleton_method(:send_async) { |*| raise 'dispatched after close' }
|
|
44
|
+
queue = Queue.new
|
|
45
|
+
sending = Thread.new { resolver.send_async(query, queue, :cancelled) }
|
|
46
|
+
entered.pop
|
|
47
|
+
resolver.close
|
|
48
|
+
release << true
|
|
49
|
+
sending.value
|
|
50
|
+
raise 'missing close completion' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::OtherResolvError)
|
|
51
|
+
raise 'retained reservation' unless K.class_variable_get(:@@query_hash).empty?
|
|
52
|
+
RUBY
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_nonpipeline_connects_use_independent_workers
|
|
56
|
+
isolated(<<~'RUBY')
|
|
57
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: false,
|
|
58
|
+
packet_timeout: 2, dnssec: false)
|
|
59
|
+
entered, release, connected = Queue.new, Queue.new, Queue.new
|
|
60
|
+
first_socket, second_socket = FrameSocket.new, FrameSocket.new
|
|
61
|
+
calls = 0
|
|
62
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
|
|
63
|
+
calls += 1
|
|
64
|
+
if calls == 1
|
|
65
|
+
entered << true
|
|
66
|
+
release.pop
|
|
67
|
+
first_socket
|
|
68
|
+
else
|
|
69
|
+
connected << true
|
|
70
|
+
second_socket
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
74
|
+
first = query('slow.example.')
|
|
75
|
+
task1 = writer.send_dnsruby(first.encode, first, :first, Queue.new, true)
|
|
76
|
+
entered.pop
|
|
77
|
+
second = query('healthy.example.')
|
|
78
|
+
task2 = writer.send_dnsruby(second.encode, second, :second, Queue.new, true)
|
|
79
|
+
Timeout.timeout(1) { connected.pop; task2.value }
|
|
80
|
+
raise 'healthy independent send never wrote' if second_socket.written.empty?
|
|
81
|
+
release << true
|
|
82
|
+
task1.value
|
|
83
|
+
[:first, :second].each { |id| SELECTOR.remove_id(id) }
|
|
84
|
+
RUBY
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_explicit_close_retires_idle_streams_but_allows_shared_queries_to_finish
|
|
88
|
+
isolated(<<~'RUBY')
|
|
89
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
90
|
+
socket = FrameSocket.new
|
|
91
|
+
writer = sender(socket)
|
|
92
|
+
writer.use_tcp = true
|
|
93
|
+
first, second = 2.times.map { Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false, use_tcp: true) }
|
|
94
|
+
[first, second].each { |resolver| resolver.single_resolvers = [writer] }
|
|
95
|
+
a, b = query('first.example.'), query('second.example.')
|
|
96
|
+
a.header.id, b.header.id = 123, 124
|
|
97
|
+
qa, qb = Queue.new, Queue.new
|
|
98
|
+
first.send_async(a, qa, :first)
|
|
99
|
+
second.send_async(b, qb, :second)
|
|
100
|
+
first.close
|
|
101
|
+
raise 'shared live query socket closed' if socket.closed?
|
|
102
|
+
raise 'retiring stream remains reusable' if SELECTOR.reusable_socket?(socket)
|
|
103
|
+
response = b.clone
|
|
104
|
+
response.header.qr = true
|
|
105
|
+
SELECTOR.process_message(response, response.encode, socket)
|
|
106
|
+
SELECTOR.send_queued_validation_responses
|
|
107
|
+
raise 'shared query failed' unless qb.size == 1 && qb.pop.last.nil?
|
|
108
|
+
raise 'retired stream stayed open' unless socket.closed?
|
|
109
|
+
assert_clean(socket)
|
|
110
|
+
second.close
|
|
111
|
+
raise 'first close did not complete once' unless qa.size == 1
|
|
112
|
+
RUBY
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_pipeline_collision_rotates_stream_without_modifying_raw_signed_requests
|
|
116
|
+
isolated(<<~'RUBY')
|
|
117
|
+
sockets = []
|
|
118
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
|
|
119
|
+
FrameSocket.new.tap do |socket|
|
|
120
|
+
socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
|
|
121
|
+
sockets << socket
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true,
|
|
125
|
+
use_tcp: true, dnssec: false)
|
|
126
|
+
# Identical question and ID, but different TSIG identities. Question-only
|
|
127
|
+
# demultiplexing cannot safely choose between these on the same stream.
|
|
128
|
+
requests = ['one', 'two'].map do |key_name|
|
|
129
|
+
query('same.example.').tap do |request|
|
|
130
|
+
request.header.id = 12345
|
|
131
|
+
request.sign!(key_name, 'MTIzNA==')
|
|
132
|
+
request.send_raw = true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
wires = requests.map(&:encode)
|
|
136
|
+
requests.each_with_index { |request, index| writer.send_async(request, Queue.new, index) }
|
|
137
|
+
raise 'ID collision reused the same stream' unless sockets.size == 2
|
|
138
|
+
sockets.each_with_index do |socket, index|
|
|
139
|
+
raise 'signed/raw bytes changed' unless socket.written == [wires[index].bytesize].pack('n') + wires[index]
|
|
140
|
+
end
|
|
141
|
+
raise 'old stream closed before completion' if sockets.first.closed?
|
|
142
|
+
SELECTOR.remove_id(0)
|
|
143
|
+
raise 'collision stream not retired after completion' unless sockets.first.closed?
|
|
144
|
+
SELECTOR.remove_id(1)
|
|
145
|
+
writer.close
|
|
146
|
+
sockets.each { |socket| raise 'idle stream retained' unless socket.closed?; assert_clean(socket) }
|
|
147
|
+
RUBY
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_close_marks_a_shared_pending_connect_for_retirement
|
|
151
|
+
isolated(<<~'RUBY')
|
|
152
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
153
|
+
entered, release = Queue.new, Queue.new
|
|
154
|
+
sockets = []
|
|
155
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
|
|
156
|
+
socket = FrameSocket.new
|
|
157
|
+
socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
|
|
158
|
+
sockets << socket
|
|
159
|
+
if sockets.size == 1
|
|
160
|
+
entered << true
|
|
161
|
+
release.pop
|
|
162
|
+
end
|
|
163
|
+
socket
|
|
164
|
+
end
|
|
165
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true, tcp_pipelining: true, dnssec: false)
|
|
166
|
+
first, second = 2.times.map { Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false) }
|
|
167
|
+
[first, second].each { |resolver| resolver.single_resolvers = [writer] }
|
|
168
|
+
queue = Queue.new
|
|
169
|
+
before = query('before-close.example.')
|
|
170
|
+
sending = Thread.new { second.send_async(before, queue, :before) }
|
|
171
|
+
entered.pop
|
|
172
|
+
first.close
|
|
173
|
+
release << true
|
|
174
|
+
sending.value
|
|
175
|
+
answer = before.clone
|
|
176
|
+
answer.header.qr = true
|
|
177
|
+
SELECTOR.process_message(answer, answer.encode, sockets.first)
|
|
178
|
+
SELECTOR.send_queued_validation_responses
|
|
179
|
+
raise 'shared pre-close query failed' unless queue.size == 1 && queue.pop.last.nil?
|
|
180
|
+
raise 'pending connect escaped retirement' unless sockets.first.closed?
|
|
181
|
+
assert_clean(sockets.first)
|
|
182
|
+
after = query('after-close.example.')
|
|
183
|
+
second.send_async(after, queue, :after)
|
|
184
|
+
answer = after.clone
|
|
185
|
+
answer.header.qr = true
|
|
186
|
+
SELECTOR.process_message(answer, answer.encode, sockets.last)
|
|
187
|
+
SELECTOR.send_queued_validation_responses
|
|
188
|
+
raise 'fresh query failed' unless queue.size == 1 && queue.pop.last.nil?
|
|
189
|
+
raise 'fresh connection is not reusable' unless sockets.size == 2 && SELECTOR.reusable_socket?(sockets.last)
|
|
190
|
+
second.close
|
|
191
|
+
sockets.each { |socket| assert_clean(socket) }
|
|
192
|
+
RUBY
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def test_pre_close_reservation_does_not_retire_a_newer_shared_pipeline
|
|
196
|
+
isolated(<<~'RUBY')
|
|
197
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
198
|
+
sockets = []
|
|
199
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
|
|
200
|
+
FrameSocket.new.tap do |socket|
|
|
201
|
+
socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
|
|
202
|
+
sockets << socket
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true, tcp_pipelining: true, dnssec: false)
|
|
206
|
+
original = writer.method(:prepare_send_async)
|
|
207
|
+
entered, release = Queue.new, Queue.new
|
|
208
|
+
paused = false
|
|
209
|
+
writer.define_singleton_method(:prepare_send_async) do |*args|
|
|
210
|
+
operation = original.call(*args)
|
|
211
|
+
unless paused
|
|
212
|
+
paused = true
|
|
213
|
+
reserved = operation
|
|
214
|
+
operation = -> { entered << true; release.pop; reserved.call }
|
|
215
|
+
end
|
|
216
|
+
operation
|
|
217
|
+
end
|
|
218
|
+
first, second = 2.times.map { Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false) }
|
|
219
|
+
[first, second].each { |resolver| resolver.single_resolvers = [writer] }
|
|
220
|
+
queue = Queue.new
|
|
221
|
+
old = query('reserved-before-close.example.')
|
|
222
|
+
sending = Thread.new { second.send_async(old, queue, :old) }
|
|
223
|
+
entered.pop
|
|
224
|
+
first.close
|
|
225
|
+
fresh = query('created-after-close.example.')
|
|
226
|
+
second.send_async(fresh, queue, :fresh)
|
|
227
|
+
SELECTOR.send_tick_to_observers
|
|
228
|
+
release << true
|
|
229
|
+
sending.value
|
|
230
|
+
raise 'retired reservation reused fresh stream' unless sockets.size == 2
|
|
231
|
+
[fresh, old].zip(sockets).each do |request, socket|
|
|
232
|
+
answer = request.clone
|
|
233
|
+
answer.header.qr = true
|
|
234
|
+
SELECTOR.process_message(answer, answer.encode, socket)
|
|
235
|
+
end
|
|
236
|
+
SELECTOR.send_queued_validation_responses
|
|
237
|
+
raise 'shared queries did not finish' unless queue.size == 2 && 2.times.all? { queue.pop.last.nil? }
|
|
238
|
+
raise 'old one-use connection retained' unless sockets.last.closed?
|
|
239
|
+
raise 'new pipeline was retired' unless SELECTOR.reusable_socket?(sockets.first)
|
|
240
|
+
second.close
|
|
241
|
+
sockets.each { |socket| assert_clean(socket) }
|
|
242
|
+
RUBY
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def test_closed_pipeline_accounting_stays_bounded
|
|
246
|
+
isolated(<<~'RUBY')
|
|
247
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| FrameSocket.new }
|
|
248
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true)
|
|
249
|
+
100.times do
|
|
250
|
+
socket, fresh = writer.tcp_pipeline_socket(0)
|
|
251
|
+
raise 'closed connection reused' unless fresh
|
|
252
|
+
socket.close
|
|
253
|
+
end
|
|
254
|
+
raise 'closed sockets accumulated' unless writer.instance_variable_get(:@use_counts).size == 1
|
|
255
|
+
RUBY
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def test_single_resolver_constructor_and_replacement_keep_pipeline_options
|
|
259
|
+
isolated(<<~'RUBY')
|
|
260
|
+
options = {tcp_pipelining: true, tcp_pipelining_max_queries: 5, server: '127.0.0.1'}.freeze
|
|
261
|
+
resolver = Dnsruby::SingleResolver.new(options)
|
|
262
|
+
2.times do
|
|
263
|
+
writer = resolver.single_resolvers.first
|
|
264
|
+
raise 'pipeline option not forwarded' unless writer.tcp_pipelining && writer.tcp_pipelining_max_queries == 5
|
|
265
|
+
resolver.server = '127.0.0.2'
|
|
266
|
+
end
|
|
267
|
+
defaults = Dnsruby::SingleResolver.new(server: '127.0.0.1')
|
|
268
|
+
raise 'incorrect pipeline defaults' unless defaults.tcp_pipelining == false && defaults.tcp_pipelining_max_queries == :infinite
|
|
269
|
+
RUBY
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_packet_timeout_does_not_create_nil_sender_and_close_is_safe
|
|
273
|
+
isolated(<<~'RUBY')
|
|
274
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
275
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false, retry_times: 1)
|
|
276
|
+
writer = resolver.single_resolvers.first
|
|
277
|
+
writer.define_singleton_method(:send_async) { |*| }
|
|
278
|
+
queue = Queue.new
|
|
279
|
+
resolver.send_async(query, queue, :timeout)
|
|
280
|
+
K.class_variable_get(:@@timeouts).transform_values! { Dnsruby::TransportClock.now - 1 }
|
|
281
|
+
K.class_variable_get(:@@timeouts).each { |id, deadline| SELECTOR.instance_variable_get(:@packet_deadlines)[id] = deadline }
|
|
282
|
+
SELECTOR.process_timeouts
|
|
283
|
+
raise 'packet timeout was not delivered' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
284
|
+
raise 'last sender moved beyond end of list' unless resolver.single_resolvers == [writer]
|
|
285
|
+
resolver.close
|
|
286
|
+
# Be tolerant of legacy callers holding or supplying the exposed array.
|
|
287
|
+
resolver.single_resolvers << nil
|
|
288
|
+
resolver.close
|
|
289
|
+
RUBY
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def test_late_success_timeout_and_error_do_not_restore_replaced_sender
|
|
293
|
+
isolated(<<~'RUBY')
|
|
294
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
295
|
+
[:success, :timeout, :error].each do |outcome|
|
|
296
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false, retry_times: 1)
|
|
297
|
+
writer = resolver.single_resolvers.first
|
|
298
|
+
writer.define_singleton_method(:send_async) { |*| }
|
|
299
|
+
queue = Queue.new
|
|
300
|
+
resolver.send_async(query, queue, outcome)
|
|
301
|
+
pending = K.class_variable_get(:@@query_hash).values.find { |settings| settings.single_resolver.equal?(writer) }
|
|
302
|
+
resolver.nameserver = '127.0.0.2'
|
|
303
|
+
replacement = resolver.single_resolvers.first
|
|
304
|
+
case outcome
|
|
305
|
+
when :success
|
|
306
|
+
answer = pending.query.clone
|
|
307
|
+
answer.header.qr = true
|
|
308
|
+
SELECTOR.push_response_for_query(pending, answer, pending.query, writer)
|
|
309
|
+
SELECTOR.send_queued_responses
|
|
310
|
+
SELECTOR.send_queued_validation_responses
|
|
311
|
+
when :timeout
|
|
312
|
+
K.class_variable_get(:@@timeouts)[pending.client_query_id] = Dnsruby::TransportClock.now - 1
|
|
313
|
+
SELECTOR.instance_variable_get(:@packet_deadlines)[pending.client_query_id] = K.class_variable_get(:@@timeouts)[pending.client_query_id]
|
|
314
|
+
SELECTOR.process_timeouts
|
|
315
|
+
when :error
|
|
316
|
+
SELECTOR.fail_query_if_current(pending.client_query_id, pending.client_queue, pending,
|
|
317
|
+
Dnsruby::OtherResolvError.new('old server failed'))
|
|
318
|
+
SELECTOR.send_queued_exceptions
|
|
319
|
+
end
|
|
320
|
+
raise "#{outcome}: no completion" unless queue.size == 1
|
|
321
|
+
result = queue.pop
|
|
322
|
+
raise 'successful old response lost' if outcome == :success && result.last
|
|
323
|
+
raise "#{outcome}: stale sender restored" unless resolver.single_resolvers == [replacement]
|
|
324
|
+
raise "#{outcome}: config changed" unless resolver.config.nameserver == ['127.0.0.2']
|
|
325
|
+
resolver.close
|
|
326
|
+
end
|
|
327
|
+
RUBY
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def test_timed_out_stream_cannot_accept_new_queries_and_existing_peers_drain
|
|
331
|
+
isolated(<<~'RUBY')
|
|
332
|
+
socket = FrameSocket.new
|
|
333
|
+
writer = sender(socket)
|
|
334
|
+
expired = settings(socket, :expired, query('expired.example.'), writer)
|
|
335
|
+
peer = settings(socket, :peer, query('peer.example.'), writer)
|
|
336
|
+
expired.query.header.id, peer.query.header.id = 7, 8
|
|
337
|
+
[expired, peer].each { |item| SELECTOR.add_to_select(item) }
|
|
338
|
+
# Both a partial prefix and a partial body must remain owned by this
|
|
339
|
+
# retiring stream until its already outstanding queries finish.
|
|
340
|
+
K.class_variable_get(:@@tcp_buffers)[socket] = [+"\0", 2, true]
|
|
341
|
+
K.class_variable_get(:@@timeouts)[:expired] = Dnsruby::TransportClock.now - 1
|
|
342
|
+
SELECTOR.instance_variable_get(:@packet_deadlines)[:expired] = K.class_variable_get(:@@timeouts)[:expired]
|
|
343
|
+
SELECTOR.process_timeouts
|
|
344
|
+
raise 'timeout missing' unless expired.client_queue.size == 1 &&
|
|
345
|
+
expired.client_queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
346
|
+
raise 'timeout killed unrelated peer' if socket.closed?
|
|
347
|
+
raise 'timed-out stream reusable' if SELECTOR.reusable_socket?(socket)
|
|
348
|
+
raise 'peer ownership lost' unless SELECTOR.query_settings_for_id(:peer).equal?(peer)
|
|
349
|
+
# Once the late frame has been consumed, a valid peer response can finish.
|
|
350
|
+
K.class_variable_get(:@@tcp_buffers).delete(socket)
|
|
351
|
+
response = peer.query.clone
|
|
352
|
+
response.header.qr = true
|
|
353
|
+
SELECTOR.process_message(response, response.encode, socket)
|
|
354
|
+
SELECTOR.send_queued_validation_responses
|
|
355
|
+
raise 'peer response lost' unless peer.client_queue.size == 2 &&
|
|
356
|
+
2.times.all? { peer.client_queue.pop.last.nil? }
|
|
357
|
+
raise 'drained stream stayed open' unless socket.closed?
|
|
358
|
+
assert_clean(socket)
|
|
359
|
+
RUBY
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def test_retirement_between_socket_selection_and_registration_retries_admission
|
|
363
|
+
isolated(<<~'RUBY')
|
|
364
|
+
[true, false].product([0, 60_001, [60_001, 60_002]]).each do |keep_peer, ports|
|
|
365
|
+
sockets, connection_ports = [], []
|
|
366
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
|
|
367
|
+
connection_ports << args[3]
|
|
368
|
+
FrameSocket.new.tap do |socket|
|
|
369
|
+
socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
|
|
370
|
+
sockets << socket
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
|
|
374
|
+
tcp_pipelining: true, dnssec: false, packet_timeout: 5, src_port: ports)
|
|
375
|
+
requests = 3.times.map { |i| query("admission#{i}.example.").tap { |q| q.header.id = i } }
|
|
376
|
+
queues = 3.times.map { Queue.new }
|
|
377
|
+
writer.send_async(requests[0], queues[0], 0)
|
|
378
|
+
writer.send_async(requests[1], queues[1], 1) if keep_peer
|
|
379
|
+
old = sockets.first
|
|
380
|
+
selected, release = Queue.new, Queue.new
|
|
381
|
+
choose = writer.method(:tcp_pipeline_socket)
|
|
382
|
+
extra_retirements = 2
|
|
383
|
+
writer.define_singleton_method(:tcp_pipeline_socket) do |port|
|
|
384
|
+
result = choose.call(port)
|
|
385
|
+
if result.first.equal?(old)
|
|
386
|
+
selected << true
|
|
387
|
+
release.pop
|
|
388
|
+
elsif extra_retirements > 0
|
|
389
|
+
# Repeated admission races must not consume the source-port pool.
|
|
390
|
+
extra_retirements -= 1
|
|
391
|
+
SELECTOR.retire_socket(result.first)
|
|
392
|
+
end
|
|
393
|
+
result
|
|
394
|
+
end
|
|
395
|
+
sending = Thread.new { writer.send_async(requests[2], queues[2], 2) }
|
|
396
|
+
selected.pop
|
|
397
|
+
pending = SELECTOR.query_settings_for_id(2)
|
|
398
|
+
original_deadline = pending.endtime
|
|
399
|
+
old_size = old.written.bytesize
|
|
400
|
+
K.class_variable_get(:@@tcp_buffers)[old] = [+'partial', 1000, false]
|
|
401
|
+
K.class_variable_get(:@@timeouts)[0] = Dnsruby::TransportClock.now - 1
|
|
402
|
+
SELECTOR.instance_variable_get(:@packet_deadlines)[0] = K.class_variable_get(:@@timeouts)[0]
|
|
403
|
+
SELECTOR.process_timeouts
|
|
404
|
+
raise 'incorrect old stream lifetime' unless old.closed? == !keep_peer
|
|
405
|
+
release << true
|
|
406
|
+
sending.value
|
|
407
|
+
current = SELECTOR.query_settings_for_id(2)
|
|
408
|
+
raise 'registration used retired or closed stream' unless sockets.size == 4 &&
|
|
409
|
+
current.socket.equal?(sockets.last) && old.written.bytesize == old_size
|
|
410
|
+
allowed_ports = ports == 0 ? (50_000..65_535) : Array(ports)
|
|
411
|
+
raise 'admission retry escaped the configured source ports' unless
|
|
412
|
+
connection_ports.all? { |port| allowed_ports.include?(port) }
|
|
413
|
+
raise 'admission retry replaced ownership or deadline' unless
|
|
414
|
+
current.send_token.equal?(pending.send_token) && current.endtime == original_deadline
|
|
415
|
+
raise 'admission retry delivered a premature error' unless queues[2].empty?
|
|
416
|
+
raise 'retirement lost existing peer' if keep_peer && !SELECTOR.query_settings_for_id(1)
|
|
417
|
+
[2, (1 if keep_peer)].compact.each do |id|
|
|
418
|
+
item = SELECTOR.query_settings_for_id(id)
|
|
419
|
+
response = requests[id].clone
|
|
420
|
+
response.header.qr = true
|
|
421
|
+
SELECTOR.process_message(response, response.encode, item.socket)
|
|
422
|
+
end
|
|
423
|
+
SELECTOR.send_queued_validation_responses
|
|
424
|
+
raise 'fresh query did not complete' unless queues[2].size == 2 &&
|
|
425
|
+
2.times.all? { queues[2].pop.last.nil? }
|
|
426
|
+
writer.close
|
|
427
|
+
sockets.each { |socket| raise 'socket retained' unless socket.closed?; assert_clean(socket) }
|
|
428
|
+
end
|
|
429
|
+
RUBY
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def test_timeout_without_partial_bytes_rotates_before_reusing_a_header_id
|
|
433
|
+
isolated(<<~'RUBY')
|
|
434
|
+
sockets = []
|
|
435
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
|
|
436
|
+
FrameSocket.new.tap do |socket|
|
|
437
|
+
socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
|
|
438
|
+
sockets << socket
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
|
|
442
|
+
tcp_pipelining: true, dnssec: false)
|
|
443
|
+
request = query
|
|
444
|
+
request.header.id = 123
|
|
445
|
+
writer.send_async(request, Queue.new, :old)
|
|
446
|
+
K.class_variable_get(:@@timeouts)[:old] = Dnsruby::TransportClock.now - 1
|
|
447
|
+
SELECTOR.instance_variable_get(:@packet_deadlines)[:old] = K.class_variable_get(:@@timeouts)[:old]
|
|
448
|
+
SELECTOR.process_timeouts
|
|
449
|
+
writer.send_async(request, Queue.new, :new)
|
|
450
|
+
raise 'late reply could reach reused ID' unless sockets.size == 2 && sockets.first.closed?
|
|
451
|
+
SELECTOR.remove_id(:new)
|
|
452
|
+
writer.close
|
|
453
|
+
sockets.each { |socket| assert_clean(socket) }
|
|
454
|
+
RUBY
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def test_removed_sender_retries_use_connections_that_close_after_the_answer
|
|
458
|
+
isolated(<<~'RUBY')
|
|
459
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
460
|
+
sockets = []
|
|
461
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
|
|
462
|
+
FrameSocket.new.tap do |socket|
|
|
463
|
+
socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
|
|
464
|
+
sockets << socket
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
|
|
468
|
+
tcp_pipelining: true, dnssec: false, retry_times: 2, retry_delay: 10)
|
|
469
|
+
queue, request = Queue.new, query
|
|
470
|
+
resolver.send_async(request, queue, :retry)
|
|
471
|
+
old_writer = resolver.single_resolvers.first
|
|
472
|
+
resolver.nameserver = '127.0.0.2'
|
|
473
|
+
original = K.class_variable_get(:@@query_hash).values.first
|
|
474
|
+
K.class_variable_get(:@@timeouts)[original.client_query_id] = Dnsruby::TransportClock.now - 1
|
|
475
|
+
SELECTOR.instance_variable_get(:@packet_deadlines)[original.client_query_id] = K.class_variable_get(:@@timeouts)[original.client_query_id]
|
|
476
|
+
SELECTOR.process_timeouts
|
|
477
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
478
|
+
retry_plan = engine.instance_variable_get(:@timeouts)[:retry][1]
|
|
479
|
+
entry = retry_plan.values.first
|
|
480
|
+
raise 'original retry plan replaced' unless entry.first.equal?(old_writer)
|
|
481
|
+
retry_plan.clear
|
|
482
|
+
retry_plan[Dnsruby::TransportClock.now - 1] = entry
|
|
483
|
+
engine.send(:update_tick_deadline, :retry)
|
|
484
|
+
engine.tick
|
|
485
|
+
raise 'retry did not make a new connection' unless sockets.size == 2
|
|
486
|
+
response = request.clone
|
|
487
|
+
response.header.qr = true
|
|
488
|
+
SELECTOR.process_message(response, response.encode, sockets.last)
|
|
489
|
+
SELECTOR.send_queued_validation_responses
|
|
490
|
+
raise 'retry response lost' unless queue.size == 1 && queue.pop.last.nil?
|
|
491
|
+
raise 'old retry left an idle socket' unless sockets.all?(&:closed?)
|
|
492
|
+
resolver.close
|
|
493
|
+
sockets.each { |socket| assert_clean(socket) }
|
|
494
|
+
RUBY
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def test_pipeline_capacity_changes_rotate_connections_and_preserve_every_peer
|
|
498
|
+
isolated(<<~'RUBY')
|
|
499
|
+
[[2, 3], [3, 2], [:infinite, 2], [2, :infinite], [3, 1]].each do |before, after|
|
|
500
|
+
sockets = []
|
|
501
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
|
|
502
|
+
FrameSocket.new.tap do |socket|
|
|
503
|
+
socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
|
|
504
|
+
sockets << socket
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
|
|
508
|
+
tcp_pipelining: true, tcp_pipelining_max_queries: before, dnssec: false)
|
|
509
|
+
requests = 3.times.map { |i| query("capacity#{i}.example.").tap { |q| q.header.id = i } }
|
|
510
|
+
queues = 3.times.map { Queue.new }
|
|
511
|
+
writer.send_async(requests[0], queues[0], 0)
|
|
512
|
+
original = sockets.first
|
|
513
|
+
writer.tcp_pipelining_max_queries = after
|
|
514
|
+
[1, 2].each { |i| writer.send_async(requests[i], queues[i], i) }
|
|
515
|
+
raise 'old connection reused after capacity change' unless sockets.size >= 2 &&
|
|
516
|
+
!SELECTOR.reusable_socket?(original) && !original.closed?
|
|
517
|
+
# Out-of-order responses also exercise draining the retired generation.
|
|
518
|
+
[2, 0, 1].each do |id|
|
|
519
|
+
current = SELECTOR.query_settings_for_id(id)
|
|
520
|
+
response = requests[id].clone
|
|
521
|
+
response.header.qr = true
|
|
522
|
+
SELECTOR.process_message(response, response.encode, current.socket)
|
|
523
|
+
end
|
|
524
|
+
SELECTOR.send_queued_validation_responses
|
|
525
|
+
raise 'capacity change lost a response' unless queues.all? do |queue|
|
|
526
|
+
queue.size == 2 && 2.times.all? { queue.pop.last.nil? }
|
|
527
|
+
end
|
|
528
|
+
writer.close
|
|
529
|
+
sockets.each { |socket| raise 'socket open' unless socket.closed?; assert_clean(socket) }
|
|
530
|
+
end
|
|
531
|
+
RUBY
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def test_tcp_read_waits_only_for_transient_errors_and_retires_fatal_errors
|
|
535
|
+
isolated(<<~'RUBY')
|
|
536
|
+
[IO::EAGAINWaitReadable, Errno::EINTR, Errno::ECONNRESET, IOError].each do |kind|
|
|
537
|
+
socket = FrameSocket.new
|
|
538
|
+
requests = 2.times.map { |i| settings(socket, i) }
|
|
539
|
+
requests.each { |item| SELECTOR.add_to_select(item) }
|
|
540
|
+
socket.define_singleton_method(:recv_nonblock) { |_| raise kind, 'injected read failure' }
|
|
541
|
+
raise 'unexpected bytes' unless SELECTOR.tcp_read(socket) == false
|
|
542
|
+
if [IO::EAGAINWaitReadable, Errno::EINTR].include?(kind)
|
|
543
|
+
raise 'transient read failed owners' if socket.closed? || requests.any? { |s| !s.client_queue.empty? }
|
|
544
|
+
requests.each { |item| SELECTOR.remove_id(item.client_query_id) }
|
|
545
|
+
else
|
|
546
|
+
raise 'fatal read left stream open' unless socket.closed?
|
|
547
|
+
requests.each do |item|
|
|
548
|
+
raise 'fatal read did not fail each owner once' unless item.client_queue.size == 1 &&
|
|
549
|
+
item.client_queue.pop.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
550
|
+
end
|
|
551
|
+
assert_clean(socket)
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
RUBY
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def test_random_source_port_collision_retries_before_registering_a_socket
|
|
558
|
+
isolated(<<~'RUBY')
|
|
559
|
+
[true, false].each do |pipeline|
|
|
560
|
+
calls = 0
|
|
561
|
+
socket = FrameSocket.new
|
|
562
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **kwargs|
|
|
563
|
+
calls += 1
|
|
564
|
+
raise Errno::EADDRINUSE if calls == 1
|
|
565
|
+
socket
|
|
566
|
+
end
|
|
567
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true,
|
|
568
|
+
tcp_pipelining: pipeline, dnssec: false)
|
|
569
|
+
writer.define_singleton_method(:get_next_src_port) { 40000 + calls }
|
|
570
|
+
queue, request = Queue.new, query
|
|
571
|
+
writer.send_async(request, queue, :collision)
|
|
572
|
+
raise 'source port collision did not retry' unless calls == 2
|
|
573
|
+
raise 'retry did not send the complete frame' unless
|
|
574
|
+
socket.written == [request.encode.bytesize].pack('n') + request.encode
|
|
575
|
+
drain_errors
|
|
576
|
+
raise 'source port collision delivered an error' unless queue.empty?
|
|
577
|
+
SELECTOR.remove_id(:collision)
|
|
578
|
+
writer.close
|
|
579
|
+
assert_clean(socket)
|
|
580
|
+
end
|
|
581
|
+
RUBY
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def test_random_udp_source_port_collision_retries_with_a_new_socket
|
|
585
|
+
isolated(<<~'RUBY')
|
|
586
|
+
sockets = []
|
|
587
|
+
UDPSocket.define_singleton_method(:new) do |*|
|
|
588
|
+
FrameSocket.new.tap do |socket|
|
|
589
|
+
first = sockets.empty?
|
|
590
|
+
socket.define_singleton_method(:bind) { |*| raise Errno::EADDRINUSE if first }
|
|
591
|
+
socket.define_singleton_method(:connect) { |*| }
|
|
592
|
+
sockets << socket
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
|
|
596
|
+
writer.define_singleton_method(:get_next_src_port) { 40000 + sockets.size }
|
|
597
|
+
queue, request = Queue.new, query
|
|
598
|
+
writer.send_async(request, queue, :collision)
|
|
599
|
+
raise 'failed UDP bind was not closed and retried' unless
|
|
600
|
+
sockets.size == 2 && sockets.first.closed? && sockets.first.written.empty?
|
|
601
|
+
raise 'UDP retry did not send the query' unless sockets.last.written == request.encode
|
|
602
|
+
drain_errors
|
|
603
|
+
raise 'UDP source port collision delivered an error' unless queue.empty?
|
|
604
|
+
SELECTOR.remove_id(:collision)
|
|
605
|
+
writer.close
|
|
606
|
+
sockets.each { |socket| assert_clean(socket) }
|
|
607
|
+
RUBY
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def test_connect_timeouts_preserve_scheduled_retries_for_both_tcp_modes
|
|
611
|
+
isolated(<<~'RUBY')
|
|
612
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
613
|
+
[true, false].each do |pipeline|
|
|
614
|
+
calls = 0
|
|
615
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| calls += 1; raise Errno::ETIMEDOUT }
|
|
616
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
|
|
617
|
+
tcp_pipelining: pipeline, dnssec: false, retry_times: 2, retry_delay: 10)
|
|
618
|
+
queue = Queue.new
|
|
619
|
+
resolver.send_async(query, queue, :connect)
|
|
620
|
+
drain_errors
|
|
621
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
622
|
+
plan = engine.instance_variable_get(:@timeouts)[:connect][1]
|
|
623
|
+
raise 'connect timeout discarded scheduled retry' if plan.empty?
|
|
624
|
+
entry = plan.values.first
|
|
625
|
+
plan.clear
|
|
626
|
+
plan[Dnsruby::TransportClock.now - 1] = entry
|
|
627
|
+
engine.send(:update_tick_deadline, :connect)
|
|
628
|
+
engine.tick
|
|
629
|
+
drain_errors
|
|
630
|
+
raise 'connect timeout did not retry' unless calls == 2
|
|
631
|
+
raise 'wrong final connect error' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
632
|
+
resolver.close
|
|
633
|
+
end
|
|
634
|
+
RUBY
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def test_worker_exceptions_reach_owner_without_reading_task_value
|
|
638
|
+
isolated(<<~'RUBY')
|
|
639
|
+
writer = sender(FrameSocket.new)
|
|
640
|
+
failure = RuntimeError.new('unexpected worker failure')
|
|
641
|
+
writer.define_singleton_method(:send_dnsruby_with_socket) { |*| raise failure }
|
|
642
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
643
|
+
queue = Queue.new
|
|
644
|
+
writer.send_dnsruby(query.encode, query, :worker, queue, true)
|
|
645
|
+
Timeout.timeout(1) do
|
|
646
|
+
Thread.pass while SELECTOR.query_settings_for_id(:worker)
|
|
647
|
+
end
|
|
648
|
+
drain_errors
|
|
649
|
+
raise 'worker error was swallowed' unless queue.size == 1 && queue.pop.last.equal?(failure)
|
|
650
|
+
raise 'worker error retained deadline' if K.class_variable_get(:@@timeouts).key?(:worker)
|
|
651
|
+
RUBY
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def test_worker_failure_after_connect_releases_an_unregistered_socket
|
|
655
|
+
isolated(<<~'RUBY')
|
|
656
|
+
socket = FrameSocket.new
|
|
657
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **kwargs| socket }
|
|
658
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true, dnssec: false)
|
|
659
|
+
failure = RuntimeError.new('unexpected registration failure')
|
|
660
|
+
construct = K::QuerySettings.method(:new)
|
|
661
|
+
K::QuerySettings.define_singleton_method(:new) do |*args|
|
|
662
|
+
raise failure if args[5] # A connected socket, after the initial reservation.
|
|
663
|
+
construct.call(*args)
|
|
664
|
+
end
|
|
665
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
666
|
+
queue = Queue.new
|
|
667
|
+
writer.send_dnsruby(query.encode, query, :worker, queue, true)
|
|
668
|
+
Timeout.timeout(1) do
|
|
669
|
+
Thread.pass until socket.closed?
|
|
670
|
+
end
|
|
671
|
+
drain_errors
|
|
672
|
+
raise 'worker error was swallowed' unless queue.size == 1 && queue.pop.last.equal?(failure)
|
|
673
|
+
raise 'worker retained ownership' if SELECTOR.query_settings_for_id(:worker)
|
|
674
|
+
assert_clean(socket)
|
|
675
|
+
RUBY
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def test_wall_clock_jumps_do_not_change_query_packet_retry_or_write_deadlines
|
|
679
|
+
isolated(<<~'RUBY')
|
|
680
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
681
|
+
clock = 1000.0
|
|
682
|
+
Dnsruby::TransportClock.define_singleton_method(:now) { clock }
|
|
683
|
+
original_time = Time.method(:now)
|
|
684
|
+
socket = FrameSocket.new
|
|
685
|
+
writer = sender(socket)
|
|
686
|
+
writer.use_tcp = true
|
|
687
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', use_tcp: true,
|
|
688
|
+
dnssec: false, packet_timeout: 5, query_timeout: 10, retry_times: 2, retry_delay: 3)
|
|
689
|
+
resolver.single_resolvers = [writer]
|
|
690
|
+
queue = Queue.new
|
|
691
|
+
resolver.send_async(query, queue, :clock)
|
|
692
|
+
# Initial retry keys precede tick only after monotonic time advances.
|
|
693
|
+
clock += 0.1
|
|
694
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
695
|
+
engine.tick
|
|
696
|
+
pending = K.class_variable_get(:@@query_hash).values.first
|
|
697
|
+
raise 'packet deadline used wall time' unless pending.endtime < 1010
|
|
698
|
+
engine_deadline, retries = engine.instance_variable_get(:@timeouts)[:clock]
|
|
699
|
+
raise 'query or retry deadline used wall time' unless engine_deadline == 1010 && retries.keys.all? { |t| t < 1010 }
|
|
700
|
+
[-86400, 86400].each do |jump|
|
|
701
|
+
Time.define_singleton_method(:now) { original_time.call + jump }
|
|
702
|
+
SELECTOR.process_timeouts
|
|
703
|
+
raise 'wall jump expired query' unless queue.empty?
|
|
704
|
+
extra = FrameSocket.new
|
|
705
|
+
writer.write_tcp_frame(extra, 'abc', clock + 1, K::SendToken.new)
|
|
706
|
+
raise 'wall jump prevented write' unless extra.written == "\0\3abc".b
|
|
707
|
+
end
|
|
708
|
+
clock = 1011
|
|
709
|
+
engine.tick
|
|
710
|
+
raise 'monotonic query deadline ignored' unless queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
711
|
+
resolver.close
|
|
712
|
+
RUBY
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def test_udp_close_and_timeout_between_ownership_check_and_write_cancel_the_datagram
|
|
716
|
+
isolated(<<~'RUBY')
|
|
717
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
718
|
+
lookup = SELECTOR.method(:query_settings_for_id)
|
|
719
|
+
[:close, :timeout].each do |cancellation|
|
|
720
|
+
socket = FrameSocket.new
|
|
721
|
+
connected, paused = false, false
|
|
722
|
+
socket.define_singleton_method(:bind) { |*| }
|
|
723
|
+
socket.define_singleton_method(:connect) { |*| connected = true }
|
|
724
|
+
UDPSocket.define_singleton_method(:new) { |*| socket }
|
|
725
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
|
|
726
|
+
udp_size: 512, packet_timeout: 1)
|
|
727
|
+
entered, release = Queue.new, Queue.new
|
|
728
|
+
SELECTOR.define_singleton_method(:query_settings_for_id) do |id|
|
|
729
|
+
current = lookup.call(id)
|
|
730
|
+
if connected && !paused
|
|
731
|
+
paused = true
|
|
732
|
+
entered << true
|
|
733
|
+
release.pop
|
|
734
|
+
end
|
|
735
|
+
current
|
|
736
|
+
end
|
|
737
|
+
queue = Queue.new
|
|
738
|
+
sending = Thread.new { resolver.send_async(query, queue, cancellation) }
|
|
739
|
+
entered.pop
|
|
740
|
+
if cancellation == :close
|
|
741
|
+
resolver.close
|
|
742
|
+
else
|
|
743
|
+
deadlines = K.class_variable_get(:@@timeouts)
|
|
744
|
+
deadlines.keys.each { |id| deadlines[id] = Dnsruby::TransportClock.now - 1 }
|
|
745
|
+
deadlines.each { |id, deadline| SELECTOR.instance_variable_get(:@packet_deadlines)[id] = deadline }
|
|
746
|
+
SELECTOR.process_timeouts
|
|
747
|
+
end
|
|
748
|
+
expected = cancellation == :close ? Dnsruby::OtherResolvError : Dnsruby::ResolvTimeout
|
|
749
|
+
raise 'missing cancellation completion' unless queue.size == 1 && queue.pop.last.is_a?(expected)
|
|
750
|
+
release << true
|
|
751
|
+
sending.value
|
|
752
|
+
drain_errors
|
|
753
|
+
raise 'cancelled UDP request sent bytes' unless socket.written.empty?
|
|
754
|
+
raise 'cancelled UDP socket leaked' unless socket.closed?
|
|
755
|
+
raise 'duplicate cancellation completion' unless queue.empty?
|
|
756
|
+
raise 'cancelled reservation retained' unless K.class_variable_get(:@@query_hash).empty?
|
|
757
|
+
raise 'cancelled queue retained' unless K.class_variable_get(:@@queries_by_queue).empty?
|
|
758
|
+
resolver.close
|
|
759
|
+
end
|
|
760
|
+
RUBY
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def test_congested_udp_write_leaves_selector_and_remains_cancellable
|
|
764
|
+
isolated(<<~'RUBY')
|
|
765
|
+
socket = FrameSocket.new
|
|
766
|
+
socket.define_singleton_method(:bind) { |*| }
|
|
767
|
+
socket.define_singleton_method(:connect) { |*| }
|
|
768
|
+
socket.define_singleton_method(:write_nonblock) { |*| raise IO::EAGAINWaitWritable }
|
|
769
|
+
UDPSocket.define_singleton_method(:new) { |*| socket }
|
|
770
|
+
entered, release = Queue.new, Queue.new
|
|
771
|
+
IO.define_singleton_method(:select) do |readers, writers, *|
|
|
772
|
+
raise 'UDP wait blocked selector' if SELECTOR.selector_thread?
|
|
773
|
+
raise 'wrong writable socket' unless readers.nil? && writers == [socket]
|
|
774
|
+
entered << true
|
|
775
|
+
release.pop
|
|
776
|
+
[[], [socket], []]
|
|
777
|
+
end
|
|
778
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
|
|
779
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
780
|
+
request, queue = query, Queue.new
|
|
781
|
+
task = writer.send_dnsruby(request.encode, request, :congested, queue, false)
|
|
782
|
+
entered.pop
|
|
783
|
+
SELECTOR.remove_id(:congested)
|
|
784
|
+
release << true
|
|
785
|
+
task.value
|
|
786
|
+
drain_errors
|
|
787
|
+
raise 'cancelled congested UDP request sent bytes' unless socket.written.empty?
|
|
788
|
+
raise 'cancelled congested UDP socket leaked' unless socket.closed?
|
|
789
|
+
raise 'cancelled congested UDP reported another error' unless queue.empty?
|
|
790
|
+
RUBY
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def test_udp_writable_wait_has_a_packet_deadline_and_never_splits_datagrams
|
|
794
|
+
isolated(<<~'RUBY')
|
|
795
|
+
socket = FrameSocket.new
|
|
796
|
+
socket.define_singleton_method(:bind) { |*| }
|
|
797
|
+
socket.define_singleton_method(:connect) { |*| }
|
|
798
|
+
socket.define_singleton_method(:write_nonblock) { |*| raise IO::EAGAINWaitWritable }
|
|
799
|
+
UDPSocket.define_singleton_method(:new) { |*| socket }
|
|
800
|
+
IO.define_singleton_method(:select) do |readers, writers, _, timeout|
|
|
801
|
+
raise 'missing packet deadline' unless timeout > 0 && timeout <= 1
|
|
802
|
+
nil
|
|
803
|
+
end
|
|
804
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', packet_timeout: 1, dnssec: false)
|
|
805
|
+
queue, request = Queue.new, query
|
|
806
|
+
writer.send_async(request, queue, :blocked)
|
|
807
|
+
drain_errors
|
|
808
|
+
raise 'UDP writable wait did not time out exactly once' unless
|
|
809
|
+
queue.size == 1 && queue.pop.last.is_a?(Dnsruby::ResolvTimeout)
|
|
810
|
+
raise 'UDP timeout retained socket or ownership' unless
|
|
811
|
+
socket.closed? && K.class_variable_get(:@@query_hash).empty?
|
|
812
|
+
|
|
813
|
+
partial = FrameSocket.new
|
|
814
|
+
partial.limit = 1
|
|
815
|
+
begin
|
|
816
|
+
writer.write_udp_packet(partial, 'abc', Dnsruby::TransportClock.now + 1, K::SendToken.new, true)
|
|
817
|
+
raise 'partial datagram accepted'
|
|
818
|
+
rescue IOError => error
|
|
819
|
+
raise 'wrong partial datagram error' unless error.message == 'Incomplete UDP datagram write'
|
|
820
|
+
end
|
|
821
|
+
raise 'UDP datagram was split across writes' unless partial.written == 'a'
|
|
822
|
+
RUBY
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def test_queue_attempt_index_tracks_replacement_registration_and_cancellation
|
|
826
|
+
isolated(<<~'RUBY')
|
|
827
|
+
first, second = Queue.new, Queue.new
|
|
828
|
+
owner, replacement_owner = Object.new, Object.new
|
|
829
|
+
SELECTOR.add_observer(first, owner)
|
|
830
|
+
SELECTOR.add_observer(first, owner)
|
|
831
|
+
SELECTOR.add_observer(second, owner)
|
|
832
|
+
counts = K.class_variable_get(:@@observer_queue_counts)
|
|
833
|
+
raise 'duplicate observer registration counted twice' unless counts.length == 1 && counts[owner] == 2
|
|
834
|
+
SELECTOR.add_observer(first, replacement_owner)
|
|
835
|
+
raise 'observer replacement lost its counts' unless counts.length == 2 && counts[owner] == 1 && counts[replacement_owner] == 1
|
|
836
|
+
|
|
837
|
+
pending = settings(FrameSocket.new, :moved)
|
|
838
|
+
pending.socket, pending.client_queue = nil, first
|
|
839
|
+
SELECTOR.reserve_send(pending)
|
|
840
|
+
replacement = settings(FrameSocket.new, :moved)
|
|
841
|
+
replacement.socket, replacement.client_queue = nil, second
|
|
842
|
+
SELECTOR.reserve_send(replacement, pending)
|
|
843
|
+
index = K.class_variable_get(:@@queries_by_queue)
|
|
844
|
+
raise 'replacement retained old queue membership' if index.key?(first)
|
|
845
|
+
raise 'replacement missing new queue membership' unless index[second] == Set[:moved]
|
|
846
|
+
raise 'stale claim consumed replacement' if SELECTOR.claim_query(pending)
|
|
847
|
+
|
|
848
|
+
live = settings(FrameSocket.new, :live)
|
|
849
|
+
live.client_queue = first
|
|
850
|
+
SELECTOR.add_to_select(live)
|
|
851
|
+
queued = settings(FrameSocket.new, :queued)
|
|
852
|
+
queued.socket, queued.client_queue = nil, first
|
|
853
|
+
SELECTOR.reserve_send(queued)
|
|
854
|
+
SELECTOR.cancel_queries_for_queue(first)
|
|
855
|
+
raise 'queue cancellation retained attempts' if index.key?(first)
|
|
856
|
+
raise 'queue cancellation removed another query' unless SELECTOR.query_settings_for_id(:moved).equal?(replacement)
|
|
857
|
+
raise 'queue cancellation removed the observer' unless
|
|
858
|
+
K.class_variable_get(:@@observers)[first].equal?(replacement_owner) && counts[replacement_owner] == 1
|
|
859
|
+
raise 'queue cancellation delivered an error' unless first.empty?
|
|
860
|
+
raise 'queue cancellation left a reusable stream' unless live.socket.closed?
|
|
861
|
+
|
|
862
|
+
closed = settings(FrameSocket.new, :moved)
|
|
863
|
+
closed.client_queue, closed.send_token = second, replacement.send_token
|
|
864
|
+
closed.is_persistent_socket = false
|
|
865
|
+
closed.socket.close
|
|
866
|
+
raise 'closed socket registered' if SELECTOR.register_query_if_current(closed, replacement)
|
|
867
|
+
raise 'failed registration retained queue membership' unless index.empty?
|
|
868
|
+
SELECTOR.remove_observer(first, replacement_owner)
|
|
869
|
+
SELECTOR.remove_observer(second, owner)
|
|
870
|
+
raise 'completed observers retained counts or ticks' unless counts.empty? && K.class_variable_get(:@@tick_observers).empty?
|
|
871
|
+
RUBY
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def test_bulk_cancellation_does_not_scan_global_queries_or_observers
|
|
875
|
+
isolated(<<~'RUBY')
|
|
876
|
+
owner = Object.new
|
|
877
|
+
queues = 2000.times.map { Queue.new }
|
|
878
|
+
queues.each_with_index do |queue, id|
|
|
879
|
+
SELECTOR.add_observer(queue, owner)
|
|
880
|
+
pending = K::QuerySettings.new(nil, nil, false, queue, id, nil, nil, nil,
|
|
881
|
+
Dnsruby::TransportClock.now + 60, 512, nil)
|
|
882
|
+
SELECTOR.reserve_send(pending)
|
|
883
|
+
end
|
|
884
|
+
queries = K.class_variable_get(:@@query_hash)
|
|
885
|
+
observers = K.class_variable_get(:@@observers)
|
|
886
|
+
[queries, observers].each do |table|
|
|
887
|
+
[:values, :each, :each_value].each do |method|
|
|
888
|
+
table.define_singleton_method(method) { |*| raise 'global ownership scan during cancellation' }
|
|
889
|
+
end
|
|
890
|
+
end
|
|
891
|
+
queues.each { |queue| SELECTOR.remove_observer(queue, owner) }
|
|
892
|
+
raise 'bulk cancellation retained state' unless queries.empty? && observers.empty? &&
|
|
893
|
+
K.class_variable_get(:@@queries_by_queue).empty? && K.class_variable_get(:@@observer_queue_counts).empty?
|
|
894
|
+
RUBY
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
private
|
|
898
|
+
|
|
899
|
+
def isolated(body)
|
|
900
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
901
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
902
|
+
File.expand_path('../lib', __dir__), '-e', "#{TestPipelineSend::FIXTURE}\nTimeout.timeout(10) do\n#{body}\nend")
|
|
903
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
904
|
+
assert_empty(stderr)
|
|
905
|
+
end
|
|
906
|
+
end
|