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.
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 +302 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/demo/rubydig.rb +0 -2
  10. data/dnsruby.gemspec +5 -4
  11. data/lib/dnsruby/cache.rb +279 -47
  12. data/lib/dnsruby/code_mapper.rb +35 -8
  13. data/lib/dnsruby/code_mappers.rb +63 -49
  14. data/lib/dnsruby/config.rb +75 -4
  15. data/lib/dnsruby/deadline_queue.rb +75 -0
  16. data/lib/dnsruby/dnssec.rb +148 -45
  17. data/lib/dnsruby/ipv6.rb +45 -5
  18. data/lib/dnsruby/key_cache.rb +202 -35
  19. data/lib/dnsruby/message/decoder.rb +85 -19
  20. data/lib/dnsruby/message/encoder.rb +27 -16
  21. data/lib/dnsruby/message/header.rb +8 -12
  22. data/lib/dnsruby/message/message.rb +53 -22
  23. data/lib/dnsruby/message/section.rb +12 -34
  24. data/lib/dnsruby/name.rb +5 -2
  25. data/lib/dnsruby/packet_sender.rb +645 -203
  26. data/lib/dnsruby/presentation.rb +147 -0
  27. data/lib/dnsruby/query_control.rb +132 -0
  28. data/lib/dnsruby/recursor.rb +795 -339
  29. data/lib/dnsruby/resolver.rb +468 -186
  30. data/lib/dnsruby/resource/CAA.rb +67 -16
  31. data/lib/dnsruby/resource/DLV.rb +6 -0
  32. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  33. data/lib/dnsruby/resource/DS.rb +27 -28
  34. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  35. data/lib/dnsruby/resource/IN.rb +19 -0
  36. data/lib/dnsruby/resource/MX.rb +2 -2
  37. data/lib/dnsruby/resource/NSEC.rb +6 -10
  38. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  39. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  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 +979 -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 +42 -20
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +21 -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 +105 -10
  167. data/.coveralls.yml +0 -2
