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
data/test/tc_name.rb CHANGED
@@ -20,6 +20,11 @@ class TestName < Minitest::Test
20
20
 
21
21
  include Dnsruby
22
22
 
23
+ # Names on either side of the 255-octet wire limit: 249 octets of label text
24
+ # in 5 labels, plus a length octet each and the root's zero octet, is 255.
25
+ AT_LIMIT_NAME = "#{'a' * 63}.#{'b' * 63}.#{'c' * 63}.#{'d' * 59}.e."
26
+ OVER_LIMIT_NAME = "#{'a' * 63}.#{'b' * 63}.#{'c' * 63}.#{'d' * 60}.e."
27
+
23
28
  def test_label_length
24
29
  Name::Label.set_max_length(Name::Label::MaxLabelLength) # Other tests may have changed this
25
30
  # Test max label length = 63
@@ -30,13 +35,102 @@ class TestName < Minitest::Test
30
35
  end
31
36
  end
32
37
 
38
+ def wire_length(name)
39
+ MessageEncoder.new { |msg| msg.put_name(name, true) }.to_s.length
40
+ end
41
+
42
+ # Test max name length=255. The limit is on the wire form, two octets more
43
+ # than the dotted presentation form without its trailing dot.
33
44
  def test_name_length
34
- # Test max name length=255
35
- begin
36
- Name.create("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123.com")
37
- assert(false, "Name of length > 255 allowed")
38
- rescue ResolvError
45
+ assert_equal(255, wire_length(Name.create(AT_LIMIT_NAME)))
46
+
47
+ error = assert_raises(ResolvError) { Name.create(OVER_LIMIT_NAME) }
48
+ assert_match(/Name length is 256,/, error.message)
49
+ end
50
+
51
+ # An oversized name must not reach the wire through Message either, and a
52
+ # name right at the limit must still survive the round trip.
53
+ def test_name_length_in_message
54
+ assert_raises(ResolvError) { Message.new(OVER_LIMIT_NAME, "A") }
55
+
56
+ message = Message.new(AT_LIMIT_NAME, "A")
57
+ message.add_answer(RR.create("#{AT_LIMIT_NAME} 3600 IN NS #{AT_LIMIT_NAME}"))
58
+ decoded = Message.decode(message.encode)
59
+ assert_equal(AT_LIMIT_NAME, decoded.question[0].qname.to_s + ".")
60
+ assert_equal(AT_LIMIT_NAME, decoded.answer[0].domainname.to_s + ".")
61
+ end
62
+
63
+ def test_deep_backward_compression_chain_is_iterative_and_preserves_position
64
+ wire = "\x07example\x03com\0".b
65
+ target = 0
66
+ 8_000.times do
67
+ offset = wire.bytesize
68
+ wire << [0xc000 | target].pack('n')
69
+ target = offset
39
70
  end
