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,369 @@
|
|
|
1
|
+
require_relative 'tc_pipeline_send'
|
|
2
|
+
|
|
3
|
+
class ReviewPacketContractsTest < Minitest::Test
|
|
4
|
+
def test_both_caches_match_query_class_and_keep_ordinary_cache_hits
|
|
5
|
+
isolated(<<~'RUBY')
|
|
6
|
+
[true, false].each do |recursive|
|
|
7
|
+
Dnsruby::PacketSender.clear_caches
|
|
8
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false, recurse: recursive)
|
|
9
|
+
sent = []
|
|
10
|
+
writer.define_singleton_method(:send_dnsruby) { |*args| sent << args }
|
|
11
|
+
internet = cache_query('IN')
|
|
12
|
+
internet.header.rd = recursive
|
|
13
|
+
writer.make_query_packet(internet)
|
|
14
|
+
Dnsruby::PacketSender.cache(internet, answer(internet, 'internet'), scope: writer.positive_cache_scope(internet))
|
|
15
|
+
chaos = cache_query('CH')
|
|
16
|
+
chaos.header.rd = recursive
|
|
17
|
+
writer.send_async(chaos, Queue.new, :miss)
|
|
18
|
+
raise 'wrong class hit cached IN answer' unless sent.length == 1
|
|
19
|
+
writer.make_query_packet(chaos)
|
|
20
|
+
Dnsruby::PacketSender.cache(chaos, answer(chaos, 'chaos'), scope: writer.positive_cache_scope(chaos))
|
|
21
|
+
[['CH', 'chaos'], ['IN', 'internet']].each do |klass, text|
|
|
22
|
+
queue = Queue.new
|
|
23
|
+
writer.send_async(cache_query(klass), queue, klass)
|
|
24
|
+
SELECTOR.send_queued_responses
|
|
25
|
+
received = queue.pop(true)
|
|
26
|
+
validated = queue.pop(true)
|
|
27
|
+
raise 'cache hit lost' unless received[2].cached && sent.length == 1
|
|
28
|
+
raise 'wrong cached class' unless received[2].question.first.qclass == Dnsruby::Classes.new(klass)
|
|
29
|
+
raise 'wrong cached answer' unless received[2].answer.first.strings == [text]
|
|
30
|
+
raise 'cache completion duplicated' unless validated[1] == Dnsruby::Resolver::EventType::VALIDATED && queue.empty?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
RUBY
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_tsig_transactions_bypass_both_cache_reads_and_verified_response_writes
|
|
37
|
+
skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
|
|
38
|
+
isolated(<<~'RUBY')
|
|
39
|
+
[true, false].each do |recursive|
|
|
40
|
+
[:sender_key, :message_key, :presigned].each do |mode|
|
|
41
|
+
Dnsruby::PacketSender.clear_caches
|
|
42
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false, recurse: recursive)
|
|
43
|
+
sent = []
|
|
44
|
+
writer.define_singleton_method(:send_dnsruby) { |*args| sent << args }
|
|
45
|
+
plain = cache_query('IN')
|
|
46
|
+
plain.header.rd = recursive
|
|
47
|
+
writer.make_query_packet(plain)
|
|
48
|
+
Dnsruby::PacketSender.cache(plain, answer(plain, 'unsigned cache'), scope: writer.positive_cache_scope(plain))
|
|
49
|
+
key = signing_key
|
|
50
|
+
signed = cache_query('IN')
|
|
51
|
+
signed.header.rd = recursive
|
|
52
|
+
if mode == :sender_key
|
|
53
|
+
writer.tsig = key
|
|
54
|
+
elsif mode == :message_key
|
|
55
|
+
signed.set_tsig(key)
|
|
56
|
+
else
|
|
57
|
+
key.apply(signed)
|
|
58
|
+
end
|
|
59
|
+
queue = Queue.new
|
|
60
|
+
writer.send_async(signed, queue, :signed)
|
|
61
|
+
raise 'signed request read shared cache' unless sent.length == 1
|
|
62
|
+
raise 'signed query not sent on wire' unless Dnsruby::Message.decode(sent.first.first).tsig
|
|
63
|
+
raise 'cache response was queued' unless K.class_variable_get(:@@queued_responses).empty?
|
|
64
|
+
unsigned = answer(signed, 'unsigned server')
|
|
65
|
+
result = writer.check_response(unsigned, unsigned.encode, signed, queue, :signed, false)
|
|
66
|
+
raise 'unsigned server answer accepted' unless result == false
|
|
67
|
+
response = answer(signed, 'private signed answer')
|
|
68
|
+
key.apply(response, signed)
|
|
69
|
+
bytes = response.encode
|
|
70
|
+
response = Dnsruby::Message.decode(bytes)
|
|
71
|
+
raise 'signed reply not verified' unless writer.check_response(response, bytes, signed, queue, :signed, false) == true
|
|
72
|
+
raise 'fixture has not exercised TSIG stripping' unless response.verified? && response.tsig.nil?
|
|
73
|
+
Dnsruby::PacketSender.clear_caches
|
|
74
|
+
Dnsruby::PacketSender.cache(signed, response)
|
|
75
|
+
writer.tsig = nil
|
|
76
|
+
writer.send_async(cache_query('IN'), Queue.new, :plain)
|
|
77
|
+
raise 'verified private answer entered shared cache' unless sent.length == 2
|
|
78
|
+
raise 'signed reply produced a cached result' unless K.class_variable_get(:@@queued_responses).empty?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
RUBY
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_invalid_headers_are_rejected_before_tsig_mutation_or_retry
|
|
85
|
+
skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
|
|
86
|
+
isolated(<<~'RUBY')
|
|
87
|
+
[:query_packet, :wrong_opcode, :wrong_id].each do |invalid_header|
|
|
88
|
+
[:ordinary, :formerr, :truncated].each do |kind|
|
|
89
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
|
|
90
|
+
retries = []
|
|
91
|
+
writer.define_singleton_method(:send_async) { |*args| retries << args }
|
|
92
|
+
q = cache_query('IN')
|
|
93
|
+
q.add_additional(Dnsruby::RR::OPT.new(4096))
|
|
94
|
+
key = signing_key
|
|
95
|
+
key.apply(q)
|
|
96
|
+
reply = answer(q, 'ignored')
|
|
97
|
+
reply.header.qr = false if invalid_header == :query_packet
|
|
98
|
+
reply.header.opcode = Dnsruby::OpCode::Update if invalid_header == :wrong_opcode
|
|
99
|
+
reply.header.rcode = Dnsruby::RCode::FORMERR if kind == :formerr
|
|
100
|
+
reply.header.tc = true if kind == :truncated
|
|
101
|
+
key.apply(reply, q)
|
|
102
|
+
reply.header.id = (q.header.id + 1) % 65_536 if invalid_header == :wrong_id
|
|
103
|
+
bytes = reply.encode
|
|
104
|
+
reply = Dnsruby::Message.decode(bytes)
|
|
105
|
+
signature = reply.tsig
|
|
106
|
+
state = reply.tsigstate
|
|
107
|
+
result = writer.check_response(reply, bytes, q, Queue.new, :ignored, false)
|
|
108
|
+
raise "invalid #{invalid_header}/#{kind} consumed request" unless result == false
|
|
109
|
+
raise 'invalid header mutated TSIG' unless reply.tsig.equal?(signature) && reply.tsigstate == state
|
|
110
|
+
raise 'invalid header caused retry' unless retries.empty?
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
RUBY
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def test_invalid_then_valid_packets_preserve_query_and_pipeline_peer_ownership
|
|
117
|
+
isolated(<<~'RUBY')
|
|
118
|
+
[:query_packet, :wrong_opcode].each do |invalid_header|
|
|
119
|
+
socket = FrameSocket.new
|
|
120
|
+
writer = sender(socket)
|
|
121
|
+
q = query('target.example.')
|
|
122
|
+
q.header.id = 100
|
|
123
|
+
target = settings(socket, :target, q, writer)
|
|
124
|
+
peer = settings(socket, :peer, query('peer.example.'), writer)
|
|
125
|
+
peer.query.header.id = 100 # Exercise matching questions before response processing.
|
|
126
|
+
SELECTOR.add_to_select(target)
|
|
127
|
+
SELECTOR.add_to_select(peer)
|
|
128
|
+
invalid = answer(q, 'invalid')
|
|
129
|
+
invalid.header.qr = false if invalid_header == :query_packet
|
|
130
|
+
invalid.header.opcode = Dnsruby::OpCode::Update if invalid_header == :wrong_opcode
|
|
131
|
+
SELECTOR.process_message(invalid, invalid.encode, socket)
|
|
132
|
+
raise 'invalid reply completed target' unless SELECTOR.query_settings_for_id(:target).equal?(target)
|
|
133
|
+
raise 'invalid reply disturbed peer' unless SELECTOR.query_settings_for_id(:peer).equal?(peer)
|
|
134
|
+
raise 'invalid reply was delivered' unless target.client_queue.empty? && peer.client_queue.empty?
|
|
135
|
+
[peer, target].each do |current|
|
|
136
|
+
response = answer(current.query, 'valid')
|
|
137
|
+
SELECTOR.process_message(response, response.encode, socket)
|
|
138
|
+
SELECTOR.send_queued_validation_responses
|
|
139
|
+
events = 2.times.map { current.client_queue.pop(true) }
|
|
140
|
+
raise 'valid response not delivered exactly once' unless current.client_queue.empty? &&
|
|
141
|
+
events.map { |event| event[1] } == [Dnsruby::Resolver::EventType::RECEIVED, Dnsruby::Resolver::EventType::VALIDATED] &&
|
|
142
|
+
events.all? { |event| event.last.nil? && event[2].answer.first.strings == ['valid'] }
|
|
143
|
+
end
|
|
144
|
+
SELECTOR.retire_socket(socket)
|
|
145
|
+
assert_clean(socket)
|
|
146
|
+
end
|
|
147
|
+
RUBY
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_edns_fallback_keeps_the_attempt_transport_and_raw_mode_stays_raw
|
|
151
|
+
isolated(<<~'RUBY')
|
|
152
|
+
[true, false].each do |tcp|
|
|
153
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false, use_tcp: !tcp)
|
|
154
|
+
retries = []
|
|
155
|
+
writer.define_singleton_method(:send_async) { |*args| retries << args }
|
|
156
|
+
q = query
|
|
157
|
+
q.add_additional(Dnsruby::RR::OPT.new(4096))
|
|
158
|
+
original = q.encode
|
|
159
|
+
reply = answer(q, 'formerr')
|
|
160
|
+
reply.header.rcode = Dnsruby::RCode::FORMERR
|
|
161
|
+
raise 'FORMERR was delivered' unless writer.check_response(reply, reply.encode, q, Queue.new, :retry, tcp) == false
|
|
162
|
+
raise 'wrong retry transport' unless retries.length == 1 && retries.first[3] == tcp
|
|
163
|
+
raise 'EDNS not disabled for retry' unless retries.first[4] && retries.first.first.get_opt.nil?
|
|
164
|
+
raise 'caller query was mutated' unless q.encode == original
|
|
165
|
+
q.send_raw = true
|
|
166
|
+
reply.header.qr = false
|
|
167
|
+
reply.header.opcode = Dnsruby::OpCode::Update
|
|
168
|
+
raise 'raw response was filtered' unless writer.check_response(reply, reply.encode, q, Queue.new, :raw, tcp) == true
|
|
169
|
+
raise 'raw response retried' unless retries.length == 1
|
|
170
|
+
end
|
|
171
|
+
RUBY
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def test_malformed_invalid_headers_leave_udp_and_tcp_owners_active_for_valid_replies
|
|
175
|
+
isolated(<<~'RUBY')
|
|
176
|
+
[false, true].each do |tcp|
|
|
177
|
+
[false, true].each do |truncated|
|
|
178
|
+
[:query_packet, :wrong_opcode].each do |invalid_header|
|
|
179
|
+
socket = FrameSocket.new
|
|
180
|
+
writer = sender(socket)
|
|
181
|
+
retries = []
|
|
182
|
+
writer.define_singleton_method(:send_async) { |*args| retries << args }
|
|
183
|
+
q = query('target.example.')
|
|
184
|
+
q.header.id = 100
|
|
185
|
+
target = settings(socket, :target, q, writer)
|
|
186
|
+
target.is_persistent_socket = tcp
|
|
187
|
+
peer_socket = tcp ? socket : FrameSocket.new
|
|
188
|
+
peer = settings(peer_socket, :peer, query('peer.example.'), writer)
|
|
189
|
+
peer.query.header.id = 101
|
|
190
|
+
SELECTOR.add_to_select(target)
|
|
191
|
+
SELECTOR.add_to_select(peer)
|
|
192
|
+
flags = invalid_header == :query_packet ? 0 : 0xa800
|
|
193
|
+
flags |= 0x0200 if truncated
|
|
194
|
+
malformed = [100, flags, 1, 0, 0, 0].pack('n6') # Question is missing.
|
|
195
|
+
inject_packet(socket, malformed, tcp)
|
|
196
|
+
raise 'malformed packet decoded' if SELECTOR.get_incoming_data(socket, 512)
|
|
197
|
+
raise 'invalid malformed header triggered fallback' unless retries.empty?
|
|
198
|
+
raise 'invalid malformed header completed target' unless SELECTOR.query_settings_for_id(:target).equal?(target)
|
|
199
|
+
raise 'invalid malformed header consumed peer' unless SELECTOR.query_settings_for_id(:peer).equal?(peer)
|
|
200
|
+
raise 'invalid malformed header delivered an error' unless target.client_queue.empty? && peer.client_queue.empty?
|
|
201
|
+
valid = answer(q, 'valid')
|
|
202
|
+
inject_packet(socket, valid.encode, tcp)
|
|
203
|
+
incoming = SELECTOR.get_incoming_data(socket, 512)
|
|
204
|
+
raise 'following valid response was lost' unless incoming
|
|
205
|
+
SELECTOR.process_message(*incoming, socket)
|
|
206
|
+
SELECTOR.send_queued_validation_responses
|
|
207
|
+
events = 2.times.map { target.client_queue.pop(true) }
|
|
208
|
+
raise 'valid response not delivered once' unless target.client_queue.empty? &&
|
|
209
|
+
events.all? { |event| event.last.nil? && event[2].answer.first.strings == ['valid'] }
|
|
210
|
+
raise 'valid target response disturbed peer' unless SELECTOR.query_settings_for_id(:peer).equal?(peer)
|
|
211
|
+
SELECTOR.remove_id(:peer)
|
|
212
|
+
assert_clean(socket)
|
|
213
|
+
assert_clean(peer_socket)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
RUBY
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_malformed_matching_and_raw_headers_keep_existing_fallback_and_error_behavior
|
|
221
|
+
isolated(<<~'RUBY')
|
|
222
|
+
[false, true].each do |tcp|
|
|
223
|
+
[false, true].each do |truncated|
|
|
224
|
+
[false, true].each do |raw|
|
|
225
|
+
socket = FrameSocket.new
|
|
226
|
+
writer = sender(socket)
|
|
227
|
+
retries = []
|
|
228
|
+
writer.define_singleton_method(:send_async) { |*args| retries << args }
|
|
229
|
+
q = query
|
|
230
|
+
q.header.id = 100
|
|
231
|
+
q.send_raw = raw
|
|
232
|
+
current = settings(socket, :current, q, writer)
|
|
233
|
+
current.is_persistent_socket = tcp
|
|
234
|
+
SELECTOR.add_to_select(current)
|
|
235
|
+
# Raw queries intentionally retain permissive header handling.
|
|
236
|
+
flags = raw ? 0x2800 : 0x8000
|
|
237
|
+
flags |= 0x0200 if truncated
|
|
238
|
+
malformed = q.encode
|
|
239
|
+
malformed[2, 2] = [flags].pack('n')
|
|
240
|
+
malformed[6, 2] = [1].pack('n') # Complete question, missing answer.
|
|
241
|
+
inject_packet(socket, malformed, tcp)
|
|
242
|
+
raise 'malformed packet decoded' if SELECTOR.get_incoming_data(socket, 512)
|
|
243
|
+
if truncated && !tcp
|
|
244
|
+
raise 'valid/raw truncated UDP lost TCP fallback' unless retries.length == 1 && retries.first[3] == true
|
|
245
|
+
raise 'fallback emitted a premature error' unless current.client_queue.empty?
|
|
246
|
+
SELECTOR.remove_id(:current)
|
|
247
|
+
else
|
|
248
|
+
raise 'malformed TCP/nontruncated packet unexpectedly retried' unless retries.empty?
|
|
249
|
+
raise 'malformed packet did not report one decode error' unless current.client_queue.length == 1 &&
|
|
250
|
+
current.client_queue.pop(true).last.is_a?(Dnsruby::DecodeError)
|
|
251
|
+
raise 'terminal decode error retained owner' if SELECTOR.query_settings_for_id(:current)
|
|
252
|
+
end
|
|
253
|
+
assert_clean(socket)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
RUBY
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def test_short_malformed_headers_only_end_tcp_transactions
|
|
261
|
+
isolated(<<~'RUBY')
|
|
262
|
+
[false, true].each do |tcp|
|
|
263
|
+
[2, 4, 11].each do |length|
|
|
264
|
+
socket = FrameSocket.new
|
|
265
|
+
writer = sender(socket)
|
|
266
|
+
writer.define_singleton_method(:send_async) { |*| raise 'incomplete header triggered retry' }
|
|
267
|
+
q = query
|
|
268
|
+
q.header.id = 100
|
|
269
|
+
current = settings(socket, :current, q, writer)
|
|
270
|
+
current.is_persistent_socket = tcp
|
|
271
|
+
SELECTOR.add_to_select(current)
|
|
272
|
+
inject_packet(socket, [100, 0x2800, 1, 0, 0, 0].pack('n6').byteslice(0, length), tcp)
|
|
273
|
+
raise 'incomplete header decoded' if SELECTOR.get_incoming_data(socket, 512)
|
|
274
|
+
if tcp
|
|
275
|
+
raise 'incomplete TCP header error was suppressed' unless current.client_queue.length == 1 &&
|
|
276
|
+
current.client_queue.pop(true).last.is_a?(Dnsruby::DecodeError)
|
|
277
|
+
raise 'incomplete TCP header retained owner' if SELECTOR.query_settings_for_id(:current)
|
|
278
|
+
else
|
|
279
|
+
raise 'incomplete UDP header ended transaction' unless current.client_queue.empty? &&
|
|
280
|
+
SELECTOR.query_settings_for_id(:current).equal?(current)
|
|
281
|
+
SELECTOR.remove_id(:current)
|
|
282
|
+
end
|
|
283
|
+
assert_clean(socket)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
RUBY
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def test_malformed_udp_questions_cannot_fail_or_retry_an_unrelated_query
|
|
290
|
+
isolated(<<~'RUBY')
|
|
291
|
+
[:name, :type, :klass, :missing, :partial, :second].product([false, true]).each do |mismatch, truncated|
|
|
292
|
+
socket = FrameSocket.new
|
|
293
|
+
writer = sender(socket)
|
|
294
|
+
writer.define_singleton_method(:send_async) { |*| raise 'unrelated question triggered fallback' }
|
|
295
|
+
q = query('target.example.')
|
|
296
|
+
q.add_question('second.example.', 'AAAA') if mismatch == :second
|
|
297
|
+
current = settings(socket, :current, q, writer)
|
|
298
|
+
current.is_persistent_socket = false
|
|
299
|
+
SELECTOR.add_to_select(current)
|
|
300
|
+
original_deadline = current.endtime
|
|
301
|
+
reply = q.clone
|
|
302
|
+
reply.header.qr = true
|
|
303
|
+
reply.header.tc = truncated
|
|
304
|
+
case mismatch
|
|
305
|
+
when :name then reply.question.first.qname = Dnsruby::Name.create('unrelated.example.')
|
|
306
|
+
when :type then reply.question.first.qtype = Dnsruby::Types::AAAA
|
|
307
|
+
when :klass then reply.question.first.qclass = Dnsruby::Classes::CH
|
|
308
|
+
when :second then reply.question.last.qname = Dnsruby::Name.create('unrelated.example.')
|
|
309
|
+
end
|
|
310
|
+
wire = reply.encode
|
|
311
|
+
wire[6, 2] = [1].pack('n') # Missing answer follows the question.
|
|
312
|
+
wire = wire.byteslice(0, mismatch == :missing ? 12 : 15) if [:missing, :partial].include?(mismatch)
|
|
313
|
+
inject_packet(socket, wire, false)
|
|
314
|
+
raise 'malformed packet decoded' if SELECTOR.get_incoming_data(socket, 512)
|
|
315
|
+
raise 'unrelated question consumed transaction' unless current.client_queue.empty? &&
|
|
316
|
+
SELECTOR.query_settings_for_id(:current).equal?(current)
|
|
317
|
+
raise 'unrelated question extended deadline' unless current.endtime == original_deadline
|
|
318
|
+
valid = q.clone
|
|
319
|
+
valid.header.qr = true
|
|
320
|
+
inject_packet(socket, valid.encode, false)
|
|
321
|
+
incoming = SELECTOR.get_incoming_data(socket, 512)
|
|
322
|
+
SELECTOR.process_message(*incoming, socket)
|
|
323
|
+
SELECTOR.send_queued_validation_responses
|
|
324
|
+
raise 'following valid answer lost' unless current.client_queue.size == 2 &&
|
|
325
|
+
2.times.map { current.client_queue.pop }.all? { |event| event.last.nil? }
|
|
326
|
+
assert_clean(socket)
|
|
327
|
+
end
|
|
328
|
+
RUBY
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
private
|
|
332
|
+
|
|
333
|
+
CONTRACT_FIXTURE = <<~'RUBY'
|
|
334
|
+
def cache_query(klass)
|
|
335
|
+
Dnsruby::Message.new('contract.example.', Dnsruby::Types::TXT, klass).tap do |message|
|
|
336
|
+
message.do_validation = false
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
def answer(query, text)
|
|
340
|
+
question = query.question.first
|
|
341
|
+
Dnsruby::Message.new(question.qname, question.qtype, question.qclass).tap do |reply|
|
|
342
|
+
reply.header.id = query.header.id
|
|
343
|
+
reply.header.qr = reply.header.aa = true
|
|
344
|
+
reply.add_answer(Dnsruby::RR.create("#{question.qname} 60 #{question.qclass} TXT \"#{text}\""))
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
def signing_key
|
|
348
|
+
Dnsruby::RR.create(name: 'contract-key.example.', type: 'TSIG', klass: 'ANY',
|
|
349
|
+
key: 'c2VjcmV0', algorithm: 'hmac-sha256')
|
|
350
|
+
end
|
|
351
|
+
def inject_packet(socket, bytes, tcp)
|
|
352
|
+
if tcp
|
|
353
|
+
socket.incoming = [bytes.bytesize].pack('n') + bytes
|
|
354
|
+
else
|
|
355
|
+
socket.define_singleton_method(:getsockopt) { |*| Struct.new(:data).new([Socket::SOCK_DGRAM].pack('i')) }
|
|
356
|
+
socket.define_singleton_method(:recvfrom_nonblock) { |*| [bytes, ['AF_INET', 53, '127.0.0.1', '127.0.0.1']] }
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
RUBY
|
|
360
|
+
|
|
361
|
+
def isolated(body)
|
|
362
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
363
|
+
source = "#{TestPipelineSend::FIXTURE}\n#{CONTRACT_FIXTURE}\nTimeout.timeout(10) do\n#{body}\nend"
|
|
364
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
365
|
+
File.expand_path('../lib', __dir__), '-e', source)
|
|
366
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
367
|
+
assert_empty(stderr)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'stringio'
|
|
3
|
+
|
|
4
|
+
class TestReviewPresentation < Minitest::Test
|
|
5
|
+
def presentation
|
|
6
|
+
Dnsruby.const_get(:Presentation)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_record_parsing_never_enumerates_learned_class_names
|
|
10
|
+
original = Dnsruby::Classes.method(:regexp)
|
|
11
|
+
own_method = Dnsruby::Classes.singleton_methods(false).include?(:regexp)
|
|
12
|
+
Dnsruby::Classes.define_singleton_method(:regexp) do
|
|
13
|
+
raise 'record parsing must use an indexed class lookup'
|
|
14
|
+
end
|
|
15
|
+
record = Dnsruby::RR.create('example. 300 IN A 192.0.2.1')
|
|
16
|
+
assert_equal('192.0.2.1', record.address.to_s)
|
|
17
|
+
record = Dnsruby::RR.create("example. 300 IN TXT 'one(two);three'")
|
|
18
|
+
assert_equal(['one(two);three'], record.strings)
|
|
19
|
+
ensure
|
|
20
|
+
if original
|
|
21
|
+
Dnsruby::Classes.singleton_class.remove_method(:regexp)
|
|
22
|
+
Dnsruby::Classes.define_singleton_method(:regexp, original) if own_method
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_numeric_and_registered_class_tokens_preserve_quote_boundaries
|
|
27
|
+
arrays = Dnsruby::CodeMapper.class_variable_get(:@@arrays)
|
|
28
|
+
original = arrays.fetch(Dnsruby::Classes)
|
|
29
|
+
replacement = Dnsruby::CodeMapper::Arrays.new
|
|
30
|
+
%i[strings stringsdown values].each do |name|
|
|
31
|
+
replacement.public_send("#{name}=", original.public_send(name).dup)
|
|
32
|
+
end
|
|
33
|
+
replacement.maxcode = original.maxcode
|
|
34
|
+
arrays[Dnsruby::Classes] = replacement
|
|
35
|
+
Dnsruby::Classes.add_pair('PRIVATE', 60_000)
|
|
36
|
+
%w[IN in CLASS65000 class65000 PRIVATE private].each do |klass|
|
|
37
|
+
text = "owner 300 #{klass} TXT('one(two);three')"
|
|
38
|
+
assert_equal(text.index('TXT') + 3, presentation.single_quoted_start(text), klass)
|
|
39
|
+
end
|
|
40
|
+
assert_nil(presentation.single_quoted_start("owner 300 UNREGISTERED TXT 'value'"))
|
|
41
|
+
assert_nil(presentation.single_quoted_start("owner 300 IN A 'value'"))
|
|
42
|
+
ensure
|
|
43
|
+
arrays[Dnsruby::Classes] = original if original
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_txt_continuation_recognizes_apostrophes_on_later_lines
|
|
47
|
+
%w[TXT SPF TYPE16 TYPE99 TYPE016 TYPE099 TYPE00016 TYPE00099].each do |type|
|
|
48
|
+
text = "owner 300 IN #{type} (\n 'one(two);three'\n 'four'\n)\n"
|
|
49
|
+
record = Dnsruby::ZoneReader.new('example.').process_io(StringIO.new(text)).first
|
|
50
|
+
assert_equal(['one(two);three', 'four'], record.strings, type)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_numeric_txt_and_spf_preserve_single_quoted_punctuation
|
|
55
|
+
%w[TYPE016 TYPE099 TYPE00016 TYPE00099].each do |type|
|
|
56
|
+
text = "owner 300 IN #{type} 'one(two);three'"
|
|
57
|
+
direct = Dnsruby::RR.create(text)
|
|
58
|
+
reader = Dnsruby::ZoneReader.new('example.')
|
|
59
|
+
normalized = reader.normalise_line(text)
|
|
60
|
+
record = reader.process_io(StringIO.new(text + "\n")).first
|
|
61
|
+
assert_equal(['one(two);three'], direct.strings, type)
|
|
62
|
+
assert_equal(direct.strings, Dnsruby::RR.create(normalized).strings, type)
|
|
63
|
+
assert_equal(direct.strings, record.strings, type)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_normalised_lines_remain_strings_accepted_by_public_rr_constructor
|
|
68
|
+
[
|
|
69
|
+
'www 300 IN A 192.0.2.1',
|
|
70
|
+
"txt 300 IN TYPE016 'one(two);three'",
|
|
71
|
+
'txt 300 IN TXT "one(two);three" \\"escaped ; ignored',
|
|
72
|
+
'https 300 IN HTTPS 1 svc alpn="h2,h3"',
|
|
73
|
+
'opaque 300 CLASS65000 TYPE65001 \\# 0'
|
|
74
|
+
].each do |text|
|
|
75
|
+
reader = Dnsruby::ZoneReader.new('example.')
|
|
76
|
+
line = reader.process_line(text.freeze)
|
|
77
|
+
assert_instance_of(String, line)
|
|
78
|
+
expected = Dnsruby::RR.create(line)
|
|
79
|
+
record = Dnsruby::ZoneReader.new('example.').process_io(StringIO.new(text + "\n")).first
|
|
80
|
+
assert_equal(expected, record, text)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_process_line_preserves_subclass_normalization_hook_and_prefix_arguments
|
|
85
|
+
seen = []
|
|
86
|
+
reader_class = Class.new(Dnsruby::ZoneReader) do
|
|
87
|
+
define_method(:normalise_line) do |line, prefix = false|
|
|
88
|
+
seen << [line, prefix]
|
|
89
|
+
# Replacement strings must be scanned even though process_line has
|
|
90
|
+
# already scanned the original input passed to this hook.
|
|
91
|
+
super(line.sub('192.0.2.1', '(192.0.2.2) ; added comment ('), prefix)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
line, type, name = reader_class.new('example.').process_line('www 300 IN A 192.0.2.1', true)
|
|
95
|
+
assert_equal(1, seen.length)
|
|
96
|
+
assert_instance_of(String, seen.first.first)
|
|
97
|
+
assert_equal(true, seen.first.last)
|
|
98
|
+
assert_equal('A', type)
|
|
99
|
+
assert_equal('www.example.', name)
|
|
100
|
+
assert_equal('192.0.2.2', Dnsruby::RR.create(line).address.to_s)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_prepend_hook_failure_does_not_leave_input_marked_as_scanned
|
|
104
|
+
first_line = nil
|
|
105
|
+
hook = Module.new do
|
|
106
|
+
define_method(:normalise_line) do |line, prefix = false|
|
|
107
|
+
unless first_line
|
|
108
|
+
first_line = line
|
|
109
|
+
raise ArgumentError, 'normalization hook rejected input'
|
|
110
|
+
end
|
|
111
|
+
super(line, prefix)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
reader_class = Class.new(Dnsruby::ZoneReader) { prepend hook }
|
|
115
|
+
reader = reader_class.new('example.')
|
|
116
|
+
assert_raises(ArgumentError) { reader.process_line('www 300 IN A 192.0.2.1') }
|
|
117
|
+
# Reusing the same String after the failed call is a direct public call,
|
|
118
|
+
# so its new grouping and comments must receive the normal scanner.
|
|
119
|
+
first_line.replace('www 300 IN A (192.0.2.3) ; added comment (')
|
|
120
|
+
line = reader.normalise_line(first_line)
|
|
121
|
+
assert_equal('192.0.2.3', Dnsruby::RR.create(line).address.to_s)
|
|
122
|
+
assert_equal('192.0.2.4', Dnsruby::RR.create(reader.process_line('next 300 IN A 192.0.2.4')).address.to_s)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_normalization_hook_can_mutate_the_input_before_super
|
|
126
|
+
reader_class = Class.new(Dnsruby::ZoneReader) do
|
|
127
|
+
def normalise_line(line, prefix = false)
|
|
128
|
+
line.sub!('192.0.2.1', '(192.0.2.2) ; added comment (')
|
|
129
|
+
super
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
text = "www 300 IN A 192.0.2.1\n"
|
|
133
|
+
record = reader_class.new('example.').process_io(StringIO.new(text)).first
|
|
134
|
+
assert_equal('www.example.', record.name.to_s(true))
|
|
135
|
+
assert_equal('192.0.2.2', record.address.to_s)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_process_io_parses_presentation_returned_by_a_replacement_hook
|
|
139
|
+
reader_class = Class.new(Dnsruby::ZoneReader) do
|
|
140
|
+
def normalise_line(line, prefix = false)
|
|
141
|
+
# Applications can provide the normalization themselves and return
|
|
142
|
+
# any valid RR presentation, including grouping and comments.
|
|
143
|
+
'custom.example. 300 IN A (192.0.2.3) ; hook supplied this record'
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
record = reader_class.new('example.').process_io(StringIO.new("www 300 IN A 192.0.2.1\n")).first
|
|
147
|
+
assert_equal('custom.example.', record.name.to_s(true))
|
|
148
|
+
assert_equal('192.0.2.3', record.address.to_s)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_process_line_scans_unchanged_input_once
|
|
152
|
+
scanner = presentation.singleton_class
|
|
153
|
+
['www 300 IN A 192.0.2.1', 'txt 300 IN TXT "one(two);three"'].each do |line|
|
|
154
|
+
scans = 0
|
|
155
|
+
tracer = TracePoint.new(:call) do |event|
|
|
156
|
+
scans += 1 if event.defined_class == scanner && event.method_id == :scan
|
|
157
|
+
end
|
|
158
|
+
normalized = tracer.enable do
|
|
159
|
+
Dnsruby::ZoneReader.new('example.').process_line(line + "\n")
|
|
160
|
+
end
|
|
161
|
+
assert_equal(1, scans, line)
|
|
162
|
+
assert_instance_of(String, normalized)
|
|
163
|
+
assert_instance_of(Dnsruby::Name, Dnsruby::RR.create(normalized).name)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_omitted_owner_and_apostrophe_in_owner_keep_legacy_behavior
|
|
168
|
+
text = "'owner 300 IN TXT ('first')\n IN TXT ('two(three);four')\n"
|
|
169
|
+
records = Dnsruby::ZoneReader.new('example.').process_io(StringIO.new(text))
|
|
170
|
+
assert_equal(['first'], records.first.strings)
|
|
171
|
+
assert_equal(['two(three);four'], records.last.strings)
|
|
172
|
+
assert_equal(records.first.name, records.last.name)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def test_plain_records_and_grouped_records_produce_identical_wire_data
|
|
176
|
+
[
|
|
177
|
+
'www.example. 60 IN A 192.0.2.1',
|
|
178
|
+
"www.example.\t60\tIN\tAAAA\t2001:db8::1",
|
|
179
|
+
'example. IN MX 10 mail.example.',
|
|
180
|
+
'example. 60 IN TXT plain',
|
|
181
|
+
'example. 60 IN HTTPS 1 . alpn=h2 port=443',
|
|
182
|
+
'é.example. 60 IN A 192.0.2.1'
|
|
183
|
+
].each do |text|
|
|
184
|
+
plain = Dnsruby::RR.create(text.freeze)
|
|
185
|
+
grouped = Dnsruby::RR.create("(#{text})")
|
|
186
|
+
assert_equal(grouped, plain, text)
|
|
187
|
+
assert_equal(grouped.clone, plain.clone, text)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_runs_preserve_unicode_quotes_escapes_and_continuation_state
|
|
192
|
+
text = "é.example. TXT ('café; (value)'\"déjà\\\"vu\") ; ignored quote '\n"
|
|
193
|
+
start = presentation.single_quoted_start(text)
|
|
194
|
+
normalized, depth, quote = presentation.scan(text, single_quoted_from: start)
|
|
195
|
+
assert_equal("é.example. TXT 'café; (value)'\"déjà\\\"vu\" ", normalized)
|
|
196
|
+
assert_equal(0, depth)
|
|
197
|
+
assert_equal(false, quote)
|
|
198
|
+
assert_equal(Encoding::UTF_8, normalized.encoding)
|
|
199
|
+
|
|
200
|
+
normalized, depth, quote = presentation.scan("début; (one)\\\nfin\" )", 1, true)
|
|
201
|
+
assert_equal("début; (one)\\\nfin\" ", normalized)
|
|
202
|
+
assert_equal(0, depth)
|
|
203
|
+
assert_equal(false, quote)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def test_runs_keep_binary_character_strings_and_trailing_escapes
|
|
207
|
+
text = "owner TXT \"\xFE\xFF (value);\" \\".b
|
|
208
|
+
normalized, depth, quote = presentation.scan(text)
|
|
209
|
+
assert_equal(text, normalized)
|
|
210
|
+
assert_equal(Encoding::BINARY, normalized.encoding)
|
|
211
|
+
assert_equal([0, false], [depth, quote])
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
|
|
3
|
+
class ReviewRecordCompatibilityTest < Minitest::Test
|
|
4
|
+
def test_decoder_byte_access_keeps_direct_multibyte_and_mutation_contracts
|
|
5
|
+
text = +'AA'
|
|
6
|
+
decoder = Dnsruby::MessageDecoder.new(text)
|
|
7
|
+
assert_equal(65, decoder.get_byte_at(1))
|
|
8
|
+
text.replace('éA')
|
|
9
|
+
assert_equal(195, decoder.get_byte_at(0))
|
|
10
|
+
assert_equal(65, decoder.get_byte_at(1))
|
|
11
|
+
assert_nil(decoder.get_byte_at(2))
|
|
12
|
+
binary = Dnsruby::MessageDecoder.new(text.b)
|
|
13
|
+
assert_equal([195, 169, 65], 3.times.map { |index| binary.get_byte_at(index) })
|
|
14
|
+
assert_nil(binary.get_byte_at(3))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_caa_clone_preserves_string_rdata_and_comparisons
|
|
18
|
+
record = Dnsruby::RR.create('example. 300 IN CAA 128 issue "ca.example"')
|
|
19
|
+
copy = record.clone
|
|
20
|
+
assert_instance_of(String, copy.rdata)
|
|
21
|
+
assert_equal(record.rdata, copy.rdata)
|
|
22
|
+
assert_equal(0, record <=> copy)
|
|
23
|
+
assert_equal(2, [record, copy].sort.length)
|
|
24
|
+
copy.property_value = 'other.example'
|
|
25
|
+
assert_equal('128 issue "ca.example"', copy.rdata)
|
|
26
|
+
assert_equal('128 issue "other.example"', copy.rdata_to_string)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_caa_binary_value_round_trips_without_presentation_reparsing
|
|
30
|
+
value = "\0\"\\\xFF".b
|
|
31
|
+
record = Dnsruby::RR.create(name: 'example.', type: 'CAA', klass: 'IN', ttl: 300,
|
|
32
|
+
flag: 0, property_tag: 'issue', property_value: value)
|
|
33
|
+
copy = record.clone
|
|
34
|
+
assert_equal(value, copy.property_value)
|
|
35
|
+
assert_instance_of(String, copy.rdata)
|
|
36
|
+
assert_equal(copy.rdata_to_string, copy.rdata)
|
|
37
|
+
assert_equal(value, copy.clone.property_value)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_escaped_origin_and_rdata_names_remain_complete
|
|
41
|
+
reader = Dnsruby::ZoneReader.new('example.')
|
|
42
|
+
reader.process_line('$ORIGIN foo\\ bar.example.')
|
|
43
|
+
address = Dnsruby::RR.create(reader.process_line('host 60 IN A 192.0.2.1'))
|
|
44
|
+
assert_equal(['host', 'foo bar', 'example'], address.name.labels.map(&:string))
|
|
45
|
+
|
|
46
|
+
mx = Dnsruby::RR.create(reader.process_line('mail 60 IN MX 10 target\\ name'))
|
|
47
|
+
assert_equal(['target name', 'foo bar', 'example'], mx.exchange.labels.map(&:string))
|
|
48
|
+
nsec = Dnsruby::RR.create(reader.process_line('owner 60 IN NSEC next\\ name A NSEC RRSIG'))
|
|
49
|
+
assert_equal(['next name', 'foo bar', 'example'], nsec.next_domain.labels.map(&:string))
|
|
50
|
+
assert_equal(%w[A NSEC RRSIG], nsec.types.map(&:to_s))
|
|
51
|
+
|
|
52
|
+
direct_mx = Dnsruby::RR.create('mail.example. 60 IN MX 10 target\\ name.example.')
|
|
53
|
+
direct_nsec = Dnsruby::RR.create('owner.example. 60 IN NSEC next\\ name.example. A NSEC')
|
|
54
|
+
assert_equal(['target name', 'example'], direct_mx.exchange.labels.map(&:string))
|
|
55
|
+
assert_equal(['next name', 'example'], direct_nsec.next_domain.labels.map(&:string))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_escaped_trailing_whitespace_in_zone_names
|
|
59
|
+
reader = Dnsruby::ZoneReader.new('example.')
|
|
60
|
+
rr = Dnsruby::RR.create(reader.process_line('alias 60 IN CNAME trailing\\ '))
|
|
61
|
+
assert_equal(['trailing ', 'example'], rr.domainname.labels.map(&:string))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_zone_txt_uses_dns_escapes_while_direct_parser_keeps_legacy_behavior
|
|
65
|
+
reader = Dnsruby::ZoneReader.new('example.')
|
|
66
|
+
%w[TXT SPF].each do |type|
|
|
67
|
+
rr = Dnsruby::RR.create(reader.process_line("owner 60 IN #{type} \"one\\ntwo\\tthree\\032four\\010five\""))
|
|
68
|
+
assert_equal(["onentwotthree four\nfive"], rr.strings)
|
|
69
|
+
quoted = Dnsruby::RR.create(reader.process_line("owner 60 IN #{type} \"say\\\"hi\\\"\\\\end\""))
|
|
70
|
+
assert_equal(['say"hi"\\end'], quoted.strings)
|
|
71
|
+
unquoted = Dnsruby::RR.create(reader.process_line("owner 60 IN #{type} one\\ two"))
|
|
72
|
+
assert_equal(['one two'], unquoted.strings)
|
|
73
|
+
end
|
|
74
|
+
assert_equal(["one\ntwo"], Dnsruby::RR::TXT.parse('"one\\ntwo"'))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_canonical_sort_keeps_references_and_normalizes_mutated_ttls
|
|
78
|
+
records = [3, 1, 2].map { |last| Dnsruby::RR.create("example. 300 IN A 192.0.2.#{last}") }
|
|
79
|
+
set = Dnsruby::RRSet.new(records)
|
|
80
|
+
originals = set.rrs
|
|
81
|
+
originals[0].ttl = 100
|
|
82
|
+
sorted = set.sort_canonical
|
|
83
|
+
assert_equal(%w[192.0.2.1 192.0.2.2 192.0.2.3], sorted.rrs.map { |rr| rr.address.to_s })
|
|
84
|
+
sorted.rrs.each { |rr| assert_same(originals.find { |old| old.address == rr.address }, rr) }
|
|
85
|
+
assert_equal([100, 100, 100], originals.map(&:ttl))
|
|
86
|
+
sorted.rrs.first.address = Dnsruby::IPv4.create('192.0.2.9')
|
|
87
|
+
assert(sorted.add(Dnsruby::RR.create('example. 100 IN A 192.0.2.1'), false))
|
|
88
|
+
assert_equal(4, sorted.length)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_canonical_sort_deduplicates_publicly_mutated_records
|
|
92
|
+
set = Dnsruby::RRSet.new([
|
|
93
|
+
Dnsruby::RR.create('example. 300 IN A 192.0.2.1'),
|
|
94
|
+
Dnsruby::RR.create('example. 300 IN A 192.0.2.2')
|
|
95
|
+
])
|
|
96
|
+
set.rrs.last.address = Dnsruby::IPv4.create('192.0.2.1')
|
|
97
|
+
sorted = set.sort_canonical
|
|
98
|
+
assert_equal(1, sorted.length)
|
|
99
|
+
assert_same(set.rrs.last, sorted.rrs.first)
|
|
100
|
+
end
|
|
101
|
+
end
|