@@ -0,0 +1,178 @@
1
+ require_relative 'spec_helper'
2
+ require 'minitest/mock'
3
+
4
+ # Functional policy and cache contracts; all discovery uses local doubles.
5
+ class ReviewValidationPrimingTest < Minitest::Test
6
+ def setup
7
+ @klass = Dnsruby::Recursor
8
+ @saved = %i[@@hints @@stale_hints @@hints_generation @@nameservers
9
+ @@zones_cache @@zone_expirations @@authority_cache].to_h do |field|
10
+ [field, @klass.class_variable_get(field)]
11
+ end
12
+ %i[@@hints @@zones_cache @@zone_expirations @@authority_cache].each do |field|
13
+ @klass.class_variable_set(field, {})
14
+ end
15
+ @klass.class_variable_set(:@@stale_hints, nil)
16
+ end
17
+
18
+ def teardown
19
+ @saved.each { |field, value| @klass.class_variable_set(field, value) }
20
+ end
21
+
22
+ def test_required_verification_errors_survive_provisional_classification
23
+ verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
24
+ message = Dnsruby::Message.new('ordinary.example.')
25
+ expected = Dnsruby::VerifyError.new('Verification service did not complete')
26
+ provisional = ->(_anchor, packet, _name) { packet.security_level = Dnsruby::Message::SecurityLevel.INSECURE; nil }
27
+ # Model the verification service's result independently of wire records.
28
+ message.stub(:rrsets, [:present]) do
29
+ verifier.stub(:find_closest_anchor_for, nil) do
30
+ verifier.stub(:try_to_follow_from_anchor, provisional) do
31
+ verifier.stub(:verify_result, ->(*) { raise expected }) do
32
+ verifier.stub(:discover_signer_keys, false) do
33
+ actual = assert_raises(Dnsruby::VerifyError) { verifier.validate(message, nil) }
34
+ assert_same(expected, actual)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def test_unsigned_alias_classification_visits_each_required_name
43
+ verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
44
+ message = Dnsruby::Message.new('alias.example.')
45
+ message.add_answer(Dnsruby::RR.create('alias.example. 60 IN CNAME target.other.'))
46
+ message.add_answer(Dnsruby::RR.create('target.other. 60 IN A 192.0.2.1'))
47
+ names = []
48
+ verifier.stub(:discover_insecure_name, ->(name, _klass) { names << name.to_s(true); true }) do
49
+ assert(verifier.validate(message, nil))
50
+ end
51
+ assert_equal(%w[alias.example. target.other.], names.sort)
52
+ assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
53
+ end
54
+
55
+ def test_escaped_dots_are_not_ancestor_boundaries
56
+ @klass.class_variable_set(:@@zones_cache, {'.' => {}, 'example.' => {}, 'b.example.' => {}})
57
+ recursor = @klass.allocate
58
+ assert_equal('example.', recursor.get_closest_known_zone_for('a\\.b.example.'))
59
+ assert_equal('example.', recursor.get_closest_known_zone_for('a\\046b.example.'))
60
+ assert_equal('example.', recursor.get_closest_known_zone_for('www.example'))
61
+ relative = Dnsruby::Name.create('a\\.b.example')
62
+ assert_equal('example.', recursor.get_closest_known_zone_for(relative))
63
+ refute(relative.absolute?)
64
+ assert_equal('.', recursor.get_closest_known_zone_for('other'))
65
+ end
66
+
67
+ def test_descendant_deadline_survives_parent_eviction_without_extending_it
68
+ clock = Time.utc(2026, 11, 15)
69
+ Time.stub(:now, -> { clock }) do
70
+ root = {'ns.root.' => addresses(300)}
71
+ @klass.class_variable_set(:@@hints, root)
72
+ @klass.class_variable_set(:@@zones_cache, {'.' => root})
73
+ @klass.class_variable_set(:@@zone_expirations, {'.' => [root, clock + 90]})
74
+ @klass.cache_zone('example', {'ns.example.' => addresses(300)}, 20, parent: '.')
75
+ @klass.cache_zone(Dnsruby::Name.create('child.example'), {'ns.child.example.' => addresses(300)}, 60, parent: 'example')
76
+ @klass.class_variable_get(:@@zones_cache).delete('example.')
77
+ @klass.class_variable_get(:@@zone_expirations).delete('example.')
78
+ recursor = @klass.allocate
79
+ clock += 19
80
+ assert_equal('child.example.', recursor.get_closest_known_zone_for('www.child.example.'))
81
+ clock += 1
82
+ assert_equal('.', recursor.get_closest_known_zone_for('www.child.example.'))
83
+ end
84
+ end
85
+
86
+ def test_partial_priming_fills_missing_root_names
87
+ packet = root_packet(ns_ttl: 60, address_ttl: 300)
88
+ packet.add_answer(Dnsruby::RR.create('. 60 IN NS b.root.example.'))
89
+ resolver, lookups = priming_resolver(packet)
90
+ @klass.set_hints([], resolver)
91
+ assert_equal([['b.root.example.', 'A'], ['b.root.example.', 'AAAA']], lookups)
92
+ assert_equal(%w[a.root.example b.root.example], @klass.class_variable_get(:@@hints).keys.sort)
93
+ end
94
+
95
+ def test_root_ns_expiration_is_independent_of_address_expiration
96
+ clock = Time.utc(2026, 11, 15)
97
+ Time.stub(:now, -> { clock }) do
98
+ resolver, = priming_resolver(root_packet(ns_ttl: 5, address_ttl: 300))
99
+ @klass.set_hints([], resolver)
100
+ assert(@klass.send(:hints_ready?))
101
+ clock += 5
102
+ refute(@klass.send(:hints_ready?))
103
+ assert_equal(['192.0.2.53'], @klass.class_variable_get(:@@hints).values.first.values)
104
+ end
105
+ end
106
+
107
+ def test_zero_ttl_priming_addresses_belong_to_the_current_lookup
108
+ resolver, = priming_resolver(root_packet(ns_ttl: 0, address_ttl: 0))
109
+ @klass.send(:with_limits, resolver) do
110
+ @klass.set_hints([], resolver)
111
+ refute(@klass.send(:hints_ready?))
112
+ generation = @klass.class_variable_get(:@@hints_generation)
113
+ assert_equal(generation, @klass.send(:ensure_hints, resolver))
114
+ zone, authorities = @klass.allocate.get_closest_known_zone_authorities_for('www.example.')
115
+ assert_equal('.', zone)
116
+ assert_equal(['192.0.2.53'], authorities.values.first.current_values)
117
+ assert_empty(@klass.class_variable_get(:@@hints).values.first.values)
118
+ end
119
+ refute(@klass.send(:hints_ready?))
120
+ end
121
+
122
+ def test_supported_name_classification_is_memoized_within_one_operation
123
+ verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
124
+ anchor = Struct.new(:klass).new(Dnsruby::Classes.IN)
125
+ calls = 0
126
+ verifier.stub(:find_closest_anchor_for, anchor) do
127
+ verifier.stub(:follow_chain, ->(*) { calls += 1; anchor }) do
128
+ verifier.send(:with_operational_validation, Dnsruby::Message.new('example.')) do
129
+ 2.times { refute(verifier.send(:discover_insecure_name, Dnsruby::Name.create('example.'), Dnsruby::Classes.IN)) }
130
+ end
131
+ end
132
+ end
133
+ assert_equal(1, calls)
134
+ end
135
+
136
+ def test_root_ns_deadline_starts_before_address_discovery
137
+ clock = Time.utc(2026, 11, 15)
138
+ Time.stub(:now, -> { clock }) do
139
+ packet = root_packet(ns_ttl: 5, address_ttl: 300)
140
+ packet.add_answer(Dnsruby::RR.create('. 5 IN NS b.root.example.'))
141
+ resolver, = priming_resolver(packet) { clock += 3 }
142
+ @klass.set_hints([], resolver)
143
+ refute(@klass.send(:hints_ready?))
144
+ assert_equal(Time.utc(2026, 11, 15) + 5, @klass.class_variable_get(:@@zone_expirations)['.'][1])
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def addresses(ttl)
151
+ @klass::AddressCache.new.tap { |cache| cache.push(['192.0.2.53', ttl]) }
152
+ end
153
+
154
+ def root_packet(ns_ttl:, address_ttl:)
155
+ packet = Dnsruby::Message.new('.', 'NS')
156
+ packet.add_answer(Dnsruby::RR.create(". #{ns_ttl} IN NS a.root.example."))
157
+ packet.add_additional(Dnsruby::RR.create("a.root.example. #{address_ttl} IN A 192.0.2.53"))
158
+ packet
159
+ end
160
+
161
+ def priming_resolver(packet, &before_send)
162
+ lookups = []
163
+ resolver = Struct.new(:query_timeout, :recurse).new(2, false)
164
+ resolver.define_singleton_method(:single_resolvers) { [Object.new] }
165
+ resolver.define_singleton_method(:query_no_validation_or_recursion) { |*| packet }
166
+ resolver.define_singleton_method(:send_async) do |query, queue|
167
+ before_send&.call
168
+ name, type = query.question.first.qname.to_s(true), query.question.first.qtype.to_s
169
+ lookups << [name, type]
170
+ response = Dnsruby::Message.new(name, type)
171
+ value = type == 'A' ? '192.0.2.54' : '2001:db8::54'
172
+ response.add_answer(Dnsruby::RR.create("#{name} 60 IN #{type} #{value}"))
173
+ queue << [lookups.length, response, nil]
174
+ lookups.length
175
+ end
176
+ [resolver, lookups]
177
+ end
178
+ end
data/test/tc_rr-opt.rb CHANGED
@@ -97,7 +97,9 @@ class TestRrOpt < Minitest::Test
97
97
  received_query = s[0]