71
+ wire << "!"
72
+ decoder = MessageDecoder.new(wire)
73
+ decoder.get_bytes(target)
74
+ assert_equal('example.com.', decoder.get_name.to_s(true))
75
+ assert_equal(target + 2, decoder.index)
76
+ assert_equal('!', decoder.get_bytes)
77
+ end
78
+
79
+ def test_compressed_names_share_suffixes_and_chain_to_complete_names
80
+ wire = "\x07example\x03com\0".b
81
+ www = wire.bytesize
82
+ wire << "\x03www".b << [0xc000].pack('n')
83
+ wire << "\x04mail".b << [0xc000].pack('n')
84
+ wire << [0xc000 | www].pack('n')
85
+ decoder = MessageDecoder.new(wire)
86
+ assert_equal(['example.com.', 'www.example.com.', 'mail.example.com.', 'www.example.com.'],
87
+ Array.new(4) { decoder.get_name.to_s(true) })
88
+ assert_equal(wire.bytesize, decoder.index)
89
+ end
90
+
91
+ def test_compressed_rdata_can_reference_a_name_before_its_length_field
92
+ wire = "\x07example\x03com\0".b + [2, 0xc000].pack('nn') + '!'
93
+ decoder = MessageDecoder.new(wire)
94
+ expected = decoder.get_name
95
+ assert_equal(expected, decoder.get_length16 { decoder.get_name })
96
+ assert_equal('!', decoder.get_bytes)
97
+ end
98
+
99
+ def test_compression_rejects_loops_forward_pointers_and_reserved_labels
100
+ malformed = [
101
+ ["\xc0\x00".b, 0], # Self pointer.
102
+ ["\xc0\x02\0".b, 0], # Forward pointer.
103
+ ["\xc0\x02\xc0\x00".b, 2], # Two-pointer cycle.
104
+ ["\x01a\xc0\x00".b, 0], # Label followed by a pointer into itself.
105
+ ["\xc0".b, 0], # Incomplete pointer.
106
+ ["\x01a".b, 0], # Missing root label.
107
+ ["\x02a".b, 0], # Truncated label.
108
+ ["\x40".b + 'a' * 64 + "\0", 0],
109
+ ["\x80\0".b, 0], # Reserved label types.
110
+ ["\xbf\0".b, 0]
111
+ ]
112
+ malformed.each do |wire, offset|
113
+ decoder = MessageDecoder.new(wire)
114
+ decoder.get_bytes(offset)
115
+ assert_raises(DecodeError, wire.unpack1('H*')) { decoder.get_name }
116
+ end
117
+ end
118
+
119
+ def test_expanded_compressed_names_obey_the_wire_length_limit
120
+ encoded = MessageEncoder.new { |encoder| encoder.put_name(Name.create(AT_LIMIT_NAME), true) }.to_s
121
+ decoder = MessageDecoder.new(encoded + [0xc000].pack('n') + "\x01x".b + [0xc000].pack('n'))
122
+ assert_equal(AT_LIMIT_NAME, decoder.get_name.to_s(true))
123
+ assert_equal(AT_LIMIT_NAME, decoder.get_name.to_s(true))
124
+ assert_raises(DecodeError) { decoder.get_name }
125
+
126
+ oversized = OVER_LIMIT_NAME.split('.').map { |label| [label.bytesize].pack('C') + label }.join + "\0"
127
+ assert_raises(DecodeError) { MessageDecoder.new(oversized).get_name }
128
+ end
129
+
130
+ def test_name_cannot_read_a_root_label_beyond_its_rdata_boundary
131
+ decoder = MessageDecoder.new([2].pack('n') + "\x01a\0".b)
132
+ assert_raises(DecodeError) { decoder.get_length16 { decoder.get_name } }
133
+ assert_equal("\0", decoder.get_bytes)
40
134
  end
41
135
 
42
136
  def test_absolute
