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,252 @@
1
+ require_relative 'spec_helper'
2
+ require 'minitest/mock'
3
+
4
+ class SupportingLookupLimitsTest < Minitest::Test
5
+ def test_missing_glue_keeps_exact_context_without_configuration_threads
6
+ control = Dnsruby::QueryControl.new
7
+ transmissions = Dnsruby::SelectThread::TransmissionBudget.new
8
+ seen, resolver = [], nil
9
+ resolve = ->(*) { seen << [Dnsruby::QueryControl.current, Dnsruby::SelectThread::TransmissionBudget.current, Thread.current]; '192.0.2.53' }
10
+ verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
11
+ name = Dnsruby::Name.create('example.')
12
+ response = Dnsruby::Message.new(name, 'NS')
13
+ response.add_answer(Dnsruby::RR.create('example. 60 IN NS ns.example.'))
14
+ Dnsruby::Dnssec.stub(:default_resolver, nil) do
15
+ Dnsruby::Config.stub(:resolve_server, resolve) do
16
+ without_configuration_threads do
17
+ Dnsruby::QueryControl.with(control) do
18
+ Dnsruby::SelectThread::TransmissionBudget.with(transmissions) do
19
+ resolver = verifier.get_nameservers_from_message(name, response)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ assert_equal(['192.0.2.53'], resolver.single_resolvers.map(&:server))
26
+ refute_empty(seen)
27
+ seen.each do |actual_control, actual_transmissions, thread|
28
+ assert_same(control, actual_control)
29
+ assert_same(transmissions, actual_transmissions)
30
+ assert_same(Thread.current, thread)
31
+ end
32
+ assert_nil(Dnsruby::QueryControl.current)
33
+ assert_nil(Dnsruby::SelectThread::TransmissionBudget.current)
34
+ ensure
35
+ resolver&.close
36
+ end
37
+
38
+ def test_either_scope_keeps_configuration_on_the_calling_thread
39
+ [:control, :transmissions].each do |scope|
40
+ seen, resolver = [], nil
41
+ resolve = ->(*) { seen << Thread.current; '192.0.2.53' }
42
+ construct = -> { resolver = Dnsruby::Resolver.new(nameserver: 'ns.example.') }
43
+ Dnsruby::Config.stub(:resolve_server, resolve) do
44
+ without_configuration_threads do
45
+ if scope == :control
46
+ Dnsruby::QueryControl.with(Dnsruby::QueryControl.new, &construct)
47
+ else
48
+ Dnsruby::SelectThread::TransmissionBudget.with(Dnsruby::SelectThread::TransmissionBudget.new, &construct)
49
+ end
50
+ end
51
+ end
52
+ assert_equal(['192.0.2.53'], resolver.single_resolvers.map(&:server))
53
+ refute_empty(seen)
54
+ assert(seen.all? { |thread| thread.equal?(Thread.current) })
55
+ ensure
56
+ resolver&.close
57
+ end
58
+ end
59
+
60
+ def test_partial_construction_is_closed_without_replacing_existing_senders
61
+ error = Dnsruby::RecursionLimitError.new('Supporting lookup budget exhausted')
62
+ resolver = Dnsruby::Resolver.new
63
+ retained, created = sender_double('192.0.2.1'), sender_double('192.0.2.2')
64
+ resolver.single_resolvers = [retained]
65
+ candidates = []
66
+ construct = ->(options) { candidates << options.fetch(:server); raise error if candidates.length == 2; created }
67
+ Dnsruby::PacketSender.stub(:new, construct) do
68
+ without_configuration_threads do
69
+ actual = assert_raises(Dnsruby::RecursionLimitError) do
70
+ Dnsruby::QueryControl.with(Dnsruby::QueryControl.new) do
71
+ resolver.nameserver = %w[first.example. second.example. third.example.]
72
+ end
73
+ end
74
+ assert_same(error, actual)
75
+ end
76
+ end
77
+ assert_equal(%w[first.example. second.example.], candidates)
78
+ assert_equal(1, created.close_count)
79
+ assert_equal(0, retained.close_count)
80
+ assert_equal([retained], resolver.single_resolvers)
81
+ ensure
82
+ resolver&.close
83
+ end
84
+
85
+ def test_deferred_configuration_retries_with_fresh_context_after_exhaustion
86
+ error = Dnsruby::RecursionLimitError.new('Supporting lookup budget exhausted')
87
+ resolver = Dnsruby::Resolver.new
88
+ resolver.config.nameserver = ['ns.example.']
89
+ attempts = 0
90
+ resolve = ->(*) { attempts += 1; raise error if attempts == 1; '192.0.2.53' }
91
+ Dnsruby::Config.stub(:resolve_server, resolve) do
92
+ without_configuration_threads do
93
+ actual = assert_raises(Dnsruby::RecursionLimitError) do
94
+ Dnsruby::QueryControl.with(Dnsruby::QueryControl.new) { resolver.add_config_nameservers }
95
+ end
96
+ assert_same(error, actual)
97
+ Dnsruby::QueryControl.with(Dnsruby::QueryControl.new) { resolver.add_config_nameservers }
98
+ end
99
+ end
100
+ assert_equal(['192.0.2.53'], resolver.single_resolvers.map(&:server))
101
+ assert_operator(attempts, :>, 1)
102
+ ensure
103
+ resolver&.close
104
+ end
105
+
106
+ def test_config_preserves_limits_from_primary_and_recursive_lookups
107
+ [:primary, :recursive].each do |stage|
108
+ expected = Dnsruby::RecursionLimitError.new('Supporting lookup budget exhausted')
109
+ with_config_lookup(stage, -> { raise expected }) do |state|
110
+ actual = assert_raises(Dnsruby::RecursionLimitError) { Dnsruby::Config.resolve_server('ns.example.') }
111
+ assert_same(expected, actual)
112
+ assert_equal(stage == :primary ? [:primary] : [:primary, :recursive], state[:calls])
113
+ assert_equal(1, state[:closed])
114
+ end
115
+ end
116
+ end
117
+
118
+ def test_query_control_cancellation_stops_configuration_without_fallback
119
+ control = Dnsruby::QueryControl.new
120
+ cancelled = nil
121
+ cancel = lambda do
122
+ control.cancel
123
+ begin
124
+ control.check!
125
+ rescue Dnsruby::QueryControl::Cancelled => error
126
+ cancelled = error
127
+ raise
128
+ end
129
+ end
130
+ with_config_lookup(:primary, cancel) do |state|
131
+ actual = assert_raises(Dnsruby::QueryControl::Cancelled) do
132
+ Dnsruby::QueryControl.with(control) { Dnsruby::Config.resolve_server('ns.example.') }
133
+ end
134
+ assert_same(cancelled, actual)
135
+ assert_equal([:primary], state[:calls])
136
+ assert_equal(1, state[:closed])
137
+ end
138
+ assert_nil(Dnsruby::QueryControl.current)
139
+ end
140
+
141
+ def test_config_preserves_internal_validation_abort_from_either_lookup
142
+ [:primary, :recursive].each do |stage|
143
+ budget = Dnsruby::ValidationBudget.new(limits: Dnsruby::Dnssec.validation_limits.merge(max_work: 1))
144
+ abort = nil
145
+ exhaust = lambda do
146
+ begin
147
+ budget.check!(2)
148
+ rescue StandardError => error
149
+ abort = error
150
+ raise
151
+ end
152
+ end
153
+ with_config_lookup(stage, exhaust) do |state|
154
+ error = assert_raises(Dnsruby::ValidationLimitError) do
155
+ Dnsruby::ValidationBudget.with(budget) { Dnsruby::Config.resolve_server('ns.example.') }
156
+ end
157
+ assert_same(abort, error.cause)
158
+ assert_same(error, abort.public_error)
159
+ assert_equal(stage == :primary ? [:primary] : [:primary, :recursive], state[:calls])
160
+ assert_equal(1, state[:closed])
161
+ end
162
+ assert_nil(Dnsruby::ValidationBudget.current)
163
+ assert_nil(Dnsruby::QueryControl.current)
164
+ end
165
+ end
166
+
167
+ def test_constructors_preserve_terminal_errors_without_retrying
168
+ [Dnsruby::RecursionLimitError, Dnsruby::QueryControl::Cancelled, Dnsruby::ValidationLimitError].each do |error_class|
169
+ constructors.each do |construct|
170
+ expected = error_class.new('Supporting lookup stopped')
171
+ calls = []
172
+ resolve = ->(name) { calls << name; raise expected }
173
+ Dnsruby::Config.stub(:resolve_server, resolve) do
174
+ without_configuration_threads do
175
+ actual = assert_raises(error_class) do
176
+ Dnsruby::QueryControl.with(Dnsruby::QueryControl.new, &construct)
177
+ end
178
+ assert_same(expected, actual)
179
+ end
180
+ end
181
+ assert_equal(['ns.example.'], calls)
182
+ end
183
+ end
184
+ end
185
+
186
+ def test_constructors_preserve_internal_validation_abort
187
+ constructors.each do |construct|
188
+ budget = Dnsruby::ValidationBudget.new(limits: Dnsruby::Dnssec.validation_limits.merge(max_work: 1))
189
+ abort, calls = nil, []
190
+ resolve = lambda do |name|
191
+ calls << name
192
+ begin
193
+ budget.check!(2)
194
+ rescue StandardError => error
195
+ abort = error
196
+ raise
197
+ end
198
+ end
199
+ Dnsruby::Config.stub(:resolve_server, resolve) do
200
+ without_configuration_threads do
201
+ error = assert_raises(Dnsruby::ValidationLimitError) do
202
+ Dnsruby::ValidationBudget.with(budget, &construct)
203
+ end
204
+ assert_same(abort, error.cause)
205
+ assert_same(error, abort.public_error)
206
+ end
207
+ end
208
+ assert_equal(['ns.example.'], calls)
209
+ assert_nil(Dnsruby::ValidationBudget.current)
210
+ assert_nil(Dnsruby::QueryControl.current)
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ def constructors
217
+ [->(*) { Dnsruby::PacketSender.new(server: 'ns.example.', ignore_config_resolv_errors: true) },
218
+ ->(*) { Dnsruby::Resolver.new(nameserver: 'ns.example.') }]
219
+ end
220
+
221
+ def without_configuration_threads(&block)
222
+ Thread.stub(:new, ->(*) { flunk('Scoped nameserver configuration must not create a thread') }, &block)
223
+ end
224
+
225
+ def sender_double(address)
226
+ Struct.new(:server, :close_count) do
227
+ def close
228
+ self.close_count += 1
229
+ end
230
+ end.new(address, 0)
231
+ end
232
+
233
+ def with_config_lookup(stage, stopped)
234
+ state = {calls: [], closed: 0}
235
+ lookup = Object.new
236
+ lookup.define_singleton_method(:query) do |*|
237
+ state[:calls] << :primary
238
+ stage == :primary ? stopped.call : Dnsruby::Message.new('ns.example.')
239
+ end
240
+ lookup.define_singleton_method(:close) { state[:closed] += 1 }
241
+ recursive = Object.new
242
+ recursive.define_singleton_method(:query) { |*| state[:calls] << :recursive; stopped.call }
243
+ build_recursive = lambda do |resolver|
244
+ flunk('Stopped configuration must not start recursive fallback') unless stage == :recursive
245
+ assert_same(lookup, resolver)
246
+ recursive
247
+ end
248
+ Dnsruby::Config.stub(:nameserver_resolver, lookup) do
249
+ Dnsruby::Recursor.stub(:new, build_recursive) { yield state }
250
+ end
251
+ end
252
+ end