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,95 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
|
|
3
|
+
class ProtocolReviewFixesTest < Minitest::Test
|
|
4
|
+
def test_repeated_compressed_names_use_independent_mutable_labels
|
|
5
|
+
encoder = Dnsruby::MessageEncoder.new
|
|
6
|
+
%w[First.Example. Second.Example. First.Example.].each do |name|
|
|
7
|
+
encoder.put_name(Dnsruby::Name.create(name))
|
|
8
|
+
end
|
|
9
|
+
decoder = Dnsruby::MessageDecoder.new(encoder.to_s)
|
|
10
|
+
names = 3.times.map { decoder.get_name }
|
|
11
|
+
assert_equal(%w[first.example. second.example. first.example.], names.map { |name| name.to_s(true) })
|
|
12
|
+
names.first.labels.first.string.replace('Changed')
|
|
13
|
+
names.first.labels.last.string.replace('Changed')
|
|
14
|
+
assert_equal('first.example.', names.last.to_s(true))
|
|
15
|
+
assert_equal('second.example.', names[1].to_s(true))
|
|
16
|
+
suffixes = decoder.instance_variable_get(:@name_suffixes)
|
|
17
|
+
assert(suffixes.keys.all? { |offset| offset < 0x4000 && offset < encoder.to_s.bytesize })
|
|
18
|
+
assert(suffixes.values.all? { |value| value.frozen? && value[0].frozen? && value[0].all?(&:frozen?) })
|
|
19
|
+
assert_equal(encoder.to_s.bytesize, decoder.index)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_rfc_name_fields_are_uncompressed_with_existing_case_rules
|
|
23
|
+
records = [
|
|
24
|
+
['SRV 0 5 443 Target.Example.', 6],
|
|
25
|
+
['DNAME Target.Example.', 0],
|
|
26
|
+
['RRSIG A 8 2 300 20300101000000 20200101000000 123 Target.Example. AQID', 18],
|
|
27
|
+
['NSEC Target.Example. A NSEC RRSIG', 0]
|
|
28
|
+
]
|
|
29
|
+
target = Dnsruby::Name.create('Target.Example.')
|
|
30
|
+
expected = Dnsruby::MessageEncoder.new { |msg| msg.put_name(target, true, false) }.to_s
|
|
31
|
+
records.each do |rdata, name_offset|
|
|
32
|
+
rr = Dnsruby::RR.create("owner.example. 300 IN #{rdata}")
|
|
33
|
+
encoder = Dnsruby::MessageEncoder.new
|
|
34
|
+
encoder.put_name(target)
|
|
35
|
+
encoder.put_rr(rr)
|
|
36
|
+
decoder = Dnsruby::MessageDecoder.new(encoder.to_s)
|
|
37
|
+
decoder.get_name
|
|
38
|
+
decoder.get_name
|
|
39
|
+
decoder.get_unpack('nnN')
|
|
40
|
+
wire_rdata = decoder.get_length16 { decoder.get_bytes }
|
|
41
|
+
assert_equal(expected, wire_rdata.byteslice(name_offset, expected.bytesize), rdata)
|
|
42
|
+
decoded = Dnsruby::MessageDecoder.new(encoder.to_s)
|
|
43
|
+
decoded.get_name
|
|
44
|
+
assert_equal(rr.clone, decoded.get_rr, rdata)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_all_known_empty_update_markers_round_trip_and_clone
|
|
49
|
+
types = %w[A AAAA AFSDB ANY APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS
|
|
50
|
+
GPOS HINFO HIP HTTPS IPSECKEY ISDN KX LOC MB MG MINFO MR MX NAPTR NS NSAP NSEC NSEC3
|
|
51
|
+
NSEC3PARAM NXT PTR PX RP RRSIG RT SOA SPF SRV SSHFP SVCB TLSA TXT URI WKS X25 TYPE65400]
|
|
52
|
+
types.each do |type|
|
|
53
|
+
update = Dnsruby::Update.new('example.')
|
|
54
|
+
records = [update.present('owner.example.', type), update.absent('owner.example.', type),
|
|
55
|
+
update.delete('owner.example.', type)]
|
|
56
|
+
decoded = Dnsruby::Message.decode(update.encode)
|
|
57
|
+
actual = decoded.answer.to_a + decoded.authority.to_a
|
|
58
|
+
assert_equal(3, actual.length, type)
|
|
59
|
+
records.zip(actual).each do |record, copy|
|
|
60
|
+
assert_equal(record.type, copy.type, type)
|
|
61
|
+
assert_equal(record.klass, copy.klass, type)
|
|
62
|
+
assert_equal(0, copy.rdlength, type)
|
|
63
|
+
assert_equal('', copy.rdata_to_string, type)
|
|
64
|
+
assert_equal(0, copy.clone.rdlength, type)
|
|
65
|
+
assert_equal(record.class.superclass, copy.class.superclass, type)
|
|
66
|
+
end
|
|
67
|
+
assert_equal('', Dnsruby::RR.create("owner.example. 0 ANY #{type}").rdata_to_string, type)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_value_dependent_update_records_keep_their_data
|
|
72
|
+
update = Dnsruby::Update.new('example.')
|
|
73
|
+
update.present('owner.example.', 'MX', '10 mail.example.')
|
|
74
|
+
update.delete('owner.example.', 'MX', '10 mail.example.')
|
|
75
|
+
decoded = Dnsruby::Message.decode(update.encode)
|
|
76
|
+
assert_equal(Dnsruby::Classes::IN, decoded.answer.first.klass)
|
|
77
|
+
assert_equal(Dnsruby::Classes::NONE, decoded.authority.first.klass)
|
|
78
|
+
assert_equal('10 mail.example.', decoded.answer.first.rdata_to_string)
|
|
79
|
+
assert_equal('10 mail.example.', decoded.authority.first.rdata_to_string)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_escaped_owner_tokens_survive_zone_normalization
|
|
83
|
+
reader = Dnsruby::ZoneReader.new('example.', 300)
|
|
84
|
+
{ 'space\\ name' => 'space name', 'trailing\\ ' => 'trailing ', 'quote\\"name' => 'quote"name',
|
|
85
|
+
'back\\\\slash' => 'back\\slash' }.each do |owner, label|
|
|
86
|
+
rr = Dnsruby::RR.create(reader.process_line("#{owner} IN TXT \"ordinary text\""))
|
|
87
|
+
assert_equal([label, 'example'], rr.name.labels.map(&:string))
|
|
88
|
+
assert_equal(['ordinary text'], rr.strings)
|
|
89
|
+
omitted = Dnsruby::RR.create(reader.process_line(' IN A 192.0.2.1'))
|
|
90
|
+
assert_equal(rr.name, omitted.name)
|
|
91
|
+
end
|
|
92
|
+
rr = Dnsruby::RR.create('space\\ name.example. 300 IN A 192.0.2.1')
|
|
93
|
+
assert_equal(['space name', 'example'], rr.name.labels.map(&:string))
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require_relative 'tc_pipeline_send'
|
|
2
|
+
|
|
3
|
+
class QueryControlTest < Minitest::Test
|
|
4
|
+
def test_cancelled_lookup_leaves_unrelated_queries_running
|
|
5
|
+
isolated(<<~'RUBY')
|
|
6
|
+
SELECTOR.singleton_class.remove_method(:notify_queue_observers)
|
|
7
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
|
|
8
|
+
do_caching: false, retry_times: 1, query_timeout: 60)
|
|
9
|
+
submitted, finished = Queue.new, Queue.new
|
|
10
|
+
resolver.single_resolvers.each do |sender|
|
|
11
|
+
sender.define_singleton_method(:send_async) { |*| }
|
|
12
|
+
end
|
|
13
|
+
send_async = resolver.method(:send_async)
|
|
14
|
+
resolver.define_singleton_method(:send_async) do |*args|
|
|
15
|
+
id = send_async.call(*args)
|
|
16
|
+
submitted << true
|
|
17
|
+
id
|
|
18
|
+
end
|
|
19
|
+
unrelated = Queue.new
|
|
20
|
+
resolver.send_async(query('unrelated.example.'), unrelated, :unrelated)
|
|
21
|
+
submitted.pop
|
|
22
|
+
control = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + 30)
|
|
23
|
+
waiter = Thread.new do
|
|
24
|
+
begin
|
|
25
|
+
Dnsruby::QueryControl.with(control) do
|
|
26
|
+
resolver.query_no_validation_or_recursion('cancelled.example.', 'A')
|
|
27
|
+
end
|
|
28
|
+
finished << :unexpected_response
|
|
29
|
+
rescue Dnsruby::QueryControl::Cancelled => error
|
|
30
|
+
finished << [error, Dnsruby::QueryControl.current]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
Timeout.timeout(3) { submitted.pop }
|
|
34
|
+
control.cancel
|
|
35
|
+
result = Timeout.timeout(3) { finished.pop }
|
|
36
|
+
waiter.join
|
|
37
|
+
raise 'wrong cancellation result' unless result.first.instance_of?(Dnsruby::QueryControl::Cancelled)
|
|
38
|
+
raise 'query control leaked into later work' unless result.last.nil?
|
|
39
|
+
engine = resolver.instance_variable_get(:@resolver_ruby)
|
|
40
|
+
raise 'cancelled another query or retained cancelled work' unless engine.instance_variable_get(:@query_list).keys == [:unrelated]
|
|
41
|
+
raise 'unrelated query completed' unless unrelated.empty?
|
|
42
|
+
resolver.close
|
|
43
|
+
raise 'lookup observer retained after close' unless K.class_variable_get(:@@observers).empty?
|
|
44
|
+
RUBY
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_root_hint_address_lookups_obey_deadlines_and_cancellation
|
|
48
|
+
isolated(<<~'RUBY')
|
|
49
|
+
[:cancel, :timeout].each do |reason|
|
|
50
|
+
control = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + (reason == :timeout ? 0.02 : 30))
|
|
51
|
+
packet = Dnsruby::Message.new('.', 'NS')
|
|
52
|
+
packet.add_answer(Dnsruby::RR.create('. 300 IN NS ns.example.'))
|
|
53
|
+
resolver = Object.new
|
|
54
|
+
[:nameservers=, :recurse=].each { |name| resolver.define_singleton_method(name) { |*| } }
|
|
55
|
+
resolver.define_singleton_method(:single_resolvers) { [Object.new] }
|
|
56
|
+
resolver.define_singleton_method(:query_no_validation_or_recursion) { |*| packet }
|
|
57
|
+
sent, cancelled = [], []
|
|
58
|
+
engine = Object.new
|
|
59
|
+
engine.define_singleton_method(:cancel_query) { |id, queue| cancelled << [id, queue] }
|
|
60
|
+
resolver.instance_variable_set(:@resolver_ruby, engine)
|
|
61
|
+
resolver.define_singleton_method(:send_async) do |_message, queue|
|
|
62
|
+
id = sent.length
|
|
63
|
+
sent << [id, queue]
|
|
64
|
+
control.cancel if reason == :cancel && sent.length == 2
|
|
65
|
+
id
|
|
66
|
+
end
|
|
67
|
+
expected = reason == :cancel ? Dnsruby::QueryControl::Cancelled : Dnsruby::QueryControl::DeadlineExceeded
|
|
68
|
+
begin
|
|
69
|
+
Dnsruby::QueryControl.with(control) do
|
|
70
|
+
Dnsruby::Recursor.set_hints(['127.0.0.1'], resolver)
|
|
71
|
+
end
|
|
72
|
+
raise 'unfinished hint lookups returned successfully'
|
|
73
|
+
rescue expected
|
|
74
|
+
end
|
|
75
|
+
raise 'both A and AAAA lookups should have started' unless sent.length == 2
|
|
76
|
+
raise 'hint lookups were not cancelled' unless cancelled == sent
|
|
77
|
+
raise 'query control leaked after hint lookup failure' unless Dnsruby::QueryControl.current.nil?
|
|
78
|
+
end
|
|
79
|
+
RUBY
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def isolated(code)
|
|
85
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-I', File.expand_path('../lib', __dir__),
|
|
86
|
+
'-e', TestPipelineSend::FIXTURE + code)
|
|
87
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
88
|
+
assert_empty(stderr)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'spec_helper'
|
|
4
|
+
|
|
5
|
+
class TestRecordCodecContracts < Minitest::Test
|
|
6
|
+
include Dnsruby
|
|
7
|
+
|
|
8
|
+
def copy_record(record)
|
|
9
|
+
message = Message.new
|
|
10
|
+
message.add_answer(record)
|
|
11
|
+
Message.decode(message.encode).answer.first
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_received_ttl_normalisation_leaves_ordinary_values_and_opt_fields_intact
|
|
15
|
+
[0, 300, 0x7fffffff].each do |ttl|
|
|
16
|
+
rr = RR.create(name: 'host.example.', type: 'A', address: '192.0.2.1', ttl: ttl)
|
|
17
|
+
assert_equal(ttl, copy_record(rr).ttl)
|
|
18
|
+
end
|
|
19
|
+
[0x80000000, 0xffffffff].each do |ttl|
|
|
20
|
+
rr = RR.create(name: 'host.example.', type: 'A', address: '192.0.2.1', ttl: ttl)
|
|
21
|
+
assert_equal(0, copy_record(rr).ttl)
|
|
22
|
+
end
|
|
23
|
+
opt = RR::OPT.new(1232, 0x80008000)
|
|
24
|
+
encoder = MessageEncoder.new { |out| out.put_rr(opt) }
|
|
25
|
+
assert_equal(opt.ttl, MessageDecoder.new(encoder.to_s).get_rr.ttl)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_a_root_name_requires_an_encoded_root_label
|
|
29
|
+
assert_equal('.', MessageDecoder.new("\0".b).get_name.to_s(true))
|
|
30
|
+
assert_raises(DecodeError) { MessageDecoder.new(''.b).get_name }
|
|
31
|
+
%w[NS CNAME PTR].each do |type|
|
|
32
|
+
assert_equal('.', copy_record(RR.create("owner.example. IN #{type} .")).domainname.to_s(true))
|
|
33
|
+
end
|
|
34
|
+
assert_equal('.', copy_record(RR.create('owner.example. IN MX 0 .')).exchange.to_s(true))
|
|
35
|
+
assert_equal('.', copy_record(RR.create('owner.example. IN SRV 0 0 0 .')).target.to_s(true))
|
|
36
|
+
assert_equal('.', copy_record(RR.create('owner.example. IN NAPTR 1 1 "" "" "" .')).replacement.to_s(true))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_empty_name_only_update_markers_keep_their_type_and_roundtrip
|
|
40
|
+
update = Update.new('example.')
|
|
41
|
+
%w[NS CNAME PTR].each do |type|
|
|
42
|
+
[update.present('owner.example.', type), update.absent('owner.example.', type),
|
|
43
|
+
update.delete('owner.example.', type)].each do |rr|
|
|
44
|
+
assert_equal(rr.type, rr.clone.type)
|
|
45
|
+
assert_equal(rr.klass, rr.clone.klass)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
copy = Message.decode(update.encode)
|
|
49
|
+
assert_equal(update.encode, copy.encode)
|
|
50
|
+
assert_equal(update.answer.map(&:type), copy.answer.map(&:type))
|
|
51
|
+
assert_equal(update.authority.map(&:type), copy.authority.map(&:type))
|
|
52
|
+
%w[ANY NONE].each do |klass|
|
|
53
|
+
rr = RR.new_from_data('owner.example.', Types::NS, klass, 0, 0, ''.b, 0)
|
|
54
|
+
assert_equal(Classes.new(klass), rr.klass)
|
|
55
|
+
assert_equal(0, rr.domainname.length)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_rrsig_algorithm_octets_preserve_private_values
|
|
60
|
+
[8, 253, 254].each do |algorithm|
|
|
61
|
+
rr = RR.create(name: 'example.', type: 'RRSIG', type_covered: 'A',
|
|
62
|
+
algorithm: algorithm, labels: 1, original_ttl: 300, expiration: 2_000_000_000,
|
|
63
|
+
inception: 1_900_000_000, key_tag: 1234, signers_name: 'example.', signature: 'signature')
|
|
64
|
+
copy = copy_record(rr)
|
|
65
|
+
assert_equal(algorithm, copy.algorithm.code)
|
|
66
|
+
assert_equal(rr.sig_data, copy.sig_data)
|
|
67
|
+
assert_equal(rr, copy)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_caa_accepts_empty_values_extension_tags_and_unquoted_values
|
|
72
|
+
['0 issue ""', '0 custom123 "opaque value"', '128 FutureTag token',
|
|
73
|
+
'0 issue ca.example', '0 issue "ca.example; account=123"'].each do |rdata|
|
|
74
|
+
rr = RR.create("example. IN CAA #{rdata}")
|
|
75
|
+
copy = copy_record(rr)
|
|
76
|
+
assert_equal(rr, copy)
|
|
77
|
+
assert_equal(rr, RR.create(rr.to_s))
|
|
78
|
+
assert_equal(rr.property_value, copy.property_value)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_caa_escapes_preserve_binary_values_and_long_strings
|
|
83
|
+
rr = RR.create('example. IN CAA 0 custom "quote\"slash\\\\tab\009byte\255"')
|
|
84
|
+
assert_equal("quote\"slash\\tab\tbyte\xff".b, rr.property_value)
|
|
85
|
+
assert_equal(rr, copy_record(rr))
|
|
86
|
+
assert_equal(rr, RR.create(rr.to_s))
|
|
87
|
+
long = RR.create(name: 'example.', type: 'CAA', flag: 0,
|
|
88
|
+
property_tag: 'custom', property_value: 'a' * 300)
|
|
89
|
+
assert_equal(long, copy_record(long))
|
|
90
|
+
assert_equal(long, RR.create(long.to_s))
|
|
91
|
+
%w[bad.tag bad-tag].each do |tag|
|
|
92
|
+
assert_raises(DecodeError) { RR.create("example. IN CAA 0 #{tag} value") }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_ds_family_equality_and_hash_use_the_same_binary_digest
|
|
97
|
+
%w[DS CDS DLV].each do |type|
|
|
98
|
+
lower = RR.create("example. IN #{type} 1234 8 2 #{'ab' * 32}")
|
|
99
|
+
upper = RR.create("example. IN #{type} 1234 8 2 #{'AB' * 32}")
|
|
100
|
+
assert_equal(lower, upper)
|
|
101
|
+
assert_equal(lower.hash, upper.hash)
|
|
102
|
+
assert_equal(:found, { lower => :found }[upper])
|
|
103
|
+
different = RR.create("example. IN #{type} 1234 8 2 #{'cd' * 32}")
|
|
104
|
+
refute_equal(lower, different)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'stringio'
|
|
3
|
+
|
|
4
|
+
class TestRecordRegressions < Minitest::Test
|
|
5
|
+
include Dnsruby
|
|
6
|
+
|
|
7
|
+
def zone(text)
|
|
8
|
+
ZoneReader.new('example.').process_io(StringIO.new(text))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def rdata(rr)
|
|
12
|
+
MessageEncoder.new { |encoder| rr.encode_rdata(encoder) }.to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def wire_rr(type, klass, data)
|
|
16
|
+
# Build this without the encoder under test so malformed RDATA can reach
|
|
17
|
+
# both decoding entry points, including their actual class handling.
|
|
18
|
+
"\0".b + [type, klass, 0, data.bytesize].pack('nnNn') + data
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_txt_layout_newlines_do_not_become_data
|
|
22
|
+
['txt 60 IN TXT', 'txt IN TXT', 'txt TXT'].each do |prefix|
|
|
23
|
+
rr = zone("#{prefix} (\"one\" ; comment\n \"two\"\n)\n").first
|
|
24
|
+
assert_equal(['one', 'two'], rr.strings, prefix)
|
|
25
|
+
assert_equal('onetwo', rr.data, prefix)
|
|
26
|
+
end
|
|
27
|
+
rr = RR.create("txt.example. 60 IN TXT (\"one\"\n \"two\")")
|
|
28
|
+
assert_equal(['one', 'two'], rr.strings)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_literal_multiline_txt_preserves_whitespace_and_quoted_punctuation
|
|
32
|
+
rr = zone("txt 60 IN TXT (\"one \n two;(three)\"\n \"four\")\n").first
|
|
33
|
+
assert_equal(["one \n two;(three)", 'four'], rr.strings)
|
|
34
|
+
copy = MessageDecoder.new(wire_rr(16, 1, rdata(rr))).get_rr
|
|
35
|
+
assert_equal(rr.strings, copy.strings)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_manual_continuation_lines_without_terminating_newlines
|
|
39
|
+
reader = ZoneReader.new('example.')
|
|
40
|
+
assert_nil(reader.process_line('txt 60 IN TXT ("one"'))
|
|
41
|
+
line = reader.process_line('"two")')
|
|
42
|
+
assert_equal(['one', 'two'], RR.create(line).strings)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_known_and_unknown_rfc3597_layout_newlines
|
|
46
|
+
{'TYPE65400' => '01020304', 'A' => 'c0000201'}.each do |type, hex|
|
|
47
|
+
record = "name 60 IN #{type} ( \\# 4 #{hex[0, 4]}\n #{hex[4, 4]} )\n"
|
|
48
|
+
assert_equal(hex, rdata(zone(record).first).unpack1('H*'), type)
|
|
49
|
+
assert_equal(hex, rdata(RR.create(record)).unpack1('H*'), type)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_single_quoted_txt_and_spf_keep_literal_punctuation
|
|
54
|
+
%w[TXT SPF TYPE16 TYPE99].each do |type|
|
|
55
|
+
text = "txt 60 IN #{type} ('one(two);three' 'four')"
|
|
56
|
+
assert_equal(['one(two);three', 'four'], RR.create(text).strings, type)
|
|
57
|
+
assert_equal(['one(two);three', 'four'], zone(text).first.strings, type)
|
|
58
|
+
inherited_owner = zone("txt 60 IN #{type} 'first'\n IN #{type} 'two(three)'\n")
|
|
59
|
+
assert_equal(['two(three)'], inherited_owner.last.strings, type)
|
|
60
|
+
end
|
|
61
|
+
assert_equal(["one \n two(three)"], zone("txt IN TXT ('one \n two(three)')\n").first.strings)
|
|
62
|
+
assert_equal(['one(two)'], zone("o'clock IN TXT 'one(two)'\n").first.strings)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_apostrophes_are_ordinary_characters_in_other_record_formats
|
|
66
|
+
record = zone("svc IN SVCB (1 o'clock.example. ; comment\n port=443)\n").first
|
|
67
|
+
assert_equal("o'clock.example.", record.target.to_s(true))
|
|
68
|
+
assert_equal('443', record.params_to_hash['port'])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_adjacent_quoted_txt_strings_keep_literal_punctuation
|
|
72
|
+
["'one''two(three);four'", "\"one\"'two(three);four'",
|
|
73
|
+
"'one'\"two(three);four\""].each do |value|
|
|
74
|
+
%w[TXT SPF].each do |type|
|
|
75
|
+
text = "txt IN #{type} #{value}"
|
|
76
|
+
assert_equal(['one', 'two(three);four'], RR.create(text).strings, text)
|
|
77
|
+
assert_equal(['one', 'two(three);four'], zone(text).first.strings, text)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_grouping_parentheses_touching_the_type_and_header
|
|
83
|
+
["txt IN TXT('one(two)')", "txt IN(TXT('one(two)'))",
|
|
84
|
+
"txt\\(name IN TXT('one(two)')", "o'clock IN TXT('one(two)')",
|
|
85
|
+
"'host.example IN TXT 'one(two)'"].each do |text|
|
|
86
|
+
assert_equal(['one(two)'], RR.create(text).strings, text)
|
|
87
|
+
assert_equal(['one(two)'], zone(text).first.strings, text)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_non_in_svcb_and_https_zone_records_remain_opaque
|
|
92
|
+
[64, 65].each do |type|
|
|
93
|
+
[Classes::CH, Classes::HS, 65_280].each do |klass|
|
|
94
|
+
rr = zone("name 60 CLASS#{klass} TYPE#{type} \\# 3 000100\n").first
|
|
95
|
+
assert_kind_of(RR::Generic, rr)
|
|
96
|
+
assert_equal(klass, rr.klass)
|
|
97
|
+
assert_equal('000100', rdata(rr).unpack1('H*'))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_svcb_target_must_be_complete_and_uncompressed
|
|
103
|
+
[64, 65].each do |type|
|
|
104
|
+
# Priority only; unterminated label list; truncated label; pointer.
|
|
105
|
+
%w[0001 00010161 00010361 0001c000].each do |hex|
|
|
106
|
+
data = [hex].pack('H*')
|
|
107
|
+
assert_raises(DecodeError, "TYPE#{type} #{hex}") do
|
|
108
|
+
RR.new_from_data('.', type, Classes::IN, 0, data.bytesize, data, 0)
|
|
109
|
+
end
|
|
110
|
+
assert_raises(DecodeError, "TYPE#{type} #{hex}") do
|
|
111
|
+
MessageDecoder.new(wire_rr(type, Classes::IN, data)).get_rr
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
data = ['000100'].pack('H*')
|
|
115
|
+
assert_equal('.', MessageDecoder.new(wire_rr(type, Classes::IN, data)).get_rr.target.to_s(true))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_empty_svcb_wire_data_only_decodes_for_update_classes
|
|
120
|
+
[64, 65].each do |type|
|
|
121
|
+
assert_raises(DecodeError) { RR.new_from_data('.', type, Classes::IN, 0, 0, ''.b, 0) }
|
|
122
|
+
assert_raises(DecodeError) { MessageDecoder.new(wire_rr(type, Classes::IN, ''.b)).get_rr }
|
|
123
|
+
[Classes::ANY, Classes::NONE].each do |klass|
|
|
124
|
+
rr = RR.new_from_data('.', type, klass, 0, 0, ''.b, 0)
|
|
125
|
+
assert_equal(klass, rr.klass)
|
|
126
|
+
assert_equal('', rdata(rr))
|
|
127
|
+
copy = MessageDecoder.new(wire_rr(type, klass, ''.b)).get_rr
|
|
128
|
+
assert_equal(klass, copy.klass)
|
|
129
|
+
assert_equal('', rdata(copy))
|
|
130
|
+
end
|
|
131
|
+
# Empty objects used to build UPDATE messages remain constructible.
|
|
132
|
+
assert_nil(RR.create(name: '.', type: type).priority)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_svcb_rejects_oversized_values_at_construction_and_after_mutation
|
|
137
|
+
[64, 65].each do |type|
|
|
138
|
+
assert_raises(DecodeError) do
|
|
139
|
+
RR.create(name: '.', type: type, priority: 1, target: '.', params: {key667: 'x' * 65_536})
|
|
140
|
+
end
|
|
141
|
+
rr = RR.create(name: '.', type: type, priority: 1, target: '.')
|
|
142
|
+
rr.params[667] = 'x' * 65_536
|
|
143
|
+
assert_raises(EncodeError) { rdata(rr) }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def test_svcb_total_rdata_length_boundary
|
|
148
|
+
rr = RR.create(name: '.', type: 64, priority: 1, target: '.')
|
|
149
|
+
rr.params[667] = 'x' * 65_528 # 2 priority + 1 root + 4 param header.
|
|
150
|
+
bytes = rdata(rr)
|
|
151
|
+
assert_equal(65_535, bytes.bytesize)
|
|
152
|
+
assert_equal(bytes, rdata(MessageDecoder.new(wire_rr(64, 1, bytes)).get_rr))
|
|
153
|
+
rr.params[667] << 'x'
|
|
154
|
+
assert_raises(EncodeError) { rdata(rr) }
|
|
155
|
+
|
|
156
|
+
rr.params = {667 => 'x' * 32_764, 668 => 'x' * 32_764}
|
|
157
|
+
assert_raises(EncodeError) { rdata(rr) }
|
|
158
|
+
rr.params = {667 => 'x' * 65_528}
|
|
159
|
+
rr.target = Name.create('a.')
|
|
160
|
+
assert_raises(EncodeError) { rdata(rr) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_mutated_utf8_svcparam_is_encoded_as_octets
|
|
164
|
+
rr = RR.create(name: '.', type: 64, priority: 1, target: '.')
|
|
165
|
+
rr.params[667] = 'é'
|
|
166
|
+
encoder = MessageEncoder.new { |output| output.put_rr(rr) }
|
|
167
|
+
copy = MessageDecoder.new(encoder.to_s).get_rr
|
|
168
|
+
assert_equal('é'.b, copy.params[667])
|
|
169
|
+
assert_equal(9, rdata(copy).bytesize)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_generic_length16_field_does_not_wrap
|
|
173
|
+
encoder = MessageEncoder.new
|
|
174
|
+
encoder.put_length16 { encoder.put_bytes('x' * 65_535) }
|
|
175
|
+
assert_equal(65_535, encoder.to_s.unpack1('n'))
|
|
176
|
+
assert_raises(EncodeError) do
|
|
177
|
+
encoder = MessageEncoder.new
|
|
178
|
+
encoder.put_length16 { encoder.put_bytes('x' * 65_536) }
|
|
179
|
+
end
|
|
180
|
+
assert_raises(EncodeError) do
|
|
181
|
+
encoder = MessageEncoder.new
|
|
182
|
+
encoder.put_length16 { encoder.put_bytes('é' * 32_768) }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def test_length16_cannot_extend_a_physical_or_nested_buffer
|
|
187
|
+
decoder = MessageDecoder.new([5].pack('n') + 'abc')
|
|
188
|
+
assert_raises(DecodeError) { decoder.get_length16 { flunk('decoded outside physical buffer') } }
|
|
189
|
+
assert_equal('abc', decoder.get_bytes, 'failed boundary check must restore the enclosing limit')
|
|
190
|
+
|
|
191
|
+
# The inner length exceeds its containing item, but not the whole packet.
|
|
192
|
+
decoder = MessageDecoder.new([4, 5].pack('nn') + 'ab' + 'trailer')
|
|
193
|
+
decoder.get_length16 do
|
|
194
|
+
assert_raises(DecodeError) { decoder.get_length16 { flunk('decoded outside nested buffer') } }
|
|
195
|
+
assert_equal('ab', decoder.get_bytes)
|
|
196
|
+
end
|
|
197
|
+
assert_equal('trailer', decoder.get_bytes)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def test_length16_restores_enclosing_limit_when_the_parser_raises
|
|
201
|
+
decoder = MessageDecoder.new([4, 2].pack('nn') + 'ab' + 'trailer')
|
|
202
|
+
decoder.get_length16 do
|
|
203
|
+
assert_raises(DecodeError) do
|
|
204
|
+
decoder.get_length16 { raise DecodeError, 'malformed nested value' }
|
|
205
|
+
end
|
|
206
|
+
assert_equal('ab', decoder.get_bytes)
|
|
207
|
+
end
|
|
208
|
+
assert_equal('trailer', decoder.get_bytes)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def test_truncated_svcb_and_https_preserve_partial_header_at_every_byte
|
|
212
|
+
bytes = ['0001000003000201bb'].pack('H*')
|
|
213
|
+
[64, 65].each do |type|
|
|
214
|
+
header = [123, 0x8200, 0, 1, 0, 0].pack('n6')
|
|
215
|
+
complete = header + wire_rr(type, Classes::IN, bytes)
|
|
216
|
+
bytes.bytesize.times do |length|
|
|
217
|
+
truncated = complete.byteslice(0, complete.bytesize - bytes.bytesize + length)
|
|
218
|
+
error = assert_raises(DecodeError, "TYPE#{type} with #{length} RDATA bytes") { Message.decode(truncated) }
|
|
219
|
+
assert(error.partial_message.header.tc)
|
|
220
|
+
assert_equal(123, error.partial_message.header.id)
|
|
221
|
+
end
|
|
222
|
+
assert_equal(bytes, Message.decode(complete).answer.first.rdata)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
data/test/tc_recur.rb
CHANGED
|
@@ -22,6 +22,7 @@ class TestRecur < Minitest::Test
|
|
|
22
22
|
r = Dnsruby::Recursor.new
|
|
23
23
|
ret = with_retries(max_attempts: 10, exceptions: [Dnsruby::ResolvError]) { r.query("uk", Dnsruby::Types.DNSKEY) }
|
|
24
24
|
assert ret.answer.length > 0, "Answer length should > 0, but was #{ret.answer.length}."
|
|
25
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, ret.security_level)
|
|
25
26
|
# ret = r.query_dorecursion("aaa.bigzone.uk-dnssec.nic.uk", Dnsruby::Types.DNSKEY)
|
|
26
27
|
# ret = r.query_dorecursion("uk-dnssec.nic.uk", Dnsruby::Types.DNSKEY)
|
|
27
28
|
end
|