98
98
  socket.connect(s[1][2], s[1][1])
99
99
  q.push(Message.decode(received_query))
100
- socket.send(received_query,0)
100
+ response = Message.decode(received_query)
101
+ response.header.qr = true
102
+ socket.send(response.encode,0)
101
103
  }
102
104
 
103
105
  # Now send query
@@ -168,12 +170,13 @@ class TestRrOpt < Minitest::Test
168
170
  received_query = s[0]
169
171
  m = Message.decode(received_query)
170
172
  q.push(m)
173
+ m.header.qr = true
171
174
  if (m.header.arcount > 0)
172
175
  # send back FORMERR
173
176
  m.header.rcode = RCode.FORMERR
174
177
  socket.send(m.encode,0,s[1][2], s[1][1])
175
178
  else
176
- socket.send(received_query,0,s[1][2], s[1][1]) # @TODO@ FORMERR if edns
179
+ socket.send(m.encode,0,s[1][2], s[1][1])
177
180
  end
178
181
  }
179
182
 
@@ -99,4 +99,23 @@ dd ee c0 11 00 02 00 01 00 00 03 84 00 05 02 6e
99
99
  'Packet read from a packet dumped by bind...'
100
100
  )
101
101
  end
102
+
103
+ def test_empty_rdata_round_trips_through_rfc3597_presentation
104
+ %w[IN CH CLASS65000].each do |klass|
105
+ rr = RR.create("empty.example. 300 #{klass} TYPE65001 \\# 0")
106
+ assert_equal('', rr.data)
107
+ assert_equal(rr, RR.create(rr.to_s))
108
+ assert_equal(rr, rr.clone)
109
+ end
110
+ end
111
+
112
+ def test_malformed_generic_presentation_raises_decode_error
113
+ ['\\#', '\\# -1', '\\# 1', '\\# 0 ff', '\\# 1 zz', '\\# 1 f', '\\# 65536'].each do |rdata|
114
+ %w[TYPE65001 TYPE64 A].each do |type|
115
+ assert_raises(DecodeError, "#{type} #{rdata}") do
116
+ RR.create("example. 300 IN #{type} #{rdata}")
117
+ end
118
+ end
119
+ end
120
+ end
102
121
  end
