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,196 @@
1
+ require_relative 'tc_pipeline_send'
2
+ require_relative 'tc_review_transport_native'
3
+
4
+ class TestReleaseTransportFixes < Minitest::Test
5
+ def isolated(source)
6
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', '-e',
7
+ TestPipelineSend::FIXTURE + "\nTimeout.timeout(5) do\n" + source + "\nend\n",
8
+ chdir: File.expand_path('..', __dir__))
9
+ assert(status.success?, "#{stdout}\n#{stderr}")
10
+ end
11
+
12
+ def test_published_configuration_serves_queries_during_nameserver_construction
13
+ isolated(<<~'RUBY')
14
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
15
+ sent = Queue.new
16
+ resolver.single_resolvers.first.define_singleton_method(:send_async) { |*| sent << true }
17
+ original = Dnsruby::PacketSender.method(:new)
18
+ entered, release = Queue.new, Queue.new
19
+ Dnsruby::PacketSender.define_singleton_method(:new) do |*args|
20
+ entered << true
21
+ release.pop
22
+ original.call(*args)
23
+ end
24
+ [:add_server, :nameserver=].each do |operation|
25
+ configuring = Thread.new { resolver.public_send(operation, '127.0.0.2') }
26
+ entered.pop
27
+ begin
28
+ caller = Thread.new { resolver.send_async(query, Queue.new) }
29
+ raise 'query waited for replacement configuration' unless caller.join(1)
30
+ caller.value
31
+ raise 'published sender was not used' unless sent.pop(true)
32
+ ensure
33
+ release << true
34
+ configuring.value
35
+ caller&.join
36
+ end
37
+ end
38
+ resolver.close
39
+ RUBY
40
+ end
41
+
42
+ def test_failed_initial_configuration_waits_for_recovery_publication
43
+ isolated(<<~'RUBY')
44
+ resolver = Dnsruby::Resolver.new(nameserver: [], dnssec: false)
45
+ raise 'expected failed initialization' unless resolver.instance_variable_get(:@configuration_error)
46
+ original = Dnsruby::PacketSender.method(:new)
47
+ entered, release, sent = Queue.new, Queue.new, Queue.new
48
+ Dnsruby::PacketSender.define_singleton_method(:new) do |*args|
49
+ entered << true
50
+ release.pop
51
+ writer = original.call(*args)
52
+ writer.define_singleton_method(:send_async) { |*| sent << true }
53
+ writer
54
+ end
55
+ configuring = Thread.new { resolver.nameserver = '127.0.0.1' }
56
+ entered.pop
57
+ caller = Thread.new { resolver.send_async(query, Queue.new) }
58
+ begin
59
+ raise 'query admitted before recovered senders were published' if caller.join(0.05)
60
+ ensure
61
+ release << true
62
+ configuring.value
63
+ end
64
+ caller.value
65
+ raise 'recovered sender was not used' unless sent.pop(true)
66
+ raise 'configuration error survived publication' if resolver.instance_variable_get(:@configuration_error)
67
+ resolver.close
68
+ RUBY
69
+ end
70
+
71
+ def test_single_resolver_timeout_setters_update_the_existing_sender
72
+ isolated(<<~'RUBY')
73
+ resolver = Dnsruby::SingleResolver.new(server: '127.0.0.1', packet_timeout: 2)
74
+ writer = resolver.single_resolvers.first
75
+ [:packet_timeout=, :query_timeout=].zip([3, 4]).each do |setter, timeout|
76
+ resolver.public_send(setter, timeout)
77
+ raise 'sender was replaced' unless resolver.single_resolvers.first.equal?(writer)
78
+ raise 'stale packet timeout' unless writer.packet_timeout == timeout &&
79
+ resolver.packet_timeout == timeout && resolver.query_timeout == timeout
80
+ end
81
+ resolver.close
82
+ RUBY
83
+ end
84
+
85
+ def test_selector_waits_for_an_empty_resolvers_exact_query_deadline
86
+ isolated(<<~'RUBY')
87
+ now = 100.0
88
+ Dnsruby::TransportClock.define_singleton_method(:now) { now }
89
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
90
+ resolver = Dnsruby::Resolver.new
91
+ resolver.single_resolvers = []
92
+ resolver.query_timeout = 0.05
93
+ results = Queue.new
94
+ resolver.send_async(query, results, :deadline)
95
+ wakeup = FrameSocket.new
96
+ K.class_variable_set(:@@wakeup_sockets, [wakeup, wakeup])
97
+ K.class_variable_get(:@@sockets) << wakeup
98
+ waits = []
99
+ IO.define_singleton_method(:select) do |_read, _write, _errors, timeout|
100
+ waits << timeout
101
+ now += timeout
102
+ nil
103
+ end
104
+ SELECTOR.do_select
105
+ raise 'deadline was rounded to a polling interval' unless (waits.first - 0.05).abs < 0.000001
106
+ result = results.pop(true)
107
+ raise 'wrong completion' unless result.first == :deadline && result.last.is_a?(Dnsruby::ResolvTimeout)
108
+ raise 'duplicate completion' unless results.empty?
109
+ RUBY
110
+ end
111
+
112
+ def test_selector_schedules_retries_at_their_exact_deadline
113
+ isolated(<<~'RUBY')
114
+ now = 100.0
115
+ Dnsruby::TransportClock.define_singleton_method(:now) { now }
116
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
117
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
118
+ retry_times: 2, retry_delay: 0.025, query_timeout: 0.2)
119
+ sent = []
120
+ resolver.single_resolvers.first.define_singleton_method(:send_async) { |*| sent << now }
121
+ results = Queue.new
122
+ resolver.send_async(query, results, :retries)
123
+ wakeup = FrameSocket.new
124
+ K.class_variable_set(:@@wakeup_sockets, [wakeup, wakeup])
125
+ K.class_variable_get(:@@sockets) << wakeup
126
+ IO.define_singleton_method(:select) do |_read, _write, _errors, timeout|
127
+ now += timeout
128
+ nil
129
+ end
130
+ SELECTOR.do_select
131
+ raise 'missing or duplicate attempt' unless sent.length == 2
132
+ raise 'retry missed its deadline' unless (sent.last - 100.05).abs < 0.000001
133
+ raise 'query did not finish' unless results.pop(true).last.is_a?(Dnsruby::ResolvTimeout)
134
+ raise 'duplicate completion' unless results.empty?
135
+ RUBY
136
+ end
137
+
138
+ def test_idle_expiration_never_closes_a_pipeline_with_an_outstanding_query
139
+ isolated(<<~'RUBY')
140
+ now = 100.0
141
+ Dnsruby::TransportClock.define_singleton_method(:now) { now }
142
+ socket = FrameSocket.new
143
+ writer = sender(socket)
144
+ writer.tcp_idle_timeout = 2
145
+ first = settings(socket, :first, query, writer)
146
+ SELECTOR.add_to_select(first)
147
+ SELECTOR.claim_query(first, true)
148
+ now = 101.0
149
+ SELECTOR.send(:expire_idle_sockets)
150
+ raise 'closed before idle deadline' if socket.closed?
151
+ second = settings(socket, :second, query, writer)
152
+ SELECTOR.add_to_select(second)
153
+ now = 110.0
154
+ SELECTOR.send(:expire_idle_sockets)
155
+ raise 'closed an active pipeline' if socket.closed?
156
+ SELECTOR.claim_query(second, true)
157
+ now = 112.0
158
+ SELECTOR.send(:expire_idle_sockets)
159
+ raise 'idle connection retained' unless socket.closed?
160
+ assert_clean(socket)
161
+ raise 'idle timer retained' unless K.class_variable_get(:@@socket_idle_deadlines).empty?
162
+ RUBY
163
+ end
164
+ end
165
+
166
+ class ReviewTransportNativeTest < Minitest::Test
167
+ def test_total_query_timeout_is_not_rounded_to_half_a_second
168
+ port, = udp_server { |_| [] }
169
+ resolver = resolver_for(port, packet_timeout: 2, query_timeout: 0.05, do_caching: false)
170
+ started = Dnsruby::TransportClock.now
171
+ assert_raises(Dnsruby::ResolvTimeout) { query(resolver) }
172
+ assert_operator(Dnsruby::TransportClock.now - started, :<, 0.3)
173
+ end
174
+
175
+ def test_idle_pipeline_closes_and_the_next_query_reconnects
176
+ server = TCPServer.new('127.0.0.1', 0)
177
+ @sockets << server
178
+ clients = Queue.new
179
+ worker do
180
+ loop do
181
+ client = server.accept
182
+ @sockets << client
183
+ clients << client
184
+ request = read_frame(client)
185
+ write_frame(client, response(request, 'connected'))
186
+ end
187
+ end
188
+ resolver = resolver_for(server.addr[1], use_tcp: true, tcp_pipelining: true,
189
+ tcp_pipelining_max_queries: :infinite, tcp_idle_timeout: 0.05, do_caching: false)
190
+ assert_equal(['connected'], query(resolver).answer.first.strings)
191
+ first = clients.pop
192
+ assert_nil(Timeout.timeout(1) { first.read(1) }, 'client should close the idle stream')
193
+ assert_equal(['connected'], query(resolver).answer.first.strings)
194
+ refute_same(first, clients.pop)
195
+ end
196
+ end
@@ -0,0 +1,157 @@
1
+ require_relative 'spec_helper'
2
+
3
+ class ReleaseZoneDefaultsTest < Minitest::Test
4
+ def record(reader, line)
5
+ Dnsruby::RR.create(reader.process_line(line))
6
+ end
7
+
8
+ def test_ttl_directive_survives_explicit_record_ttls_and_origin_changes
9
+ reader = Dnsruby::ZoneReader.new('example.', 900)
10
+ reader.process_line('$TTL 1h')
11
+ assert_equal(60, record(reader, 'one 60 IN A 192.0.2.1').ttl)
12
+ assert_equal(3600, record(reader, 'two IN A 192.0.2.2').ttl)
13
+ assert_equal(120, record(reader, 'three IN 2m A 192.0.2.3').ttl)
14
+ reader.process_line('$ORIGIN sub')
15
+ assert_equal(3600, record(reader, 'four IN A 192.0.2.4').ttl)
16
+ reader.process_line('$TTL 0')
17
+ assert_equal(60, record(reader, 'five 60 IN A 192.0.2.5').ttl)
18
+ assert_equal(0, record(reader, 'six IN A 192.0.2.6').ttl)
19
+ end
20
+
21
+ def test_previous_ttl_inheritance_remains_available_without_a_directive
22
+ reader = Dnsruby::ZoneReader.new('example.', 900)
23
+ assert_equal(900, record(reader, 'one IN A 192.0.2.1').ttl)
24
+ record(reader, 'two 60 IN A 192.0.2.2')
25
+ assert_equal(60, record(reader, 'three IN A 192.0.2.3').ttl)
26
+ end
27
+
28
+ def test_single_quoted_text_survives_both_header_orders_and_omitted_owner
29
+ %w[TXT SPF TYPE16 TYPE99].each do |type|
30
+ reader = Dnsruby::ZoneReader.new('example.')
31
+ ["www IN 1h #{type} 'one ; (two)'", "www 1h IN #{type} 'one ; (two)'",
32
+ " CLASS1 3600 #{type} 'one ; (two)'"].each do |line|
33
+ rr = record(reader, line)
34
+ assert_equal(['one ; (two)'], rr.strings)
35
+ assert_equal(3600, rr.ttl)
36
+ end
37
+ end
38
+ end
39
+
40
+ def test_single_quoted_continuation_and_double_quoted_text
41
+ reader = Dnsruby::ZoneReader.new('example.')
42
+ assert_nil(reader.process_line("www IN 60 TXT ( 'one ;'\n"))
43
+ rr = record(reader, " 'two (three)' )")
44
+ assert_equal(['one ;', 'two (three)'], rr.strings)
45
+ assert_equal(['one ; (two)'], record(reader, 'www IN 1h TXT "one ; (two)"').strings)
46
+ end
47
+
48
+ def test_rdata_domain_names_are_qualified_against_the_origin
49
+ reader = Dnsruby::ZoneReader.new('example.', 60)
50
+ examples = {
51
+ 'KX 10 mail' => :exchange,
52
+ 'NSEC next A NSEC RRSIG' => :next_domain,
53
+ 'NXT next A NXT' => :next_domain,
54
+ 'RRSIG A 8 2 60 20300101000000 20200101000000 1234 signer AQID' => :signers_name,
55
+ 'IPSECKEY 10 3 2 gateway AQID' => :gateway
56
+ }
57
+ examples.each do |rdata, field|
58
+ rr = record(reader, "host IN #{rdata}")
59
+ assert(rr.public_send(field).absolute?, rdata)
60
+ assert(rr.public_send(field).subdomain_of?(Dnsruby::Name.create('example.')), rdata)
61
+ message = Dnsruby::Message.new
62
+ message.add_answer(rr)
63
+ decoded = Dnsruby::Message.decode(message.encode).answer.first
64
+ assert_equal(rr.public_send(field), decoded.public_send(field), rdata)
65
+ end
66
+ hip = record(reader, 'host IN HIP 2 00112233 AQID rendezvous other.example.net.')
67
+ assert_equal(['rendezvous.example.', 'other.example.net.'], hip.rsvs.map { |name| name.to_s(true) })
68
+ ipv4 = record(reader, 'host IN IPSECKEY 10 1 2 192.0.2.1 AQID')
69
+ assert_equal('192.0.2.1', ipv4.gateway.to_s)
70
+ end
71
+
72
+ def test_relative_rdata_names_preserve_escaped_final_dots
73
+ reader = Dnsruby::ZoneReader.new('example.', 60)
74
+ rr = record(reader, 'host IN NSEC next\\. A NSEC RRSIG')
75
+ assert_equal(['next.', 'example'], rr.next_domain.labels.map(&:string))
76
+ rr = record(reader, 'host IN KX 10 mail\\.')
77
+ assert_equal(['mail.', 'example'], rr.exchange.labels.map(&:string))
78
+ end
79
+
80
+ def test_compression_snapshots_mutable_label_strings
81
+ text = +'before'
82
+ name = Dnsruby::Name.new([Dnsruby::Name::Label.new(text), Dnsruby::Name::Label.new('example')], true)
83
+ encoder = Dnsruby::MessageEncoder.new
84
+ encoder.put_name(name)
85
+ text.replace('after')
86
+ encoder.put_name(name)
87
+ encoder.put_name(Dnsruby::Name.create('before.example.'))
88
+ decoder = Dnsruby::MessageDecoder.new(encoder.to_s)
89
+ assert_equal(['before.example.', 'after.example.', 'before.example.'],
90
+ 3.times.map { decoder.get_name.to_s(true) })
91
+ end
92
+
93
+ def test_generic_rdata_bypasses_all_type_specific_name_and_interval_rewrites
94
+ examples = [
95
+ 'MX 10 Mail.Other.', 'KX 10 Mail.Other.', 'NS Ns.Other.',
96
+ 'AFSDB 1 Host.Other.', 'NAPTR 10 20 "s" "SIP+D2U" "" Replacement.Other.',
97
+ 'RT 10 Host.Other.', 'SRV 0 5 5060 Service.Other.', 'CNAME Alias.Other.',
98
+ 'MB Mail.Other.', 'MG Mail.Other.', 'MR Mail.Other.', 'PTR Pointer.Other.',
99
+ 'DNAME Target.Other.',
100
+ 'SOA Ns.Other. Hostmaster.Other. 1 3600 600 86400 300',
101
+ 'MINFO One.Other. Two.Other.', 'PX 10 One.Other. Two.Other.',
102
+ 'RP Mailbox.Other. Text.Other.', 'NSEC Next.Other. A NSEC RRSIG',
103
+ 'NXT Next.Other. A NXT',
104
+ 'RRSIG NSEC 8 2 60 20300101000000 20200101000000 1234 Signer.Other. AQID',
105
+ 'IPSECKEY 10 3 2 Gateway.Other. AQID',
106
+ 'HIP 2 00112233 AQID Rendezvous.Other.',
107
+ 'SVCB 1 Target.Other. alpn=h2', 'HTTPS 1 Target.Other. port=443'
108
+ ]
109
+ examples.each do |presentation|
110
+ original = Dnsruby::RR.create("owner.origin. 60 IN #{presentation}")
111
+ bytes = rdata_bytes(original)
112
+ generic = "\\# #{bytes.bytesize} #{bytes.unpack1('H*').upcase}"
113
+ [original.type.to_s, "TYPE#{original.type.code}"].each do |type|
114
+ reader = Dnsruby::ZoneReader.new('origin.')
115
+ normalized = reader.process_line("owner IN 1m #{type} #{generic}")
116
+ assert_equal(generic, normalized.split(/\s+/, 5).last.strip, presentation)
117
+ parsed = Dnsruby::RR.create(normalized)
118
+ assert_equal('owner.origin.', parsed.name.to_s(true), presentation)
119
+ assert_equal(60, parsed.ttl, presentation)
120
+ assert_equal(bytes, rdata_bytes(parsed), presentation)
121
+ end
122
+ end
123
+ end
124
+
125
+ def test_generic_rdata_preserves_zone_defaults_continuations_and_prefix_contract
126
+ original = Dnsruby::RR.create('owner.origin. 60 IN NSEC Next.Other. A NSEC RRSIG')
127
+ bytes = rdata_bytes(original)
128
+ hex = bytes.unpack1('H*')
129
+ reader = Dnsruby::ZoneReader.new('origin.')
130
+ reader.process_line('$TTL 2h')
131
+ assert_nil(reader.process_line("owner IN 1m TYPE47 ( \\# #{bytes.bytesize} #{hex[0, 8]}"))
132
+ explicit = record(reader, "#{hex[8..]} )")
133
+ inherited = record(reader, " IN TYPE47 \\# #{bytes.bytesize} #{hex}")
134
+ assert_equal([60, 7200], [explicit.ttl, inherited.ttl])
135
+ assert_equal('owner.origin.', inherited.name.to_s(true))
136
+ assert_equal(bytes, rdata_bytes(explicit))
137
+ assert_equal(bytes, rdata_bytes(inherited))
138
+
139
+ signature = Dnsruby::RR.create('owner.origin. 60 IN RRSIG NSEC 8 2 60 20300101000000 20200101000000 1234 Signer.Other. AQID')
140
+ signature_bytes = rdata_bytes(signature)
141
+ generic = "\\# #{signature_bytes.bytesize} #{signature_bytes.unpack1('H*')}"
142
+ line, prefix, owner = reader.normalise_line("owner 60 IN TYPE46 #{generic}", true)
143
+ assert_equal('ZZNSEC', prefix)
144
+ assert_equal('owner.origin.', owner)
145
+ assert_equal(generic, line.split(/\s+/, 5).last.strip)
146
+ assert_equal(signature_bytes, rdata_bytes(Dnsruby::RR.create(line)))
147
+ empty = record(reader, 'empty IN TYPE65000 \\# 0')
148
+ assert_equal(7200, empty.ttl)
149
+ assert_equal(''.b, rdata_bytes(empty))
150
+ end
151
+
152
+ private
153
+
154
+ def rdata_bytes(rr)
155
+ Dnsruby::MessageEncoder.new { |encoder| rr.encode_rdata(encoder, false) }.to_s
156
+ end
157
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'spec_helper'
2
+
3
+ class ReleaseZoneTtlsTest < Minitest::Test
4
+ def test_time_suffixes_in_directives_records_and_soa_intervals
5
+ reader = Dnsruby::ZoneReader.new('example.')
6
+ reader.process_line('$TTL 1H30m')
7
+ inherited = Dnsruby::RR.create(reader.process_line('www IN A 192.0.2.1'))
8
+ assert_equal(5400, inherited.ttl)
9
+ explicit = Dnsruby::RR.create(reader.process_line('mail 2h IN A 192.0.2.2'))
10
+ assert_equal(7200, explicit.ttl)
11
+ soa = Dnsruby::RR.create(reader.process_line('@ IN 1h SOA ns.example. hostmaster.example. 1 2h 10m 1w 5m'))
12
+ assert_equal([3600, 7200, 600, 604800, 300], [soa.ttl, soa.refresh, soa.retry, soa.expire, soa.minimum])
13
+ assert_equal(3630, reader.get_ttl('1h30'))
14
+ end
15
+
16
+ def test_class_before_ttl_and_ttl_before_class_are_equivalent
17
+ %w[3600 1h 1H 30m30m].each do |ttl|
18
+ expected = Dnsruby::RR.create(Dnsruby::ZoneReader.new('example.').process_line("www #{ttl} IN A 192.0.2.1"))
19
+ actual = Dnsruby::RR.create(Dnsruby::ZoneReader.new('example.').process_line("www IN #{ttl} A 192.0.2.1"))
20
+ assert_equal(expected, actual)
21
+ assert_equal(expected.ttl, actual.ttl)
22
+ end
23
+ end
24
+
25
+ def test_class_before_ttl_preserves_public_normalisation_hook_and_quoted_data
26
+ reader_class = Class.new(Dnsruby::ZoneReader) do
27
+ attr_reader :normalised
28
+ def normalise_line(*args)
29
+ @normalised = true
30
+ super
31
+ end
32
+ end
33
+ reader = reader_class.new('example.')
34
+ rr = Dnsruby::RR.create(reader.process_line('www IN 1h TXT "one ; two"'))
35
+ assert(reader.normalised)
36
+ assert_equal(3600, rr.ttl)
37
+ assert_equal(['one ; two'], rr.strings)
38
+ end
39
+ end
@@ -15,8 +15,22 @@
15
15
  # ++
