dnsruby 1.74.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +302 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/demo/rubydig.rb +0 -2
- data/dnsruby.gemspec +5 -4
- data/lib/dnsruby/cache.rb +279 -47
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +75 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +645 -203
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +795 -339
- data/lib/dnsruby/resolver.rb +468 -186
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +19 -0
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +979 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +42 -20
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +21 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +105 -10
- data/.coveralls.yml +0 -2
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
require_relative 'tc_pipeline_send'
|
|
2
|
+
|
|
3
|
+
class ReviewTransportFixesTest < Minitest::Test
|
|
4
|
+
def test_resolver_early_errors_keep_the_public_completion_protocol
|
|
5
|
+
isolated(<<~'CODE')
|
|
6
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
|
|
7
|
+
invalid = query
|
|
8
|
+
rr = Dnsruby::RR.get_class(62001, 1).new('x'.b * 65_536)
|
|
9
|
+
rr.name, rr.ttl, rr.type, rr.klass = 'size.example.', 0, 62001, 1
|
|
10
|
+
invalid.add_additional(rr)
|
|
11
|
+
[invalid, sized_message(65_536)].each do |message|
|
|
12
|
+
begin
|
|
13
|
+
resolver.send_message(message)
|
|
14
|
+
raise 'encoding error returned as a successful Message'
|
|
15
|
+
rescue Dnsruby::EncodeError
|
|
16
|
+
end
|
|
17
|
+
reply, error = resolver.send_message!(message)
|
|
18
|
+
raise 'bang response tuple lost encoding error' unless reply.nil? && error.is_a?(Dnsruby::EncodeError)
|
|
19
|
+
queue = Queue.new
|
|
20
|
+
resolver.send_async(message, queue, :invalid)
|
|
21
|
+
tuple = queue.pop(true)
|
|
22
|
+
raise 'malformed async encode tuple' unless tuple.length == 3 && tuple[0] == :invalid &&
|
|
23
|
+
tuple[1].nil? && tuple[2].is_a?(Dnsruby::EncodeError)
|
|
24
|
+
raise 'duplicate early completion' unless queue.empty?
|
|
25
|
+
end
|
|
26
|
+
queue = Queue.new
|
|
27
|
+
resolver.send_async(Object.new, queue, :wrong_type)
|
|
28
|
+
tuple = queue.pop(true)
|
|
29
|
+
raise 'malformed wrong-type tuple' unless tuple.length == 3 && tuple[0] == :wrong_type &&
|
|
30
|
+
tuple[1].nil? && tuple[2].is_a?(ArgumentError)
|
|
31
|
+
raise 'early error acquired transport state' unless K.class_variable_get(:@@query_hash).empty?
|
|
32
|
+
resolver.close
|
|
33
|
+
CODE
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_duplicate_id_error_leaves_the_original_query_owned
|
|
37
|
+
isolated(<<~'CODE')
|
|
38
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
39
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false, use_tcp: true)
|
|
40
|
+
socket = FrameSocket.new
|
|
41
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **options| socket }
|
|
42
|
+
first, duplicate = Queue.new, Queue.new
|
|
43
|
+
resolver.send_async(query('first.example.'), first, :same)
|
|
44
|
+
written = socket.written.dup
|
|
45
|
+
resolver.send_async(query('second.example.'), duplicate, :same)
|
|
46
|
+
tuple = duplicate.pop(true)
|
|
47
|
+
raise 'malformed duplicate tuple' unless tuple.length == 3 && tuple[0] == :same &&
|
|
48
|
+
tuple[1].nil? && tuple[2].is_a?(ArgumentError)
|
|
49
|
+
raise 'duplicate query wrote bytes' unless socket.written == written
|
|
50
|
+
raise 'duplicate query completed the original' unless first.empty?
|
|
51
|
+
resolver.close
|
|
52
|
+
tuple = first.pop(true)
|
|
53
|
+
raise 'original query lost after duplicate' unless tuple == [:same, nil, tuple.last] &&
|
|
54
|
+
tuple.last.is_a?(Dnsruby::OtherResolvError)
|
|
55
|
+
drain_errors
|
|
56
|
+
raise 'duplicate completion' unless first.empty? && duplicate.empty?
|
|
57
|
+
raise 'query state retained' unless K.class_variable_get(:@@query_hash).empty?
|
|
58
|
+
CODE
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_pipeline_configuration_changes_rotate_streams_while_peers_drain
|
|
62
|
+
isolated(<<~'CODE')
|
|
63
|
+
created = []
|
|
64
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **options|
|
|
65
|
+
socket = FrameSocket.new
|
|
66
|
+
socket.define_singleton_method(:connect) { |*| raise Errno::EISCONN }
|
|
67
|
+
created << [socket, args]
|
|
68
|
+
socket
|
|
69
|
+
end
|
|
70
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', port: 5301,
|
|
71
|
+
use_tcp: true, tcp_pipelining: true, dnssec: false)
|
|
72
|
+
send_query = ->(id) do
|
|
73
|
+
q = query("configuration-#{id}.example.")
|
|
74
|
+
q.header.id = id + 100
|
|
75
|
+
writer.send_dnsruby(q.encode, q, id, Queue.new, true)
|
|
76
|
+
end
|
|
77
|
+
send_query.call(0)
|
|
78
|
+
original = created.last.first
|
|
79
|
+
send_query.call(1)
|
|
80
|
+
raise 'random source port selection rotated a reusable stream' unless created.length == 1
|
|
81
|
+
changes = [
|
|
82
|
+
-> { writer.port = 5302 },
|
|
83
|
+
-> { writer.server = '127.0.0.2' },
|
|
84
|
+
-> { writer.src_address = '127.0.0.3' },
|
|
85
|
+
-> { writer.src_port = [61_001, 61_002] },
|
|
86
|
+
-> { writer.src_port << 61_003 },
|
|
87
|
+
-> { writer.server = '::1'; writer.src_address6 = '::1' }
|
|
88
|
+
]
|
|
89
|
+
changes.each_with_index do |change, index|
|
|
90
|
+
previous = created.last.first
|
|
91
|
+
change.call
|
|
92
|
+
send_query.call(index + 2)
|
|
93
|
+
raise 'configuration reused old endpoint or binding' unless created.length == index + 2
|
|
94
|
+
raise 'rotation closed pending peer' if previous.closed?
|
|
95
|
+
raise 'new stream was already closed' if created.last.first.closed?
|
|
96
|
+
end
|
|
97
|
+
raise 'changed port not used' unless created[1][1][1] == 5302
|
|
98
|
+
raise 'changed server not used' unless created[2][1][0] == '127.0.0.2'
|
|
99
|
+
raise 'changed source not used' unless created[3][1][2] == '127.0.0.3'
|
|
100
|
+
raise 'changed IPv6 source not used' unless created.last[1][0, 3] == ['::1', 5302, '::1']
|
|
101
|
+
SELECTOR.remove_id(0)
|
|
102
|
+
raise 'first stream closed before last peer drained' if original.closed?
|
|
103
|
+
SELECTOR.remove_id(1)
|
|
104
|
+
raise 'retired stream remained open after last peer' unless original.closed?
|
|
105
|
+
(2..7).each { |id| SELECTOR.remove_id(id) }
|
|
106
|
+
writer.close
|
|
107
|
+
CODE
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_single_resolver_server_change_keeps_the_configured_port
|
|
111
|
+
resolver = Dnsruby::SingleResolver.new(server: '127.0.0.1', port: 5301)
|
|
112
|
+
resolver.server = '127.0.0.2'
|
|
113
|
+
assert_equal(5301, resolver.port)
|
|
114
|
+
assert_equal(5301, resolver.single_resolvers.first.port)
|
|
115
|
+
ensure
|
|
116
|
+
resolver.close if resolver
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_total_wire_limit_rejects_before_pipeline_admission_and_preserves_peers
|
|
120
|
+
isolated(<<~'CODE')
|
|
121
|
+
socket = FrameSocket.new
|
|
122
|
+
writer = sender(socket)
|
|
123
|
+
ordinary, queue = query('peer.example.'), Queue.new
|
|
124
|
+
ordinary.header.id = 100
|
|
125
|
+
writer.send_dnsruby(ordinary.encode, ordinary, :peer, queue, true)
|
|
126
|
+
peer = SELECTOR.query_settings_for_id(:peer)
|
|
127
|
+
boundary = sized_message(65_535)
|
|
128
|
+
boundary.header.id = 101
|
|
129
|
+
writer.send_dnsruby(boundary.encode, boundary, :boundary, Queue.new, true)
|
|
130
|
+
before = socket.written.dup
|
|
131
|
+
rejected = sized_message(65_536)
|
|
132
|
+
rejected.header.id = 102
|
|
133
|
+
errors = Queue.new
|
|
134
|
+
writer.send_dnsruby(rejected.encode, rejected, :oversized, errors, true)
|
|
135
|
+
drain_errors
|
|
136
|
+
event = errors.pop(true)
|
|
137
|
+
raise 'total size did not produce EncodeError' unless event[0] == :oversized && event.last.is_a?(Dnsruby::EncodeError)
|
|
138
|
+
raise 'oversized frame wrote or closed the pipeline' unless socket.written == before && !socket.closed?
|
|
139
|
+
raise 'oversized query acquired registration' if SELECTOR.query_settings_for_id(:oversized)
|
|
140
|
+
raise 'oversized query displaced peer' unless SELECTOR.query_settings_for_id(:peer).equal?(peer)
|
|
141
|
+
last = query('last.example.')
|
|
142
|
+
last.header.id = 103
|
|
143
|
+
writer.send_dnsruby(last.encode, last, :last, Queue.new, true)
|
|
144
|
+
expected = [ordinary, boundary, last].map { |q| [q.encode.bytesize].pack('n') + q.encode }.join
|
|
145
|
+
raise 'pipeline frames corrupted at wire boundary' unless socket.written == expected
|
|
146
|
+
raise 'duplicate encode error' unless errors.empty?
|
|
147
|
+
[:peer, :boundary, :last].each { |id| SELECTOR.remove_id(id) }
|
|
148
|
+
CODE
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_tsig_growth_is_checked_after_packet_preparation
|
|
152
|
+
skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
|
|
153
|
+
isolated(<<~'CODE')
|
|
154
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true, dnssec: false)
|
|
155
|
+
writer.tsig = ['key.example.', ['secret'].pack('m0')]
|
|
156
|
+
message = sized_message(65_520)
|
|
157
|
+
raise 'fixture must fit before signing' unless message.encode.bytesize == 65_520
|
|
158
|
+
queue = Queue.new
|
|
159
|
+
Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **options| raise 'oversized signed packet opened a socket' }
|
|
160
|
+
writer.send_async(message, queue, :signed)
|
|
161
|
+
drain_errors
|
|
162
|
+
event = queue.pop(true)
|
|
163
|
+
raise 'signed packet size error lost' unless event[0] == :signed && event.last.is_a?(Dnsruby::EncodeError)
|
|
164
|
+
raise 'oversized signed packet acquired query state' unless K.class_variable_get(:@@query_hash).empty?
|
|
165
|
+
CODE
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
SIZE_FIXTURE = <<~'CODE'
|
|
171
|
+
def sized_message(size)
|
|
172
|
+
q = query('size.example.')
|
|
173
|
+
q.send_raw = true
|
|
174
|
+
rr = Dnsruby::RR.get_class(62001, 1).new('')
|
|
175
|
+
rr.name, rr.ttl, rr.type, rr.klass = 'size.example.', 0, 62001, 1
|
|
176
|
+
q.add_additional(rr)
|
|
177
|
+
padding = size - q.encode.bytesize
|
|
178
|
+
full = Dnsruby::RR.get_class(62001, 1).new('x'.b * padding)
|
|
179
|
+
full.name, full.ttl, full.type, full.klass = 'size.example.', 0, 62001, 1
|
|
180
|
+
q.remove_additional
|
|
181
|
+
q.add_additional(full)
|
|
182
|
+
raise 'wrong fixture wire length' unless q.encode.bytesize == size
|
|
183
|
+
q
|
|
184
|
+
end
|
|
185
|
+
CODE
|
|
186
|
+
|
|
187
|
+
def isolated(body)
|
|
188
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
189
|
+
source = "#{TestPipelineSend::FIXTURE}\n#{SIZE_FIXTURE}\nTimeout.timeout(10) do\n#{body}\nend"
|
|
190
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
191
|
+
File.expand_path('../lib', __dir__), '-e', source)
|
|
192
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
193
|
+
assert_empty(stderr)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require_relative 'socket_helpers'
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
# Exercise cache and retry policy through the public API and real wire packets.
|
|
7
|
+
class ReviewTransportNativeTest < Minitest::Test
|
|
8
|
+
def setup
|
|
9
|
+
Dnsruby::PacketSender.clear_caches
|
|
10
|
+
@workers = []
|
|
11
|
+
@sockets = []
|
|
12
|
+
@resolvers = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def teardown
|
|
16
|
+
@resolvers.each(&:close)
|
|
17
|
+
@workers.each { |worker| worker.kill if worker.alive? }
|
|
18
|
+
@workers.each(&:value)
|
|
19
|
+
@sockets.each { |socket| socket.close unless socket.closed? }
|
|
20
|
+
Dnsruby::PacketSender.clear_caches
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_recursive_and_authoritative_caches_keep_question_classes_separate
|
|
24
|
+
[true, false].each do |recurse|
|
|
25
|
+
Dnsruby::PacketSender.clear_caches
|
|
26
|
+
port, requests = udp_server do |request|
|
|
27
|
+
response(request, request.question.first.qclass.to_s)
|
|
28
|
+
end
|
|
29
|
+
resolver = resolver_for(port, recurse: recurse)
|
|
30
|
+
%w[IN CH IN CH].each_with_index do |klass, index|
|
|
31
|
+
reply = query(resolver, klass)
|
|
32
|
+
assert_equal(klass, reply.question.first.qclass.to_s)
|
|
33
|
+
assert_equal(klass, reply.answer.first.klass.to_s)
|
|
34
|
+
assert_equal([klass], reply.answer.first.strings)
|
|
35
|
+
assert_equal(index >= 2, !!reply.cached)
|
|
36
|
+
end
|
|
37
|
+
assert_equal(2, requests.size, 'both classes should subsequently hit their own cache')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_unsigned_cache_cannot_satisfy_a_tsig_query
|
|
42
|
+
skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
|
|
43
|
+
[true, false].each do |recurse|
|
|
44
|
+
Dnsruby::PacketSender.clear_caches
|
|
45
|
+
port, requests = udp_server { |request| response(request, 'unsigned') }
|
|
46
|
+
resolver = resolver_for(port, recurse: recurse)
|
|
47
|
+
assert_equal(['unsigned'], query(resolver).answer.first.strings)
|
|
48
|
+
resolver.tsig = tsig_key
|
|
49
|
+
assert_raises(Dnsruby::TsigNotSignedResponseError) { query(resolver) }
|
|
50
|
+
assert_equal(2, requests.size, 'authentication must require a new wire exchange')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_verified_tsig_reply_cannot_enter_the_shared_unsigned_cache
|
|
55
|
+
skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
|
|
56
|
+
[true, false].each do |recurse|
|
|
57
|
+
Dnsruby::PacketSender.clear_caches
|
|
58
|
+
key = tsig_key
|
|
59
|
+
port, requests = udp_server do |request|
|
|
60
|
+
reply = response(request, request.tsig ? 'private' : 'public')
|
|
61
|
+
key.apply(reply, request) if request.tsig
|
|
62
|
+
reply
|
|
63
|
+
end
|
|
64
|
+
resolver = resolver_for(port, recurse: recurse)
|
|
65
|
+
resolver.tsig = key
|
|
66
|
+
signed = query(resolver)
|
|
67
|
+
assert(signed.verified?)
|
|
68
|
+
assert_nil(signed.tsig, 'verification strips the wire TSIG record')
|
|
69
|
+
assert_equal(['private'], signed.answer.first.strings)
|
|
70
|
+
resolver.tsig = nil
|
|
71
|
+
unsigned = query(resolver)
|
|
72
|
+
assert_equal(['public'], unsigned.answer.first.strings)
|
|
73
|
+
refute(unsigned.cached)
|
|
74
|
+
assert(query(resolver).cached)
|
|
75
|
+
assert_equal(2, requests.size)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_caller_opt_receives_a_reply_larger_than_the_configured_udp_default
|
|
80
|
+
[false, true].each do |dnssec|
|
|
81
|
+
port, requests = udp_server do |request|
|
|
82
|
+
reply = response(request, 'placeholder')
|
|
83
|
+
reply.answer.first.strings = Array.new(7) { 'x' * 240 }
|
|
84
|
+
reply
|
|
85
|
+
end
|
|
86
|
+
resolver = resolver_for(port, udp_size: 1232, dnssec: dnssec, do_caching: false)
|
|
87
|
+
request = Dnsruby::Message.new('large-opt.example.', 'TXT')
|
|
88
|
+
request.do_caching = request.do_validation = false
|
|
89
|
+
option = Dnsruby::RR::OPT::Option.new(65000, 'opaque'.b)
|
|
90
|
+
request.add_additional(Dnsruby::RR::OPT.new(4096, 0, [option]))
|
|
91
|
+
reply = Timeout.timeout(6) { resolver.send_message(request) }
|
|
92
|
+
refute(reply.cached, 'each variant must receive its own UDP reply')
|
|
93
|
+
assert_equal(Array.new(7) { 'x' * 240 }, reply.answer.first.strings)
|
|
94
|
+
assert_operator(reply.answersize, :>, 1232)
|
|
95
|
+
assert_operator(reply.answersize, :<=, 4096)
|
|
96
|
+
sent = requests.pop(true)
|
|
97
|
+
assert_equal(4096, sent.get_opt.payloadsize)
|
|
98
|
+
assert_equal([[65000, 'opaque']], sent.get_opt.options(nil).map { |opt| [opt.code, opt.data] })
|
|
99
|
+
assert_equal(1232, resolver.udp_size)
|
|
100
|
+
assert_empty(requests)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_authenticated_badtime_and_badtrunc_errors_are_delivered_after_forged_errors
|
|
105
|
+
[18, 22].each do |error_code|
|
|
106
|
+
key = tsig_key
|
|
107
|
+
port, requests = udp_server do |request|
|
|
108
|
+
reply = response(request, 'server error')
|
|
109
|
+
reply.header.rcode = Dnsruby::RCode::NOTAUTH
|
|
110
|
+
signer = key.dup
|
|
111
|
+
signer.error = error_code
|
|
112
|
+
signer.time_signed = request.tsig.time_signed
|
|
113
|
+
if error_code == 18
|
|
114
|
+
signer.other_data = [0, Time.now.to_i + 3600].pack('nN')
|
|
115
|
+
signer.other_size = 6
|
|
116
|
+
end
|
|
117
|
+
signature = signer.generate(reply, request)
|
|
118
|
+
reply.add_additional(signature)
|
|
119
|
+
valid = reply.encode
|
|
120
|
+
forged = Dnsruby::Message.decode(valid)
|
|
121
|
+
forged.tsig.mac = forged.tsig.mac.dup
|
|
122
|
+
forged.tsig.mac.setbyte(0, forged.tsig.mac.getbyte(0) ^ 1)
|
|
123
|
+
[forged.encode, valid]
|
|
124
|
+
end
|
|
125
|
+
resolver = resolver_for(port, do_caching: false)
|
|
126
|
+
resolver.tsig = key
|
|
127
|
+
error = assert_raises(Dnsruby::TsigError) { query(resolver) }
|
|
128
|
+
assert_includes(error.message, error_code == 18 ? 'BADTIME' : 'BADTRUNC')
|
|
129
|
+
assert_equal(1, requests.size, 'authenticated error should complete the original transaction')
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_default_udp_advertisement_retries_truncated_responses_over_tcp
|
|
134
|
+
[false, true].each do |dnssec|
|
|
135
|
+
port, udp_requests, tcp = udp_server(tcp: true) do |request|
|
|
136
|
+
reply = response(request, 'truncated')
|
|
137
|
+
reply.header.tc = true
|
|
138
|
+
reply
|
|
139
|
+
end
|
|
140
|
+
tcp_requests = Queue.new
|
|
141
|
+
worker do
|
|
142
|
+
client = tcp.accept
|
|
143
|
+
begin
|
|
144
|
+
request = read_frame(client)
|
|
145
|
+
tcp_requests << request
|
|
146
|
+
reply = response(request, 'complete')
|
|
147
|
+
reply.answer.first.strings = Array.new(7) { 'x' * 240 }
|
|
148
|
+
write_frame(client, reply)
|
|
149
|
+
ensure
|
|
150
|
+
client.close
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
resolver = resolver_for(port, dnssec: dnssec, do_caching: false)
|
|
154
|
+
request = Dnsruby::Message.new('truncated.example.', 'TXT')
|
|
155
|
+
request.do_caching = request.do_validation = false
|
|
156
|
+
reply = Timeout.timeout(6) { resolver.send_message(request) }
|
|
157
|
+
refute(reply.cached)
|
|
158
|
+
refute(reply.header.tc)
|
|
159
|
+
assert_equal(Array.new(7) { 'x' * 240 }, reply.answer.first.strings)
|
|
160
|
+
assert_operator(reply.encode.bytesize, :>, 1232)
|
|
161
|
+
assert_equal(1232, resolver.udp_size)
|
|
162
|
+
udp_request = udp_requests.pop(true)
|
|
163
|
+
tcp_request = tcp_requests.pop(true)
|
|
164
|
+
assert_equal(1232, udp_request.get_opt.payloadsize)
|
|
165
|
+
assert_equal(dnssec, udp_request.get_opt.dnssec_ok)
|
|
166
|
+
assert_equal(udp_request.header.id, tcp_request.header.id)
|
|
167
|
+
assert_equal(udp_request.question, tcp_request.question)
|
|
168
|
+
assert_empty(udp_requests)
|
|
169
|
+
assert_empty(tcp_requests)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def test_message_and_resolver_keys_authenticate_edns_and_tcp_retries
|
|
174
|
+
[false, true].product([:message, :resolver]).each do |dnssec, key_source|
|
|
175
|
+
key = tsig_key
|
|
176
|
+
received = 0
|
|
177
|
+
observed = Queue.new
|
|
178
|
+
port, _, tcp = udp_server(tcp: true) do |request, bytes|
|
|
179
|
+
signature = request.tsig
|
|
180
|
+
raise 'invalid UDP request MAC' unless signature &&
|
|
181
|
+
key.generate(request, nil, '', bytes, signature).mac == signature.mac
|
|
182
|
+
observed << [:udp, request.get_opt]
|
|
183
|
+
reply = response(request, 'retry')
|
|
184
|
+
reply.header.rcode = Dnsruby::RCode::FORMERR if received.zero?
|
|
185
|
+
reply.header.tc = true unless received.zero?
|
|
186
|
+
received += 1
|
|
187
|
+
key.apply(reply, request)
|
|
188
|
+
reply
|
|
189
|
+
end
|
|
190
|
+
worker do
|
|
191
|
+
client = tcp.accept
|
|
192
|
+
begin
|
|
193
|
+
length = client.read(2).unpack1('n')
|
|
194
|
+
bytes = client.read(length)
|
|
195
|
+
request = Dnsruby::Message.decode(bytes)
|
|
196
|
+
signature = request.tsig
|
|
197
|
+
raise 'invalid TCP request MAC' unless signature &&
|
|
198
|
+
key.generate(request, nil, '', bytes, signature).mac == signature.mac
|
|
199
|
+
observed << [:tcp, request.get_opt]
|
|
200
|
+
reply = response(request, 'authenticated')
|
|
201
|
+
key.apply(reply, request)
|
|
202
|
+
write_frame(client, reply)
|
|
203
|
+
ensure
|
|
204
|
+
client.close
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
resolver = resolver_for(port, dnssec: dnssec, do_caching: false)
|
|
208
|
+
request = Dnsruby::Message.new('signed-retry.example.', 'TXT')
|
|
209
|
+
request.header.rd = false
|
|
210
|
+
request.do_caching = request.do_validation = false
|
|
211
|
+
key_source == :message ? request.set_tsig(key) : resolver.tsig = key
|
|
212
|
+
reply = Timeout.timeout(6) { resolver.send_message(request) }
|
|
213
|
+
assert(reply.verified?)
|
|
214
|
+
assert_equal(['authenticated'], reply.answer.first.strings)
|
|
215
|
+
attempts = 3.times.map { observed.pop(true) }
|
|
216
|
+
assert_equal([:udp, :udp, :tcp], attempts.map(&:first))
|
|
217
|
+
assert_equal([true, false, false], attempts.map { |_, opt| !!opt })
|
|
218
|
+
assert_equal(1232, attempts.first.last.payloadsize)
|
|
219
|
+
assert_equal(dnssec, attempts.first.last.dnssec_ok)
|
|
220
|
+
assert_empty(observed)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def test_explicit_tcp_edns_retry_stays_on_tcp
|
|
225
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
226
|
+
@sockets << server
|
|
227
|
+
requests = Queue.new
|
|
228
|
+
worker do
|
|
229
|
+
2.times do |index|
|
|
230
|
+
client = server.accept
|
|
231
|
+
begin
|
|
232
|
+
request = read_frame(client)
|
|
233
|
+
requests << request
|
|
234
|
+
reply = response(request, 'tcp')
|
|
235
|
+
reply.header.rcode = Dnsruby::RCode::FORMERR if index.zero?
|
|
236
|
+
write_frame(client, reply)
|
|
237
|
+
ensure
|
|
238
|
+
client.close
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
resolver = resolver_for(server.addr[1], use_tcp: true, do_caching: false)
|
|
243
|
+
message = Dnsruby::Message.new('review.example.', 'TXT')
|
|
244
|
+
message.do_caching = message.do_validation = false
|
|
245
|
+
message.add_additional(Dnsruby::RR::OPT.new(4096))
|
|
246
|
+
reply = Timeout.timeout(6) { resolver.send_message(message) }
|
|
247
|
+
assert_equal(['tcp'], reply.answer.first.strings)
|
|
248
|
+
assert_equal(2, requests.size)
|
|
249
|
+
assert(requests.pop.get_opt)
|
|
250
|
+
assert_nil(requests.pop.get_opt)
|
|
251
|
+
assert(message.get_opt, 'the caller retains the original EDNS request')
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def test_udp_ignores_query_packets_and_wrong_opcodes_until_valid_reply
|
|
255
|
+
port, requests = udp_server do |request|
|
|
256
|
+
invalid_responses(request) + [response(request, 'valid')]
|
|
257
|
+
end
|
|
258
|
+
reply = query(resolver_for(port, do_caching: false))
|
|
259
|
+
assert_equal(['valid'], reply.answer.first.strings)
|
|
260
|
+
assert_equal(1, requests.size)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def test_tcp_ignores_query_frames_and_wrong_opcodes_until_valid_reply
|
|
264
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
265
|
+
@sockets << server
|
|
266
|
+
worker do
|
|
267
|
+
client = server.accept
|
|
268
|
+
begin
|
|
269
|
+
request = read_frame(client)
|
|
270
|
+
(invalid_responses(request) + [response(request, 'valid')]).each do |reply|
|
|
271
|
+
write_frame(client, reply)
|
|
272
|
+
end
|
|
273
|
+
ensure
|
|
274
|
+
client.close
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
reply = query(resolver_for(server.addr[1], use_tcp: true, do_caching: false))
|
|
278
|
+
assert_equal(['valid'], reply.answer.first.strings)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_udp_ignores_malformed_packets_with_unrelated_headers_without_retrying
|
|
282
|
+
port, requests = udp_server do |request|
|
|
283
|
+
malformed = invalid_responses(request).flat_map do |invalid|
|
|
284
|
+
[false, true].map do |truncated|
|
|
285
|
+
invalid.header.tc = truncated
|
|
286
|
+
bytes = invalid.encode
|
|
287
|
+
# Advertise an absent additional record, forcing Message.decode to
|
|
288
|
+
# expose its partially decoded header through DecodeError.
|
|
289
|
+
bytes[10, 2] = [1].pack('n')
|
|
290
|
+
bytes
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
malformed + [response(request, 'valid')]
|
|
294
|
+
end
|
|
295
|
+
reply = query(resolver_for(port, do_caching: false))
|
|
296
|
+
assert_equal(['valid'], reply.answer.first.strings)
|
|
297
|
+
assert_equal(1, requests.size)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def test_udp_ignores_incomplete_and_unrelated_malformed_questions_before_a_valid_reply
|
|
301
|
+
port, requests = udp_server do |request|
|
|
302
|
+
unrelated = Dnsruby::Message.new('unrelated.example.', 'AAAA', 'CH')
|
|
303
|
+
unrelated.header.id = request.header.id
|
|
304
|
+
unrelated.header.qr = true
|
|
305
|
+
malformed = [false, true].map do |truncated|
|
|
306
|
+
unrelated.header.tc = truncated
|
|
307
|
+
unrelated.encode.tap { |wire| wire[6, 2] = [1].pack('n') }
|
|
308
|
+
end
|
|
309
|
+
incomplete = [2, 11, 12, 15].map { |length| malformed.first.byteslice(0, length) }
|
|
310
|
+
incomplete + malformed + [response(request, 'valid')]
|
|
311
|
+
end
|
|
312
|
+
reply = query(resolver_for(port, do_caching: false))
|
|
313
|
+
assert_equal(['valid'], reply.answer.first.strings)
|
|
314
|
+
assert_equal(1, requests.size)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def test_short_deadline_query_on_busy_resolver_is_sent_before_its_deadline
|
|
318
|
+
port, requests = udp_server do |request|
|
|
319
|
+
request.question.first.qname.to_s == 'pending.example' ? [] : response(request, 'fast')
|
|
320
|
+
end
|
|
321
|
+
resolver = resolver_for(port, do_caching: false)
|
|
322
|
+
pending = Dnsruby::Message.new('pending.example.', 'TXT')
|
|
323
|
+
pending.do_caching = pending.do_validation = false
|
|
324
|
+
pending_results = Queue.new
|
|
325
|
+
resolver.send_async(pending, pending_results, :pending)
|
|
326
|
+
Timeout.timeout(2) { requests.pop }
|
|
327
|
+
sleep 0.03 # Start just after the selector's initial tick, while the first query remains pending.
|
|
328
|
+
resolver.query_timeout = 0.2
|
|
329
|
+
assert_equal(['fast'], query(resolver).answer.first.strings)
|
|
330
|
+
assert_equal(1, requests.size, 'the new query must reach the server while the first remains pending')
|
|
331
|
+
assert(pending_results.empty?)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
private
|
|
335
|
+
|
|
336
|
+
def worker(&block)
|
|
337
|
+
thread = Thread.new(&block)
|
|
338
|
+
thread.report_on_exception = false
|
|
339
|
+
@workers << thread
|
|
340
|
+
thread
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def udp_server(tcp: false, &handler)
|
|
344
|
+
if tcp
|
|
345
|
+
socket, listener = TestSockets.udp_tcp_pair
|
|
346
|
+
@sockets << listener
|
|
347
|
+
else
|
|
348
|
+
socket = UDPSocket.new
|
|
349
|
+
socket.bind('127.0.0.1', 0)
|
|
350
|
+
end
|
|
351
|
+
@sockets << socket
|
|
352
|
+
requests = Queue.new
|
|
353
|
+
worker do
|
|
354
|
+
loop do
|
|
355
|
+
bytes, peer = socket.recvfrom(65_535)
|
|
356
|
+
request = Dnsruby::Message.decode(bytes)
|
|
357
|
+
requests << request
|
|
358
|
+
replies = handler.call(request, bytes)
|
|
359
|
+
replies = [replies] unless replies.is_a?(Array)
|
|
360
|
+
replies.each do |reply|
|
|
361
|
+
bytes = reply.is_a?(String) ? reply : reply.encode
|
|
362
|
+
socket.send(bytes, 0, peer[3], peer[1])
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
[socket.addr[1], requests, listener]
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def resolver_for(port, options = {})
|
|
370
|
+
resolver = Dnsruby::Resolver.new({nameserver: '127.0.0.1', port: port,
|
|
371
|
+
dnssec: false, do_validation: false,
|
|
372
|
+
packet_timeout: 1, query_timeout: 3, retry_times: 1}.merge(options))
|
|
373
|
+
@resolvers << resolver
|
|
374
|
+
resolver
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def query(resolver, klass = 'IN')
|
|
378
|
+
Timeout.timeout(6) { resolver.query('review.example.', 'TXT', klass) }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def response(request, text)
|
|
382
|
+
question = request.question.first
|
|
383
|
+
reply = Dnsruby::Message.new
|
|
384
|
+
reply.header.id = request.header.id
|
|
385
|
+
reply.header.qr = reply.header.aa = reply.header.ra = true
|
|
386
|
+
reply.header.rd = request.header.rd
|
|
387
|
+
reply.header.opcode = request.header.opcode
|
|
388
|
+
reply.add_question(question.qname, question.qtype, question.qclass)
|
|
389
|
+
reply.add_answer(Dnsruby::RR.create("#{question.qname} 60 #{question.qclass} TXT \"#{text}\""))
|
|
390
|
+
reply
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def invalid_responses(request)
|
|
394
|
+
query_packet = response(request, 'invalid-query')
|
|
395
|
+
query_packet.header.qr = false
|
|
396
|
+
wrong_opcode = response(request, 'invalid-opcode')
|
|
397
|
+
wrong_opcode.header.opcode = 2 # STATUS
|
|
398
|
+
[query_packet, wrong_opcode]
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def tsig_key
|
|
402
|
+
Dnsruby::RR.create(name: 'review-key.example.', type: 'TSIG', klass: 'ANY',
|
|
403
|
+
key: 'c2VjcmV0', algorithm: 'hmac-sha256')
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def read_frame(socket)
|
|
407
|
+
length = socket.read(2).unpack1('n')
|
|
408
|
+
Dnsruby::Message.decode(socket.read(length))
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def write_frame(socket, reply)
|
|
412
|
+
bytes = reply.encode
|
|
413
|
+
socket.write([bytes.bytesize].pack('n') + bytes)
|
|
414
|
+
end
|
|
415
|
+
end
|