data/test/tc_rr.rb CHANGED
@@ -376,6 +376,20 @@ class TestRR < Minitest::Test
376
376
  assert_equal a1.hash, a3.hash
377
377
  end
378
378
 
379
+ # RR.create has long accepted incomplete grouping around otherwise complete
380
+ # RDATA (including the TLSA certificate fixtures). A zone file must balance
381
+ # its continuation lines, but this standalone constructor remains permissive.
382
+ def test_unbalanced_grouping_in_standalone_record_strings
383
+ prefix = '_443._tcp.example. 60 IN TLSA 3 0 1 '
384
+ digest = 'aa' * 32
385
+ expected = RR.create(prefix + digest)
386
+ ["( #{digest}", "#{digest} )", "( #{digest} )"].each do |rdata|
387
+ rr = RR.create(prefix + rdata)
388
+ assert_equal(expected, rr, rdata)
389
+ assert_equal([digest].pack('H*'), rr.databin, rdata)
390
+ end
391
+ end
392
+
379
393
  def _test_duplicate_answer(method_as_symbol)
380
394
  expected_count = case method_as_symbol
381
395
  when :add_answer
@@ -0,0 +1,50 @@
1
+ require_relative 'spec_helper'
2
+
3
+ class RRSetGroupingTest < Minitest::Test
4
+ def test_grouping_preserves_order_class_and_signature_membership
5
+ signature = Dnsruby::RR.create('Host.Example. 300 IN RRSIG A 8 2 300 20300101000000 20200101000000 123 example. AQID')
6
+ records = [signature,
7
+ Dnsruby::RR.create('other.example. 300 IN A 192.0.2.2'),
8
+ Dnsruby::RR.create('host.example. 200 IN A 192.0.2.1'),
9
+ Dnsruby::RR.create('host.example. 100 IN A 192.0.2.3'),
10
+ Dnsruby::RR.create('host.example. 300 CH TXT "other class"')]
11
+ section = Dnsruby::Section.new.concat(records)
12
+ sets = section.rrsets
13
+ assert_equal([Dnsruby::Types::A, Dnsruby::Types::A, Dnsruby::Types::TXT], sets.map(&:type).map(&:code))
14
+ assert_equal(1, sets.first.num_sigs)
15
+ assert_equal(2, sets.first.rrs.length)
16
+ assert_equal([100, 100], sets.first.rrs.map(&:ttl))
17
+ assert_equal([200, 100], records[2..3].map(&:ttl), 'Grouping must not mutate source records')
18
+ assert_equal(1, section.rrsets('RRSIG').length)
19
+ assert_equal(2, section.rrsets('A').length)
20
+ assert_equal(0, section.rrsets('AAAA').length)
21
+ end
22
+
23
+ def test_grouping_copies_each_record_once
24
+ records = Array.new(40) { |i| Dnsruby::RR.create("host#{i}.example. 300 IN A 192.0.2.1") }
25
+ copies = 0
26
+ records.each do |record|
27
+ original = record.method(:clone)
28
+ record.define_singleton_method(:clone) { copies += 1; original.call }
29
+ end
30
+ sets = Dnsruby::Section.new.concat(records).rrsets
31
+ assert_equal(40, sets.length)
32
+ assert_equal(40, copies)
33
+ assert_equal(records.map(&:name), sets.map(&:name))
34
+ end
35
+
36
+ def test_grouping_observes_validation_work_limits
37
+ section = Dnsruby::Section.new.concat(Array.new(3) { |i| Dnsruby::RR.create("host#{i}.example. 300 IN A 192.0.2.1") })
38
+ limits = Dnsruby::ValidationBudget::DEFAULT_LIMITS.merge(max_work: 2)
39
+ budget = Dnsruby::ValidationBudget.new(limits: limits)
40
+ assert_raises(Dnsruby::ValidationLimitError) { Dnsruby::ValidationBudget.with(budget) { section.rrsets } }
41
+ end
42
+
43
+ def test_rejected_rrset_add_does_not_clone_the_record
44
+ set = Dnsruby::RRSet.new(Dnsruby::RR.create('one.example. 300 IN A 192.0.2.1'))
45
+ other = Dnsruby::RR.create('two.example. 300 IN A 192.0.2.2')
46
+ other.define_singleton_method(:clone) { raise 'Unexpected clone' }
47
+ refute(set.add(other))
48
+ assert_equal(1, set.length)
49
+ end
50
+ end
@@ -0,0 +1,58 @@
1
+ require_relative 'spec_helper'
2
+
3
+ class RRSetReviewFixesTest < Minitest::Test
4
+ def record(address = '192.0.2.1')
5
+ Dnsruby::RR.create("host.example. 300 IN A #{address}")
6
+ end
7
+
8
+ def signature
9
+ Dnsruby::RR.create('host.example. 300 IN RRSIG A 8 2 300 20300101000000 20200101000000 123 example. AQID')
10
+ end
11
+
12
+ def test_add_set_and_delete_signature_preserve_membership
13
+ source = Dnsruby::RRSet.new([record, signature])
14
+ target = Dnsruby::RRSet.new(record('192.0.2.2'))
15
+ assert(target.add(source))
16
+ assert_equal(3, target.length)
17
+ assert_equal(1, target.num_sigs)
18
+ assert_equal(2, target.rrs.length)
19
+ assert(target.delete(target.sigs.first))
20
+ assert_equal(0, target.num_sigs)
21
+ assert_empty(target.sigs)
22
+ assert_equal(2, target.rrs.length)
23
+ refute(target.add(Dnsruby::RRSet.new))
24
+ end
25
+
26
+ def test_bulk_membership_index_is_discarded_before_records_escape
27
+ original = record
28
+ set = Dnsruby::RRSet.new([original, record('192.0.2.2'), original])
29
+ assert_equal(2, set.length)
30
+ assert_nil(set.instance_variable_get(:@membership_index))
31
+ set[0].address = Dnsruby::IPv4.create('192.0.2.3')
32
+ assert(set.add(record))
33
+ assert_equal(3, set.length)
34
+ assert(set.add(record('192.0.2.3')))
35
+ assert_equal(3, set.length)
36
+ assert_equal('192.0.2.1', original.address.to_s)
37
+ end
38
+
39
+ def test_add_set_without_cloning_keeps_the_requested_aliasing
40
+ source = Dnsruby::RRSet.new(record)
41
+ target = Dnsruby::RRSet.new
42
+ target.add(source, false)
43
+ assert_same(source[0], target[0])
44
+ source[0].address = Dnsruby::IPv4.create('192.0.2.2')
45
+ target.add(record)
46
+ assert_equal(2, target.length)
47
+ end
48
+
49
+ def test_bulk_grouping_deduplicates_and_normalizes_ttls
50
+ records = [record, record('192.0.2.2'), record]
51
+ records.last.ttl = 100
52
+ sets = Dnsruby::Section.new.concat(records).rrsets
53
+ assert_equal(1, sets.length)
54
+ assert_equal(2, sets.first.length)
55
+ assert_equal([100, 100], sets.first.rrs.map(&:ttl))
56
+ assert_equal([300, 300, 100], records.map(&:ttl))
57
+ end
58
+ end
data/test/tc_rrsig.rb CHANGED
@@ -50,8 +50,28 @@ class RrsigTest < Minitest::Test
50
50
  assert(rr.type_covered == Dnsruby::Types.TYPE731)