16
16
 
17
17
  require_relative 'spec_helper'
18
+ require 'minitest/mock'
19
+ require_relative 'local_dns_server'
18
20
 
19
21
  class TestResolverConfig < Minitest::Test
22
+ include LocalDnsServer
23
+
24
+ BAD_SERVER = 'invalid-nameserver.test.'.freeze
25
+
26
+ def without_external_nameserver_lookup
27
+ original = Dnsruby::Config.method(:resolve_server)
28
+ lookup = lambda do |server|
29
+ raise ArgumentError, 'deliberately unusable nameserver fixture' if server == BAD_SERVER
30
+ original.call(server)
31
+ end
32
+ Dnsruby::Config.stub(:resolve_server, lookup) { yield }
33
+ end
20
34
 
21
35
  GoodInput = {
22
36
  "port" => 54,
@@ -73,7 +87,7 @@ class TestResolverConfig < Minitest::Test
73
87
  end
74
88
 
75
89
  def test_single_resolver
76
- [Dnsruby::SingleResolver.new()].each {|res|
90
+ [Dnsruby::SingleResolver.new('127.0.0.1')].each {|res|
77
91
  # [Dnsruby::SingleResolver.new({:nameserver => ["127.0.0.1"]}),
78
92
  # Dnsruby::SingleResolver.new({:nameserver => ["::1"]})].each {|res|
79
93
  GoodInput.each do | param, value |
@@ -85,7 +99,7 @@ class TestResolverConfig < Minitest::Test
85
99
  end
86
100
 
87
101
  def test_dns
88
- res = Dnsruby::DNS.new
102
+ res = Dnsruby::DNS.new(nameserver: ['127.0.0.1'])
89
103
  LookupInput.each do | param, value |
90
104
  res.config.send(param+"=", value)
91
105
  assert_equal(res.config.send(param), value, "setting #param sticks");
@@ -93,32 +107,40 @@ class TestResolverConfig < Minitest::Test
93
107
  end
94
108
 
95
109
  def test_bad_ns
96
- begin
97
- res = Dnsruby::Resolver.new(:nameserver => ["ns32.domiancontrol.com."])
98
- res.query("example.com")
99
- assert(false, "Should not query with bad nameserver")
100
- rescue Exception => e
101
- # OK
110
+ without_external_nameserver_lookup do
111
+ resolver = Dnsruby::Resolver.new(nameserver: BAD_SERVER, query_timeout: 0.1, do_caching: false)
112
+ assert_raises(Dnsruby::ResolvTimeout) { resolver.query('example.test.') }
113
+ ensure
114
+ resolver&.close
102
115
  end
103
116
  end
104
117
 
105
118
  def test_bad_ns_post_init
106
- begin
107
- resolver = Dnsruby::Resolver.new(recurse: false, do_caching: false, retry_times: 2)
108
- resolver.nameserver = ["ns32.domiancontrol.com."]
109
- assert(false, "Should not allow bad nameserver")
110
- resolver.query("example.com")
111
- rescue ArgumentError
119
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1')
120
+ without_external_nameserver_lookup do
121
+ assert_raises(ArgumentError) { resolver.nameserver = [BAD_SERVER] }
122
+ assert_equal(['127.0.0.1'], resolver.single_resolvers.map(&:server))
112
123
  end
124
+ ensure
125
+ resolver&.close
113
126
  end
114
127
 
115
128
  def test_one_good_ns_rest_bad
116
- begin
117
- resolver = Dnsruby::Resolver.new(recurse: false, do_caching: false, retry_times: 2)
118
- resolver.nameserver = ["ns32.domiancontrol.com.", "1.1.1.1"]
119
- resolver.query("example.com")
120
- rescue ArgumentError
121
- assert(false, "Should allow one good nameserver")
129
+ without_external_nameserver_lookup do
130
+ with_local_dns_server(:udp, answer: 'example.test. 60 IN A 192.0.2.42') do |resolver|
131
+ resolver.recurse = false
132
+ resolver.nameserver = [BAD_SERVER, '127.0.0.1']
133
+ assert_equal(['127.0.0.1'], resolver.single_resolvers.map(&:server))
134
+ response = resolver.query('example.test.')
135
+ assert_equal('192.0.2.42', response.answer.first.address.to_s)
136
+ end
137
+ end
138
+ end
139
+
140
+ def test_actual_servfail_is_reported
141
+ with_local_dns_server(:udp, rcode: Dnsruby::RCode::SERVFAIL) do |resolver|
142
+ error = assert_raises(Dnsruby::ServFail) { resolver.query('example.test.') }
143
+ assert_equal('SERVFAIL', error.response.rcode.to_s)
122
144
  end
123
145
  end
124
146
  end
data/test/tc_res_opt.rb CHANGED
@@ -15,11 +15,31 @@
15
15
  # ++
16
16
 
17
17
  require_relative 'spec_helper'
18
+ require 'minitest/mock'
18
19
 
19
20
  class TestResOpt < Minitest::Test
20
21
 
21
22
  include Dnsruby
22
23
 
24
+ def test_source_port_selection_uses_secure_randomness_with_existing_ranges
25
+ sender = PacketSender.new(server: '127.0.0.1')
26
+ [0, 15_535].each do |offset|
27
+ SecureRandom.stub(:random_number, lambda { |count| assert_equal(15_536, count); offset }) do
28
+ assert_equal(50_000 + offset, sender.get_next_src_port)
29
+ end
30
+ end
31
+ sender.src_port = [56_353, 60_001, 65_535]
32
+ 3.times do |index|
33
+ SecureRandom.stub(:random_number, lambda { |count| assert_equal(3, count); index }) do
34
+ assert_equal(sender.src_port[index], sender.get_next_src_port)
35
+ end
36
+ end
37
+ sender.src_port = 56_353
38
+ assert_equal(56_353, sender.get_next_src_port)
39
+ ensure
40
+ sender&.close
41
+ end
42
+
23
43
  def test_dns_file
24
44
 
25
45
  # .txt because this test will run under windows, unlike the other file
@@ -184,4 +204,4 @@ class TestResOpt < Minitest::Test
184
204
  # Dnsruby.log.level=Logger::ERROR
185
205
  end
186
206
  end
187
- end
207
+ end
data/test/tc_resolver.rb CHANGED
@@ -14,17 +14,14 @@
14
14
  # limitations under the License.
15
15
  # ++
16
16
  require_relative 'spec_helper'
17
+ require_relative 'local_dns_server'
17
18
 
18
19
  require 'socket'
19
20
 
20
- # @TODO@ We also need a test server so we can control behaviour of server to test
21
- # different aspects of retry strategy.
22
- # Of course, with Ruby's limit of 256 open sockets per process, we'd need to run
23
- # the server in a different Ruby process.
24
-
25
21
  class TestResolver < Minitest::Test
26
22
 
27
23
  include Dnsruby
24
+ include LocalDnsServer
28
25
 
29
26
  Thread::abort_on_exception = true
30
27
 
@@ -170,13 +167,19 @@ class TestResolver < Minitest::Test
170
167
  # end
171
168
  #
172
169
  def test_nxdomain
173
- resolver = Resolver.new
174
- q = Queue.new
175
- resolver .send_async(Message.new(BAD_DOMAIN_NAME, Types.A), q, 1)
176
- id, m, error = q.pop
177
- assert(id==1, "Id should have been 1 but was #{id}")
178
- assert(m.rcode == RCode.NXDOMAIN, "Expected NXDOMAIN but got #{m.rcode} instead.")
179
- assert_error_is_exception(error, NXDomain)
170
+ with_local_dns_server(:udp, rcode: RCode.NXDOMAIN) do |resolver|
171
+ message = Message.new(BAD_DOMAIN_NAME, Types.A)
172
+ message.do_caching = false
173
+ message.do_validation = false
174
+ q = Queue.new
175
+ resolver.send_async(message, q, 1)
176
+ id, response, error = q.pop
177
+ assert_equal(1, id)
178
+ assert_error_is_exception(error, NXDomain)
179
+ assert_valid_response(response)
180
+ assert_equal(RCode.NXDOMAIN, response.rcode)
181
+ assert_equal(message.header.id, response.header.id)
182
+ end
180
183
  end
181
184
 
182
185
  def test_timeouts
@@ -319,16 +322,16 @@ class TestResolver < Minitest::Test
319
322
 
320
323
  def test_custom_udp_size
321
324
  [false, true].each do |dnssec_setting|
322
- [512, 513].each do |udp_size_setting|
325
+ [512, 513, 1220, 1232, 4096].each do |udp_size_setting|
323
326
  res = Dnsruby::Resolver.new(:udp_size => udp_size_setting, :dnssec => dnssec_setting)
324
327
  sent_msg = capture_sent_message(res, "example.com")
325
328
 
326
329
  # The OPT RR is only present if the udp_size is > 512
327
330
  opt = sent_msg.get_opt
328
331
  if dnssec_setting
329
- # DNSSEC overrides udp_size to 4096
332
+ # The resolver's minimum UDP size with DNSSEC enabled is 1220.
330
333
  assert(opt)
331
- assert_equal(4096, opt.klass.code)
334
+ assert_equal([1220, udp_size_setting].max, opt.klass.code)
332
335
  else
333
336
  if udp_size_setting <= 512
334
337
  assert_nil(opt)
@@ -339,7 +342,7 @@ class TestResolver < Minitest::Test
339
342
  end
340
343
  if opt
341
344
  assert_equal(0, opt.version)
342
- assert_equal(0, opt.flags)
345
+ assert_equal(dnssec_setting ? 0x8000 : 0, opt.flags)
343
346
  end
344
347
  end
345
348
  end
@@ -358,7 +361,7 @@ class TestResolver < Minitest::Test
358
361
  assert_equal(1, sent_msg.additional.length) # No duplicate added
359
362
  assert_equal(pre_size, opt.klass.code) # Original payload size unchanged
360
363
  assert_equal(0, opt.version)
361
- assert_equal(0, opt.flags) # Flags unchanged (code doesn't modify existing OPT)
364
+ assert_equal(dnssec_setting ? 0x8000 : 0, opt.flags)
362
365
  end
363
366
  end
364
367
  end
@@ -369,7 +372,7 @@ class TestRawQuery < Minitest::Test
369
372
 
370
373
  KEY_NAME = 'key-name'
371
374
  KEY = '0123456789'
372
- ALGO = 'hmac-md5'
375
+ ALGO = 'hmac-sha256'
373
376
 
374
377
  class CustomError < RuntimeError; end
375
378
 
@@ -491,4 +494,3 @@ class TestRawQuery < Minitest::Test
491
494
  res.query("a.root-servers.net.", "A")
492
495
  end
493
496
  end
494
-