dnsruby 1.73.1 → 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 +314 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/dnsruby.gemspec +6 -4
- data/lib/dnsruby/cache.rb +279 -49
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +77 -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 +664 -207
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +798 -341
- data/lib/dnsruby/resolver.rb +472 -176
- 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 +21 -2
- 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/OPT.rb +2 -3
- 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 +986 -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 +53 -2
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +34 -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 +127 -14
- data/.coveralls.yml +0 -2
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
require 'timeout'
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'rbconfig'
|
|
7
|
+
|
|
8
|
+
class TestMalformedTcp < Minitest::Test
|
|
9
|
+
# Keep selector fixtures off IO.select: a live resolver in another test may
|
|
10
|
+
# share these tables. Only the dedicated allocated receiver uses the fakes.
|
|
11
|
+
class SocketBuffer
|
|
12
|
+
attr_reader :closed
|
|
13
|
+
alias closed? closed
|
|
14
|
+
attr_accessor :udp_response
|
|
15
|
+
|
|
16
|
+
def initialize(bytes = ''.b)
|
|
17
|
+
@bytes = bytes.dup
|
|
18
|
+
@closed = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def recv_nonblock(length)
|
|
22
|
+
@bytes.slice!(0, [length, 1].min) # Fragment prefix and body alike.
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def close
|
|
26
|
+
@closed = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def recvfrom_nonblock(_length)
|
|
30
|
+
@udp_response
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def setup
|
|
35
|
+
@selector_class = Dnsruby::SelectThread
|
|
36
|
+
@temporary_variables = []
|
|
37
|
+
{ :@@mutex => Mutex.new, :@@query_hash => {}, :@@queries_by_queue => {}.compare_by_identity, :@@socket_hash => {},
|
|
38
|
+
:@@timeouts => {}, :@@sockets => Set.new, :@@tcp_buffers => {},
|
|
39
|
+
:@@socket_owners => {}, :@@retiring_sockets => Set.new,
|
|
40
|
+
:@@socket_is_persistent => {}, :@@socket_remaining_queries => {}, :@@queued_exceptions => [] }.each do |name, value|
|
|
41
|
+
next if @selector_class.class_variable_defined?(name)
|
|
42
|
+
@selector_class.class_variable_set(name, value)
|
|
43
|
+
@temporary_variables << name
|
|
44
|
+
end
|
|
45
|
+
@selector = @selector_class.send(:allocate)
|
|
46
|
+
@selector.define_singleton_method(:tcp?) { |_| true }
|
|
47
|
+
@selector.define_singleton_method(:notify_queue_observers) { |*| }
|
|
48
|
+
@registered = []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def teardown
|
|
52
|
+
table(:mutex).synchronize do
|
|
53
|
+
@registered.each do |settings|
|
|
54
|
+
table(:query_hash).delete(settings.client_query_id)
|
|
55
|
+
table(:queries_by_queue).delete(settings.client_queue)
|
|
56
|
+
table(:timeouts).delete(settings.client_query_id)
|
|
57
|
+
[:socket_hash, :tcp_buffers, :socket_is_persistent, :socket_remaining_queries, :socket_owners].each do |name|
|
|
58
|
+
table(name).delete(settings.socket)
|
|
59
|
+
end
|
|
60
|
+
table(:retiring_sockets).delete(settings.socket)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
@temporary_variables.each { |name| @selector_class.remove_class_variable(name) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_short_matching_frames_report_only_the_matching_pipeline_query
|
|
67
|
+
[2, 4, 11].each do |length|
|
|
68
|
+
socket = SocketBuffer.new
|
|
69
|
+
first = register(socket, 12_345)
|
|
70
|
+
second = register(socket, 12_346)
|
|
71
|
+
packet = [12_346, 0x8000, 0, 0, 0, 0].pack('n6')[0, length]
|
|
72
|
+
@selector.stub(:tcp_read, packet) { assert_nil(@selector.get_incoming_data(socket, 0)) }
|
|
73
|
+
event = second.client_queue.pop(true)
|
|
74
|
+
assert_equal(second.client_query_id, event[0])
|
|
75
|
+
assert_kind_of(Dnsruby::DecodeError, event.last)
|
|
76
|
+
assert(first.client_queue.empty?)
|
|
77
|
+
assert_same(first, table(:query_hash)[first.client_query_id])
|
|
78
|
+
refute(socket.closed)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_unknown_and_unidentifiable_pipeline_frames_leave_queries_active
|
|
83
|
+
socket = SocketBuffer.new
|
|
84
|
+
first = register(socket, 12_350)
|
|
85
|
+
second = register(socket, 12_351)
|
|
86
|
+
packets = ['', 'x', [12_352].pack('n'),
|
|
87
|
+
[12_352, 0x8000, 1, 0, 0, 0].pack('n6'),
|
|
88
|
+
[12_352, 0x8000, 0, 0, 0, 0].pack('n6')]
|
|
89
|
+
packets.each do |packet|
|
|
90
|
+
@selector.stub(:tcp_read, packet) { assert_nil(@selector.get_incoming_data(socket, 0)) }
|
|
91
|
+
[first, second].each do |settings|
|
|
92
|
+
assert(settings.client_queue.empty?)
|
|
93
|
+
assert_same(settings, table(:query_hash)[settings.client_query_id])
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
refute(socket.closed)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_unidentifiable_frame_on_single_query_reports_decode_error
|
|
100
|
+
['', 'x'].each do |packet|
|
|
101
|
+
settings = register(SocketBuffer.new, 12_355)
|
|
102
|
+
@selector.stub(:tcp_read, packet) { assert_nil(@selector.get_incoming_data(settings.socket, 0)) }
|
|
103
|
+
assert_kind_of(Dnsruby::DecodeError, settings.client_queue.pop(true).last)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_duplicate_ids_on_one_pipeline_socket_are_ambiguous
|
|
108
|
+
socket = SocketBuffer.new
|
|
109
|
+
first = register(socket, 12_356)
|
|
110
|
+
second = register(socket, 12_356)
|
|
111
|
+
[2, 12].each do |length|
|
|
112
|
+
packet = [12_356, 0x8000, 1, 0, 0, 0].pack('n6')[0, length]
|
|
113
|
+
@selector.stub(:tcp_read, packet) { assert_nil(@selector.get_incoming_data(socket, 0)) }
|
|
114
|
+
[first, second].each do |settings|
|
|
115
|
+
assert(settings.client_queue.empty?)
|
|
116
|
+
assert_same(settings, table(:query_hash)[settings.client_query_id])
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_valid_in_order_replies_with_duplicate_ids_keep_existing_behavior
|
|
122
|
+
socket = SocketBuffer.new
|
|
123
|
+
first = register(socket, 12_357)
|
|
124
|
+
second = register(socket, 12_357)
|
|
125
|
+
second.query = Dnsruby::Message.new('second.example.')
|
|
126
|
+
second.query.header.id = 12_357
|
|
127
|
+
delivered = []
|
|
128
|
+
@selector.stub(:push_to_client, ->(*args) { delivered << args }) do
|
|
129
|
+
[first, second].each do |settings|
|
|
130
|
+
settings.single_resolver = Dnsruby::PacketSender.allocate
|
|
131
|
+
response = settings.query.clone
|
|
132
|
+
response.header.qr = true
|
|
133
|
+
@selector.stub(:tcp_read, response.encode) do
|
|
134
|
+
decoded, bytes = @selector.get_incoming_data(socket, 0)
|
|
135
|
+
@selector.send_response_to_client(decoded, bytes, socket)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
assert_equal([first.client_query_id, second.client_query_id], delivered.map(&:first))
|
|
140
|
+
assert(delivered.all? { |args| args[3].nil? })
|
|
141
|
+
assert_equal([], table(:socket_hash)[socket])
|
|
142
|
+
refute(socket.closed)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_reverse_order_duplicate_ids_match_question_before_tsig_checks
|
|
146
|
+
socket = SocketBuffer.new
|
|
147
|
+
first = register(socket, 12_358)
|
|
148
|
+
second = register(socket, 12_358)
|
|
149
|
+
second.query = Dnsruby::Message.new('second.example.')
|
|
150
|
+
second.query.header.id = 12_358
|
|
151
|
+
[first, second].each do |settings|
|
|
152
|
+
settings.single_resolver = Dnsruby::PacketSender.allocate
|
|
153
|
+
settings.single_resolver.define_singleton_method(:check_tsig) do |request, response, _|
|
|
154
|
+
raise 'TSIG checked against the wrong query' unless request.question.first.qname == response.question.first.qname
|
|
155
|
+
:okay
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
delivered = []
|
|
159
|
+
@selector.stub(:push_to_client, ->(*args) { delivered << args }) do
|
|
160
|
+
[second, first].each do |settings|
|
|
161
|
+
response = settings.query.clone
|
|
162
|
+
response.header.qr = true
|
|
163
|
+
@selector.send_response_to_client(response, response.encode, socket)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
assert_equal([second.client_query_id, first.client_query_id], delivered.map(&:first))
|
|
167
|
+
assert(delivered.all? { |args| args[3].nil? })
|
|
168
|
+
assert_equal([], table(:socket_hash)[socket])
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_matching_ids_on_other_sockets_do_not_supply_tcp_peer_metadata
|
|
172
|
+
first = register(SocketBuffer.new, 12_360, '192.0.2.1', 53)
|
|
173
|
+
second = register(SocketBuffer.new, 12_360, '192.0.2.2', 54)
|
|
174
|
+
response = second.query.clone
|
|
175
|
+
response.header.qr = true
|
|
176
|
+
wire = response.encode
|
|
177
|
+
@selector.stub(:tcp_read, wire) do
|
|
178
|
+
decoded, bytes = @selector.get_incoming_data(second.socket, 0)
|
|
179
|
+
assert_equal('192.0.2.2', decoded.answerip)
|
|
180
|
+
assert_equal(wire, bytes)
|
|
181
|
+
end
|
|
182
|
+
# The same ID on another socket does not make an unsolicited frame ours.
|
|
183
|
+
second.query.header.id = 12_361
|
|
184
|
+
@selector.stub(:tcp_read, wire) { assert_nil(@selector.get_incoming_data(second.socket, 0)) }
|
|
185
|
+
assert(first.client_queue.empty?)
|
|
186
|
+
assert(second.client_queue.empty?)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def test_malformed_udp_requires_matching_id_address_and_port
|
|
190
|
+
socket = SocketBuffer.new
|
|
191
|
+
settings = register(socket, 12_370, '192.0.2.1', 53)
|
|
192
|
+
[
|
|
193
|
+
[12_371, '192.0.2.1', 53],
|
|
194
|
+
[12_370, '192.0.2.2', 53],
|
|
195
|
+
[12_370, '192.0.2.1', 54],
|
|
196
|
+
].each do |id, source, port|
|
|
197
|
+
socket.udp_response = [[id, 0x8000, 1, 0, 0, 0].pack('n6'), ['AF_INET', port, source, source]]
|
|
198
|
+
@selector.stub(:tcp?, false) { assert_nil(@selector.get_incoming_data(socket, 512)) }
|
|
199
|
+
assert(settings.client_queue.empty?)
|
|
200
|
+
assert_same(settings, table(:query_hash)[settings.client_query_id])
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def test_malformed_truncated_tcp_response_is_terminal_without_retrying
|
|
205
|
+
settings = register(SocketBuffer.new, 12_372)
|
|
206
|
+
sender = Object.new
|
|
207
|
+
sender.define_singleton_method(:send_async) { |*| raise 'Unexpected TCP retry' }
|
|
208
|
+
settings.single_resolver = sender
|
|
209
|
+
packet = [settings.query.header.id, 0x8200, 1, 0, 0, 0].pack('n6')
|
|
210
|
+
@selector.stub(:tcp_read, packet) do
|
|
211
|
+
assert_nil(@selector.get_incoming_data(settings.socket, 0))
|
|
212
|
+
# A second buffered frame after terminal delivery cannot start a retry.
|
|
213
|
+
assert_nil(@selector.get_incoming_data(settings.socket, 0))
|
|
214
|
+
end
|
|
215
|
+
assert_kind_of(Dnsruby::DecodeError, settings.client_queue.pop(true).last)
|
|
216
|
+
assert(settings.client_queue.empty?)
|
|
217
|
+
refute(table(:query_hash).key?(settings.client_query_id))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_malformed_truncated_udp_respects_transport_options
|
|
221
|
+
[[true, false], [false, true], [true, true]].each do |no_tcp, ignore_truncation|
|
|
222
|
+
socket = SocketBuffer.new
|
|
223
|
+
settings = register(socket, 12_373)
|
|
224
|
+
settings.ignore_truncation = ignore_truncation
|
|
225
|
+
sender = Object.new
|
|
226
|
+
sender.define_singleton_method(:no_tcp) { no_tcp }
|
|
227
|
+
sender.define_singleton_method(:send_async) { |*| raise 'Unexpected UDP fallback' }
|
|
228
|
+
settings.single_resolver = sender
|
|
229
|
+
packet = settings.query.encode
|
|
230
|
+
packet[2, 2] = [0x8200].pack('n')
|
|
231
|
+
packet[6, 2] = [1].pack('n') # A complete matching question, then a missing answer.
|
|
232
|
+
socket.udp_response = [packet, ['AF_INET', 53, '127.0.0.1', '127.0.0.1']]
|
|
233
|
+
@selector.stub(:tcp?, false) { assert_nil(@selector.get_incoming_data(socket, 512)) }
|
|
234
|
+
assert_kind_of(Dnsruby::DecodeError, settings.client_queue.pop(true).last)
|
|
235
|
+
refute(table(:query_hash).key?(settings.client_query_id))
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def test_fragmented_zero_one_and_two_byte_frames_do_not_consume_next_frame
|
|
240
|
+
[0, 1, 2].each do |length|
|
|
241
|
+
short = 'x' * length
|
|
242
|
+
valid = Dnsruby::Message.new('following.example.').encode
|
|
243
|
+
socket = SocketBuffer.new([short.bytesize].pack('n') + short + [valid.bytesize].pack('n') + valid)
|
|
244
|
+
# Register only for fixture cleanup; tcp_read itself does not decode.
|
|
245
|
+
register(socket, 12_380 + length)
|
|
246
|
+
frames = []
|
|
247
|
+
(short.bytesize + valid.bytesize + 8).times do
|
|
248
|
+
frame = @selector.tcp_read(socket)
|
|
249
|
+
frames << frame if frame
|
|
250
|
+
break if frames.length == 2
|
|
251
|
+
end
|
|
252
|
+
assert_equal([short, valid], frames)
|
|
253
|
+
refute(socket.closed)
|
|
254
|
+
refute(table(:tcp_buffers).key?(socket))
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def test_error_for_removed_query_is_harmless
|
|
259
|
+
socket = SocketBuffer.new
|
|
260
|
+
assert_nil(@selector.send_exception_to_client(Dnsruby::DecodeError.new, socket, Object.new))
|
|
261
|
+
@selector.stub(:tcp_read, Dnsruby::Message.new.encode) do
|
|
262
|
+
assert_nil(@selector.get_incoming_data(socket, 0))
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def test_eof_after_fragment_fails_all_pending_queries_and_clears_framing
|
|
267
|
+
socket = SocketBuffer.new("\x00".b)
|
|
268
|
+
first = register(socket, 12_390)
|
|
269
|
+
second = register(socket, 12_391)
|
|
270
|
+
assert_equal(false, @selector.tcp_read(socket))
|
|
271
|
+
assert_equal(false, @selector.tcp_read(socket))
|
|
272
|
+
[first, second].each do |settings|
|
|
273
|
+
assert_kind_of(Dnsruby::SocketEofResolvError, settings.client_queue.pop(true).last)
|
|
274
|
+
refute(table(:query_hash).key?(settings.client_query_id))
|
|
275
|
+
end
|
|
276
|
+
assert(socket.closed)
|
|
277
|
+
[:socket_hash, :tcp_buffers, :socket_is_persistent, :socket_remaining_queries].each do |name|
|
|
278
|
+
refute(table(name).key?(socket))
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def test_idle_selector_drains_deliveries_registered_at_shutdown_boundary
|
|
283
|
+
# The singleton tables and IO.select stub must be isolated from any real
|
|
284
|
+
# resolver running elsewhere in the suite. No network sockets are needed.
|
|
285
|
+
script = <<~'RUBY'
|
|
286
|
+
require 'dnsruby'
|
|
287
|
+
require 'timeout'
|
|
288
|
+
klass = Dnsruby::SelectThread
|
|
289
|
+
[:closed_query, :response, :validation].each do |kind|
|
|
290
|
+
selector = klass.send(:allocate)
|
|
291
|
+
wakeup = Object.new
|
|
292
|
+
wakeup.define_singleton_method(:closed?) { false }
|
|
293
|
+
socket = Object.new
|
|
294
|
+
socket.define_singleton_method(:closed?) { true }
|
|
295
|
+
query = Dnsruby::Message.new('late.example.')
|
|
296
|
+
query.do_validation = query.do_caching = false
|
|
297
|
+
response = query.clone
|
|
298
|
+
response.header.qr = true
|
|
299
|
+
queue = Queue.new
|
|
300
|
+
settings = Dnsruby::SelectThread::QuerySettings.new
|
|
301
|
+
settings.query, settings.client_query_id = query, :late
|
|
302
|
+
settings.client_queue, settings.socket = queue, socket
|
|
303
|
+
settings.endtime = Dnsruby::TransportClock.now + 60
|
|
304
|
+
{
|
|
305
|
+
mutex: Mutex.new, sockets: Set.new([wakeup]), timeouts: {}, query_hash: {}, queries_by_queue: {}.compare_by_identity,
|
|
306
|
+
socket_hash: {}, socket_is_persistent: {}, socket_remaining_queries: {},
|
|
307
|
+
tcp_buffers: {}, select_thread: Thread.current, observers: {}, observer_queue_counts: {}.compare_by_identity,
|
|
308
|
+
queued_exceptions: [], queued_responses: [], queued_validation_responses: [],
|
|
309
|
+
tick_observers: [], wakeup_sockets: [wakeup, wakeup]
|
|
310
|
+
}.each { |name, value| klass.class_variable_set("@@#{name}", value) }
|
|
311
|
+
selector.define_singleton_method(:wake_up_select_thread) {}
|
|
312
|
+
selector.define_singleton_method(:send_tick_to_observers) {}
|
|
313
|
+
selector.define_singleton_method(:notify_queue_observers) { |*| }
|
|
314
|
+
iterations = 0
|
|
315
|
+
selector.define_singleton_method(:process_timeouts) do
|
|
316
|
+
# IO.select has returned on the last otherwise-idle iteration. A
|
|
317
|
+
# producer gets the GVL here, just before the shutdown mutex is taken.
|
|
318
|
+
if iterations == 11
|
|
319
|
+
case kind
|
|
320
|
+
when :closed_query then add_to_select(settings)
|
|
321
|
+
when :response then push_response_to_select(:late, queue, response, query, nil)
|
|
322
|
+
when :validation then push_validation_response_to_select(:late, queue, response, nil, query, nil)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
original_select = IO.method(:select)
|
|
327
|
+
IO.define_singleton_method(:select) { |*| iterations += 1; nil }
|
|
328
|
+
begin
|
|
329
|
+
Timeout.timeout(2) { selector.do_select }
|
|
330
|
+
ensure
|
|
331
|
+
IO.define_singleton_method(:select, original_select)
|
|
332
|
+
end
|
|
333
|
+
expected = kind == :response ? 2 : 1
|
|
334
|
+
raise "#{kind}: stranded delivery" unless queue.length == expected
|
|
335
|
+
if kind == :closed_query
|
|
336
|
+
raise 'wrong error' unless queue.pop.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
337
|
+
end
|
|
338
|
+
[:queued_exceptions, :queued_responses, :queued_validation_responses].each do |name|
|
|
339
|
+
raise "#{kind}: pending #{name}" unless klass.class_variable_get("@@#{name}").empty?
|
|
340
|
+
end
|
|
341
|
+
# Publish the stop decision under the mutex, before the Ruby thread exits.
|
|
342
|
+
raise 'stopping selector still advertised as running' if selector.select_thread_alive?
|
|
343
|
+
end
|
|
344
|
+
RUBY
|
|
345
|
+
assert_selector_subprocess(script)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def test_simultaneous_delivery_producers_start_only_one_selector
|
|
349
|
+
assert_selector_subprocess(<<~'RUBY')
|
|
350
|
+
klass = Dnsruby::SelectThread
|
|
351
|
+
selector = klass.send(:allocate)
|
|
352
|
+
original_new = Thread.method(:new)
|
|
353
|
+
dead_worker = original_new.call {}.tap(&:join)
|
|
354
|
+
{
|
|
355
|
+
mutex: Mutex.new, select_thread: dead_worker, queued_exceptions: [],
|
|
356
|
+
queued_responses: [], queued_validation_responses: []
|
|
357
|
+
}.each { |name, value| klass.class_variable_set("@@#{name}", value) }
|
|
358
|
+
selector.define_singleton_method(:wake_up_select_thread) {}
|
|
359
|
+
factory_entered, release_factory = Queue.new, Queue.new
|
|
360
|
+
workers, release_workers = Queue.new, Queue.new
|
|
361
|
+
selector.define_singleton_method(:do_select) do
|
|
362
|
+
workers << Thread.current
|
|
363
|
+
release_workers.pop
|
|
364
|
+
end
|
|
365
|
+
Thread.define_singleton_method(:new) do |*args, &block|
|
|
366
|
+
if Thread.current[:selector_producer]
|
|
367
|
+
factory_entered << true
|
|
368
|
+
release_factory.pop
|
|
369
|
+
end
|
|
370
|
+
original_new.call(*args, &block)
|
|
371
|
+
end
|
|
372
|
+
producers = []
|
|
373
|
+
started = Queue.new
|
|
374
|
+
begin
|
|
375
|
+
[:exception, :response, :validation].each do |kind|
|
|
376
|
+
producers << original_new.call do
|
|
377
|
+
Thread.current[:selector_producer] = true
|
|
378
|
+
query = Dnsruby::Message.new('parallel.example.')
|
|
379
|
+
started << kind
|
|
380
|
+
case kind
|
|
381
|
+
when :exception
|
|
382
|
+
selector.push_exception_to_select(kind, Queue.new, Dnsruby::OtherResolvError.new, nil)
|
|
383
|
+
when :response
|
|
384
|
+
selector.push_response_to_select(kind, Queue.new, query, query, nil)
|
|
385
|
+
when :validation
|
|
386
|
+
selector.push_validation_response_to_select(kind, Queue.new, query, nil, query, nil)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
# Hold the first producer inside Thread.new before starting its peers.
|
|
390
|
+
factory_entered.pop if kind == :exception
|
|
391
|
+
end
|
|
392
|
+
3.times { started.pop }
|
|
393
|
+
# Each producer is now blocked either at the selector mutex or at the
|
|
394
|
+
# intercepted thread factory. No timing delay determines the outcome.
|
|
395
|
+
Thread.pass until producers.all? { |thread| thread.status == 'sleep' }
|
|
396
|
+
factories = 1 + factory_entered.length
|
|
397
|
+
3.times { release_factory << true }
|
|
398
|
+
producers.each(&:value)
|
|
399
|
+
# Every created worker must enter do_select before checking its count.
|
|
400
|
+
active_workers = factories.times.map { workers.pop }
|
|
401
|
+
raise "started #{factories} selector workers" unless factories == 1
|
|
402
|
+
counts = [:queued_exceptions, :queued_responses, :queued_validation_responses].map do |name|
|
|
403
|
+
klass.class_variable_get("@@#{name}").length
|
|
404
|
+
end
|
|
405
|
+
raise "lost producer work: #{counts.inspect}" unless counts == [1, 1, 1]
|
|
406
|
+
active_workers.each { release_workers << true }
|
|
407
|
+
active_workers.each(&:value)
|
|
408
|
+
ensure
|
|
409
|
+
Thread.define_singleton_method(:new, original_new)
|
|
410
|
+
producers.each { |thread| thread.kill if thread.alive? }
|
|
411
|
+
worker = klass.class_variable_get(:@@select_thread)
|
|
412
|
+
worker.kill if worker && worker.alive?
|
|
413
|
+
until workers.empty?
|
|
414
|
+
thread = workers.pop(true)
|
|
415
|
+
thread.kill if thread.alive?
|
|
416
|
+
end
|
|
417
|
+
Array(active_workers).each { |thread| thread.kill if thread.alive? }
|
|
418
|
+
end
|
|
419
|
+
RUBY
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def test_stopped_selector_restarts_before_the_old_ruby_thread_exits
|
|
423
|
+
assert_selector_subprocess(<<~'RUBY')
|
|
424
|
+
klass = Dnsruby::SelectThread
|
|
425
|
+
original_select = IO.method(:select)
|
|
426
|
+
IO.define_singleton_method(:select) { |*| nil }
|
|
427
|
+
begin
|
|
428
|
+
[:exception, :response, :validation, :closed_registration].each do |kind|
|
|
429
|
+
selector = klass.send(:allocate)
|
|
430
|
+
wakeup = Object.new
|
|
431
|
+
wakeup.define_singleton_method(:closed?) { false }
|
|
432
|
+
{
|
|
433
|
+
mutex: Mutex.new, sockets: Set.new([wakeup]), timeouts: {}, query_hash: {}, queries_by_queue: {}.compare_by_identity,
|
|
434
|
+
socket_hash: {}, socket_is_persistent: {}, socket_remaining_queries: {},
|
|
435
|
+
tcp_buffers: {}, select_thread: nil, observers: {}, observer_queue_counts: {}.compare_by_identity, tick_observers: [],
|
|
436
|
+
queued_exceptions: [], queued_responses: [], queued_validation_responses: [],
|
|
437
|
+
wakeup_sockets: [wakeup, wakeup]
|
|
438
|
+
}.each { |name, value| klass.class_variable_set("@@#{name}", value) }
|
|
439
|
+
selector.define_singleton_method(:wake_up_select_thread) {}
|
|
440
|
+
selector.define_singleton_method(:send_tick_to_observers) {}
|
|
441
|
+
selector.define_singleton_method(:notify_queue_observers) { |*| }
|
|
442
|
+
stopped, release_old, start_old, replacements = Queue.new, Queue.new, Queue.new, Queue.new
|
|
443
|
+
implementation = selector.method(:do_select)
|
|
444
|
+
old_worker = nil
|
|
445
|
+
replacement = nil
|
|
446
|
+
selector.define_singleton_method(:do_select) do
|
|
447
|
+
implementation.call
|
|
448
|
+
if Thread.current.equal?(old_worker)
|
|
449
|
+
stopped << true
|
|
450
|
+
release_old.pop
|
|
451
|
+
else
|
|
452
|
+
replacements << Thread.current
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
begin
|
|
456
|
+
old_worker = Thread.new { start_old.pop; selector.do_select }
|
|
457
|
+
klass.class_variable_set(:@@select_thread, old_worker)
|
|
458
|
+
start_old << true
|
|
459
|
+
stopped.pop
|
|
460
|
+
raise 'old worker exited before publication' unless old_worker.alive?
|
|
461
|
+
query = Dnsruby::Message.new('late.example.')
|
|
462
|
+
query.do_validation = query.do_caching = false
|
|
463
|
+
response = query.clone
|
|
464
|
+
response.header.qr = true
|
|
465
|
+
queue = Queue.new
|
|
466
|
+
error = Dnsruby::OtherResolvError.new('late error')
|
|
467
|
+
case kind
|
|
468
|
+
when :exception
|
|
469
|
+
selector.push_exception_to_select(:late, queue, error, nil)
|
|
470
|
+
when :response
|
|
471
|
+
selector.push_response_to_select(:late, queue, response, query, nil)
|
|
472
|
+
when :validation
|
|
473
|
+
selector.push_validation_response_to_select(:late, queue, response, nil, query, nil)
|
|
474
|
+
when :closed_registration
|
|
475
|
+
closed_socket = Object.new
|
|
476
|
+
closed_socket.define_singleton_method(:closed?) { true }
|
|
477
|
+
settings = klass::QuerySettings.new(query.encode, query, false, queue, :late,
|
|
478
|
+
closed_socket, '127.0.0.1', 53, Dnsruby::TransportClock.now + 60, 512, nil)
|
|
479
|
+
selector.add_to_select(settings)
|
|
480
|
+
end
|
|
481
|
+
expected_types = case kind
|
|
482
|
+
when :exception, :closed_registration then [Dnsruby::Resolver::EventType::ERROR]
|
|
483
|
+
when :response then [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED]
|
|
484
|
+
when :validation then [Dnsruby::Resolver::EventType::VALIDATED]
|
|
485
|
+
end
|
|
486
|
+
events = Timeout.timeout(2, Timeout::Error, "#{kind}: stranded delivery while old worker is alive") do
|
|
487
|
+
expected_types.map { queue.pop }
|
|
488
|
+
end
|
|
489
|
+
replacement = Timeout.timeout(2, Timeout::Error, "#{kind}: replacement did not stop") { replacements.pop }
|
|
490
|
+
replacement.value
|
|
491
|
+
raise "#{kind}: old worker already exited" unless old_worker.alive?
|
|
492
|
+
raise "#{kind}: wrong delivery types" unless events.map { |event| event[1] } == expected_types
|
|
493
|
+
raise "#{kind}: wrong client ID" unless events.all? { |event| event[0] == :late }
|
|
494
|
+
raise "#{kind}: duplicate delivery" unless queue.empty?
|
|
495
|
+
case kind
|
|
496
|
+
when :exception
|
|
497
|
+
raise 'wrong exception' unless events.first.last.equal?(error)
|
|
498
|
+
when :closed_registration
|
|
499
|
+
raise 'wrong EOF error' unless events.first.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
500
|
+
raise 'closed registration retained' unless klass.class_variable_get(:@@query_hash).empty? &&
|
|
501
|
+
klass.class_variable_get(:@@timeouts).empty? &&
|
|
502
|
+
!klass.class_variable_get(:@@sockets).include?(closed_socket)
|
|
503
|
+
else
|
|
504
|
+
raise "#{kind}: wrong response" unless events.all? { |event| event[2].equal?(response) && event.last.nil? }
|
|
505
|
+
end
|
|
506
|
+
[:queued_exceptions, :queued_responses, :queued_validation_responses].each do |name|
|
|
507
|
+
raise "#{kind}: pending #{name}" unless klass.class_variable_get("@@#{name}").empty?
|
|
508
|
+
end
|
|
509
|
+
ensure
|
|
510
|
+
release_old << true
|
|
511
|
+
old_worker.kill if old_worker && old_worker.alive?
|
|
512
|
+
old_worker.join if old_worker
|
|
513
|
+
worker = klass.class_variable_get(:@@select_thread)
|
|
514
|
+
worker.kill if worker && worker.alive?
|
|
515
|
+
replacement.kill if replacement && replacement.alive?
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
ensure
|
|
519
|
+
IO.define_singleton_method(:select, original_select)
|
|
520
|
+
end
|
|
521
|
+
RUBY
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def test_send_failure_on_reused_pipeline_reports_the_registered_query
|
|
525
|
+
sender = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: true)
|
|
526
|
+
socket = Object.new
|
|
527
|
+
socket.define_singleton_method(:write_nonblock) { |*| raise IOError, 'closed during send' }
|
|
528
|
+
queue = Queue.new
|
|
529
|
+
query = Dnsruby::Message.new('late.example.')
|
|
530
|
+
errors = []
|
|
531
|
+
selector = Object.new
|
|
532
|
+
current = nil
|
|
533
|
+
selector.define_singleton_method(:query_settings_for_id) { |_| current }
|
|
534
|
+
selector.define_singleton_method(:reserve_send) { |settings, _previous| current = settings; true }
|
|
535
|
+
selector.define_singleton_method(:selector_thread?) { false }
|
|
536
|
+
selector.define_singleton_method(:register_query_if_current) { |settings, _| current = settings; true }
|
|
537
|
+
selector.define_singleton_method(:fail_tcp_send) do |settings, error|
|
|
538
|
+
errors << [settings.client_query_id, settings.client_queue, error, nil]
|
|
539
|
+
end
|
|
540
|
+
Dnsruby::SelectThread.stub(:instance, selector) do
|
|
541
|
+
sender.stub(:tcp_pipeline_socket, [socket, false]) do
|
|
542
|
+
sender.send_dnsruby(query.encode, query, :late_send, queue, true)
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
assert_equal(1, errors.length)
|
|
546
|
+
assert_equal([:late_send, queue], errors.first[0, 2])
|
|
547
|
+
assert_kind_of(Dnsruby::SocketEofResolvError, errors.first[2])
|
|
548
|
+
assert_nil(errors.first[3])
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def test_eof_callback_registration_gets_error_without_retaining_closed_socket
|
|
552
|
+
# add_to_select queues the second EOF in a class variable. Run the complete
|
|
553
|
+
# callback scenario in a child so a live singleton cannot steal its queue.
|
|
554
|
+
assert_selector_subprocess(<<~'RUBY')
|
|
555
|
+
klass = Dnsruby::SelectThread
|
|
556
|
+
{
|
|
557
|
+
mutex: Mutex.new, query_hash: {}, queries_by_queue: {}.compare_by_identity, socket_hash: {}, timeouts: {}, sockets: Set.new,
|
|
558
|
+
tcp_buffers: {}, socket_is_persistent: {}, socket_remaining_queries: {}, queued_exceptions: []
|
|
559
|
+
}.each { |name, value| klass.class_variable_set("@@#{name}", value) }
|
|
560
|
+
selector = klass.send(:allocate)
|
|
561
|
+
selector.define_singleton_method(:check_select_thread_synchronized) {}
|
|
562
|
+
selector.define_singleton_method(:wake_up_select_thread) {}
|
|
563
|
+
closed = false
|
|
564
|
+
socket = Object.new
|
|
565
|
+
socket.define_singleton_method(:closed?) { closed }
|
|
566
|
+
socket.define_singleton_method(:close) { closed = true }
|
|
567
|
+
socket.define_singleton_method(:recv_nonblock) { |_| ''.b }
|
|
568
|
+
first, second = [:first, :second].map do |id|
|
|
569
|
+
settings = klass::QuerySettings.new
|
|
570
|
+
settings.query = Dnsruby::Message.new("#{id}.example.")
|
|
571
|
+
settings.client_query_id = [Object.new, settings.query]
|
|
572
|
+
settings.client_queue, settings.socket = Queue.new, socket
|
|
573
|
+
settings.endtime = Dnsruby::TransportClock.now + 60
|
|
574
|
+
settings.is_persistent_socket = true
|
|
575
|
+
settings.tcp_pipelining_max_queries = :infinite
|
|
576
|
+
settings
|
|
577
|
+
end
|
|
578
|
+
selector.add_to_select(first)
|
|
579
|
+
notifications = []
|
|
580
|
+
selector.define_singleton_method(:notify_queue_observers) do |_queue, id|
|
|
581
|
+
raise 'callback under selector mutex' if klass.class_variable_get(:@@mutex).owned?
|
|
582
|
+
notifications << id
|
|
583
|
+
# A sender resumes registration during an earlier query's EOF callback.
|
|
584
|
+
add_to_select(second) if id == first.client_query_id
|
|
585
|
+
end
|
|
586
|
+
raise 'unexpected EOF result' unless selector.tcp_read(socket) == false
|
|
587
|
+
selector.send_queued_exceptions
|
|
588
|
+
[first, second].each do |settings|
|
|
589
|
+
event = settings.client_queue.pop(true)
|
|
590
|
+
raise 'wrong EOF error' unless event.last.is_a?(Dnsruby::SocketEofResolvError)
|
|
591
|
+
raise 'duplicate EOF' unless settings.client_queue.empty?
|
|
592
|
+
[:query_hash, :timeouts].each do |name|
|
|
593
|
+
raise "retained #{name}" if klass.class_variable_get("@@#{name}").key?(settings.client_query_id)
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
raise 'missing callback' unless notifications == [first.client_query_id, second.client_query_id]
|
|
597
|
+
[:socket_hash, :tcp_buffers, :socket_is_persistent, :socket_remaining_queries].each do |name|
|
|
598
|
+
raise "retained #{name}" if klass.class_variable_get("@@#{name}").key?(socket)
|
|
599
|
+
end
|
|
600
|
+
raise 'retained socket' if klass.class_variable_get(:@@sockets).include?(socket)
|
|
601
|
+
raise 'undelivered EOF' unless klass.class_variable_get(:@@queued_exceptions).empty?
|
|
602
|
+
RUBY
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
private
|
|
606
|
+
|
|
607
|
+
def assert_selector_subprocess(script)
|
|
608
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
609
|
+
# A child-side deadline also bounds broken synchronization regressions;
|
|
610
|
+
# capture3 otherwise waits forever for a child blocked at a test gate.
|
|
611
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
612
|
+
File.expand_path('../lib', __dir__), '-r', 'dnsruby', '-r', 'timeout',
|
|
613
|
+
'-e', "Timeout.timeout(10) do\n#{script}\nend")
|
|
614
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
615
|
+
assert_empty(stderr)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def table(name)
|
|
619
|
+
@selector_class.class_variable_get("@@#{name}")
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def register(socket, id, destination = '127.0.0.1', port = 53)
|
|
623
|
+
settings = Dnsruby::SelectThread::QuerySettings.new
|
|
624
|
+
settings.query = Dnsruby::Message.new('malformed.example.')
|
|
625
|
+
settings.query.header.id = id
|
|
626
|
+
settings.client_query_id = [Object.new, settings.query]
|
|
627
|
+
settings.client_queue = Queue.new
|
|
628
|
+
settings.socket = socket
|
|
629
|
+
settings.dest_server = destination
|
|
630
|
+
settings.dest_port = port
|
|
631
|
+
table(:mutex).synchronize do
|
|
632
|
+
table(:query_hash)[settings.client_query_id] = settings
|
|
633
|
+
(table(:queries_by_queue)[settings.client_queue] ||= Set.new) << settings.client_query_id
|
|
634
|
+
(table(:socket_hash)[socket] ||= []) << settings.client_query_id
|
|
635
|
+
table(:socket_is_persistent)[socket] = true
|
|
636
|
+
end
|
|
637
|
+
@registered << settings
|
|
638
|
+
settings
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
class TestMalformedTcpTransport < Minitest::Test
|
|
643
|
+
def test_malformed_query_does_not_block_an_unrelated_query_or_its_timeout
|
|
644
|
+
[:two_bytes, :four_bytes, :truncated, :unknown_id].each do |kind|
|
|
645
|
+
listener = TCPServer.new('127.0.0.1', 0)
|
|
646
|
+
connections = []
|
|
647
|
+
exchanges = []
|
|
648
|
+
release = Queue.new
|
|
649
|
+
server = Thread.new do
|
|
650
|
+
begin
|
|
651
|
+
# Both queries must already be outstanding when the malformed reply
|
|
652
|
+
# arrives; resolver retry scheduling is separate from selector liveness.
|
|
653
|
+
requests = 2.times.map do
|
|
654
|
+
connection = listener.accept
|
|
655
|
+
connections << connection
|
|
656
|
+
length = connection.read(2).unpack('n').first
|
|
657
|
+
[connection, Dnsruby::Message.decode(connection.read(length))]
|
|
658
|
+
end
|
|
659
|
+
requests.each do |connection, response|
|
|
660
|
+
if response.question.first.qname.to_s == 'broken.example'
|
|
661
|
+
wire = case kind
|
|
662
|
+
when :two_bytes
|
|
663
|
+
[response.header.id].pack('n')
|
|
664
|
+
when :four_bytes
|
|
665
|
+
[response.header.id, 0x8000].pack('n2')
|
|
666
|
+
when :truncated
|
|
667
|
+
[response.header.id, 0x8200, 1, 0, 0, 0].pack('n6')
|
|
668
|
+
when :unknown_id
|
|
669
|
+
[(response.header.id + 1) % 65_536, 0x8000, 0, 0, 0, 0].pack('n6')
|
|
670
|
+
end
|
|
671
|
+
else
|
|
672
|
+
response.header.qr = true
|
|
673
|
+
response.add_answer(Dnsruby::RR.create('healthy.example. 60 IN A 192.0.2.25'))
|
|
674
|
+
wire = response.encode
|
|
675
|
+
end
|
|
676
|
+
exchanges << [response.question.first.qname.to_s, wire.bytesize]
|
|
677
|
+
connection.write([wire.bytesize].pack('n') + wire)
|
|
678
|
+
end
|
|
679
|
+
release.pop # The unanswered query must time out while its peer stays open.
|
|
680
|
+
rescue StandardError => error
|
|
681
|
+
error
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', port: listener.addr[1],
|
|
685
|
+
use_tcp: true, tcp_pipelining: false, do_caching: false, dnssec: false, retry_times: 1,
|
|
686
|
+
# The second async query is sent on the next resolver tick (up to 0.5s).
|
|
687
|
+
packet_timeout: 1, query_timeout: 2)
|
|
688
|
+
queue = Queue.new
|
|
689
|
+
[:broken, :healthy].each do |id|
|
|
690
|
+
query = Dnsruby::Message.new("#{id}.example.")
|
|
691
|
+
query.do_caching = false
|
|
692
|
+
query.do_validation = false
|
|
693
|
+
resolver.send_async(query, queue, id)
|
|
694
|
+
end
|
|
695
|
+
results = Timeout.timeout(5) { 2.times.map { queue.pop } }
|
|
696
|
+
broken = results.find { |item| item.first == :broken }
|
|
697
|
+
healthy = results.find { |item| item.first == :healthy }
|
|
698
|
+
assert_kind_of(kind == :unknown_id ? Dnsruby::ResolvTimeout : Dnsruby::DecodeError, broken.last)
|
|
699
|
+
assert_nil(healthy.last, "#{kind}: #{exchanges.inspect}, server status: #{server.status.inspect}")
|
|
700
|
+
assert_equal(['192.0.2.25'], healthy[1].answer.map { |rr| rr.address.to_s })
|
|
701
|
+
assert(Dnsruby::SelectThread.instance.select_thread_alive?)
|
|
702
|
+
release << true
|
|
703
|
+
server_result = Timeout.timeout(5) { server.value }
|
|
704
|
+
raise server_result if server_result.is_a?(StandardError)
|
|
705
|
+
ensure
|
|
706
|
+
resolver.close if resolver
|
|
707
|
+
server.kill.join if server
|
|
708
|
+
connections.each { |connection| connection.close unless connection.closed? } if connections
|
|
709
|
+
listener.close if listener && !listener.closed?
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
end
|