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,462 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
|
|
5
|
+
class TestPipelineRetry < Minitest::Test
|
|
6
|
+
# Native response checking and sending run unchanged. Fake transport methods
|
|
7
|
+
# inject the competing completion at a known boundary, without real sockets.
|
|
8
|
+
# A child owns all singleton tables, cache hooks, and socket constructors.
|
|
9
|
+
FIXTURE = <<~'RUBY'
|
|
10
|
+
require 'dnsruby'
|
|
11
|
+
require 'timeout'
|
|
12
|
+
Dnsruby.log.level = Logger::FATAL
|
|
13
|
+
|
|
14
|
+
class RetrySocket
|
|
15
|
+
attr_accessor :on_send
|
|
16
|
+
attr_reader :writes
|
|
17
|
+
def initialize(tcp)
|
|
18
|
+
@tcp, @closed, @writes = tcp, false, []
|
|
19
|
+
end
|
|
20
|
+
def closed?; @closed; end
|
|
21
|
+
def close; @closed = true; end
|
|
22
|
+
def bind(*); end
|
|
23
|
+
def connect(*); end
|
|
24
|
+
def getsockopt(*)
|
|
25
|
+
raise IOError, 'closed stream' if closed?
|
|
26
|
+
Struct.new(:data).new([@tcp ? Socket::SOCK_STREAM : Socket::SOCK_DGRAM].pack('i'))
|
|
27
|
+
end
|
|
28
|
+
def send(bytes, *)
|
|
29
|
+
raise IOError, 'closed stream' if closed?
|
|
30
|
+
@writes << bytes.dup
|
|
31
|
+
@on_send.call if @on_send
|
|
32
|
+
bytes.bytesize
|
|
33
|
+
end
|
|
34
|
+
def write_nonblock(bytes); send(bytes, 0); end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class RetryFixture
|
|
38
|
+
attr_reader :selector, :sender, :original, :udp, :tcp, :replacement, :winner_error
|
|
39
|
+
|
|
40
|
+
def initialize(kind = :formerr, pipeline: true, initial_tcp: kind == :formerr)
|
|
41
|
+
@klass = Dnsruby::SelectThread
|
|
42
|
+
{
|
|
43
|
+
mutex: Mutex.new, sockets: Set.new, query_hash: {}, queries_by_queue: {}.compare_by_identity, socket_hash: {}, timeouts: {},
|
|
44
|
+
socket_is_persistent: {}, socket_remaining_queries: {}, tcp_buffers: {},
|
|
45
|
+
queued_exceptions: [], queued_responses: [], queued_validation_responses: [],
|
|
46
|
+
observers: {}, observer_queue_counts: {}.compare_by_identity, tick_observers: [], select_thread: Thread.current
|
|
47
|
+
}.each { |name, value| @klass.class_variable_set("@@#{name}", value) }
|
|
48
|
+
@selector = @klass.send(:allocate)
|
|
49
|
+
@selector.define_singleton_method(:wake_up_select_thread) {}
|
|
50
|
+
@selector.define_singleton_method(:check_select_thread_synchronized) {}
|
|
51
|
+
@selector.define_singleton_method(:notify_queue_observers) { |*| }
|
|
52
|
+
@deferred_sends = []
|
|
53
|
+
deferred_sends = @deferred_sends
|
|
54
|
+
defer_send = @selector.method(:defer_send)
|
|
55
|
+
@selector.define_singleton_method(:defer_send) do |*args, &operation|
|
|
56
|
+
defer_send.call(*args, &operation).tap { |task| deferred_sends << task }
|
|
57
|
+
end
|
|
58
|
+
selector = @selector
|
|
59
|
+
@klass.define_singleton_method(:instance) { selector }
|
|
60
|
+
@sender = Dnsruby::PacketSender.new(server: '127.0.0.1', tcp_pipelining: pipeline,
|
|
61
|
+
tcp_pipelining_max_queries: :infinite, packet_timeout: 1, dnssec: false)
|
|
62
|
+
@udp, @tcp = RetrySocket.new(false), RetrySocket.new(true)
|
|
63
|
+
udp, tcp = @udp, @tcp
|
|
64
|
+
UDPSocket.define_singleton_method(:new) { |*| udp }
|
|
65
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **options| tcp }
|
|
66
|
+
@sender.define_singleton_method(:tcp_pipeline_socket) { |_| [tcp, false] }
|
|
67
|
+
@kind = kind
|
|
68
|
+
@original = settings(RetrySocket.new(initial_tcp), :retry, @sender)
|
|
69
|
+
@original.is_persistent_socket = pipeline && initial_tcp
|
|
70
|
+
@original.query.add_additional(Dnsruby::RR::OPT.new(4096)) if kind == :formerr
|
|
71
|
+
@selector.add_to_select(@original)
|
|
72
|
+
Thread.current[:dnsruby_response_query] = nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def table(name)
|
|
76
|
+
@klass.class_variable_get("@@#{name}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def settings(socket, id, sender = @sender)
|
|
80
|
+
query = Dnsruby::Message.new('retry.example.')
|
|
81
|
+
query.do_validation = query.do_caching = false
|
|
82
|
+
query.header.rd = true
|
|
83
|
+
@klass::QuerySettings.new(query.encode, query, false, Queue.new, id, socket,
|
|
84
|
+
'127.0.0.1', 53, Dnsruby::TransportClock.now + 60, 512, sender)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def response(settings = @original)
|
|
88
|
+
settings.query.clone.tap do |message|
|
|
89
|
+
message.header.qr = true
|
|
90
|
+
message.header.rcode = Dnsruby::RCode::NOERROR
|
|
91
|
+
message.header.tc = false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def retry_response
|
|
96
|
+
message = response
|
|
97
|
+
if @kind == :formerr
|
|
98
|
+
message.header.rcode = Dnsruby::RCode::FORMERR
|
|
99
|
+
else
|
|
100
|
+
message.header.tc = true
|
|
101
|
+
end
|
|
102
|
+
message
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def compete(action)
|
|
106
|
+
case action
|
|
107
|
+
when :live
|
|
108
|
+
when :failed
|
|
109
|
+
@winner_error = Dnsruby::SocketEofResolvError.new('original send failed')
|
|
110
|
+
current = @selector.query_settings_for_id(:retry)
|
|
111
|
+
if current && current.socket.nil?
|
|
112
|
+
@selector.fail_query_if_current(:retry, current.client_queue, current, @winner_error)
|
|
113
|
+
else
|
|
114
|
+
@selector.fail_tcp_send(@original, @winner_error)
|
|
115
|
+
end
|
|
116
|
+
when :expired
|
|
117
|
+
table(:timeouts)[:retry] = Dnsruby::TransportClock.now - 1
|
|
118
|
+
@selector.instance_variable_get(:@packet_deadlines)[:retry] = table(:timeouts)[:retry]
|
|
119
|
+
@selector.process_timeouts
|
|
120
|
+
when :replaced
|
|
121
|
+
@replacement = settings(RetrySocket.new(true), :retry)
|
|
122
|
+
@selector.add_to_select(@replacement)
|
|
123
|
+
else
|
|
124
|
+
raise "unknown competing action #{action}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def check_retry(action = nil, wait: true)
|
|
129
|
+
message = retry_response
|
|
130
|
+
wire = message.encode
|
|
131
|
+
if action
|
|
132
|
+
fixture = self
|
|
133
|
+
if @kind == :formerr
|
|
134
|
+
send_retry = @sender.method(:send_async)
|
|
135
|
+
@sender.define_singleton_method(:send_async) do |*args|
|
|
136
|
+
fixture.compete(action)
|
|
137
|
+
send_retry.call(*args)
|
|
138
|
+
end
|
|
139
|
+
else
|
|
140
|
+
# This getter is reached in the native truncation branch immediately
|
|
141
|
+
# before it calls send_async with use_tcp=true.
|
|
142
|
+
truncated = message.header.method(:tc)
|
|
143
|
+
message.header.define_singleton_method(:tc) do
|
|
144
|
+
fixture.compete(action)
|
|
145
|
+
truncated.call
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
@selector.process_message(message, wire, @original.socket)
|
|
150
|
+
raise 'response context leaked' if Thread.current[:dnsruby_response_query]
|
|
151
|
+
@deferred_sends.shift.value until !wait || @deferred_sends.empty?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def events(settings = @original)
|
|
155
|
+
@selector.send_queued_exceptions
|
|
156
|
+
@selector.send_queued_responses
|
|
157
|
+
@selector.send_queued_validation_responses
|
|
158
|
+
result = []
|
|
159
|
+
result << settings.client_queue.pop(true) until settings.client_queue.empty?
|
|
160
|
+
result
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def assert_stale(action)
|
|
164
|
+
actual = events
|
|
165
|
+
case action
|
|
166
|
+
when :failed, :expired
|
|
167
|
+
error_class = action == :failed ? Dnsruby::SocketEofResolvError : Dnsruby::ResolvTimeout
|
|
168
|
+
raise "#{action}: wrong terminal deliveries #{actual.inspect}" unless actual.length == 1 &&
|
|
169
|
+
actual.first[0, 3] == [:retry, Dnsruby::Resolver::EventType::ERROR, nil] &&
|
|
170
|
+
actual.first.last.is_a?(error_class)
|
|
171
|
+
raise 'wrong failure owner' if action == :failed && !actual.first.last.equal?(@winner_error)
|
|
172
|
+
raise "#{action}: original query resurrected" if @selector.query_settings_for_id(:retry)
|
|
173
|
+
raise "#{action}: stale timeout retained" if table(:timeouts).key?(:retry)
|
|
174
|
+
when :replaced
|
|
175
|
+
raise 'stale attempt published a delivery' unless actual.empty?
|
|
176
|
+
raise 'replacement registration consumed' unless @selector.query_settings_for_id(:retry).equal?(@replacement)
|
|
177
|
+
raise 'replacement received stale delivery' unless events(@replacement).empty?
|
|
178
|
+
raise 'replacement timeout removed' unless table(:timeouts)[:retry] == @replacement.endtime
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def assert_success(settings, message)
|
|
183
|
+
@selector.process_message(message, message.encode, settings.socket)
|
|
184
|
+
expected = [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED].map do |type|
|
|
185
|
+
[settings.client_query_id, type, message, nil]
|
|
186
|
+
end
|
|
187
|
+
actual = events(settings)
|
|
188
|
+
raise "wrong success deliveries: #{actual.inspect}" unless actual == expected
|
|
189
|
+
raise 'completed registration retained' if @selector.query_settings_for_id(settings.client_query_id)
|
|
190
|
+
raise 'completed timeout retained' if table(:timeouts).key?(settings.client_query_id)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def assert_no_retry_writes
|
|
194
|
+
raise 'stale UDP retry was sent' unless @udp.writes.empty?
|
|
195
|
+
raise 'stale TCP retry was sent' unless @tcp.writes.empty?
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
RUBY
|
|
199
|
+
|
|
200
|
+
def test_native_formerr_and_truncated_responses_retry_and_complete
|
|
201
|
+
assert_isolated(<<~'RUBY')
|
|
202
|
+
[:formerr, :truncated].each do |kind|
|
|
203
|
+
[true, false].each do |pipeline|
|
|
204
|
+
fixture = RetryFixture.new(kind, pipeline: pipeline)
|
|
205
|
+
fixture.check_retry
|
|
206
|
+
current = fixture.selector.query_settings_for_id(:retry)
|
|
207
|
+
raise "#{kind}: retry missing" unless current && !current.equal?(fixture.original)
|
|
208
|
+
raise 'retry changed the client queue' unless current.client_queue.equal?(fixture.original.client_queue)
|
|
209
|
+
raise 'premature retry delivery' unless fixture.events.empty?
|
|
210
|
+
raise "#{kind}: retry did not use TCP" unless current.socket.equal?(fixture.tcp) && !fixture.tcp.writes.empty?
|
|
211
|
+
raise 'TCP FORMERR changed transport' unless fixture.udp.writes.empty?
|
|
212
|
+
frame = fixture.tcp.writes.join
|
|
213
|
+
raise 'invalid retry frame' unless frame.unpack1('n') == frame.bytesize - 2
|
|
214
|
+
sent = Dnsruby::Message.decode(frame.byteslice(2..))
|
|
215
|
+
raise 'FORMERR retry retained EDNS' if kind == :formerr && sent.header.arcount != 0
|
|
216
|
+
raise 'retry changed DNS transaction ID' unless sent.header.id == fixture.original.query.header.id
|
|
217
|
+
fixture.assert_success(current, fixture.response(current))
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
RUBY
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_native_protocol_retries_get_a_fresh_packet_deadline
|
|
224
|
+
assert_isolated(<<~'RUBY')
|
|
225
|
+
[:formerr, :truncated].each do |kind|
|
|
226
|
+
[true, false].each do |pipeline|
|
|
227
|
+
fixture = RetryFixture.new(kind, pipeline: pipeline)
|
|
228
|
+
crossed = false
|
|
229
|
+
cross_old_deadline = -> do
|
|
230
|
+
crossed = true
|
|
231
|
+
fixture.original.endtime = Dnsruby::TransportClock.now - 1
|
|
232
|
+
fixture.table(:timeouts)[:retry] = fixture.original.endtime
|
|
233
|
+
# No timeout has claimed this attempt: its response is already
|
|
234
|
+
# being processed, and its protocol retry has its own deadline.
|
|
235
|
+
end
|
|
236
|
+
fixture.udp.define_singleton_method(:connect) { |*| cross_old_deadline.call }
|
|
237
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **options|
|
|
238
|
+
cross_old_deadline.call
|
|
239
|
+
fixture.tcp
|
|
240
|
+
end
|
|
241
|
+
fixture.sender.define_singleton_method(:tcp_pipeline_socket) do |_|
|
|
242
|
+
cross_old_deadline.call
|
|
243
|
+
[fixture.tcp, false]
|
|
244
|
+
end
|
|
245
|
+
fixture.check_retry
|
|
246
|
+
raise 'connection boundary not exercised' unless crossed
|
|
247
|
+
current = fixture.selector.query_settings_for_id(:retry)
|
|
248
|
+
raise 'live protocol retry discarded' unless current && !current.equal?(fixture.original)
|
|
249
|
+
raise 'retry inherited old deadline' unless current.endtime > Dnsruby::TransportClock.now
|
|
250
|
+
raise 'premature retry event' unless fixture.events.empty?
|
|
251
|
+
raise 'retry was not sent over TCP' if fixture.tcp.writes.empty?
|
|
252
|
+
fixture.assert_success(current, fixture.response(current))
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
RUBY
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def test_deferred_protocol_retry_uses_its_own_packet_deadline
|
|
259
|
+
assert_isolated(<<~'RUBY')
|
|
260
|
+
fixture = RetryFixture.new(:truncated)
|
|
261
|
+
lock = Mutex.new
|
|
262
|
+
fixture.sender.instance_variable_set(:@pipeline_mutex, lock)
|
|
263
|
+
lock.lock
|
|
264
|
+
send_query = fixture.sender.method(:send_dnsruby)
|
|
265
|
+
worker = nil
|
|
266
|
+
fixture.sender.define_singleton_method(:send_dnsruby) { |*args| worker = send_query.call(*args) }
|
|
267
|
+
begin
|
|
268
|
+
fixture.check_retry(wait: false)
|
|
269
|
+
raise 'selector retry was not deferred' unless worker.respond_to?(:value)
|
|
270
|
+
fixture.original.endtime = Dnsruby::TransportClock.now - 1
|
|
271
|
+
fixture.table(:timeouts)[:retry] = fixture.original.endtime
|
|
272
|
+
lock.unlock
|
|
273
|
+
worker.value
|
|
274
|
+
current = fixture.selector.query_settings_for_id(:retry)
|
|
275
|
+
raise 'unclaimed retry discarded after writer wait' unless current && !current.equal?(fixture.original)
|
|
276
|
+
raise 'deferred retry inherited old deadline' unless current.endtime > Dnsruby::TransportClock.now
|
|
277
|
+
raise 'premature retry event' unless fixture.events.empty?
|
|
278
|
+
raise 'deferred retry not sent' if fixture.tcp.writes.empty?
|
|
279
|
+
fixture.assert_success(current, fixture.response(current))
|
|
280
|
+
ensure
|
|
281
|
+
lock.unlock if lock.owned?
|
|
282
|
+
worker.join if worker.is_a?(Thread)
|
|
283
|
+
end
|
|
284
|
+
RUBY
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_native_retries_cannot_resurrect_failed_expired_or_replaced_settings
|
|
288
|
+
assert_isolated(<<~'RUBY')
|
|
289
|
+
[:formerr, :truncated].each do |kind|
|
|
290
|
+
[:failed, :expired, :replaced].each do |action|
|
|
291
|
+
fixture = RetryFixture.new(kind)
|
|
292
|
+
fixture.check_retry(action)
|
|
293
|
+
fixture.assert_stale(action)
|
|
294
|
+
fixture.assert_no_retry_writes
|
|
295
|
+
if action == :replaced
|
|
296
|
+
fixture.assert_success(fixture.replacement, fixture.response(fixture.replacement))
|
|
297
|
+
else
|
|
298
|
+
fixture.selector.process_timeouts
|
|
299
|
+
raise 'second terminal delivery' unless fixture.events.empty?
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
RUBY
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def test_retry_encode_and_oversized_udp_errors_claim_only_the_original_attempt
|
|
307
|
+
assert_isolated(<<~'RUBY')
|
|
308
|
+
[:encode, :oversized_udp].each do |failure|
|
|
309
|
+
[:live, :failed, :expired, :replaced].each do |action|
|
|
310
|
+
fixture = RetryFixture.new
|
|
311
|
+
fixture.sender.no_tcp = true if failure == :oversized_udp
|
|
312
|
+
reached = false
|
|
313
|
+
fixture.sender.define_singleton_method(:make_query_packet) do |*|
|
|
314
|
+
reached = true
|
|
315
|
+
fixture.compete(action)
|
|
316
|
+
if failure == :encode
|
|
317
|
+
raise Dnsruby::EncodeError, 'retry encoding failed'
|
|
318
|
+
else
|
|
319
|
+
'x' * 65_535
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
fixture.check_retry
|
|
323
|
+
raise 'early-error hook not reached' unless reached
|
|
324
|
+
if action == :live
|
|
325
|
+
expected = failure == :encode ? Dnsruby::EncodeError : IOError
|
|
326
|
+
events = fixture.events
|
|
327
|
+
raise "#{failure}: wrong early error" unless events.length == 1 &&
|
|
328
|
+
events.first[1] == Dnsruby::Resolver::EventType::ERROR && events.first.last.is_a?(expected)
|
|
329
|
+
raise 'early error retained original registration' if fixture.selector.query_settings_for_id(:retry)
|
|
330
|
+
fixture.selector.process_timeouts
|
|
331
|
+
raise 'early error delivered twice' unless fixture.events.empty?
|
|
332
|
+
else
|
|
333
|
+
fixture.assert_stale(action)
|
|
334
|
+
end
|
|
335
|
+
fixture.assert_no_retry_writes
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
RUBY
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def test_retry_cache_hit_claims_only_the_original_attempt
|
|
342
|
+
assert_isolated(<<~'RUBY')
|
|
343
|
+
[:live, :failed, :expired, :replaced].each do |action|
|
|
344
|
+
fixture = RetryFixture.new
|
|
345
|
+
fixture.original.query.do_caching = true
|
|
346
|
+
cached = fixture.response
|
|
347
|
+
cached.cached = true
|
|
348
|
+
reached = false
|
|
349
|
+
cache = Dnsruby::PacketSender.class_variable_get(:@@recursive_cache)
|
|
350
|
+
cache.define_singleton_method(:find) do |*|
|
|
351
|
+
reached = true
|
|
352
|
+
fixture.compete(action)
|
|
353
|
+
cached
|
|
354
|
+
end
|
|
355
|
+
fixture.check_retry
|
|
356
|
+
raise 'cache lookup hook not reached' unless reached
|
|
357
|
+
if action == :live
|
|
358
|
+
expected = [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED].map do |type|
|
|
359
|
+
[:retry, type, cached, nil]
|
|
360
|
+
end
|
|
361
|
+
raise 'cache hit delivered incorrectly' unless fixture.events == expected
|
|
362
|
+
raise 'cached retry retained original registration' if fixture.selector.query_settings_for_id(:retry)
|
|
363
|
+
raise 'cached retry retained timeout' if fixture.table(:timeouts).key?(:retry)
|
|
364
|
+
else
|
|
365
|
+
fixture.assert_stale(action)
|
|
366
|
+
end
|
|
367
|
+
fixture.assert_no_retry_writes
|
|
368
|
+
end
|
|
369
|
+
RUBY
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def test_nonpipeline_retry_write_error_does_not_report_after_another_completion
|
|
373
|
+
assert_isolated(<<~'RUBY')
|
|
374
|
+
[:live, :failed, :expired, :replaced].each do |action|
|
|
375
|
+
fixture = RetryFixture.new(:formerr, pipeline: false, initial_tcp: false)
|
|
376
|
+
reached = false
|
|
377
|
+
fixture.udp.on_send = -> { raise IOError, 'UDP retry send failed' }
|
|
378
|
+
write = fixture.sender.method(:write_udp_packet)
|
|
379
|
+
fixture.sender.define_singleton_method(:write_udp_packet) do |*args|
|
|
380
|
+
begin
|
|
381
|
+
write.call(*args)
|
|
382
|
+
rescue IOError
|
|
383
|
+
# A native write cannot call the selector while holding the send
|
|
384
|
+
# token. Schedule the competing completion after that lock releases
|
|
385
|
+
# but before PacketSender claims the failed attempt.
|
|
386
|
+
reached = true
|
|
387
|
+
fixture.compete(action)
|
|
388
|
+
raise
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
fixture.check_retry
|
|
392
|
+
raise 'nonpipeline send hook not reached' unless reached
|
|
393
|
+
raise 'failed retry socket retained' unless fixture.udp.closed?
|
|
394
|
+
if action == :live
|
|
395
|
+
events = fixture.events
|
|
396
|
+
raise 'live retry error not delivered once' unless events.length == 1 &&
|
|
397
|
+
events.first[1] == Dnsruby::Resolver::EventType::ERROR && events.first.last.is_a?(IOError)
|
|
398
|
+
raise 'failed retry registration retained' if fixture.selector.query_settings_for_id(:retry)
|
|
399
|
+
else
|
|
400
|
+
fixture.assert_stale(action)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
RUBY
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def test_nested_response_checks_restore_context_on_success_and_exception
|
|
407
|
+
assert_isolated(<<~'RUBY')
|
|
408
|
+
[:success, :exception].each do |mode|
|
|
409
|
+
fixture = RetryFixture.new
|
|
410
|
+
selector = fixture.selector
|
|
411
|
+
outer = fixture.original
|
|
412
|
+
inner_sender = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
|
|
413
|
+
inner = fixture.settings(RetrySocket.new(true), :inner, inner_sender)
|
|
414
|
+
selector.add_to_select(inner)
|
|
415
|
+
outer_response, inner_response = fixture.response(outer), fixture.response(inner)
|
|
416
|
+
sentinel = Object.new
|
|
417
|
+
Thread.current[:dnsruby_response_query] = sentinel
|
|
418
|
+
original_outer_check = fixture.sender.method(:check_response)
|
|
419
|
+
original_inner_check = inner_sender.method(:check_response)
|
|
420
|
+
inner_sender.define_singleton_method(:check_response) do |*args|
|
|
421
|
+
raise 'inner context missing' unless response_query_settings(:inner).equal?(inner)
|
|
422
|
+
raise 'wrong ID inherited context' if response_query_settings(:retry)
|
|
423
|
+
raise 'wrong sender inherited context' if fixture.sender.response_query_settings(:inner)
|
|
424
|
+
raise IOError, 'nested response failure' if mode == :exception
|
|
425
|
+
original_inner_check.call(*args)
|
|
426
|
+
end
|
|
427
|
+
fixture.sender.define_singleton_method(:check_response) do |*args|
|
|
428
|
+
raise 'outer context missing' unless response_query_settings(:retry).equal?(outer)
|
|
429
|
+
begin
|
|
430
|
+
selector.process_message(inner_response, inner_response.encode, inner.socket)
|
|
431
|
+
ensure
|
|
432
|
+
raise 'inner check lost outer context' unless Thread.current[:dnsruby_response_query].equal?(outer)
|
|
433
|
+
end
|
|
434
|
+
original_outer_check.call(*args)
|
|
435
|
+
end
|
|
436
|
+
begin
|
|
437
|
+
selector.process_message(outer_response, outer_response.encode, outer.socket)
|
|
438
|
+
raise 'nested exception was swallowed' if mode == :exception
|
|
439
|
+
raise 'inner response missing' unless fixture.events(inner).length == 2
|
|
440
|
+
raise 'outer response missing' unless fixture.events(outer).length == 2
|
|
441
|
+
rescue IOError => error
|
|
442
|
+
raise unless mode == :exception && error.message == 'nested response failure'
|
|
443
|
+
raise 'failed check published a response' unless fixture.events(inner).empty? && fixture.events(outer).empty?
|
|
444
|
+
ensure
|
|
445
|
+
raise 'caller context not restored' unless Thread.current[:dnsruby_response_query].equal?(sentinel)
|
|
446
|
+
Thread.current[:dnsruby_response_query] = nil
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
RUBY
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
private
|
|
453
|
+
|
|
454
|
+
def assert_isolated(body)
|
|
455
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
456
|
+
script = FIXTURE + "\nTimeout.timeout(10) do\n" + body + "\nend\n"
|
|
457
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
458
|
+
File.expand_path('../lib', __dir__), '-e', script)
|
|
459
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
460
|
+
assert_empty(stderr)
|
|
461
|
+
end
|
|
462
|
+
end
|