@@ -0,0 +1,322 @@
1
+ require_relative 'spec_helper'
2
+ require 'minitest/mock'
3
+ require 'tempfile'
4
+
5
+ class TestNameserverBootstrap < Minitest::Test
6
+ def with_resolv_conf(contents)
7
+ Tempfile.create('dnsruby-nameserver-bootstrap') do |file|
8
+ file.write(contents)
9
+ file.flush
10
+ yield file.path
11
+ end
12
+ end
13
+
14
+ def answer_for(hostname, address = nil)
15
+ message = Dnsruby::Message.new(hostname)
16
+ if address
17
+ type = address.include?(':') ? 'AAAA' : 'A'
18
+ message.add_answer(Dnsruby::RR.create("#{hostname} 60 IN #{type} #{address}"))
19
+ end
20
+ message
21
+ end
22
+
23
+ # Exercise real Resolver and PacketSender initialization, replacing only DNS
24
+ # queries. Bound configuration reads and construction so a regression cannot
25
+ # recreate issue #179's unbounded chain of resolver threads. Worker threads
26
+ # record their observations through queues instead of performing assertions.
27
+ def with_lookup(defaults, response, recursive_response: nil, lookup_error: nil)
28
+ original_new = Dnsruby::Resolver.method(:new)
29
+ state = { queries: Queue.new, recursors: Queue.new, closed: Queue.new,
30
+ resolvers: Queue.new, counts: Hash.new(0) }
31
+ mutex = Mutex.new
32
+ count = lambda do |kind|
33
+ mutex.synchronize do
34
+ state[:counts][kind] += 1
35
+ raise 'Nameserver bootstrap reentered too many times' if state[:counts][kind] > 12
36
+ end
37
+ end
38
+ default_config = lambda do
39
+ count.call(:config_reads)
40
+ config = defaults.respond_to?(:call) ? defaults.call : defaults
41
+ config.merge(nameserver: Array(config[:nameserver]).dup)
42
+ end
43
+ factory = lambda do |*args|
44
+ count.call(:resolver_constructions)
45
+ resolver = original_new.call(*args)
46
+ state[:resolvers] << resolver
47
+ original_close = resolver.method(:close)
48
+ resolver.define_singleton_method(:close) do
49
+ state[:closed] << self
50
+ original_close.call
51
+ end
52
+ resolver.define_singleton_method(:query) do |hostname, *|
53
+ servers = single_resolvers.map(&:server)
54
+ state[:queries] << [self, hostname.to_s, servers]
55
+ raise lookup_error if lookup_error
56
+ response
57
+ end
58
+ resolver
59
+ end
60
+ recursive_factory = lambda do |*args|
61
+ state[:recursors] << args
62
+ recursor = Object.new
63
+ recursor.define_singleton_method(:query) do |hostname, *|
64
+ recursive_response || Dnsruby::Message.new(hostname)
65
+ end
66
+ recursor
67
+ end
68
+ Dnsruby::Config.stub(:default_config_hash, default_config) do
69
+ Dnsruby::Resolver.stub(:new, factory) do
70
+ Dnsruby::Recursor.stub(:new, recursive_factory) do
71
+ yield state, original_new
72
+ end
73
+ end
74
+ end
75
+ ensure
76
+ unless state.nil?
77
+ state[:resolvers].pop(true).close until state[:resolvers].empty?
78
+ end
79
+ end
80
+
81
+ def test_resolv_conf_preserves_hostnames_literals_and_token_order
82
+ servers = %w[ns.example.test 192.0.2.1 localhost 2001:db8::1 fe80::1%en0 ns.example.test]
83
+ contents = "nameserver #{servers.first(3).join(' ')} # comment\n" \
84
+ "nameserver #{servers.drop(3).join(' ')} ; comment\n"
85
+ with_resolv_conf(contents) do |path|
86
+ assert_equal(servers, Dnsruby::Config.parse_resolv_conf(path)[:nameserver])
87
+ end
88
+ end
89
+
90
+ def test_overlong_nameserver_from_file_does_not_hide_a_valid_server_or_override
91
+ previous_limit = Dnsruby::Name::Label.class_variable_get(:@@max_length)
92
+ Dnsruby::Name::Label.set_max_length(Dnsruby::Name::Label::MaxLabelLength)
93
+ invalid = ('a' * 64) + '.example.'
94
+ with_resolv_conf("nameserver #{invalid}\nnameserver 127.0.0.1\n") do |path|
95
+ defaults = Dnsruby::Config.parse_resolv_conf(path)
96
+ Dnsruby::Config.stub(:default_config_hash, defaults) do
97
+ config = Dnsruby::Config.new
98
+ config.get_ready
99
+ assert_equal(['127.0.0.1'], config.nameserver)
100
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.2')
101
+ assert_equal(['127.0.0.2'], resolver.config.nameserver)
102
+ assert_equal(['127.0.0.2'], resolver.single_resolvers.map(&:server))
103
+ assert_raises(Dnsruby::ResolvError) do
104
+ Dnsruby::Config.new.set_config_info(nameserver: [invalid])
105
+ end
106
+ end
107
+ end
108
+ ensure
109
+ Dnsruby::Name::Label.set_max_length(previous_limit)
110
+ end
111
+
112
+ def test_failed_nameserver_replacement_preserves_config_and_active_senders
113
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1')
114
+ original = resolver.single_resolvers.first
115
+ assert_raises(ArgumentError) { resolver.nameserver = [] }
116
+ assert_equal(['127.0.0.1'], resolver.config.nameserver)
117
+ assert_equal([original], resolver.single_resolvers)
118
+ resolver.nameserver = '127.0.0.2'
119
+ assert_equal(['127.0.0.2'], resolver.config.nameserver)
120
+ assert_equal(['127.0.0.2'], resolver.single_resolvers.map(&:server))
121
+ end
122
+
123
+ def test_invalid_default_nameserver_fails_without_recursive_initialization
124
+ with_resolv_conf("nameserver 8.8.8.8,\n") do |path|
125
+ defaults = -> { Dnsruby::Config.parse_resolv_conf(path) }
126
+ with_lookup(defaults, answer_for('unused.example.test')) do |state, original_new|
127
+ resolver = original_new.call
128
+ error = assert_raises(ArgumentError) { resolver.add_config_nameservers }
129
+ assert_equal('No valid nameservers found', error.message)
130
+ assert_equal(['8.8.8.8,'], resolver.config.nameserver)
131
+ assert_operator(state[:counts][:config_reads], :<=, 3)
132
+ assert_equal(0, state[:counts][:resolver_constructions])
133
+ assert(state[:queries].empty?)
134
+ assert(state[:recursors].empty?)
135
+ ensure
136
+ resolver.close if resolver
137
+ end
138
+ end
139
+ end
140
+
141
+ def test_scoped_looking_dns_names_do_not_hide_valid_fallbacks
142
+ contents = "nameserver fe80::1%\nnameserver fe80::gggg%en0\n" \
143
+ "nameserver 192.0.2.1\nnameserver fe80::1%3\n"
144
+ with_resolv_conf(contents) do |path|
145
+ defaults = -> { Dnsruby::Config.parse_resolv_conf(path) }
146
+ [nil, {config_info: path}, {config_info: {search: ['example.test']}}].each do |options|
147
+ with_lookup(defaults, nil) do |state, original_new|
148
+ resolver = options ? original_new.call(options) : original_new.call
149
+ resolver.add_config_nameservers unless options
150
+ assert_equal(['fe80::1%', 'fe80::gggg%en0', '192.0.2.1', 'fe80::1%3'], resolver.config.nameserver)
151
+ assert_equal(['192.0.2.1', 'fe80::1%3'], resolver.single_resolvers.map(&:server))
152
+ queried = []
153
+ queried << state[:queries].pop(true)[1] until state[:queries].empty?
154
+ # PacketSender also retries with nil after a failed server setter.
155
+ assert_equal(['', '', 'fe80::1%', 'fe80::gggg%en0'].sort, queried.sort)
156
+ assert_equal(4, state[:counts][:resolver_constructions])
157
+ ensure
158
+ resolver.close if resolver
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ def test_invalid_scoped_defaults_do_not_block_explicit_nameserver
165
+ with_resolv_conf("nameserver fe80::1%\n") do |path|
166
+ defaults = -> { Dnsruby::Config.parse_resolv_conf(path) }
167
+ with_lookup(defaults, nil) do |state, original_new|
168
+ ['192.0.2.53', {nameserver: '2001:db8::53'}].each do |options|
169
+ resolver = original_new.call(options)
170
+ expected = options.is_a?(Hash) ? options[:nameserver] : options
171
+ assert_equal([expected], resolver.single_resolvers.map(&:server))
172
+ ensure
173
+ resolver.close if resolver
174
+ end
175
+ resolver = original_new.call
176
+ assert_raises(ArgumentError) { resolver.add_config_nameservers }
177
+ assert_equal(['fe80::1%'], resolver.config.nameserver)
178
+ assert(state[:queries].empty?)
179
+ assert_equal(0, state[:counts][:resolver_constructions])
180
+ ensure
181
+ resolver.close if resolver
182
+ end
183
+ end
184
+ end
185
+
186
+ def test_explicit_config_hash_preserves_scoped_looking_dns_names
187
+ config = Dnsruby::Config.new
188
+ servers = ['fe80::1%', '192.0.2.1']
189
+ config.set_config_info(nameserver: servers)
190
+ assert_equal(servers, config.nameserver)
191
+ assert_raises(ArgumentError) { Dnsruby::Config.ipv6_server_address(servers.first) }
192
+ end
193
+
194
+ def test_default_file_hostname_uses_literal_nameserver_without_changing_config
195
+ hostname = 'ns.example.test'
196
+ with_resolv_conf("nameserver #{hostname}\nnameserver 192.0.2.1\n") do |path|
197
+ defaults = -> { Dnsruby::Config.parse_resolv_conf(path) }
198
+ with_lookup(defaults, answer_for(hostname, '192.0.2.53')) do |state, original_new|
199
+ resolver = original_new.call
200
+ resolver.add_config_nameservers
201
+ assert_equal([hostname, '192.0.2.1'], resolver.config.nameserver)
202
+ assert_equal(['192.0.2.53', '192.0.2.1'], resolver.single_resolvers.map(&:server))
203
+ lookup = state[:queries].pop(true)
204
+ assert_equal(hostname, lookup[1])
205
+ assert_equal(['192.0.2.1'], lookup[2])
206
+ assert_same(lookup[0], state[:closed].pop(true))
207
+ assert(state[:queries].empty?)
208
+ assert_equal(1, state[:counts][:resolver_constructions])
209
+ ensure
210
+ resolver.close if resolver
211
+ end
212
+ end
213
+ end
214
+
215
+ def test_custom_file_hostname_uses_system_nameserver
216
+ hostname = 'custom-ns.example.test'
217
+ with_resolv_conf("nameserver #{hostname}\n") do |path|
218
+ with_lookup({nameserver: ['192.0.2.1']}, answer_for(hostname, '192.0.2.53')) do |state, original_new|
219
+ resolver = original_new.call(config_info: path)
220
+ assert_equal([hostname], resolver.config.nameserver)
221
+ assert_equal(['192.0.2.53'], resolver.single_resolvers.map(&:server))
222
+ assert_equal([hostname, ['192.0.2.1']], state[:queries].pop(true).drop(1))
223
+ assert(state[:queries].empty?)
224
+ ensure
225
+ resolver.close if resolver
226
+ end
227
+ end
228
+ end
229
+
230
+ def test_localhost_file_nameserver_does_not_require_dns
231
+ with_resolv_conf("nameserver localhost\n") do |path|
232
+ with_lookup(-> { Dnsruby::Config.parse_resolv_conf(path) }, nil) do |state, original_new|
233
+ resolver = original_new.call(config_info: path)
234
+ assert_equal(['localhost'], resolver.config.nameserver)
235
+ assert_equal(['127.0.0.1'], resolver.single_resolvers.map(&:server))
236
+ assert(state[:queries].empty?)
237
+ assert_equal(0, state[:counts][:resolver_constructions])
238
+ ensure
239
+ resolver.close if resolver
240
+ end
241
+ end
242
+ end
243
+
244
+ def test_explicit_hostname_and_name_object_remain_supported
245
+ hostname = 'explicit-ns.example.test'
246
+ with_lookup({nameserver: ['192.0.2.1']}, answer_for(hostname, '192.0.2.53')) do |state, original_new|
247
+ resolver = original_new.call(nameserver: hostname)
248
+ assert_equal(['192.0.2.53'], resolver.single_resolvers.map(&:server))
249
+ assert_equal('192.0.2.53', Dnsruby::Config.resolve_server(Dnsruby::Name.create(hostname)))
250
+ 2.times do
251
+ assert_equal([hostname, ['192.0.2.1']], state[:queries].pop(true).drop(1))
252
+ end
253
+ assert(state[:queries].empty?)
254
+ ensure
255
+ resolver.close if resolver
256
+ end
257
+ end
258
+
259
+ def test_bootstrap_keeps_ipv6_scopes_and_localhost_but_excludes_hostnames
260
+ hostname = 'ns.example.test'
261
+ servers = [hostname, '192.0.2.1', '2001:db8::1', 'fe80::1%en0',
262
+ 'fe80::1%3', 'localhost', '8.8.8.8,', 'fe80::1%']
263
+ with_lookup({nameserver: servers}, answer_for(hostname, '192.0.2.53')) do |state, _|
264
+ assert_equal('192.0.2.53', Dnsruby::Config.resolve_server(hostname))
265
+ assert_equal(['192.0.2.1', '2001:db8::1', 'fe80::1%en0', 'fe80::1%3', '127.0.0.1'],
266
+ state[:queries].pop(true)[2])
267
+ assert_equal([hostname, '192.0.2.1', '2001:db8::1', 'fe80::1%en0',
268
+ 'fe80::1%3', 'localhost', '8.8.8.8,', 'fe80::1%'], servers)
269
+ assert(state[:queries].empty?)
270
+ end
271
+ end
272
+
273
+ def test_all_hostname_defaults_fail_before_creating_helper_resolver
274
+ hostname = 'ns.example.test'
275
+ with_lookup({nameserver: [hostname, 'other-ns.example.test']}, nil) do |state, _|
276
+ error = assert_raises(ArgumentError) { Dnsruby::Config.resolve_server(hostname) }
277
+ assert_match(/No nameserver addresses available/, error.message)
278
+ assert_equal(1, state[:counts][:config_reads])
279
+ assert_equal(0, state[:counts][:resolver_constructions])
280
+ assert(state[:queries].empty?)
281
+ assert(state[:recursors].empty?)
282
+ end
283
+ end
284
+
285
+ def test_recursive_fallback_accepts_addresses_and_reuses_safe_resolver
286
+ hostname = 'ns.example.test'
287
+ { '192.0.2.53' => '192.0.2.53', '2001:db8::53' => '2001:DB8::53' }.each do |address, expected|
288
+ with_lookup({nameserver: [hostname, '192.0.2.1']}, answer_for(hostname),
289
+ recursive_response: answer_for(hostname, address)) do |state, _|
290
+ assert_equal(expected, Dnsruby::Config.resolve_server(hostname))
291
+ lookup = state[:queries].pop(true)
292
+ assert_equal(['192.0.2.1'], lookup[2])
293
+ assert_equal([lookup[0]], state[:recursors].pop(true))
294
+ assert_same(lookup[0], state[:closed].pop(true))
295
+ assert_equal(1, state[:counts][:resolver_constructions])
296
+ assert(state[:queries].empty?)
297
+ assert(state[:recursors].empty?)
298
+ end
299
+ end
300
+ end
301
+
302
+ def test_recursive_fallback_after_query_error_and_without_address
303
+ hostname = 'ns.example.test'
304
+ [answer_for(hostname, '192.0.2.53'), answer_for(hostname)].each do |response|
305
+ with_lookup({nameserver: [hostname, '192.0.2.1']}, nil,
306
+ lookup_error: Dnsruby::ResolvTimeout.new('simulated timeout'),
307
+ recursive_response: response) do |state, _|
308
+ if response.answer.empty?
309
+ error = assert_raises(ArgumentError) { Dnsruby::Config.resolve_server(hostname) }
310
+ assert_match(/Recursor can't locate/, error.message)
311
+ else
312
+ assert_equal('192.0.2.53', Dnsruby::Config.resolve_server(hostname))
313
+ end
314
+ lookup = state[:queries].pop(true)
315
+ assert_equal(['192.0.2.1'], lookup[2])
316
+ assert_equal([lookup[0]], state[:recursors].pop(true))
317
+ assert_same(lookup[0], state[:closed].pop(true))
318
+ assert_equal(1, state[:counts][:resolver_constructions])
319
+ end
320
+ end
321
+ end
322
+ end
data/test/tc_nsec.rb CHANGED
@@ -126,50 +126,9 @@ class NsecTest < Minitest::Test
126
126
  end
127
127
  end
128
128
 
129
- def test_examples_from_rfc_4035_wildcard_expansion
130
- # Grab the example responses from RFC4035 and make sure that they pass.
131
- # Then, try changing some of the NSEC values (ignoring the RRSIGs for now)
132
- # and make sure that they fail verification for that reason
133
- m = Message.new
134
- m.header.rcode =
135
- m.add_question(Question.new("a.z.w.example.", Types.MX))
136
- m.add_answer(RR.create("a.z.w.example. 3600 IN MX 1 ai.example."))
137
- m.add_answer(RR.create("a.z.w.example. 3600 RRSIG MX 5 4 3600 20040509183619 (
138
- 20040409183619 38519 example.
139
- OMK8rAZlepfzLWW75Dxd63jy2wswESzxDKG2
140
- f9AMN1CytCd10cYISAxfAdvXSZ7xujKAtPbc
141
- tvOQ2ofO7AZJ+d01EeeQTVBPq4/6KCWhqe2X
142
- TjnkVLNvvhnc0u28aoSsG0+4InvkkOHknKxw
143
- 4kX18MMR34i8lC36SR5xBni8vHI= )"))
144
- m.add_authority(RR.create("x.y.w.example. 3600 NSEC xx.example. MX RRSIG NSEC"))
145
- begin
146
- Dnssec.anchor_verifier.verify_nsecs(m)
147
- fail("Should have failed with bad number of labels in RRSIG")
148
- rescue VerifyError
149
- end
150
- m.answer.delete(RR.create("a.z.w.example. 3600 RRSIG MX 5 4 3600 20040509183619 (
151
- 20040409183619 38519 example.
152
- OMK8rAZlepfzLWW75Dxd63jy2wswESzxDKG2
153
- f9AMN1CytCd10cYISAxfAdvXSZ7xujKAtPbc
154
- tvOQ2ofO7AZJ+d01EeeQTVBPq4/6KCWhqe2X
155
- TjnkVLNvvhnc0u28aoSsG0+4InvkkOHknKxw
156
- 4kX18MMR34i8lC36SR5xBni8vHI= )"))
157
- m.add_answer(RR.create("a.z.w.example. 3600 RRSIG MX 5 2 3600 20040509183619 (
158
- 20040409183619 38519 example.
159
- OMK8rAZlepfzLWW75Dxd63jy2wswESzxDKG2
160
- f9AMN1CytCd10cYISAxfAdvXSZ7xujKAtPbc
161
- tvOQ2ofO7AZJ+d01EeeQTVBPq4/6KCWhqe2X
162
- TjnkVLNvvhnc0u28aoSsG0+4InvkkOHknKxw
163
- 4kX18MMR34i8lC36SR5xBni8vHI= )"))
164
- Dnssec.anchor_verifier.verify_nsecs(m)
165
- m.authority.delete(RR.create("x.y.w.example. 3600 NSEC xx.example. MX RRSIG NSEC"))
166
- m.add_authority(RR.create("x.y.w.example. 3600 NSEC z.w.example. MX RRSIG NSEC"))
167
- begin
168
- Dnssec.anchor_verifier.verify_nsecs(m)
169
- fail("Should have failed with bad NSEC")
170
- rescue VerifyError
171
- end
172
- end
129
+ # Positive wildcard expansion from RFC 4035 is covered by
130
+ # DnssecWildcardProofsTest#test_examples_from_rfc_4035_wildcard_expansion,
131
+ # where both the answer and its denial proof undergo signature verification.
173
132
 
174
133
  def test_examples_from_rfc_4035_wildcard_no_data
175
134
  # Grab the example responses from RFC4035 and make sure that they pass.
@@ -223,4 +182,4 @@ class NsecTest < Minitest::Test
223
182
  # fail
224
183
  # end
225
184
  #
226
- end
185
+ end
data/test/tc_nsec3.rb CHANGED
@@ -69,6 +69,22 @@ class Nsec3Test < Minitest::Test
69
69
  assert_equal(nsec.to_s, nsec3.to_s)
70
70
  end
71
71
 
72
+ def test_future_flags_round_trip_but_are_unusable_for_denial
73
+ record = Dnsruby::RR.create(INPUT)
74
+ record.flags = 2
75
+ message = Dnsruby::Message.new
76
+ message.add_authority(record)
77
+ decoded = Dnsruby::Message.decode(message.encode).authority.first
78
+ assert_equal(2, decoded.flags)
79
+ assert_equal(record.to_s, decoded.to_s)
80
+ refute(decoded.supported_parameters?)
81
+ refute(decoded.usable_hash?)
82
+ [0, 1].each do |flags|
83
+ decoded.flags = flags
84
+ assert(decoded.usable_hash?)
85
+ end
86
+ end
87
+
72
88
  def test_calculate_hash
73
89
  input = [
74
90
  [ "example" , "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom"],
@@ -113,7 +129,7 @@ class Nsec3Test < Minitest::Test
113
129
  # rescue DecodeError
114
130
  # end
115
131
  begin
116
- nsec.flags = 2
132
+ nsec.flags = 256
117
133
  fail
118
134
  rescue DecodeError
119
135
  end