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
data/test/tc_tcp_pipelining.rb
CHANGED
|
@@ -15,241 +15,132 @@
|
|
|
15
15
|
# ++
|
|
16
16
|
|
|
17
17
|
require_relative 'spec_helper'
|
|
18
|
-
require_relative '
|
|
19
|
-
|
|
20
|
-
# The TCPPipeliningServer links our NioTcpPipeliningHandler on
|
|
21
|
-
# the loopback interface.
|
|
22
|
-
class TCPPipeliningServer < Async::DNS::Server
|
|
23
|
-
PORT = 53937
|
|
24
|
-
IP = '127.0.0.1'
|
|
25
|
-
|
|
26
|
-
DEFAULT_MAX_REQUESTS = 4
|
|
27
|
-
DEFAULT_TIMEOUT = 3
|
|
28
|
-
|
|
29
|
-
@@stats = Stats.new
|
|
30
|
-
|
|
31
|
-
def self.stats
|
|
32
|
-
@@stats
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def initialize(**options)
|
|
36
|
-
super(options)
|
|
37
|
-
|
|
38
|
-
@handlers = []
|
|
39
|
-
@handlers << NioTcpPipeliningHandler.new(self, IP, PORT, DEFAULT_MAX_REQUESTS, DEFAULT_TIMEOUT) #4 max request
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def process(name, resource_class, transaction)
|
|
43
|
-
@logger.debug "name: #{name}"
|
|
44
|
-
transaction.respond!("93.184.216.34", { resource_class: ::Resolv::DNS::Resource::IN::A })
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
end
|
|
18
|
+
require_relative 'tcp_pipelining_server'
|
|
19
|
+
require 'timeout'
|
|
48
20
|
|
|
49
21
|
class TestTCPPipelining < Minitest::Test
|
|
50
|
-
|
|
51
|
-
class << self
|
|
52
|
-
attr_accessor :query_id
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def self.init
|
|
56
|
-
unless @initialized
|
|
57
|
-
@initialized = true
|
|
58
|
-
@query_id = 0
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
@@server = nil
|
|
22
|
+
WAIT_TIMEOUT = 5
|
|
63
23
|
|
|
64
24
|
def setup
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@@server.run
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Instantiate our resolver. The resolver will use the same pipeline as much as possible.
|
|
84
|
-
# If a timeout occurs or max_request_per_connection a new connection should be initiated
|
|
85
|
-
@@resolver ||= Dnsruby::Resolver.new(
|
|
86
|
-
use_tcp: true,
|
|
87
|
-
do_caching: false,
|
|
88
|
-
tcp_pipelining: true,
|
|
89
|
-
dnssec: false,
|
|
90
|
-
packet_timeout: 10,
|
|
91
|
-
tcp_pipelining_max_queries: 10,
|
|
92
|
-
nameserver: TCPPipeliningServer::IP,
|
|
93
|
-
port: TCPPipeliningServer::PORT)
|
|
25
|
+
@server = PipelineTest::Server.new
|
|
26
|
+
@query_id = 0
|
|
27
|
+
@resolver = Dnsruby::Resolver.new(
|
|
28
|
+
use_tcp: true,
|
|
29
|
+
do_caching: false,
|
|
30
|
+
tcp_pipelining: true,
|
|
31
|
+
dnssec: false,
|
|
32
|
+
retry_times: 1,
|
|
33
|
+
packet_timeout: WAIT_TIMEOUT,
|
|
34
|
+
query_timeout: WAIT_TIMEOUT,
|
|
35
|
+
tcp_pipelining_max_queries: 10,
|
|
36
|
+
nameserver: PipelineTest::Server::IP,
|
|
37
|
+
port: @server.port
|
|
38
|
+
)
|
|
94
39
|
end
|
|
95
40
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
message = Dnsruby::Message.new(name)
|
|
103
|
-
# self.class.query_id identifies our query, must be different for each message
|
|
104
|
-
@@resolver.send_async(message, queue, self.class.query_id)
|
|
105
|
-
self.class.query_id += 1
|
|
41
|
+
def teardown
|
|
42
|
+
@resolver.close if @resolver
|
|
43
|
+
ensure
|
|
44
|
+
@server.stop if @server
|
|
45
|
+
wait_for_client_close if @resolver
|
|
46
|
+
end
|
|
106
47
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
48
|
+
def send_async_messages(count, queue)
|
|
49
|
+
Array.new(count) do
|
|
50
|
+
id = @query_id
|
|
51
|
+
@query_id += 1
|
|
52
|
+
message = Dnsruby::Message.new("pipeline-#{id}.example.invalid")
|
|
53
|
+
message.do_caching = false
|
|
54
|
+
message.do_validation = false
|
|
55
|
+
@resolver.send_async(message, queue, id)
|
|
56
|
+
id
|
|
110
57
|
end
|
|
111
58
|
end
|
|
112
59
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
_response_id, response, exception = queue.pop
|
|
117
|
-
assert_nil(exception)
|
|
118
|
-
assert(response.is_a?(Dnsruby::Message))
|
|
60
|
+
def responses_for(ids, queue)
|
|
61
|
+
responses = Timeout.timeout(WAIT_TIMEOUT + 1) do
|
|
62
|
+
Array.new(ids.length) { queue.pop }
|
|
119
63
|
end
|
|
64
|
+
@server.stats # Propagate any exception in the server thread.
|
|
65
|
+
assert_equal ids.sort, responses.map(&:first).sort
|
|
66
|
+
responses.sort_by(&:first)
|
|
120
67
|
end
|
|
121
68
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
69
|
+
def verify_responses(ids, queue)
|
|
70
|
+
responses_for(ids, queue).each do |id, response, exception|
|
|
71
|
+
assert_nil exception
|
|
72
|
+
assert_instance_of Dnsruby::Message, response
|
|
73
|
+
assert_equal "pipeline-#{id}.example.invalid", response.question.first.qname.to_s
|
|
74
|
+
assert_equal PipelineTest::Server::ANSWER, response.answer.first.address.to_s
|
|
128
75
|
end
|
|
129
76
|
end
|
|
130
77
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
78
|
+
def wait_for_timeouts(count)
|
|
79
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + WAIT_TIMEOUT
|
|
80
|
+
until @server.stats[:timeouts] == count && @server.stats[:connections].zero?
|
|
81
|
+
flunk "Timed out waiting for #{count} connection expiry/expiries: #{@server.stats}" if
|
|
82
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
83
|
+
sleep 0.01
|
|
137
84
|
end
|
|
85
|
+
# A server-side close does not mean the client's select thread has seen
|
|
86
|
+
# the FIN yet. Reconnect only after it has processed EOF.
|
|
87
|
+
wait_for_client_close
|
|
138
88
|
end
|
|
139
89
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
assert(i<max, "Max wait for timeout reached #{TCPPipeliningServer.stats.timeout_count} timeounts < #{timeout_count}")
|
|
90
|
+
def wait_for_client_close
|
|
91
|
+
sender = @resolver.single_resolvers.first
|
|
92
|
+
socket = sender.instance_variable_get(:@pipeline_socket)
|
|
93
|
+
Timeout.timeout(WAIT_TIMEOUT) do
|
|
94
|
+
sleep 0.01 while socket && !socket.closed?
|
|
146
95
|
end
|
|
147
96
|
end
|
|
148
97
|
|
|
149
|
-
#
|
|
150
|
-
#
|
|
98
|
+
# Multiple outstanding queries share one TCP connection; after it expires,
|
|
99
|
+
# the next batch must use a new connection.
|
|
151
100
|
def test_TCP_pipelining_timeout
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
# Test basic pipelining. All request should go on the same tcp connection.
|
|
163
|
-
# TCPPipeliningServer.stats.accept_count should be 1.
|
|
164
|
-
send_async_messages(3, query_queue)
|
|
165
|
-
verify_responses(3, query_queue)
|
|
166
|
-
|
|
167
|
-
assert_equal(accept_count + 1, TCPPipeliningServer.stats.accept_count)
|
|
168
|
-
|
|
169
|
-
connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
|
|
170
|
-
timeout_wait(timeout_count + 1, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
|
|
171
|
-
|
|
172
|
-
assert_equal(timeout_count + 1, TCPPipeliningServer.stats.timeout_count)
|
|
173
|
-
|
|
174
|
-
# Initiate another 3 queries, check accept_count and timeout_count
|
|
175
|
-
send_async_messages(3, query_queue)
|
|
176
|
-
verify_responses(3, query_queue)
|
|
177
|
-
|
|
178
|
-
connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
|
|
179
|
-
timeout_wait(timeout_count + 2, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
|
|
180
|
-
|
|
181
|
-
assert_equal(accept_count + 2, TCPPipeliningServer.stats.accept_count)
|
|
182
|
-
assert_equal(timeout_count + 2, TCPPipeliningServer.stats.timeout_count)
|
|
183
|
-
|
|
184
|
-
connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
|
|
101
|
+
queue = Queue.new
|
|
102
|
+
verify_responses(send_async_messages(3, queue), queue)
|
|
103
|
+
assert_equal 1, @server.stats[:accepts]
|
|
104
|
+
wait_for_timeouts(1)
|
|
105
|
+
|
|
106
|
+
verify_responses(send_async_messages(3, queue), queue)
|
|
107
|
+
assert_equal 2, @server.stats[:accepts]
|
|
108
|
+
wait_for_timeouts(2)
|
|
109
|
+
assert_equal 2, @server.stats[:timeouts]
|
|
185
110
|
end
|
|
186
111
|
|
|
187
|
-
#
|
|
112
|
+
# The server closes the pipeline between sends, and a single subsequent
|
|
113
|
+
# query must reconnect successfully.
|
|
188
114
|
def test_TCP_pipelining_timeout_in_send
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
# Initiate another 2 queries wait and then send a final query
|
|
199
|
-
# Check accept_count. Wait for timeout and verify we got 2 additional timeouts.
|
|
200
|
-
send_async_messages(2, query_queue)
|
|
201
|
-
verify_responses(2, query_queue)
|
|
202
|
-
|
|
203
|
-
accept_wait(accept_count+1, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
|
|
204
|
-
connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
|
|
205
|
-
|
|
206
|
-
send_async_messages(1, query_queue)
|
|
207
|
-
|
|
208
|
-
verify_responses(1, query_queue)
|
|
209
|
-
|
|
210
|
-
assert_equal(accept_count + 2, TCPPipeliningServer.stats.accept_count)
|
|
211
|
-
|
|
212
|
-
connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
|
|
213
|
-
|
|
214
|
-
timeout_wait(timeout_count + 2, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
|
|
115
|
+
queue = Queue.new
|
|
116
|
+
verify_responses(send_async_messages(2, queue), queue)
|
|
117
|
+
assert_equal 1, @server.stats[:accepts]
|
|
118
|
+
wait_for_timeouts(1)
|
|
119
|
+
|
|
120
|
+
verify_responses(send_async_messages(1, queue), queue)
|
|
121
|
+
assert_equal 2, @server.stats[:accepts]
|
|
122
|
+
wait_for_timeouts(2)
|
|
123
|
+
assert_equal 2, @server.stats[:timeouts]
|
|
215
124
|
end
|
|
216
125
|
|
|
217
|
-
#
|
|
218
|
-
#
|
|
126
|
+
# The peer answers only four of six outstanding queries before closing.
|
|
127
|
+
# Each unanswered query must receive EOF, rather than a resolver timeout.
|
|
219
128
|
def test_TCP_pipelining_socket_eof
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
send_async_messages(6, query_queue)
|
|
229
|
-
|
|
230
|
-
responses = []
|
|
231
|
-
|
|
232
|
-
6.times do
|
|
233
|
-
response = query_queue.pop
|
|
234
|
-
responses << response
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
responses.sort_by { |response| response[0] }
|
|
238
|
-
|
|
239
|
-
step = 0
|
|
240
|
-
|
|
241
|
-
responses.each do | response |
|
|
242
|
-
_response_id, response, exception = response
|
|
243
|
-
if step < TCPPipeliningServer::DEFAULT_MAX_REQUESTS
|
|
244
|
-
assert_nil(exception, "Exception not nil for msg #{step} < #{TCPPipeliningServer::DEFAULT_MAX_REQUESTS} requests")
|
|
245
|
-
assert(response.is_a?(Dnsruby::Message))
|
|
129
|
+
queue = Queue.new
|
|
130
|
+
ids = send_async_messages(6, queue)
|
|
131
|
+
responses_for(ids, queue).each do |id, response, exception|
|
|
132
|
+
if id < PipelineTest::Server::MAX_REQUESTS
|
|
133
|
+
assert_nil exception
|
|
134
|
+
assert_instance_of Dnsruby::Message, response
|
|
135
|
+
assert_equal "pipeline-#{id}.example.invalid", response.question.first.qname.to_s
|
|
136
|
+
assert_equal PipelineTest::Server::ANSWER, response.answer.first.address.to_s
|
|
246
137
|
else
|
|
247
|
-
|
|
248
|
-
assert_nil
|
|
138
|
+
assert_instance_of Dnsruby::SocketEofResolvError, exception
|
|
139
|
+
assert_nil response
|
|
249
140
|
end
|
|
250
|
-
step += 1
|
|
251
141
|
end
|
|
252
|
-
|
|
253
|
-
|
|
142
|
+
wait_for_timeouts(1)
|
|
143
|
+
assert_equal 1, @server.stats[:accepts]
|
|
144
|
+
assert_equal 1, @server.stats[:limits]
|
|
254
145
|
end
|
|
255
146
|
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
|
|
5
|
+
class TestTransportFork < Minitest::Test
|
|
6
|
+
def test_child_discards_parent_transactions_descriptors_and_worker_state
|
|
7
|
+
skip 'fork unavailable' unless Process.respond_to?(:fork)
|
|
8
|
+
code = <<~'CHILD'
|
|
9
|
+
require 'dnsruby'
|
|
10
|
+
require 'timeout'
|
|
11
|
+
Dnsruby.log.level = Logger::FATAL
|
|
12
|
+
# Own dispatch explicitly: no DNS service or elapsed timeout is needed.
|
|
13
|
+
class Dnsruby::SelectThread
|
|
14
|
+
def get_socket_pair
|
|
15
|
+
Socket.pair(:UNIX, :STREAM, 0)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def do_select
|
|
19
|
+
sleep
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
Timeout.timeout(10) do
|
|
23
|
+
selector = Dnsruby::SelectThread.instance
|
|
24
|
+
klass = Dnsruby::SelectThread
|
|
25
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
|
|
26
|
+
sender = resolver.single_resolvers.first
|
|
27
|
+
sender.define_singleton_method(:send_async) { |*| }
|
|
28
|
+
request = Dnsruby::Message.new('fork.example.')
|
|
29
|
+
request.do_caching = request.do_validation = false
|
|
30
|
+
results = Queue.new
|
|
31
|
+
resolver.send_async(request, results, :reused)
|
|
32
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
33
|
+
inherited_attempt = engine.instance_variable_get(:@query_list)[:reused][3].first
|
|
34
|
+
socket, peer = Socket.pair(:UNIX, :STREAM, 0)
|
|
35
|
+
pending_socket, pending_peer = Socket.pair(:UNIX, :STREAM, 0)
|
|
36
|
+
selector.track_socket(pending_socket) # A socket still connecting/writing.
|
|
37
|
+
settings = klass::QuerySettings.new(request.encode, request, false, Queue.new,
|
|
38
|
+
:parent_stream, socket, '127.0.0.1', 53, Dnsruby::TransportClock.now + 60, 512, sender)
|
|
39
|
+
settings.is_persistent_socket = true
|
|
40
|
+
settings.tcp_pipelining_max_queries = :infinite
|
|
41
|
+
selector.add_to_select(settings)
|
|
42
|
+
sender.instance_variable_set(:@pipeline_socket, socket)
|
|
43
|
+
sender.instance_variable_set(:@use_counts, { socket => 1 })
|
|
44
|
+
writer_lock = Mutex.new
|
|
45
|
+
writer_lock.lock
|
|
46
|
+
sender.instance_variable_set(:@pipeline_mutex, writer_lock)
|
|
47
|
+
wakeups = klass.class_variable_get(:@@wakeup_sockets).dup
|
|
48
|
+
started, release = Queue.new, Queue.new
|
|
49
|
+
selector.defer_send(sender) { started << true; release.pop }
|
|
50
|
+
started.pop
|
|
51
|
+
|
|
52
|
+
child_status = ForkTestHelper.run do
|
|
53
|
+
begin
|
|
54
|
+
# A retained reference must be as safe as fetching the singleton.
|
|
55
|
+
raise 'inherited transaction survived' if selector.query_settings_for_id(:parent_stream)
|
|
56
|
+
raise 'singleton changed' unless Dnsruby::SelectThread.instance.equal?(selector)
|
|
57
|
+
raise 'inherited descriptors open' unless ([socket, pending_socket] + wakeups).all?(&:closed?)
|
|
58
|
+
[:query_hash, :socket_hash, :timeouts, :observers, :observer_queue_counts,
|
|
59
|
+
:tcp_buffers, :socket_owners, :socket_remaining_queries,
|
|
60
|
+
:queued_exceptions, :queued_responses, :queued_validation_responses,
|
|
61
|
+
:validations, :tick_observers].each do |name|
|
|
62
|
+
raise "inherited #{name}" unless klass.class_variable_get("@@#{name}").empty?
|
|
63
|
+
end
|
|
64
|
+
raise 'inherited sockets still selected' unless klass.class_variable_get(:@@sockets).size == 1
|
|
65
|
+
raise 'new wakeup pair unavailable' unless klass.class_variable_get(:@@wakeup_sockets).none?(&:closed?)
|
|
66
|
+
raise 'inherited workers retained' if selector.instance_variable_get(:@send_workers)
|
|
67
|
+
selector.defer_send(sender) { :child_work }.value == :child_work or raise 'new worker failed'
|
|
68
|
+
|
|
69
|
+
# A real pipeline send must choose a child-owned connection before
|
|
70
|
+
# taking the writer lock that the parent held when it forked.
|
|
71
|
+
child_socket, child_peer = Socket.pair(:UNIX, :STREAM, 0)
|
|
72
|
+
sender.tcp_pipelining = true
|
|
73
|
+
sender.define_singleton_method(:connect_tcp) do |*|
|
|
74
|
+
selector.track_socket(child_socket)
|
|
75
|
+
end
|
|
76
|
+
direct_results = Queue.new
|
|
77
|
+
sender.send_dnsruby(request.encode, request, :child_stream, direct_results, true)
|
|
78
|
+
raise 'child did not open its own pipeline' unless sender.instance_variable_get(:@pipeline_socket).equal?(child_socket)
|
|
79
|
+
raise 'inherited writer mutex retained' if sender.instance_variable_get(:@pipeline_mutex).equal?(writer_lock)
|
|
80
|
+
raise 'inherited use counts retained' if sender.instance_variable_get(:@use_counts).key?(socket)
|
|
81
|
+
frame_size = child_peer.read(2).unpack1('n')
|
|
82
|
+
child_request = Dnsruby::Message.decode(child_peer.read(frame_size))
|
|
83
|
+
raise 'child frame incorrect' unless child_request.question == request.question
|
|
84
|
+
direct_answer = request.clone
|
|
85
|
+
direct_answer.header.qr = true
|
|
86
|
+
selector.process_message(direct_answer, direct_answer.encode, child_socket)
|
|
87
|
+
selector.send_queued_validation_responses
|
|
88
|
+
raise 'child pipeline response failed' unless direct_results.size == 2 &&
|
|
89
|
+
2.times.map { direct_results.pop }.all? { |event| event.last.nil? }
|
|
90
|
+
sender.close
|
|
91
|
+
child_peer.close
|
|
92
|
+
|
|
93
|
+
# Resolver IDs also belong to the parent and can be reused by child
|
|
94
|
+
# transactions without delivering a spurious duplicate-ID failure.
|
|
95
|
+
resolver.send_async(request.clone, results, :reused)
|
|
96
|
+
current = engine.instance_variable_get(:@query_list)[:reused]
|
|
97
|
+
raise 'child query rejected' unless current && results.empty?
|
|
98
|
+
pending = selector.query_settings_for_id(current[3].first)
|
|
99
|
+
raise 'child attempt absent' unless pending
|
|
100
|
+
answer = request.clone
|
|
101
|
+
answer.header.qr = true
|
|
102
|
+
answer.add_answer(Dnsruby::RR.create('fork.example. 60 IN A 192.0.2.1'))
|
|
103
|
+
selector.push_response_for_query(pending, answer, pending.query, sender)
|
|
104
|
+
selector.send_queued_responses
|
|
105
|
+
raise 'child completion failed' unless results.size == 1 && results.pop.last.nil?
|
|
106
|
+
resolver.close
|
|
107
|
+
exit! 0
|
|
108
|
+
rescue Exception => error
|
|
109
|
+
warn error.full_message
|
|
110
|
+
exit! 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
raise 'fork child failed' unless child_status.success?
|
|
114
|
+
raise 'parent descriptor closed' if ([socket, pending_socket] + wakeups).any?(&:closed?)
|
|
115
|
+
raise 'parent transaction discarded' unless selector.query_settings_for_id(inherited_attempt)
|
|
116
|
+
raise 'parent observer discarded' unless klass.class_variable_get(:@@observers).size == 1
|
|
117
|
+
raise 'parent received child result' unless results.empty?
|
|
118
|
+
peer.write('parent')
|
|
119
|
+
raise 'parent connection damaged' unless socket.read(6) == 'parent'
|
|
120
|
+
release << true
|
|
121
|
+
writer_lock.unlock
|
|
122
|
+
resolver.close
|
|
123
|
+
[socket, peer, pending_socket, pending_peer].each { |io| io.close unless io.closed? }
|
|
124
|
+
end
|
|
125
|
+
CHILD
|
|
126
|
+
source = "require #{File.expand_path('fork_test_helper', __dir__).inspect}\n#{code}"
|
|
127
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-I',
|
|
128
|
+
File.expand_path('../lib', __dir__), '-e', source)
|
|
129
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
130
|
+
assert_empty(stderr)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_fork_timeout_reaps_the_child_and_closes_its_output_pipes
|
|
134
|
+
skip 'fork unavailable' unless Process.respond_to?(:fork)
|
|
135
|
+
source = <<~RUBY
|
|
136
|
+
require #{File.expand_path('fork_test_helper', __dir__).inspect}
|
|
137
|
+
# Start the timer service before forking, as the transport tests do.
|
|
138
|
+
Timeout.timeout(3) do
|
|
139
|
+
status = ForkTestHelper.run(timeout: 0.05) { sleep }
|
|
140
|
+
raise 'stalled child reported success' unless status.exitstatus == 1
|
|
141
|
+
end
|
|
142
|
+
begin
|
|
143
|
+
Process.wait(-1, Process::WNOHANG)
|
|
144
|
+
raise 'child was not reaped'
|
|
145
|
+
rescue Errno::ECHILD
|
|
146
|
+
end
|
|
147
|
+
RUBY
|
|
148
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-e', source)
|
|
149
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
150
|
+
assert_match(/Timeout::Error/, stderr)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_parent_interruption_also_reaps_its_waiting_child
|
|
154
|
+
skip 'fork unavailable' unless Process.respond_to?(:fork)
|
|
155
|
+
source = <<~RUBY
|
|
156
|
+
require #{File.expand_path('fork_test_helper', __dir__).inspect}
|
|
157
|
+
class ParentInterrupted < StandardError; end
|
|
158
|
+
# This thread exists only in the parent, so the child cannot inherit
|
|
159
|
+
# a pending Timeout request and finish before the parent is interrupted.
|
|
160
|
+
interrupter = Thread.new { sleep 0.1; Thread.main.raise ParentInterrupted }
|
|
161
|
+
begin
|
|
162
|
+
ForkTestHelper.run { sleep }
|
|
163
|
+
raise 'parent was not interrupted'
|
|
164
|
+
rescue ParentInterrupted
|
|
165
|
+
ensure
|
|
166
|
+
interrupter.kill
|
|
167
|
+
interrupter.join
|
|
168
|
+
end
|
|
169
|
+
begin
|
|
170
|
+
Process.wait(-1, Process::WNOHANG)
|
|
171
|
+
raise 'child was not reaped'
|
|
172
|
+
rescue Errno::ECHILD
|
|
173
|
+
end
|
|
174
|
+
RUBY
|
|
175
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-e', source)
|
|
176
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
177
|
+
assert_empty(stderr)
|
|
178
|
+
end
|
|
179
|
+
end
|
data/test/tc_tsig.rb
CHANGED
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
require_relative 'spec_helper'
|
|
18
18
|
|
|
19
|
-
require "digest/md5"
|
|
20
19
|
class TestTSig < Minitest::Test
|
|
21
20
|
|
|
22
21
|
include Dnsruby
|
|
23
22
|
|
|
23
|
+
# The online fixture must configure this key for HMAC-SHA256 on the server.
|
|
24
24
|
KEY_NAME="rubytsig"
|
|
25
25
|
KEY = "8n6gugn4aJ7MazyNlMccGKH1WxD2B3UvN/O/RA6iBupO2/03u9CTa3Ewz3gBWTSBCH3crY4Kk+tigNdeJBAvrw=="
|
|
26
26
|
def is_empty(string)
|
|
@@ -52,7 +52,7 @@ class TestTSig < Minitest::Test
|
|
|
52
52
|
:type => "TSIG",
|
|
53
53
|
:ttl => 0,
|
|
54
54
|
:klass => "ANY",
|
|
55
|
-
:algorithm => "hmac-
|
|
55
|
+
:algorithm => "hmac-sha256",
|
|
56
56
|
:fudge => 300,
|
|
57
57
|
:key => KEY,
|
|
58
58
|
:error => 0
|