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
@@ -0,0 +1,583 @@
1
+ require_relative 'tc_pipeline_send'
2
+
3
+ class ReviewFollowupTransportTest < Minitest::Test
4
+ def test_cancelled_connects_release_all_workers_in_both_tcp_modes
5
+ [false, true].each do |pipeline|
6
+ [:close, :deadline].each do |cancellation|
7
+ isolated("pipeline = #{pipeline}; cancellation = #{cancellation.inspect}\n" + <<~'RUBY')
8
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
9
+ K.class_variable_set(:@@select_thread, Thread.current)
10
+ waiting, wrote = Queue.new, Queue.new
11
+ sockets, resolvers, queues = [], [], []
12
+ socket_mutex = Mutex.new
13
+ Socket.define_singleton_method(:new) do |*|
14
+ socket = FrameSocket.new
15
+ blocked = socket_mutex.synchronize { sockets << socket; sockets.size <= 4 }
16
+ socket.define_singleton_method(:bind) { |*| 0 }
17
+ socket.define_singleton_method(:connect_nonblock) do |*, **|
18
+ waiting << true if blocked
19
+ blocked ? :wait_writable : 0
20
+ end
21
+ socket.on_write = ->(*) { wrote << socket }
22
+ socket
23
+ end
24
+ IO.define_singleton_method(:select) do |readers, writers, _, duration|
25
+ raise 'invalid connect wait' unless readers.nil? && writers.length == 1 && duration <= 0.05
26
+ sleep(duration)
27
+ nil
28
+ end
29
+ 4.times do |id|
30
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
31
+ use_tcp: true, tcp_pipelining: pipeline, packet_timeout: 60,
32
+ query_timeout: 60, retry_times: 1)
33
+ queue = Queue.new
34
+ resolvers << resolver
35
+ queues << queue
36
+ resolver.send_async(query("cancelled-#{id}.example."), queue, id)
37
+ end
38
+ 4.times { waiting.pop }
39
+ resolvers.each_with_index do |resolver, id|
40
+ if cancellation == :close
41
+ resolver.close
42
+ else
43
+ engine = resolver.instance_variable_get(:@resolver_ruby)
44
+ engine.instance_variable_get(:@timeouts)[id][0] = Dnsruby::TransportClock.now - 1
45
+ engine.send(:update_tick_deadline, id)
46
+ engine.tick
47
+ end
48
+ end
49
+ healthy = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
50
+ use_tcp: true, tcp_pipelining: pipeline, packet_timeout: 1,
51
+ query_timeout: 2, retry_times: 1)
52
+ request, completion = query('healthy.example.'), Queue.new
53
+ healthy.send_async(request, completion, :healthy)
54
+ socket = Timeout.timeout(2) { wrote.pop }
55
+ Timeout.timeout(2) { Thread.pass until sockets.first(4).all?(&:closed?) }
56
+ raise 'canceled connection wrote bytes' unless sockets.first(4).all? { |s| s.written.empty? }
57
+ response = request.clone
58
+ response.header.qr = true
59
+ SELECTOR.process_message(response, response.encode, socket)
60
+ SELECTOR.send_queued_validation_responses
61
+ raise 'healthy query did not finish' unless completion.size == 1 && completion.pop.last.nil?
62
+ expected = cancellation == :close ? Dnsruby::OtherResolvError : Dnsruby::ResolvTimeout
63
+ queues.each do |queue|
64
+ raise 'cancellation did not complete exactly once' unless queue.size == 1 && queue.pop.last.is_a?(expected)
65
+ end
66
+ (resolvers + [healthy]).each(&:close)
67
+ drain_errors
68
+ raise 'canceled attempt resurrected' unless K.class_variable_get(:@@query_hash).empty?
69
+ raise 'late canceled completion' unless queues.all?(&:empty?)
70
+ sockets.each { |s| assert_clean(s) }
71
+ RUBY
72
+ end
73
+ end
74
+ end
75
+
76
+ def test_nonblocking_connector_checks_error_deadline_binding_and_cancellation
77
+ isolated(<<~'RUBY')
78
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1')
79
+ [:refused, :deadline, :cancelled, :bound_error, :success].each do |mode|
80
+ socket, bound, target = FrameSocket.new, nil, nil
81
+ socket.define_singleton_method(:bind) do |address|
82
+ bound = Socket.unpack_sockaddr_in(address)
83
+ raise Errno::EADDRINUSE if mode == :bound_error
84
+ end
85
+ socket.define_singleton_method(:connect_nonblock) do |address, **options|
86
+ target = Socket.unpack_sockaddr_in(address)
87
+ raise 'blocking connect requested' unless options[:exception] == false
88
+ :wait_writable
89
+ end
90
+ socket.define_singleton_method(:getsockopt) do |level, option|
91
+ raise 'SO_ERROR not queried' unless level == Socket::SOL_SOCKET && option == Socket::SO_ERROR
92
+ Struct.new(:int).new(mode == :refused ? Errno::ECONNREFUSED::Errno : 0)
93
+ end
94
+ Socket.define_singleton_method(:new) { |family, type, _| raise 'wrong family/type' unless family == Socket::AF_INET && type == Socket::SOCK_STREAM; socket }
95
+ token = K::SendToken.new
96
+ IO.define_singleton_method(:select) do |_, writers, _, duration|
97
+ raise 'unbounded wait' unless duration > 0 && duration <= 0.05 && writers == [socket]
98
+ token.cancel if mode == :cancelled
99
+ if mode == :deadline || mode == :cancelled
100
+ sleep(duration)
101
+ nil
102
+ else
103
+ [[], writers, []]
104
+ end
105
+ end
106
+ started = Dnsruby::TransportClock.now
107
+ begin
108
+ result = writer.send(:connect_tcp, '127.0.0.2', 5300, '127.0.0.3', 55000,
109
+ started + (mode == :cancelled ? 60 : 0.15), token)
110
+ raise 'unexpected connection success' unless mode == :success && result.equal?(socket)
111
+ rescue StandardError => error
112
+ expected = {refused: Errno::ECONNREFUSED, deadline: Dnsruby::ResolvTimeout,
113
+ bound_error: Errno::EADDRINUSE, cancelled: Dnsruby::PacketSender.const_get(:SendCancelled)}[mode]
114
+ raise error unless expected && error.is_a?(expected)
115
+ raise 'failed connection retained descriptor' unless socket.closed?
116
+ end
117
+ raise 'source binding changed' unless bound == [55000, '127.0.0.3']
118
+ raise 'destination changed' unless mode == :bound_error || target == [5300, '127.0.0.2']
119
+ elapsed = Dnsruby::TransportClock.now - started
120
+ raise 'deadline expired early' if mode == :deadline && elapsed < 0.14
121
+ raise 'cancellation waited for deadline' if mode == :cancelled && elapsed >= 2
122
+ socket.close
123
+ end
124
+ RUBY
125
+ end
126
+
127
+ def test_closed_completion_queue_does_not_interrupt_other_resolvers
128
+ isolated(<<~'RUBY')
129
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
130
+ sockets = []
131
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*| FrameSocket.new.tap { |s| sockets << s } }
132
+ resolvers = 2.times.map do
133
+ Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false,
134
+ use_tcp: true, do_validation: false, do_caching: false)
135
+ end
136
+ requests, queues = 2.times.map { |id| query("queue-#{id}.example.") }, [Queue.new, Queue.new]
137
+ resolvers.each_with_index { |resolver, id| resolver.send_async(requests[id], queues[id], id) }
138
+ queues.first.close
139
+ sockets.each_with_index do |socket, id|
140
+ response = requests[id].clone
141
+ response.header.qr = true
142
+ SELECTOR.process_message(response, response.encode, socket)
143
+ SELECTOR.send_queued_validation_responses
144
+ end
145
+ raise 'healthy completion lost' unless queues.last.size == 1 && queues.last.pop.last.nil?
146
+ raise 'closed owner retained' unless K.class_variable_get(:@@query_hash).empty? &&
147
+ K.class_variable_get(:@@observers).empty?
148
+ resolvers.each(&:close)
149
+ # Direct PacketSender recipients can also abandon an event queue.
150
+ queue = Queue.new
151
+ queue.close
152
+ SELECTOR.push_exception_to_select(:abandoned, queue, IOError.new('gone'), nil)
153
+ SELECTOR.send_queued_exceptions
154
+ SELECTOR.push_response_to_select(:abandoned, queue, query, query, nil)
155
+ SELECTOR.send_queued_responses
156
+ RUBY
157
+ end
158
+
159
+ def test_tsig_discards_unsigned_and_bad_mac_until_valid_response_or_deadline
160
+ isolated(<<~'RUBY')
161
+ [:unsigned, :bad_mac, :timeout].each do |mode|
162
+ socket = FrameSocket.new
163
+ writer = sender(socket)
164
+ request = query
165
+ key = Dnsruby::RR.create(name: 'key.example.', type: 'TSIG', klass: 'ANY',
166
+ key: 'c2VjcmV0', algorithm: 'hmac-sha256')
167
+ key.apply(request)
168
+ live = settings(socket, mode, request, writer)
169
+ SELECTOR.add_to_select(live)
170
+ reply = Dnsruby::Message.new('pipeline.example.')
171
+ reply.header.id, reply.header.qr = request.header.id, true
172
+ if mode == :bad_mac
173
+ wrong = key.dup
174
+ wrong.key = 'd3Jvbmc='
175
+ wrong.apply(reply, request)
176
+ end
177
+ bytes = reply.encode
178
+ SELECTOR.process_message(Dnsruby::Message.decode(bytes), bytes, socket)
179
+ raise 'unauthenticated reply completed query' unless SELECTOR.query_settings_for_id(mode).equal?(live) && live.client_queue.empty?
180
+ if mode == :timeout
181
+ K.class_variable_get(:@@timeouts)[mode] = Dnsruby::TransportClock.now - 1
182
+ SELECTOR.instance_variable_get(:@packet_deadlines)[mode] = K.class_variable_get(:@@timeouts)[mode]
183
+ SELECTOR.process_timeouts
184
+ raise 'timeout lost TSIG diagnostic' unless live.client_queue.size == 1 &&
185
+ live.client_queue.pop.last.is_a?(Dnsruby::TsigNotSignedResponseError)
186
+ else
187
+ valid = Dnsruby::Message.new('pipeline.example.')
188
+ valid.header.id, valid.header.qr = request.header.id, true
189
+ key.apply(valid, request)
190
+ bytes = valid.encode
191
+ SELECTOR.process_message(Dnsruby::Message.decode(bytes), bytes, socket)
192
+ SELECTOR.send_queued_validation_responses
193
+ raise 'authentic response lost' unless live.client_queue.size == 2 &&
194
+ 2.times.map { live.client_queue.pop }.all? { |event| event.last.nil? && event[2].verified? }
195
+ raise 'completed query retained' if SELECTOR.query_settings_for_id(mode)
196
+ writer.close
197
+ end
198
+ end
199
+ RUBY
200
+ end
201
+
202
+ def test_authenticated_tsig_errors_complete_but_forged_errors_keep_waiting
203
+ isolated(<<~'RUBY')
204
+ def error_wire(key, request, error_code, variant)
205
+ response = Dnsruby::Message.new(variant == :wrong_question ? 'wrong.example.' : 'pipeline.example.')
206
+ response.header.id, response.header.qr = request.header.id, true
207
+ response.header.rcode = variant == :wrong_rcode ? Dnsruby::RCode::NOERROR : Dnsruby::RCode::NOTAUTH
208
+ signer = key.dup
209
+ signer.error = error_code
210
+ signer.time_signed = request.tsig.time_signed
211
+ if error_code == 18 && variant != :bad_other_data
212
+ signer.other_data = [0, Time.now.to_i + 3600].pack('nN')
213
+ signer.other_size = 6
214
+ end
215
+ signer.name = 'different-key.example.' if variant == :wrong_key
216
+ signer.algorithm = 'hmac-sha1' if variant == :wrong_algorithm
217
+ if variant == :bad_other_data
218
+ signature = signer.dup
219
+ signature.original_id = response.header.id
220
+ input = [request.tsig.mac_size].pack('n') + request.tsig.mac + response.encode +
221
+ signer.sig_data(signer, signer.time_signed)
222
+ signature.mac = signer.calculate_mac(signer.algorithm, input)
223
+ signature.mac_size = signature.mac.bytesize
224
+ else
225
+ signature = signer.generate(response, request)
226
+ end
227
+ if variant == :bad_mac
228
+ signature.mac = signature.mac.dup
229
+ signature.mac.setbyte(0, signature.mac.getbyte(0) ^ 1)
230
+ elsif variant == :unsigned
231
+ signature.mac, signature.mac_size = ''.b, 0
232
+ end
233
+ response.add_additional(signature)
234
+ response.encode
235
+ end
236
+
237
+ [18, 22].each do |error_code|
238
+ [:valid, :bad_mac, :unsigned, :wrong_key, :wrong_algorithm, :wrong_rcode, :wrong_question].each do |variant|
239
+ socket = FrameSocket.new
240
+ writer = sender(socket)
241
+ request = query
242
+ key = Dnsruby::RR.create(name: 'key.example.', type: 'TSIG', klass: 'ANY',
243
+ key: 'c2VjcmV0', algorithm: 'hmac-sha256')
244
+ key.apply(request)
245
+ live = settings(socket, [error_code, variant], request, writer)
246
+ SELECTOR.add_to_select(live)
247
+ deadline = live.endtime
248
+ wire = error_wire(key, request, error_code, variant)
249
+ if variant == :valid
250
+ standalone = Dnsruby::Message.decode(wire)
251
+ raise 'TSIG verify changed its error return contract' unless key.verify(request, standalone, wire.dup) == false
252
+ raise 'valid server error was not authenticated' unless standalone.verified? && standalone.tsigerror == error_code
253
+ end
254
+ SELECTOR.process_message(Dnsruby::Message.decode(wire), wire, socket)
255
+ if variant != :valid
256
+ raise 'forged TSIG error claimed query' unless SELECTOR.query_settings_for_id(live.client_query_id).equal?(live)
257
+ raise 'forged TSIG error published event' unless live.client_queue.empty?
258
+ raise 'discard extended the deadline' unless live.endtime == deadline
259
+ wire = error_wire(key, request, error_code, :valid)
260
+ SELECTOR.process_message(Dnsruby::Message.decode(wire), wire, socket)
261
+ end
262
+ raise "authenticated TSIG error #{error_code}/#{variant} completed #{live.client_queue.size} times" unless live.client_queue.size == 1
263
+ event = live.client_queue.pop
264
+ name = error_code == 18 ? 'BADTIME' : 'BADTRUNC'
265
+ raise 'TSIG error diagnostic lost' unless event.last.is_a?(Dnsruby::TsigError) && event.last.message.include?(name)
266
+ raise 'verified server error state lost' unless event[2].verified? && event[2].tsigerror == error_code
267
+ raise 'authenticated error retained attempt' if SELECTOR.query_settings_for_id(live.client_query_id)
268
+ SELECTOR.retire_socket(socket)
269
+ assert_clean(socket)
270
+ end
271
+ end
272
+ # Even a valid MAC cannot make malformed BADTIME Other Data acceptable.
273
+ request = query
274
+ key = Dnsruby::RR.create(name: 'key.example.', type: 'TSIG', klass: 'ANY', key: 'c2VjcmV0')
275
+ key.apply(request)
276
+ wire = error_wire(key, request, 18, :bad_other_data)
277
+ decoded = Dnsruby::Message.decode(wire)
278
+ raise 'malformed signed BADTIME accepted' unless key.verify(request, decoded, wire) == false && !decoded.verified?
279
+ RUBY
280
+ end
281
+
282
+ def test_edns_preserves_existing_opt_options_payload_and_flags
283
+ [1232, 4096].product([false, true]).each do |payload, dnssec|
284
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: dnssec, udp_size: 1232)
285
+ request = Dnsruby::Message.new('opt.example.')
286
+ option = Dnsruby::RR::OPT::Option.new(65000, 'opaque'.b)
287
+ opt = Dnsruby::RR::OPT.new(payload, 0x0040, [option])
288
+ request.add_additional(opt)
289
+ writer.make_query_packet(request)
290
+ assert_same(opt, request.get_opt)
291
+ decoded = Dnsruby::Message.decode(request.encode)
292
+ assert_equal(1, decoded.additional.count { |rr| rr.type == Dnsruby::Types::OPT })
293
+ assert_equal(payload, decoded.get_opt.payloadsize)
294
+ assert_equal(0x0040 | (dnssec ? 0x8000 : 0), decoded.get_opt.flags)
295
+ assert_equal([[65000, 'opaque']], decoded.get_opt.options(nil).map { |o| [o.code, o.data] })
296
+ end
297
+ end
298
+
299
+ def test_authenticated_notauth_response_remains_a_server_error
300
+ request = Dnsruby::Message.new('authenticated-error.example.')
301
+ key = Dnsruby::RR.create(name: 'key.example.', type: 'TSIG', klass: 'ANY',
302
+ key: 'c2VjcmV0', algorithm: 'hmac-sha256')
303
+ key.apply(request)
304
+ reply = Dnsruby::Message.new('authenticated-error.example.')
305
+ reply.header.id, reply.header.qr, reply.header.rcode = request.header.id, true, Dnsruby::RCode::NOTAUTH
306
+ key.apply(reply, request)
307
+ bytes = reply.encode
308
+ reply = Dnsruby::Message.decode(bytes)
309
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
310
+ assert_equal(true, writer.check_response(reply, bytes, request, Queue.new, :notauth, false))
311
+ assert(reply.verified?)
312
+ assert_instance_of(Dnsruby::NotAuth, reply.get_exception)
313
+ end
314
+
315
+ def test_tsig_generate_and_apply_preserve_the_error_fields_covered_by_the_mac
316
+ [18, 22].product([:generate, :apply]).each do |error_code, operation|
317
+ request = Dnsruby::Message.new('signed-error.example.')
318
+ key = Dnsruby::RR.create(name: 'key.example.', type: 'TSIG', klass: 'ANY',
319
+ key: 'c2VjcmV0', algorithm: 'hmac-sha256')
320
+ key.apply(request)
321
+ signer = key.dup
322
+ signer.error = error_code
323
+ signer.time_signed = request.tsig.time_signed
324
+ expected_data = error_code == 18 ? [0, Time.now.to_i + 3600].pack('nN') : ''.b
325
+ signer.other_data, signer.other_size = expected_data, expected_data.bytesize
326
+ reply = Dnsruby::Message.new('signed-error.example.')
327
+ reply.header.id, reply.header.qr, reply.header.rcode = request.header.id, true, Dnsruby::RCode::NOTAUTH
328
+ if operation == :generate
329
+ reply.add_additional(signer.generate(reply, request))
330
+ else
331
+ signer.apply(reply, request)
332
+ end
333
+ wire = reply.encode
334
+ decoded = Dnsruby::Message.decode(wire)
335
+ assert_equal(expected_data.bytesize, decoded.tsig.other_size)
336
+ assert_equal(expected_data, decoded.tsig.other_data)
337
+ assert_equal(false, key.verify(request, decoded, wire))
338
+ assert(decoded.verified?, "#{operation} produced an invalid signed error")
339
+ assert_equal(error_code, decoded.tsigerror)
340
+ end
341
+ end
342
+
343
+ def test_rechecking_stripped_tsig_errors_does_not_reuse_the_previous_authentication
344
+ [18, 22].each do |error_code|
345
+ request = Dnsruby::Message.new('signed-error.example.')
346
+ key = Dnsruby::RR.create(name: 'key.example.', type: 'TSIG', klass: 'ANY',
347
+ key: 'c2VjcmV0', algorithm: 'hmac-sha256')
348
+ key.apply(request)
349
+ signer = key.dup
350
+ signer.error = error_code
351
+ signer.time_signed = request.tsig.time_signed
352
+ signer.other_data = error_code == 18 ? [0, Time.now.to_i + 3600].pack('nN') : ''.b
353
+ signer.other_size = signer.other_data.bytesize
354
+ reply = Dnsruby::Message.new('signed-error.example.')
355
+ reply.header.id, reply.header.qr, reply.header.rcode = request.header.id, true, Dnsruby::RCode::NOTAUTH
356
+ signer.apply(reply, request)
357
+ wire = reply.encode
358
+ decoded = Dnsruby::Message.decode(wire)
359
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
360
+ queue = Queue.new
361
+ assert_instance_of(Dnsruby::TsigError, writer.check_response(decoded, wire, request, queue, :repeat, false))
362
+ assert(decoded.verified?)
363
+ assert_nil(decoded.tsig)
364
+ assert_equal(false, writer.check_response(decoded, wire, request, queue, :repeat, false))
365
+ refute(decoded.verified?)
366
+ assert_equal(Dnsruby::RCode::FORMERR, decoded.tsigerror)
367
+ assert(queue.empty?)
368
+ end
369
+ end
370
+
371
+ def test_udp_receive_capacity_uses_the_opt_advertised_by_each_attempt
372
+ isolated(<<~'RUBY')
373
+ [[1232, 4096], [4096, 1232], [1232, 0]].each do |configured, advertised|
374
+ socket = FrameSocket.new
375
+ socket.define_singleton_method(:bind) { |*| }
376
+ socket.define_singleton_method(:connect) { |*| }
377
+ socket.define_singleton_method(:getsockopt) { |*| Struct.new(:data).new([Socket::SOCK_DGRAM].pack('i')) }
378
+ UDPSocket.define_singleton_method(:new) { |*| socket }
379
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false, udp_size: configured)
380
+ request = query('large-opt.example.')
381
+ request.add_additional(Dnsruby::RR::OPT.new(advertised))
382
+ queue = Queue.new
383
+ writer.send_async(request, queue, :opt)
384
+ live = SELECTOR.query_settings_for_id(:opt)
385
+ raise 'receive capacity disagrees with OPT' unless live.udp_packet_size == [advertised, 512].max
386
+ response = request.clone
387
+ response.header.qr = true
388
+ count = advertised > configured ? 7 : 1
389
+ response.add_answer(Dnsruby::RR.create('large-opt.example. 0 IN TXT ' +
390
+ Array.new(count) { '"' + 'x' * 240 + '"' }.join(' ')))
391
+ wire = response.encode
392
+ raise 'fixture did not exceed configured capacity' if count == 7 && wire.bytesize <= configured
393
+ socket.define_singleton_method(:recvfrom_nonblock) do |capacity|
394
+ [wire.byteslice(0, capacity), ['AF_INET', 53, '127.0.0.1', '127.0.0.1']]
395
+ end
396
+ incoming = SELECTOR.get_incoming_data(socket, live.udp_packet_size)
397
+ SELECTOR.process_message(*incoming, socket) if incoming
398
+ SELECTOR.send_queued_validation_responses
399
+ raise 'valid advertised-size reply was truncated' unless queue.size == 2 &&
400
+ 2.times.map { queue.pop }.all? { |event| event.last.nil? && event[2].answer.first.strings.size == count }
401
+ raise 'completed OPT query retained' if SELECTOR.query_settings_for_id(:opt)
402
+ assert_clean(socket)
403
+ end
404
+ RUBY
405
+ end
406
+
407
+ def test_explicit_udp_size_is_honoured_with_dnssec_in_either_option_order
408
+ [Dnsruby::Resolver, Dnsruby::SingleResolver, Dnsruby::PacketSender].each do |klass|
409
+ [{udp_size: 1232, dnssec: true}, {dnssec: true, udp_size: 1232}].each do |settings|
410
+ options = klass == Dnsruby::Resolver ? {nameserver: '127.0.0.1'} : {server: '127.0.0.1'}
411
+ object = klass.new(options.merge(settings))
412
+ assert_equal(1232, object.udp_size)
413
+ writer = object.is_a?(Dnsruby::Resolver) ? object.single_resolvers.first : object
414
+ request = Dnsruby::Message.new('payload.example.')
415
+ assert_equal(1232, writer.udp_packet_size)
416
+ assert_equal(1232, Dnsruby::Message.decode(writer.make_query_packet(request)).get_opt.payloadsize)
417
+ end
418
+ end
419
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false, udp_size: 1232)
420
+ assert_equal(1232, Dnsruby::Message.decode(writer.make_query_packet(Dnsruby::Message.new('payload.example.'))).get_opt.payloadsize)
421
+ writer.udp_size = 100
422
+ assert_equal(512, writer.udp_packet_size)
423
+ assert_nil(Dnsruby::Message.decode(writer.make_query_packet(Dnsruby::Message.new('payload.example.'))).get_opt)
424
+ end
425
+
426
+ def test_default_and_explicit_udp_sizes_match_the_wire_and_receive_capacity
427
+ assert_equal(1232, Dnsruby::Resolver::DefaultUDPSize)
428
+ assert_equal(1220, Dnsruby::Resolver::AbsoluteMinDnssecUdpSize)
429
+ assert_equal(4096, Dnsruby::Resolver::MinDnssecUdpSize)
430
+ isolated(<<~'RUBY')
431
+ [Dnsruby::Resolver, Dnsruby::SingleResolver, Dnsruby::PacketSender].product(
432
+ [false, true], [nil, 512, 1220, 1232, 4096]).each do |klass, dnssec, configured|
433
+ socket = FrameSocket.new
434
+ socket.define_singleton_method(:bind) { |*| }
435
+ socket.define_singleton_method(:connect) { |*| }
436
+ socket.define_singleton_method(:getsockopt) { |*| Struct.new(:data).new([Socket::SOCK_DGRAM].pack('i')) }
437
+ UDPSocket.define_singleton_method(:new) { |*| socket }
438
+ options = klass == Dnsruby::Resolver ? {nameserver: '127.0.0.1'} : {server: '127.0.0.1'}
439
+ options[:udp_size] = configured if configured
440
+ options[:dnssec] = dnssec
441
+ object = klass.new(options)
442
+ writer = object.is_a?(Dnsruby::Resolver) ? object.single_resolvers.first : object
443
+ expected = configured || 1232
444
+ expected = [expected, 1220].max if dnssec
445
+ raise "wrong configured size for #{klass}" unless object.udp_size == expected && writer.udp_size == expected
446
+ request, queue = query('udp-limit.example.'), Queue.new
447
+ writer.send_async(request, queue, :limit)
448
+ live = SELECTOR.query_settings_for_id(:limit)
449
+ raise 'wrong receive capacity' unless live.udp_packet_size == expected
450
+ sent = Dnsruby::Message.decode(socket.written)
451
+ if expected > 512
452
+ raise 'wrong OPT count' unless sent.additional.count { |rr| rr.type == Dnsruby::Types::OPT } == 1
453
+ raise 'wrong advertised capacity' unless sent.get_opt.payloadsize == expected
454
+ raise 'wrong DNSSEC flag' unless sent.get_opt.dnssec_ok == dnssec
455
+ else
456
+ raise 'classic DNS unexpectedly advertised EDNS' if sent.get_opt
457
+ end
458
+ response = request.clone
459
+ response.header.qr = true
460
+ wire = response.encode
461
+ socket.define_singleton_method(:recvfrom_nonblock) do |capacity|
462
+ raise 'receive buffer differs from advertisement' unless capacity == expected
463
+ [wire, ['AF_INET', 53, '127.0.0.1', '127.0.0.1']]
464
+ end
465
+ incoming = SELECTOR.get_incoming_data(socket, live.udp_packet_size)
466
+ SELECTOR.process_message(*incoming, socket)
467
+ SELECTOR.send_queued_validation_responses
468
+ raise 'response did not complete' unless queue.size == 2 &&
469
+ 2.times.map { queue.pop }.all? { |event| event.last.nil? }
470
+ object.close
471
+ assert_clean(socket)
472
+ end
473
+ RUBY
474
+ end
475
+
476
+ def test_questionless_error_matching_is_tcp_only_and_edns_retry_retains_transport
477
+ [false, true].product([false, true]).each do |tcp, edns|
478
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
479
+ retries = []
480
+ writer.define_singleton_method(:send_async) { |*args| retries << args }
481
+ request = Dnsruby::Message.new('error.example.')
482
+ request.add_additional(Dnsruby::RR::OPT.new(1232)) if edns
483
+ error = Dnsruby::Message.new
484
+ error.header.id, error.header.qr, error.header.rcode = request.header.id, true, Dnsruby::RCode::FORMERR
485
+ result = writer.check_response(error, error.encode, request, Queue.new, :error, tcp)
486
+ assert_equal(tcp && !edns, result)
487
+ assert_equal(tcp && edns ? 1 : 0, retries.size)
488
+ if tcp && edns
489
+ assert_equal(true, retries.first[3])
490
+ assert_equal(true, retries.first[4])
491
+ assert_nil(retries.first[0].get_opt)
492
+ end
493
+ error.header.rcode = Dnsruby::RCode::NOERROR
494
+ assert_equal(false, writer.check_response(error, error.encode, request, Queue.new, :error, tcp))
495
+ end
496
+ end
497
+
498
+ def test_public_senders_prepare_messages_before_signing_and_preserve_existing_signatures
499
+ isolated(<<~'RUBY')
500
+ captured = []
501
+ Dnsruby::PacketSender.define_method(:send_dnsruby) { |wire, *| captured << wire }
502
+ [Dnsruby::Resolver, Dnsruby::SingleResolver, Dnsruby::PacketSender].product(
503
+ [false, true], [false, true], [false, true], [false, true],
504
+ [:message, :resolver, :both, :signed, :decoded, :raw_message, :raw_unsigned]).each do |klass, dnssec, rd, recurse, tcp, mode|
505
+ message_key = Dnsruby::RR.create(name: 'message-key.example.', type: 'TSIG', klass: 'ANY',
506
+ key: 'c2VjcmV0', algorithm: 'hmac-sha256')
507
+ resolver_key = Dnsruby::RR.create(name: 'resolver-key.example.', type: 'TSIG', klass: 'ANY',
508
+ key: 'b3RoZXI=', algorithm: 'hmac-sha256')
509
+ options = klass == Dnsruby::Resolver ? {nameserver: '127.0.0.1'} : {server: '127.0.0.1'}
510
+ object = klass.new(options.merge(dnssec: dnssec, recurse: recurse, use_tcp: tcp))
511
+ object.tsig = resolver_key unless [:message, :raw_message, :raw_unsigned].include?(mode)
512
+ request = query('signed-query.example.')
513
+ request.header.rd, request.header.ad = rd, true
514
+ request.add_additional(Dnsruby::RR::OPT.new(4096, 0x0040,
515
+ [Dnsruby::RR::OPT::Option.new(65000, 'opaque'.b)]))
516
+ request.send_raw = [:raw_message, :raw_unsigned].include?(mode)
517
+ if [:signed, :decoded].include?(mode)
518
+ message_key.apply(request)
519
+ original_wire = request.encode
520
+ request = Dnsruby::Message.decode(original_wire) if mode == :decoded
521
+ elsif [:message, :both, :raw_message].include?(mode)
522
+ request.set_tsig(message_key)
523
+ end
524
+ request.do_caching = request.do_validation = false
525
+ object.send_async(request, Queue.new, :signed)
526
+ raise "missing send for #{klass}/#{mode}" unless captured.size == 1
527
+ wire = captured.shift
528
+ decoded = Dnsruby::Message.decode(wire)
529
+ if [:signed, :decoded].include?(mode)
530
+ raise 'pre-signed packet was changed' unless wire == original_wire
531
+ end
532
+ protected = [:signed, :decoded, :raw_message, :raw_unsigned].include?(mode)
533
+ raise 'wrong RD policy' unless decoded.header.rd == (protected ? rd : recurse)
534
+ raise 'wrong DO policy' unless decoded.get_opt.dnssec_ok == (!protected && dnssec)
535
+ raise 'caller OPT payload changed' unless decoded.get_opt.payloadsize == 4096
536
+ raise 'caller OPT options changed' unless decoded.get_opt.options(nil).map { |opt| [opt.code, opt.data] } == [[65000, 'opaque']]
537
+ raise 'caller OPT flag lost' unless decoded.get_opt.flags & 0x0040 == 0x0040
538
+ if mode == :raw_unsigned
539
+ raise 'raw unsigned packet was signed' if decoded.tsig
540
+ else
541
+ key = mode == :resolver ? resolver_key : message_key
542
+ signature = decoded.tsig
543
+ raise 'wrong signing key precedence' unless signature && signature.name == key.name
544
+ raise "invalid request MAC for #{klass}/#{mode}/#{dnssec}/#{rd}/#{recurse}/#{tcp}" unless
545
+ key.generate(decoded, nil, '', wire, signature).mac == signature.mac
546
+ raise 'duplicate TSIG' unless decoded.additional.count { |rr| rr.type == Dnsruby::Types::TSIG } == 1
547
+ end
548
+ object.close
549
+ end
550
+ RUBY
551
+ end
552
+
553
+ def test_socket_index_retains_duplicate_helper_ids_and_cleans_up_replacements
554
+ isolated(<<~'RUBY')
555
+ first_socket, second_socket = FrameSocket.new, FrameSocket.new
556
+ first = settings(first_socket, :first, query('first.example.'))
557
+ second = settings(first_socket, :second, query('second.example.'))
558
+ first.query.header.id = second.query.header.id = 123
559
+ [first, second].each { |s| SELECTOR.add_to_select(s) }
560
+ raise 'missing collision' unless SELECTOR.query_id_in_use?(first_socket, 123)
561
+ replacement = settings(second_socket, :first, first.query)
562
+ SELECTOR.add_to_select(replacement)
563
+ raise 'peer collision removed' unless SELECTOR.query_id_in_use?(first_socket, 123)
564
+ raise 'replacement collision missing' unless SELECTOR.query_id_in_use?(second_socket, 123)
565
+ SELECTOR.remove_id(:second)
566
+ raise 'old collision retained' if SELECTOR.query_id_in_use?(first_socket, 123)
567
+ SELECTOR.remove_id(:first)
568
+ raise 'replacement collision retained' if SELECTOR.query_id_in_use?(second_socket, 123)
569
+ [first_socket, second_socket].each { |socket| assert_clean(socket) }
570
+ RUBY
571
+ end
572
+
573
+ private
574
+
575
+ def isolated(body)
576
+ options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
577
+ source = "#{TestPipelineSend::FIXTURE}\nTimeout.timeout(12) do\n#{body}\nend"
578
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
579
+ File.expand_path('../lib', __dir__), '-e', source)
580
+ assert(status.success?, "#{stdout}\n#{stderr}")
581
+ assert_empty(stderr)
582
+ end
583
+ end