51
51
  end
52
52
 
53
+ def test_type_covered_rejects_out_of_range_before_mapping_or_wire_truncation
54
+ table = Dnsruby::CodeMapper.class_variable_get(:@@arrays).fetch(Dnsruby::Types)
55
+ before = [table.strings.dup, table.stringsdown.dup, table.values.dup, table.maxcode]
56
+ [65_536, 65_538, 2**128].each do |code|
57
+ text = "x. 60 IN RRSIG TYPE#{code} 8 1 60 20300101000000 20200101000000 12345 x. AA=="
58
+ assert_raises(Dnsruby::DecodeError) { Dnsruby::RR.create(text) }
59
+ assert_raises(ArgumentError) { Dnsruby::ZoneReader.new('x.').process_line(text) }
60
+ end
61
+ assert_equal(before, [table.strings, table.stringsdown, table.values, table.maxcode])
62
+ record = Dnsruby::RR.create(INPUT)
63
+ [-1, 65_536].each do |code|
64
+ assert_raises(Dnsruby::DecodeError) { record.type_covered = code }
65
+ assert_equal(Dnsruby::Types.A, record.type_covered)
66
+ end
67
+ record.type_covered = 'TYPE65535'
68
+ message = Dnsruby::Message.new
69
+ message.add_answer(record)
70
+ assert_equal(65_535, Dnsruby::Message.decode(message.encode).answer.first.type_covered.code)
71
+ end
72
+
53
73
  def test_string_with_comments
