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.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +314 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/dnsruby.gemspec +6 -4
  10. data/lib/dnsruby/cache.rb +279 -49
  11. data/lib/dnsruby/code_mapper.rb +35 -8
  12. data/lib/dnsruby/code_mappers.rb +63 -49
  13. data/lib/dnsruby/config.rb +77 -4
  14. data/lib/dnsruby/deadline_queue.rb +75 -0
  15. data/lib/dnsruby/dnssec.rb +148 -45
  16. data/lib/dnsruby/ipv6.rb +45 -5
  17. data/lib/dnsruby/key_cache.rb +202 -35
  18. data/lib/dnsruby/message/decoder.rb +85 -19
  19. data/lib/dnsruby/message/encoder.rb +27 -16
  20. data/lib/dnsruby/message/header.rb +8 -12
  21. data/lib/dnsruby/message/message.rb +53 -22
  22. data/lib/dnsruby/message/section.rb +12 -34
  23. data/lib/dnsruby/name.rb +5 -2
  24. data/lib/dnsruby/packet_sender.rb +664 -207
  25. data/lib/dnsruby/presentation.rb +147 -0
  26. data/lib/dnsruby/query_control.rb +132 -0
  27. data/lib/dnsruby/recursor.rb +798 -341
  28. data/lib/dnsruby/resolver.rb +472 -176
  29. data/lib/dnsruby/resource/CAA.rb +67 -16
  30. data/lib/dnsruby/resource/DLV.rb +6 -0
  31. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  32. data/lib/dnsruby/resource/DS.rb +27 -28
  33. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  34. data/lib/dnsruby/resource/IN.rb +21 -2
  35. data/lib/dnsruby/resource/MX.rb +2 -2
  36. data/lib/dnsruby/resource/NSEC.rb +6 -10
  37. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  38. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  39. data/lib/dnsruby/resource/OPT.rb +2 -3
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +986 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +53 -2
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +34 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +127 -14
  167. 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