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,169 @@
1
+ require_relative 'spec_helper'
2
+ require 'timeout'
3
+
4
+ class RecursorAddressFallbackTest < Minitest::Test
5
+ def test_failed_family_keeps_the_other_address_family
6
+ with_address_lookup(['ns.example.']) do |run, calls, _recursor|
7
+ result = run.call do |name, type|
8
+ raise Dnsruby::ServFail, 'AAAA unavailable' if type == 'AAAA'
9
+ address_answer(name, type)
10
+ end
11
+ assert_equal(['192.0.2.53'], result)
12
+ assert_equal(%w[A AAAA], drain(calls).map(&:last).sort)
13
+ end
14
+ end
15
+
16
+ def test_failed_nameserver_does_not_hide_another_nameserver
17
+ with_address_lookup(%w[bad.example. ns.example.]) do |run, calls, _recursor|
18
+ result = run.call do |name, type|
19
+ raise Dnsruby::NXDomain, 'name unavailable' if name == 'bad.example.'
20
+ address_answer(name, type)
21
+ end
22
+ assert_equal(['192.0.2.53', '2001:db8::53'], result)
23
+ assert_equal(4, drain(calls).length)
24
+ end
25
+ end
26
+
27
+ def test_ordinary_lookup_failures_return_no_authority
28
+ with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
29
+ assert_nil(run.call { |_name, _type| raise Dnsruby::Refused, 'lookup refused' })
30
+ end
31
+ end
32
+
33
+ def test_validation_limit_is_preserved_and_cancels_a_waiting_sibling
34
+ with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
35
+ waiting = Queue.new
36
+ finished = Queue.new
37
+ limit = Dnsruby::ValidationLimitError.new('validation budget exhausted')
38
+ error = assert_raises(Dnsruby::ValidationLimitError) do
39
+ run.call do |_name, type|
40
+ if type == 'A'
41
+ waiting.pop
42
+ raise limit
43
+ end
44
+ begin
45
+ waiting << true
46
+ Dnsruby::QueryControl.current.pop(Dnsruby::QueryControl::Queue.new)
47
+ ensure
48
+ finished << true
49
+ end
50
+ end
51
+ end
52
+ assert_same(limit, error)
53
+ assert_equal(true, finished.pop(true))
54
+ end
55
+ end
56
+
57
+ def test_parent_cancellation_stops_all_address_workers
58
+ with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
59
+ parent = Dnsruby::QueryControl.new
60
+ started = Queue.new
61
+ finished = Queue.new
62
+ canceller = Thread.new { 2.times { started.pop }; parent.cancel }
63
+ assert_raises(Dnsruby::QueryControl::Cancelled) do
64
+ Dnsruby::QueryControl.with(parent) do
65
+ run.call do |_name, _type|
66
+ begin
67
+ started << true
68
+ Dnsruby::QueryControl.current.pop(Dnsruby::QueryControl::Queue.new)
69
+ ensure
70
+ finished << true
71
+ end
72
+ end
73
+ end
74
+ end
75
+ assert_equal([true, true], 2.times.map { finished.pop(true) })
76
+ ensure
77
+ canceller&.kill
78
+ canceller&.join
79
+ end
80
+ end
81
+
82
+ def test_one_worker_runs_the_other_family_inline_without_deadlock
83
+ with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
84
+ parent_thread = Thread.current
85
+ family_threads = Queue.new
86
+ release_worker = Queue.new
87
+ limits = Dnsruby::Recursor::DEFAULT_RECURSION_LIMITS.merge(max_workers: 1)
88
+ Dnsruby::Recursor.send(:with_limits, nil, limits) do |budget|
89
+ result = run.call do |name, type|
90
+ family_threads << [type, Thread.current]
91
+ type == 'AAAA' ? release_worker.pop : release_worker.push(true)
92
+ address_answer(name, type)
93
+ end
94
+ assert_equal(['192.0.2.53', '2001:db8::53'], result)
95
+ threads = drain(family_threads).to_h
96
+ assert_same(parent_thread, threads['A'])
97
+ refute_same(parent_thread, threads['AAAA'])
98
+ assert(budget.reserve_worker, 'Completed discovery must release its worker slot')
99
+ budget.release_worker
100
+ end
101
+ end
102
+ end
103
+
104
+ def test_worker_creation_failure_releases_its_slot
105
+ with_address_lookup(['ns.example.']) do |run, _calls, _recursor|
106
+ limits = Dnsruby::Recursor::DEFAULT_RECURSION_LIMITS.merge(max_workers: 1)
107
+ Dnsruby::Recursor.send(:with_limits, nil, limits) do |budget|
108
+ Thread.stub(:new, ->(*) { raise ThreadError, 'Worker unavailable' }) do
109
+ assert_raises(ThreadError) { run.call { |name, type| address_answer(name, type) } }
110
+ end
111
+ assert(budget.reserve_worker)
112
+ budget.release_worker
113
+ end
114
+ end
115
+ end
116
+
117
+ def test_recursion_limit_does_not_try_another_nameserver
118
+ with_address_lookup(%w[first.example. second.example.]) do |run, calls, _recursor|
119
+ error = Dnsruby::RecursionLimitError.new('Lookup work limit reached')
120
+ actual = assert_raises(Dnsruby::RecursionLimitError) { run.call { |*| raise error } }
121
+ assert_same(error, actual)
122
+ assert_equal(['first.example.'], drain(calls).map(&:first).uniq)
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ def address_answer(name, type)
129
+ answer = Dnsruby::Message.new(name, type)
130
+ address = type == 'AAAA' ? '2001:db8::53' : '192.0.2.53'
131
+ answer.add_answer(Dnsruby::RR.create("#{name} 300 IN #{type} #{address}"))
132
+ answer
133
+ end
134
+
135
+ def drain(queue)
136
+ Array.new(queue.length) { queue.pop(true) }
137
+ end
138
+
139
+ def with_address_lookup(names)
140
+ klass = Dnsruby::Recursor
141
+ original_hints = klass.class_variable_get(:@@hints)
142
+ original_cache = klass.class_variable_get(:@@authority_cache)
143
+ klass.class_variable_set(:@@hints, {})
144
+ klass.class_variable_set(:@@authority_cache, {})
145
+ recursor = klass.allocate
146
+ recursor.ipv6_ok = true
147
+ original_recursion = recursor.method(:_dorecursion)
148
+ authorities = names.to_h { |name| [name, klass::AddressCache.new] }
149
+ calls = Queue.new
150
+ run = lambda do |&lookup|
151
+ recursor.define_singleton_method(:_dorecursion) do |name, type, *_args|
152
+ if name == 'www.example.'
153
+ authorities.values.flat_map(&:values).map(&:downcase).sort
154
+ else
155
+ calls << [name, type]
156
+ lookup.call(name, type)
157
+ end
158
+ end
159
+ Timeout.timeout(3) do
160
+ original_recursion.call('www.example.', 'A', Dnsruby::Classes::IN,
161
+ 'example.', authorities, 0, true)
162
+ end
163
+ end
164
+ yield run, calls, recursor
165
+ ensure
166
+ klass.class_variable_set(:@@hints, original_hints)
167
+ klass.class_variable_set(:@@authority_cache, original_cache)
168
+ end
169
+ end
@@ -0,0 +1,139 @@
1
+ require_relative 'spec_helper'
2
+
3
+ class RecursorLimitsTest < Minitest::Test
4
+ def setup
5
+ @resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', query_timeout: 0)
6
+ @recursor = Dnsruby::Recursor.new(@resolver)
7
+ end
8
+
9
+ def teardown
10
+ @resolver.close
11
+ end
12
+
13
+ def test_defaults_are_finite_and_invalid_replacements_are_atomic
14
+ original = @recursor.recursion_limits
15
+ assert_equal(30.0, original[:timeout])
16
+ assert(original.frozen?)
17
+ [{timeout: 0}, {max_queries: -1}, {max_workers: 0}, {unknown: 1}].each do |settings|
18
+ assert_raises(ArgumentError) { @recursor.recursion_limits = settings }
19
+ assert_same(original, @recursor.recursion_limits)
20
+ end
21
+ @recursor.recursion_limits = {timeout: 10, max_queries: 20}
22
+ assert_equal(20, @recursor.recursion_limits[:max_queries])
23
+ assert_equal(8, @recursor.recursion_limits[:max_workers])
24
+ end
25
+
26
+ def test_default_zero_resolver_timeout_still_has_a_recursion_deadline
27
+ @recursor.define_singleton_method(:query_internal) do |*|
28
+ Dnsruby::QueryControl.current.deadline - Dnsruby::TransportClock.now
29
+ end
30
+ remaining = @recursor.query('example.', 'A', 'IN', true)
31
+ assert_operator(remaining, :>, 29)
32
+ assert_operator(remaining, :<=, 30)
33
+ end
34
+
35
+ def test_recursion_deadline_stops_a_wait_and_restores_thread_context
36
+ @recursor.recursion_limits = {timeout: 0.02}
37
+ @recursor.define_singleton_method(:query_internal) do |*|
38
+ Dnsruby::QueryControl.current.pop(Dnsruby::QueryControl::Queue.new)
39
+ end
40
+ assert_raises(Dnsruby::ResolvTimeout) { @recursor.query('example.', 'A', 'IN', true) }
41
+ assert_nil(Dnsruby::QueryControl.current)
42
+ assert_nil(Dnsruby::SelectThread::TransmissionBudget.current)
43
+ end
44
+
45
+ def test_shorter_parent_deadline_keeps_parent_error
46
+ parent = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + 0.02)
47
+ @recursor.define_singleton_method(:query_internal) do |*|
48
+ Dnsruby::QueryControl.current.pop(Dnsruby::QueryControl::Queue.new)
49
+ end
50
+ assert_raises(Dnsruby::QueryControl::DeadlineExceeded) do
51
+ Dnsruby::QueryControl.with(parent) { @recursor.query('example.', 'A', 'IN', true) }
52
+ end
53
+ end
54
+
55
+ def test_supporting_queries_share_transmission_and_work_limits
56
+ @recursor.recursion_limits = {max_queries: 2, max_steps: 2}
57
+ Dnsruby::Recursor.send(:with_limits, @resolver, @recursor.recursion_limits) do |budget|
58
+ transmissions = Dnsruby::SelectThread::TransmissionBudget.current
59
+ 2.times do |i|
60
+ Dnsruby::Recursor.send(:with_limits, @resolver) do |nested|
61
+ assert_same(budget, nested)
62
+ nested.step!
63
+ assert(transmissions.admit('192.0.2.53', 53, false, Dnsruby::Question.new("host#{i}.example.", 'A', 'IN')))
64
+ end
65
+ end
66
+ assert_raises(Dnsruby::RecursionLimitError) { budget.step! }
67
+ assert_raises(Dnsruby::RecursionLimitError) do
68
+ transmissions.admit('192.0.2.54', 53, false, Dnsruby::Question.new('another.example.', 'A', 'IN'))
69
+ end
70
+ end
71
+ end
72
+
73
+ def test_worker_slots_are_released_and_dependency_paths_restore
74
+ Dnsruby::Recursor.send(:with_limits, @resolver, @recursor.recursion_limits.merge(max_workers: 1)) do |budget|
75
+ assert(budget.reserve_worker)
76
+ refute(budget.reserve_worker)
77
+ budget.release_worker
78
+ assert(budget.reserve_worker)
79
+ budget.release_worker
80
+ assert_equal(:done, budget.with_dependency('ns.example.', 'A', 'IN') { :done })
81
+ budget.with_dependency('ns.example.', 'A', 'IN') do
82
+ refute(budget.with_dependency('NS.EXAMPLE.', 'A', 'IN') { flunk 'Repeated dependency ran' })
83
+ assert_equal(:other_family, budget.with_dependency('ns.example.', 'AAAA', 'IN') { :other_family })
84
+ end
85
+ assert_empty(Thread.current[:dnsruby_recursion_dependencies])
86
+ end
87
+ end
88
+
89
+ def test_transmission_limit_ends_the_resolvers_remaining_retry_plan
90
+ engine = Dnsruby::ResolverRuby.allocate
91
+ message = Dnsruby::Message.new('example.')
92
+ client_queue, select_queue = Queue.new, Queue.new
93
+ sender = Object.new
94
+ engine.instance_variable_set(:@query_list, {request: [message, client_queue, select_queue, [:another_attempt]]})
95
+ completion = nil
96
+ engine.define_singleton_method(:send_result_and_stop_querying) { |*args| completion = args }
97
+ error = Dnsruby::RecursionLimitError.new('Transmission limit reached')
98
+ engine.handle_error_response(select_queue, [sender, message, :request, 0], error, nil)
99
+ assert_equal([client_queue, :request, select_queue, nil, error], completion)
100
+ end
101
+
102
+ def test_root_bootstrap_batches_share_the_enclosing_transmission_limit
103
+ klass = Dnsruby::Recursor
104
+ saved = %i[@@hints @@stale_hints @@hints_generation @@nameservers @@zones_cache
105
+ @@zone_expirations @@authority_cache].to_h { |key| [key, klass.class_variable_get(key)] }
106
+ root = Dnsruby::Message.new('.', 'NS')
107
+ root.add_answer(Dnsruby::RR.create('. 300 IN NS ns.example.'))
108
+ budgets, queued = [], []
109
+ @resolver.define_singleton_method(:query_no_validation_or_recursion) do |*|
110
+ transmission = Dnsruby::SelectThread::TransmissionBudget.current
111
+ budgets << transmission
112
+ transmission.admit('192.0.2.53', 53, false, root.question.first)
113
+ root
114
+ end
115
+ @resolver.define_singleton_method(:send_async) do |message, queue|
116
+ queued << queue.length
117
+ transmission = Dnsruby::SelectThread::TransmissionBudget.current
118
+ budgets << transmission
119
+ transmission.admit('192.0.2.53', 53, false, message.question.first)
120
+ answer = Dnsruby::Message.new
121
+ type = message.question.first.qtype.to_s
122
+ address = type == 'A' ? '192.0.2.53' : '2001:db8::53'
123
+ answer.add_answer(Dnsruby::RR.create("ns.example. 300 IN #{type} #{address}"))
124
+ queue << [type, answer, nil]
125
+ type
126
+ end
127
+ limits = @recursor.recursion_limits.merge(max_queries: 3, max_workers: 1)
128
+ klass.send(:with_limits, @resolver, limits) do
129
+ klass.set_hints([], @resolver)
130
+ assert_equal([0, 0], queued, 'Each address batch must finish before the next begins')
131
+ assert_equal(1, budgets.map(&:object_id).uniq.length)
132
+ assert_raises(Dnsruby::RecursionLimitError) do
133
+ budgets.first.admit('192.0.2.53', 53, false, Dnsruby::Question.new('example.', 'A', 'IN'))
134
+ end
135
+ end
136
+ ensure
137
+ saved&.each { |key, value| klass.class_variable_set(key, value) }
138
+ end
139
+ end
@@ -0,0 +1,288 @@
1
+ require_relative 'spec_helper'
2
+ require 'minitest/mock'
3
+ require 'timeout'
4
+
5
+ class ReleaseAuxiliaryFixesTest < Minitest::Test
6
+ def test_controlled_queue_wakes_for_each_producer_alias_and_preserves_nil
7
+ [:push, :<<, :enq].each do |method|
8
+ queue = Dnsruby::QueryControl::Queue.new
9
+ control = Dnsruby::QueryControl.new
10
+ started = Queue.new
11
+ waiter = Thread.new { started << true; control.pop(queue) }
12
+ started.pop
13
+ queue.public_send(method, nil)
14
+ assert_nil(Timeout.timeout(2) { waiter.value })
15
+ ensure
16
+ waiter&.kill
17
+ waiter&.join
18
+ end
19
+ end
20
+
21
+ def test_parent_cancellation_wakes_an_event_queue_without_consuming_a_result
22
+ queue = Dnsruby::QueryControl::Queue.new
23
+ parent = Dnsruby::QueryControl.new
24
+ child = Dnsruby::QueryControl.new(parent: parent)
25
+ ready = Queue.new
26
+ original_pop = queue.method(:pop)
27
+ queue.define_singleton_method(:pop) do |*args|
28
+ ready << true
29
+ original_pop.call(*args)
30
+ end
31
+ waiter = Thread.new do
32
+ child.pop(queue)
33
+ rescue Dnsruby::QueryControl::Cancelled => error
34
+ error
35
+ end
36
+ ready.pop
37
+ parent.cancel
38
+ assert_instance_of(Dnsruby::QueryControl::Cancelled, Timeout.timeout(2) { waiter.value })
39
+ queue << :later
40
+ assert_equal(:later, queue.pop)
41
+ ensure
42
+ waiter&.kill
43
+ waiter&.join
44
+ end
45
+
46
+ def test_event_queue_obeys_deadlines_and_closed_queues
47
+ queue = Dnsruby::QueryControl::Queue.new
48
+ control = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + 0.01)
49
+ assert_raises(Dnsruby::QueryControl::DeadlineExceeded) do
50
+ Timeout.timeout(2) { control.pop(queue) }
51
+ end
52
+ queue.close
53
+ assert_nil(Dnsruby::QueryControl.new.pop(queue))
54
+ end
55
+
56
+ def test_compression_table_stops_recording_unrepresentable_offsets
57
+ name = Dnsruby::Name.create('late.example.')
58
+ encoder = Dnsruby::MessageEncoder.new
59
+ # Exercise the offset decision directly, without constructing an oversized
60
+ # or malformed DNS packet.
61
+ encoder.to_s.stub(:bytesize, 0x4000) { encoder.put_name(name) }
62
+ literal = encoder.to_s.dup
63
+ encoder.to_s.stub(:bytesize, 0x4000) { encoder.put_name(name) }
64
+ assert_equal(literal + literal, encoder.to_s)
65
+ end
66
+
67
+ def test_late_names_can_still_compress_to_an_early_suffix
68
+ encoder = Dnsruby::MessageEncoder.new
69
+ encoder.put_name(Dnsruby::Name.create('example.'))
70
+ offset = encoder.to_s.bytesize
71
+ encoder.to_s.stub(:bytesize, 0x4000) do
72
+ encoder.put_name(Dnsruby::Name.create('late.example.'))
73
+ end
74
+ assert_equal("\x04late\xc0\x00".b, encoder.to_s.byteslice(offset..-1))
75
+ end
76
+
77
+ def test_zone_transfer_reuse_returns_current_ixfr_deltas_after_axfr
78
+ soa1 = Dnsruby::RR.create('example. 300 IN SOA ns.example. hostmaster.example. 1 60 60 3600 60')
79
+ soa2 = Dnsruby::RR.create('example. 300 IN SOA ns.example. hostmaster.example. 2 60 60 3600 60')
80
+ old_address = Dnsruby::RR.create('www.example. 300 IN A 192.0.2.1')
81
+ new_address = Dnsruby::RR.create('www.example. 300 IN A 192.0.2.2')
82
+ responses = [Dnsruby::Message.new('example.', 'AXFR'), Dnsruby::Message.new('example.', 'IXFR')]
83
+ [soa1, old_address, soa1].each { |rr| responses[0].add_answer!(rr) }
84
+ [soa2, soa1, old_address, soa2, new_address, soa2].each { |rr| responses[1].add_answer!(rr) }
85
+ transfer = Dnsruby::ZoneTransfer.new
86
+ transfer.define_singleton_method(:send_message) do |_, request|
87
+ responses.first.header.id = request.header.id
88
+ responses.first.header.qr = true
89
+ end
90
+ transfer.define_singleton_method(:receive_message) { |*| responses.shift }
91
+ socket = Object.new
92
+ socket.define_singleton_method(:close) { }
93
+ socket.define_singleton_method(:closed?) { false }
94
+ Socket.stub(:tcp, socket) do
95
+ assert_equal([soa1, old_address], transfer.do_transfer('example.', '192.0.2.53'))
96
+ transfer.transfer_type = Dnsruby::Types::IXFR
97
+ transfer.serial = 1
98
+ deltas = transfer.do_transfer('example.', '192.0.2.53')
99
+ assert_equal(1, deltas.length)
100
+ assert_instance_of(Dnsruby::ZoneTransfer::Delta, deltas.first)
101
+ assert_equal([1, 2], [deltas.first.start, deltas.first.end])
102
+ assert_includes(deltas.first.adds, new_address)
103
+ assert_includes(deltas.first.deletes, old_address)
104
+ end
105
+ end
106
+
107
+ def test_tsig_stream_copies_own_their_accumulation_buffer
108
+ signer = Dnsruby::RR.create(name: 'key.example.', type: 'TSIG', klass: 'ANY',
109
+ key: ['secret'].pack('m0'))
110
+ query = Dnsruby::Message.new('example.', 'A')
111
+ signer.apply(query)
112
+ reply = Dnsruby::Message.new('example.', 'A')
113
+ reply.header.qr = true
114
+ signer.dup.apply(reply, query)
115
+ wire = reply.encode.freeze
116
+ assert(signer.verify_envelope(Dnsruby::Message.decode(wire), wire))
117
+ copied_signer = signer.dup
118
+ prior = signer.instance_variable_get(:@buf).dup
119
+ intermediate = Dnsruby::Message.new('example.', 'A').encode.freeze
120
+ assert(copied_signer.verify_envelope(Dnsruby::Message.decode(intermediate), intermediate))
121
+ assert_equal(prior, signer.instance_variable_get(:@buf))
122
+ assert_equal(prior + intermediate, copied_signer.instance_variable_get(:@buf))
123
+ end
124
+
125
+ def test_missing_nameserver_addresses_preserve_policy_and_release_cache_mutex
126
+ recursor_class = Dnsruby::Recursor
127
+ mutex = recursor_class.class_variable_get(:@@mutex)
128
+ original_hints = recursor_class.class_variable_get(:@@hints)
129
+ original_cache = recursor_class.class_variable_get(:@@authority_cache)
130
+ recursor_class.class_variable_set(:@@hints, {})
131
+ recursor_class.class_variable_set(:@@authority_cache, {})
132
+ recursor = recursor_class.allocate
133
+ recursor.ipv6_ok = true
134
+ original_recursion = recursor.method(:_dorecursion)
135
+ calls = Queue.new
136
+ recursor.define_singleton_method(:_dorecursion) do |name, type, klass, zone, authorities, depth, no_validation|
137
+ if name == 'www.example.'
138
+ calls << [:retry, no_validation, authorities.fetch('ns.example.').values.map(&:downcase).sort]
139
+ :finished
140
+ else
141
+ mutex.synchronize do
142
+ calls << [type, no_validation, Dnsruby::QueryControl.current.deadline]
143
+ end
144
+ answer = Dnsruby::Message.new(name, type, klass)
145
+ address = type == 'AAAA' ? '2001:db8::53' : '192.0.2.53'
146
+ answer.add_answer(Dnsruby::RR.create("#{name} 300 IN #{type} #{address}"))
147
+ answer
148
+ end
149
+ end
150
+ authorities = {'ns.example.' => recursor_class::AddressCache.new}
151
+ parent = Dnsruby::QueryControl.new(deadline: Dnsruby::TransportClock.now + 2)
152
+ result = Dnsruby::QueryControl.with(parent) do
153
+ Timeout.timeout(3) do
154
+ original_recursion.call('www.example.', 'A', Dnsruby::Classes::IN, 'example.', authorities, 0, true)
155
+ end
156
+ end
157
+ assert_equal(:finished, result)
158
+ events = 3.times.map { calls.pop }
159
+ assert_equal(%w[A AAAA], events.take(2).map(&:first).sort)
160
+ events.take(2).each do |event|
161
+ assert_equal(true, event[1])
162
+ assert_equal(parent.deadline, event[2])
163
+ end
164
+ assert_equal([:retry, true, ['192.0.2.53', '2001:db8::53']], events.last)
165
+ ensure
166
+ recursor_class.class_variable_set(:@@hints, original_hints) if recursor_class
167
+ recursor_class.class_variable_set(:@@authority_cache, original_cache) if recursor_class
168
+ end
169
+
170
+ def test_ds_queries_use_cached_parent_authorities_and_preserve_the_question
171
+ with_cached_recursor_authorities do |recursor, zones, calls|
172
+ ['child.example', 'child.example.', Dnsruby::Name.create('child.example'),
173
+ Dnsruby::Name.create('child.example.')].each do |name|
174
+ ['DS', 43, Dnsruby::Types.new('DS')].each do |type|
175
+ was_absolute = name.absolute? if name.is_a?(Dnsruby::Name)
176
+ response = recursor.query(name, type, Dnsruby::Classes::IN, true)
177
+ args = calls.last
178
+ expected_name = Dnsruby::Name.create(name)
179
+ expected_name.absolute = true
180
+ assert_equal(expected_name, response.question.first.qname)
181
+ assert_equal(was_absolute, name.absolute?) if name.is_a?(Dnsruby::Name)
182
+ assert_equal(expected_name.to_s(true), args[0])
183
+ assert_same(type, args[1])
184
+ assert_equal('example.', args[3])
185
+ assert_authority_snapshot(zones.fetch('example.'), args[4])
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ def test_ds_queries_for_top_level_names_and_root_use_root_authorities
192
+ with_cached_recursor_authorities do |recursor, zones, calls|
193
+ ['example', 'example.', '.', Dnsruby::Name.create('.')].each do |name|
194
+ response = recursor.query(name, 'DS', Dnsruby::Classes::IN, true)
195
+ args = calls.last
196
+ expected_name = Dnsruby::Name.create(name)
197
+ expected_name.absolute = true
198
+ assert_equal(expected_name, response.question.first.qname)
199
+ assert_equal(expected_name.to_s(true), args[0])
200
+ assert_equal('.', args[3])
201
+ assert_authority_snapshot(zones.fetch('.'), args[4])
202
+ end
203
+ end
204
+ end
205
+
206
+ def test_other_record_types_still_use_cached_child_authorities
207
+ with_cached_recursor_authorities do |recursor, zones, calls|
208
+ ['DNSKEY', 'A', Dnsruby::Types.new('NS')].each do |type|
209
+ response = recursor.query('child.example.', type, Dnsruby::Classes::IN, true)
210
+ args = calls.last
211
+ assert_equal(Dnsruby::Types.new(type), response.question.first.qtype)
212
+ assert_equal('child.example.', args[3])
213
+ assert_authority_snapshot(zones.fetch('child.example.'), args[4])
214
+ end
215
+ end
216
+ end
217
+
218
+ def test_recursion_requests_dnssec_data_whenever_it_will_validate
219
+ recursor_class = Dnsruby::Recursor
220
+ original_cache = recursor_class.class_variable_get(:@@authority_cache)
221
+ recursor_class.class_variable_set(:@@authority_cache, {})
222
+ addresses = recursor_class::AddressCache.new
223
+ addresses.push(['192.0.2.53', 300])
224
+ authorities = {'ns.example.' => addresses}
225
+ transport = Dnsruby::Resolver.new(nameserver: ['192.0.2.53'])
226
+ recursor = recursor_class.new(transport)
227
+ wire = nil
228
+ validates = nil
229
+ transport.define_singleton_method(:send_message) do |query|
230
+ validates = query.do_validation
231
+ wire = single_resolvers.first.make_query_packet(query)
232
+ response = Dnsruby::Message.new('child.example.', 'DNSKEY')
233
+ response.header.aa = true
234
+ response
235
+ end
236
+ Dnsruby::Resolver.stub(:new, transport) do
237
+ [nil, false, true].product([false, true]).each do |dnssec, no_validation|
238
+ recursor.dnssec = dnssec
239
+ recursor._dorecursion('child.example.', 'DNSKEY', Dnsruby::Classes::IN,
240
+ 'child.example.', authorities, 0, no_validation)
241
+ query = Dnsruby::Message.decode(wire)
242
+ assert_equal(!!dnssec || !no_validation, query.get_opt.dnssec_ok,
243
+ "dnssec=#{dnssec.inspect}, no_validation=#{no_validation}")
244
+ assert_equal(!no_validation, validates)
245
+ end
246
+ end
247
+ ensure
248
+ transport&.close
249
+ recursor_class.class_variable_set(:@@authority_cache, original_cache) if recursor_class
250
+ end
251
+
252
+ private
253
+
254
+ def assert_authority_snapshot(original, copy)
255
+ assert_equal(original.keys, copy.keys)
256
+ refute_same(original, copy)
257
+ original.each do |name, addresses|
258
+ assert_equal(addresses.values, copy.fetch(name).values)
259
+ refute_same(addresses, copy.fetch(name))
260
+ end
261
+ end
262
+
263
+ def with_cached_recursor_authorities
264
+ recursor_class = Dnsruby::Recursor
265
+ original_hints = recursor_class.class_variable_get(:@@hints)
266
+ original_zones = recursor_class.class_variable_get(:@@zones_cache)
267
+ zones = ['.', 'example.', 'child.example.'].to_h do |zone|
268
+ [zone, { "ns.#{zone}" => recursor_class::AddressCache.new }]
269
+ end
270
+ zones.fetch('.').values.first.push(['192.0.2.53', 300])
271
+ recursor_class.class_variable_set(:@@hints, zones.fetch('.'))
272
+ recursor_class.class_variable_set(:@@zones_cache, zones)
273
+ recursor = recursor_class.allocate
274
+ recursor.instance_variable_set(:@resolver, Struct.new(:recurse).new)
275
+ calls = []
276
+ recursor.define_singleton_method(:_dorecursion) do |*args|
277
+ calls << args
278
+ Dnsruby::Message.new(*args.first(3)).tap do |response|
279
+ response.header.qr = response.header.aa = true
280
+ response.add_authority(Dnsruby::RR.create("#{args[3]} 60 IN SOA ns.example. hostmaster.example. 1 60 60 60 30"))
281
+ end
282
+ end
283
+ yield recursor, zones, calls
284
+ ensure
285
+ recursor_class.class_variable_set(:@@hints, original_hints) if recursor_class
286
+ recursor_class.class_variable_set(:@@zones_cache, original_zones) if recursor_class
287
+ end
288
+ end