54
74
  r = Dnsruby::RR.create("tjeb.nl. 3600 IN RRSIG NSEC3PARAM 7 2 3600 20090630164649 20090602164649 53177 tjeb.nl. Fw70WQMviRFGyeze3MUpfafaAcWIvHRpnq4ZK3lxexrR1p+rLxK5C4qVKU71XYrPYR7XEBxgUG1oyKNOhFOVyx31EjC462dz7Vxn6UDpD1LIwNnD28+oHfS9AFzGKcn4zUZqT+8IvOO1jiS9c3Y8WAkOloN9AwGIIKWU8zAp1n4= ;{id = 53177}")
55
75
  assert_equal("Fw70WQMviRFGyeze3MUpfafaAcWIvHRpnq4ZK3lxexrR1p+rLxK5C4qVKU71XYrPYR7XEBxgUG1oyKNOhFOVyx31EjC462dz7Vxn6UDpD1LIwNnD28+oHfS9AFzGKcn4zUZqT+8IvOO1jiS9c3Y8WAkOloN9AwGIIKWU8zAp1n4=", ([r.signature].pack("m*")).gsub(/\n/,"").chomp)
56
76
  end
57
- end
77
+ end
@@ -0,0 +1,153 @@
1
+ require_relative 'spec_helper'
2
+ require 'minitest/mock'
3
+ require 'tempfile'
4
+
5
+ class TestScopedIPv6 < Minitest::Test
6
+ SCOPED_SERVERS = %w[
7
+ fe80::feed:face:c0ff:ee00%en0
8
+ fe80::5054:ff:fe8b:6576%br0
9
+ fe80::1%3
10
+ FE80:0:0:0:0:0:0:ABCD%Eth0
11
+ fe80::1%eth0.100
12
+ fe80::1%veth-1
13
+ fe80::1%interface_1
14
+ ].freeze
15
+
16
+ def without_hostname_lookup
17
+ unexpected = ->(*) { flunk('A scoped literal must not trigger DNS resolution') }
18
+ Dnsruby::Resolver.stub(:new, unexpected) do
19
+ Dnsruby::Recursor.stub(:new, unexpected) { yield }
20
+ end
21
+ end
22
+
23
+ def test_scoped_nameservers_are_validated_without_dns_and_preserved
24
+ without_hostname_lookup do
25
+ SCOPED_SERVERS.each do |server|
26
+ assert_equal(server, Dnsruby::Config.resolve_server(server))
27
+ config = Dnsruby::Config.new
28
+ config.nameserver = [server]
29
+ assert_equal([server], config.nameserver)
30
+ assert_equal(Dnsruby::IPv6.create(server.split('%').first),
31
+ Dnsruby::Config.ipv6_server_address(server))
32
+ end
33
+ end
34
+ end
35
+
36
+ def test_explicit_ipv6_endpoint_validation_rejects_malformed_literals
37
+ invalid = [
38
+ 'fe80::1%', 'fe80::1%en0%en1', 'fe80::1%en 0',
39
+ "fe80::1%en0\n", "fe80::1%en0\0", 'fe80::1%en0/64',
40
+ 'fe80::1/64%en0', '[fe80::1%en0]', '192.0.2.1%en0',
41
+ 'example.test%en0', '%en0', 'fe80::gggg%en0',
42
+ '1:2:3:4:5:6:7::8%en0', '1:2:3:4:5:6::192.0.2.1%3',
43
+ ]
44
+ without_hostname_lookup do
45
+ invalid.each do |server|
46
+ assert_raises(ArgumentError, server) { Dnsruby::Config.ipv6_server_address(server) }
47
+ refute(Dnsruby::Config.scoped_ipv6_server?(server), server)
48
+ end
49
+ end
50
+ end
51
+
52
+ def test_percent_containing_dns_names_remain_nameserver_names
53
+ queried = []
54
+ lookup = Object.new
55
+ lookup.define_singleton_method(:query) do |name|
56
+ queried << name.to_s
57
+ response = Dnsruby::Message.new(name)
58
+ response.add_answer(Dnsruby::RR.create(name: name, type: 'A', address: '192.0.2.53'))
59
+ Dnsruby::Message.decode(response.encode)
60
+ end
61
+ lookup.define_singleton_method(:close) { }
62
+ Dnsruby::Config.stub(:nameserver_resolver, lookup) do
63
+ ['ns%blue.example', 'ns:blue%en0.example', 'example.test%en0', '%en0',
64
+ '192.0.2.1%en0', 'fe80::1%', 'fe80::1%en0/64', 'fe80::gggg%en0'].each do |name|
65
+ [name, Dnsruby::Name.create(name)].each do |input|
66
+ config = Dnsruby::Config.new
67
+ config.nameserver = [input]
68
+ assert_equal([input], config.nameserver)
69
+ assert_equal('192.0.2.53', Dnsruby::Config.resolve_server(input))
70
+ assert_equal(name, queried.last)
71
+ resolver = Dnsruby::Resolver.new(nameserver: [input])
72
+ assert_equal(['192.0.2.53'], resolver.single_resolvers.map(&:server))
73
+ resolver.close
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def test_resolver_constructor_forms_keep_scoped_nameserver
80
+ server = SCOPED_SERVERS.first
81
+ [server, {nameserver: server}, {nameserver: [server, '192.0.2.1']}].each do |options|
82
+ resolver = Dnsruby::Resolver.new(options)
83
+ expected = options.is_a?(Hash) ? Array(options[:nameserver]) : [server]
84
+ assert_equal(expected, resolver.single_resolvers.map(&:server))
85
+ ensure
86
+ resolver.close if resolver
87
+ end
88
+ single = Dnsruby::SingleResolver.new(server)
89
+ assert_equal(server, single.server)
90
+ end
91
+
92
+ def test_resolv_conf_retains_scoped_server_and_ipv4_fallback
93
+ Tempfile.create('dnsruby-scoped-resolv') do |file|
94
+ file.write("nameserver #{SCOPED_SERVERS.first}\nnameserver 192.168.0.1\n")
95
+ file.flush
96
+ resolver = Dnsruby::Resolver.new(config_info: file.path)
97
+ expected = [SCOPED_SERVERS.first, '192.168.0.1']
98
+ assert_equal(expected, resolver.config.nameserver)
99
+ assert_equal(expected, resolver.single_resolvers.map(&:server))
100
+ ensure
101
+ resolver.close if resolver
102
+ end
103
+ end
104
+
105
+ def test_server_setter_switches_address_family
106
+ sender = Dnsruby::PacketSender.new('192.0.2.1')
107
+ refute(sender.instance_variable_get(:@ipv6))
108
+ SCOPED_SERVERS.each do |server|
109
+ sender.server = server
110
+ assert_equal(server, sender.server)
111
+ assert(sender.instance_variable_get(:@ipv6))
112
+ end
113
+ sender.server = '192.0.2.2'
114
+ refute(sender.instance_variable_get(:@ipv6))
115
+ end
116
+
117
+ def test_unscoped_addresses_remain_supported
118
+ ['192.0.2.1', '2001:db8::1', 'fe80::1', '::1'].each do |server|
119
+ assert_equal(server, Dnsruby::Config.resolve_server(server))
120
+ end
121
+ [Dnsruby::IPv4.create('192.0.2.1'), Dnsruby::IPv6.create('2001:db8::1')].each do |server|
122
+ assert_same(server, Dnsruby::Config.resolve_server(server))
123
+ end
124
+ end
125
+
126
+ def test_config_accepts_mixed_plain_and_scoped_literals_without_dns
127
+ servers = ['192.0.2.1', '2001:DB8:0:0:0:0:0:53', '::1',
128
+ '::ffff:192.0.2.53', SCOPED_SERVERS.first, 'fe80::1%3']
129
+ without_hostname_lookup do
130
+ config = Dnsruby::Config.new
131
+ config.nameserver = servers
132
+ assert_equal(servers, config.nameserver)
133
+ Tempfile.create('dnsruby-mixed-ip-resolv') do |file|
134
+ servers.each { |server| file.puts("nameserver #{server}") }
135
+ file.flush
136
+ assert_equal(servers, Dnsruby::Config.parse_resolv_conf(file.path)[:nameserver])
137
+ config = Dnsruby::Config.new
138
+ config.set_config_info(file.path)
139
+ assert_equal(servers, config.nameserver)
140
+ end
141
+ end
142
+ end
143
+
144
+ def test_scope_does_not_become_dns_record_data
145
+ assert_raises(ArgumentError) { Dnsruby::IPv6.create('fe80::1%en0') }
146
+ assert_raises(ArgumentError) { Dnsruby::RR.create('host.test. 60 IN AAAA fe80::1%en0') }
147
+ message = Dnsruby::Message.new
148
+ message.add_answer(Dnsruby::RR.create('host.test. 60 IN AAAA fe80::1'))
149
+ answer = Dnsruby::Message.decode(message.encode).answer.first
150
+ assert_equal('FE80::1', answer.address.to_s)
151
+ assert_equal(16, answer.address.address.bytesize)
152
+ end
153
+ end