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,195 @@
1
+ require_relative 'tc_pipeline_send'
2
+
3
+ class ReviewTransportFixesTest < Minitest::Test
4
+ def test_resolver_early_errors_keep_the_public_completion_protocol
5
+ isolated(<<~'CODE')
6
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false)
7
+ invalid = query
8
+ rr = Dnsruby::RR.get_class(62001, 1).new('x'.b * 65_536)
9
+ rr.name, rr.ttl, rr.type, rr.klass = 'size.example.', 0, 62001, 1
10
+ invalid.add_additional(rr)
11
+ [invalid, sized_message(65_536)].each do |message|
12
+ begin
13
+ resolver.send_message(message)
14
+ raise 'encoding error returned as a successful Message'
15
+ rescue Dnsruby::EncodeError
16
+ end
17
+ reply, error = resolver.send_message!(message)
18
+ raise 'bang response tuple lost encoding error' unless reply.nil? && error.is_a?(Dnsruby::EncodeError)
19
+ queue = Queue.new
20
+ resolver.send_async(message, queue, :invalid)
21
+ tuple = queue.pop(true)
22
+ raise 'malformed async encode tuple' unless tuple.length == 3 && tuple[0] == :invalid &&
23
+ tuple[1].nil? && tuple[2].is_a?(Dnsruby::EncodeError)
24
+ raise 'duplicate early completion' unless queue.empty?
25
+ end
26
+ queue = Queue.new
27
+ resolver.send_async(Object.new, queue, :wrong_type)
28
+ tuple = queue.pop(true)
29
+ raise 'malformed wrong-type tuple' unless tuple.length == 3 && tuple[0] == :wrong_type &&
30
+ tuple[1].nil? && tuple[2].is_a?(ArgumentError)
31
+ raise 'early error acquired transport state' unless K.class_variable_get(:@@query_hash).empty?
32
+ resolver.close
33
+ CODE
34
+ end
35
+
36
+ def test_duplicate_id_error_leaves_the_original_query_owned
37
+ isolated(<<~'CODE')
38
+ SELECTOR.singleton_class.remove_method(:notify_queue_observers)
39
+ resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', dnssec: false, use_tcp: true)
40
+ socket = FrameSocket.new
41
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **options| socket }
42
+ first, duplicate = Queue.new, Queue.new
43
+ resolver.send_async(query('first.example.'), first, :same)
44
+ written = socket.written.dup
45
+ resolver.send_async(query('second.example.'), duplicate, :same)
46
+ tuple = duplicate.pop(true)
47
+ raise 'malformed duplicate tuple' unless tuple.length == 3 && tuple[0] == :same &&
48
+ tuple[1].nil? && tuple[2].is_a?(ArgumentError)
49
+ raise 'duplicate query wrote bytes' unless socket.written == written
50
+ raise 'duplicate query completed the original' unless first.empty?
51
+ resolver.close
52
+ tuple = first.pop(true)
53
+ raise 'original query lost after duplicate' unless tuple == [:same, nil, tuple.last] &&
54
+ tuple.last.is_a?(Dnsruby::OtherResolvError)
55
+ drain_errors
56
+ raise 'duplicate completion' unless first.empty? && duplicate.empty?
57
+ raise 'query state retained' unless K.class_variable_get(:@@query_hash).empty?
58
+ CODE
59
+ end
60
+
61
+ def test_pipeline_configuration_changes_rotate_streams_while_peers_drain
62
+ isolated(<<~'CODE')
63
+ created = []
64
+ Dnsruby::PacketSender.define_method(:connect_tcp) do |*args, **options|
65
+ socket = FrameSocket.new
66
+ socket.define_singleton_method(:connect) { |*| raise Errno::EISCONN }
67
+ created << [socket, args]
68
+ socket
69
+ end
70
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', port: 5301,
71
+ use_tcp: true, tcp_pipelining: true, dnssec: false)
72
+ send_query = ->(id) do
73
+ q = query("configuration-#{id}.example.")
74
+ q.header.id = id + 100
75
+ writer.send_dnsruby(q.encode, q, id, Queue.new, true)
76
+ end
77
+ send_query.call(0)
78
+ original = created.last.first
79
+ send_query.call(1)
80
+ raise 'random source port selection rotated a reusable stream' unless created.length == 1
81
+ changes = [
82
+ -> { writer.port = 5302 },
83
+ -> { writer.server = '127.0.0.2' },
84
+ -> { writer.src_address = '127.0.0.3' },
85
+ -> { writer.src_port = [61_001, 61_002] },
86
+ -> { writer.src_port << 61_003 },
87
+ -> { writer.server = '::1'; writer.src_address6 = '::1' }
88
+ ]
89
+ changes.each_with_index do |change, index|
90
+ previous = created.last.first
91
+ change.call
92
+ send_query.call(index + 2)
93
+ raise 'configuration reused old endpoint or binding' unless created.length == index + 2
94
+ raise 'rotation closed pending peer' if previous.closed?
95
+ raise 'new stream was already closed' if created.last.first.closed?
96
+ end
97
+ raise 'changed port not used' unless created[1][1][1] == 5302
98
+ raise 'changed server not used' unless created[2][1][0] == '127.0.0.2'
99
+ raise 'changed source not used' unless created[3][1][2] == '127.0.0.3'
100
+ raise 'changed IPv6 source not used' unless created.last[1][0, 3] == ['::1', 5302, '::1']
101
+ SELECTOR.remove_id(0)
102
+ raise 'first stream closed before last peer drained' if original.closed?
103
+ SELECTOR.remove_id(1)
104
+ raise 'retired stream remained open after last peer' unless original.closed?
105
+ (2..7).each { |id| SELECTOR.remove_id(id) }
106
+ writer.close
107
+ CODE
108
+ end
109
+
110
+ def test_single_resolver_server_change_keeps_the_configured_port
111
+ resolver = Dnsruby::SingleResolver.new(server: '127.0.0.1', port: 5301)
112
+ resolver.server = '127.0.0.2'
113
+ assert_equal(5301, resolver.port)
114
+ assert_equal(5301, resolver.single_resolvers.first.port)
115
+ ensure
116
+ resolver.close if resolver
117
+ end
118
+
119
+ def test_total_wire_limit_rejects_before_pipeline_admission_and_preserves_peers
120
+ isolated(<<~'CODE')
121
+ socket = FrameSocket.new
122
+ writer = sender(socket)
123
+ ordinary, queue = query('peer.example.'), Queue.new
124
+ ordinary.header.id = 100
125
+ writer.send_dnsruby(ordinary.encode, ordinary, :peer, queue, true)
126
+ peer = SELECTOR.query_settings_for_id(:peer)
127
+ boundary = sized_message(65_535)
128
+ boundary.header.id = 101
129
+ writer.send_dnsruby(boundary.encode, boundary, :boundary, Queue.new, true)
130
+ before = socket.written.dup
131
+ rejected = sized_message(65_536)
132
+ rejected.header.id = 102
133
+ errors = Queue.new
134
+ writer.send_dnsruby(rejected.encode, rejected, :oversized, errors, true)
135
+ drain_errors
136
+ event = errors.pop(true)
137
+ raise 'total size did not produce EncodeError' unless event[0] == :oversized && event.last.is_a?(Dnsruby::EncodeError)
138
+ raise 'oversized frame wrote or closed the pipeline' unless socket.written == before && !socket.closed?
139
+ raise 'oversized query acquired registration' if SELECTOR.query_settings_for_id(:oversized)
140
+ raise 'oversized query displaced peer' unless SELECTOR.query_settings_for_id(:peer).equal?(peer)
141
+ last = query('last.example.')
142
+ last.header.id = 103
143
+ writer.send_dnsruby(last.encode, last, :last, Queue.new, true)
144
+ expected = [ordinary, boundary, last].map { |q| [q.encode.bytesize].pack('n') + q.encode }.join
145
+ raise 'pipeline frames corrupted at wire boundary' unless socket.written == expected
146
+ raise 'duplicate encode error' unless errors.empty?
147
+ [:peer, :boundary, :last].each { |id| SELECTOR.remove_id(id) }
148
+ CODE
149
+ end
150
+
151
+ def test_tsig_growth_is_checked_after_packet_preparation
152
+ skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
153
+ isolated(<<~'CODE')
154
+ writer = Dnsruby::PacketSender.new(server: '127.0.0.1', use_tcp: true, dnssec: false)
155
+ writer.tsig = ['key.example.', ['secret'].pack('m0')]
156
+ message = sized_message(65_520)
157
+ raise 'fixture must fit before signing' unless message.encode.bytesize == 65_520
158
+ queue = Queue.new
159
+ Dnsruby::PacketSender.define_method(:connect_tcp) { |*args, **options| raise 'oversized signed packet opened a socket' }
160
+ writer.send_async(message, queue, :signed)
161
+ drain_errors
162
+ event = queue.pop(true)
163
+ raise 'signed packet size error lost' unless event[0] == :signed && event.last.is_a?(Dnsruby::EncodeError)
164
+ raise 'oversized signed packet acquired query state' unless K.class_variable_get(:@@query_hash).empty?
165
+ CODE
166
+ end
167
+
168
+ private
169
+
170
+ SIZE_FIXTURE = <<~'CODE'
171
+ def sized_message(size)
172
+ q = query('size.example.')
173
+ q.send_raw = true
174
+ rr = Dnsruby::RR.get_class(62001, 1).new('')
175
+ rr.name, rr.ttl, rr.type, rr.klass = 'size.example.', 0, 62001, 1
176
+ q.add_additional(rr)
177
+ padding = size - q.encode.bytesize
178
+ full = Dnsruby::RR.get_class(62001, 1).new('x'.b * padding)
179
+ full.name, full.ttl, full.type, full.klass = 'size.example.', 0, 62001, 1
180
+ q.remove_additional
181
+ q.add_additional(full)
182
+ raise 'wrong fixture wire length' unless q.encode.bytesize == size
183
+ q
184
+ end
185
+ CODE
186
+
187
+ def isolated(body)
188
+ options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
189
+ source = "#{TestPipelineSend::FIXTURE}\n#{SIZE_FIXTURE}\nTimeout.timeout(10) do\n#{body}\nend"
190
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
191
+ File.expand_path('../lib', __dir__), '-e', source)
192
+ assert(status.success?, "#{stdout}\n#{stderr}")
193
+ assert_empty(stderr)
194
+ end
195
+ end
@@ -0,0 +1,415 @@
1
+ require_relative 'spec_helper'
2
+ require_relative 'socket_helpers'
3
+ require 'socket'
4
+ require 'timeout'
5
+
6
+ # Exercise cache and retry policy through the public API and real wire packets.
7
+ class ReviewTransportNativeTest < Minitest::Test
8
+ def setup
9
+ Dnsruby::PacketSender.clear_caches
10
+ @workers = []
11
+ @sockets = []
12
+ @resolvers = []
13
+ end
14
+
15
+ def teardown
16
+ @resolvers.each(&:close)
17
+ @workers.each { |worker| worker.kill if worker.alive? }
18
+ @workers.each(&:value)
19
+ @sockets.each { |socket| socket.close unless socket.closed? }
20
+ Dnsruby::PacketSender.clear_caches
21
+ end
22
+
23
+ def test_recursive_and_authoritative_caches_keep_question_classes_separate
24
+ [true, false].each do |recurse|
25
+ Dnsruby::PacketSender.clear_caches
26
+ port, requests = udp_server do |request|
27
+ response(request, request.question.first.qclass.to_s)
28
+ end
29
+ resolver = resolver_for(port, recurse: recurse)
30
+ %w[IN CH IN CH].each_with_index do |klass, index|
31
+ reply = query(resolver, klass)
32
+ assert_equal(klass, reply.question.first.qclass.to_s)
33
+ assert_equal(klass, reply.answer.first.klass.to_s)
34
+ assert_equal([klass], reply.answer.first.strings)
35
+ assert_equal(index >= 2, !!reply.cached)
36
+ end
37
+ assert_equal(2, requests.size, 'both classes should subsequently hit their own cache')
38
+ end
39
+ end
40
+
41
+ def test_unsigned_cache_cannot_satisfy_a_tsig_query
42
+ skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
43
+ [true, false].each do |recurse|
44
+ Dnsruby::PacketSender.clear_caches
45
+ port, requests = udp_server { |request| response(request, 'unsigned') }
46
+ resolver = resolver_for(port, recurse: recurse)
47
+ assert_equal(['unsigned'], query(resolver).answer.first.strings)
48
+ resolver.tsig = tsig_key
49
+ assert_raises(Dnsruby::TsigNotSignedResponseError) { query(resolver) }
50
+ assert_equal(2, requests.size, 'authentication must require a new wire exchange')
51
+ end
52
+ end
53
+
54
+ def test_verified_tsig_reply_cannot_enter_the_shared_unsigned_cache
55
+ skip('OpenSSL HMAC unavailable') unless defined?(OpenSSL::HMAC)
56
+ [true, false].each do |recurse|
57
+ Dnsruby::PacketSender.clear_caches
58
+ key = tsig_key
59
+ port, requests = udp_server do |request|
60
+ reply = response(request, request.tsig ? 'private' : 'public')
61
+ key.apply(reply, request) if request.tsig
62
+ reply
63
+ end
64
+ resolver = resolver_for(port, recurse: recurse)
65
+ resolver.tsig = key
66
+ signed = query(resolver)
67
+ assert(signed.verified?)
68
+ assert_nil(signed.tsig, 'verification strips the wire TSIG record')
69
+ assert_equal(['private'], signed.answer.first.strings)
70
+ resolver.tsig = nil
71
+ unsigned = query(resolver)
72
+ assert_equal(['public'], unsigned.answer.first.strings)
73
+ refute(unsigned.cached)
74
+ assert(query(resolver).cached)
75
+ assert_equal(2, requests.size)
76
+ end
77
+ end
78
+
79
+ def test_caller_opt_receives_a_reply_larger_than_the_configured_udp_default
80
+ [false, true].each do |dnssec|
81
+ port, requests = udp_server do |request|
82
+ reply = response(request, 'placeholder')
83
+ reply.answer.first.strings = Array.new(7) { 'x' * 240 }
84
+ reply
85
+ end
86
+ resolver = resolver_for(port, udp_size: 1232, dnssec: dnssec, do_caching: false)
87
+ request = Dnsruby::Message.new('large-opt.example.', 'TXT')
88
+ request.do_caching = request.do_validation = false
89
+ option = Dnsruby::RR::OPT::Option.new(65000, 'opaque'.b)
90
+ request.add_additional(Dnsruby::RR::OPT.new(4096, 0, [option]))
91
+ reply = Timeout.timeout(6) { resolver.send_message(request) }
92
+ refute(reply.cached, 'each variant must receive its own UDP reply')
93
+ assert_equal(Array.new(7) { 'x' * 240 }, reply.answer.first.strings)
94
+ assert_operator(reply.answersize, :>, 1232)
95
+ assert_operator(reply.answersize, :<=, 4096)
96
+ sent = requests.pop(true)
97
+ assert_equal(4096, sent.get_opt.payloadsize)
98
+ assert_equal([[65000, 'opaque']], sent.get_opt.options(nil).map { |opt| [opt.code, opt.data] })
99
+ assert_equal(1232, resolver.udp_size)
100
+ assert_empty(requests)
101
+ end
102
+ end
103
+
104
+ def test_authenticated_badtime_and_badtrunc_errors_are_delivered_after_forged_errors
105
+ [18, 22].each do |error_code|
106
+ key = tsig_key
107
+ port, requests = udp_server do |request|
108
+ reply = response(request, 'server error')
109
+ reply.header.rcode = Dnsruby::RCode::NOTAUTH
110
+ signer = key.dup
111
+ signer.error = error_code
112
+ signer.time_signed = request.tsig.time_signed
113
+ if error_code == 18
114
+ signer.other_data = [0, Time.now.to_i + 3600].pack('nN')
115
+ signer.other_size = 6
116
+ end
117
+ signature = signer.generate(reply, request)
118
+ reply.add_additional(signature)
119
+ valid = reply.encode
120
+ forged = Dnsruby::Message.decode(valid)
121
+ forged.tsig.mac = forged.tsig.mac.dup
122
+ forged.tsig.mac.setbyte(0, forged.tsig.mac.getbyte(0) ^ 1)
123
+ [forged.encode, valid]
124
+ end
125
+ resolver = resolver_for(port, do_caching: false)
126
+ resolver.tsig = key
127
+ error = assert_raises(Dnsruby::TsigError) { query(resolver) }
128
+ assert_includes(error.message, error_code == 18 ? 'BADTIME' : 'BADTRUNC')
129
+ assert_equal(1, requests.size, 'authenticated error should complete the original transaction')
130
+ end
131
+ end
132
+
133
+ def test_default_udp_advertisement_retries_truncated_responses_over_tcp
134
+ [false, true].each do |dnssec|
135
+ port, udp_requests, tcp = udp_server(tcp: true) do |request|
136
+ reply = response(request, 'truncated')
137
+ reply.header.tc = true
138
+ reply
139
+ end
140
+ tcp_requests = Queue.new
141
+ worker do
142
+ client = tcp.accept
143
+ begin
144
+ request = read_frame(client)
145
+ tcp_requests << request
146
+ reply = response(request, 'complete')
147
+ reply.answer.first.strings = Array.new(7) { 'x' * 240 }
148
+ write_frame(client, reply)
149
+ ensure
150
+ client.close
151
+ end
152
+ end
153
+ resolver = resolver_for(port, dnssec: dnssec, do_caching: false)
154
+ request = Dnsruby::Message.new('truncated.example.', 'TXT')
155
+ request.do_caching = request.do_validation = false
156
+ reply = Timeout.timeout(6) { resolver.send_message(request) }
157
+ refute(reply.cached)
158
+ refute(reply.header.tc)
159
+ assert_equal(Array.new(7) { 'x' * 240 }, reply.answer.first.strings)
160
+ assert_operator(reply.encode.bytesize, :>, 1232)
161
+ assert_equal(1232, resolver.udp_size)
162
+ udp_request = udp_requests.pop(true)
163
+ tcp_request = tcp_requests.pop(true)
164
+ assert_equal(1232, udp_request.get_opt.payloadsize)
165
+ assert_equal(dnssec, udp_request.get_opt.dnssec_ok)
166
+ assert_equal(udp_request.header.id, tcp_request.header.id)
167
+ assert_equal(udp_request.question, tcp_request.question)
168
+ assert_empty(udp_requests)
169
+ assert_empty(tcp_requests)
170
+ end
171
+ end
172
+
173
+ def test_message_and_resolver_keys_authenticate_edns_and_tcp_retries
174
+ [false, true].product([:message, :resolver]).each do |dnssec, key_source|
175
+ key = tsig_key
176
+ received = 0
177
+ observed = Queue.new
178
+ port, _, tcp = udp_server(tcp: true) do |request, bytes|
179
+ signature = request.tsig
180
+ raise 'invalid UDP request MAC' unless signature &&
181
+ key.generate(request, nil, '', bytes, signature).mac == signature.mac
182
+ observed << [:udp, request.get_opt]
183
+ reply = response(request, 'retry')
184
+ reply.header.rcode = Dnsruby::RCode::FORMERR if received.zero?
185
+ reply.header.tc = true unless received.zero?
186
+ received += 1
187
+ key.apply(reply, request)
188
+ reply
189
+ end
190
+ worker do
191
+ client = tcp.accept
192
+ begin
193
+ length = client.read(2).unpack1('n')
194
+ bytes = client.read(length)
195
+ request = Dnsruby::Message.decode(bytes)
196
+ signature = request.tsig
197
+ raise 'invalid TCP request MAC' unless signature &&
198
+ key.generate(request, nil, '', bytes, signature).mac == signature.mac
199
+ observed << [:tcp, request.get_opt]
200
+ reply = response(request, 'authenticated')
201
+ key.apply(reply, request)
202
+ write_frame(client, reply)
203
+ ensure
204
+ client.close
205
+ end
206
+ end
207
+ resolver = resolver_for(port, dnssec: dnssec, do_caching: false)
208
+ request = Dnsruby::Message.new('signed-retry.example.', 'TXT')
209
+ request.header.rd = false
210
+ request.do_caching = request.do_validation = false
211
+ key_source == :message ? request.set_tsig(key) : resolver.tsig = key
212
+ reply = Timeout.timeout(6) { resolver.send_message(request) }
213
+ assert(reply.verified?)
214
+ assert_equal(['authenticated'], reply.answer.first.strings)
215
+ attempts = 3.times.map { observed.pop(true) }
216
+ assert_equal([:udp, :udp, :tcp], attempts.map(&:first))
217
+ assert_equal([true, false, false], attempts.map { |_, opt| !!opt })
218
+ assert_equal(1232, attempts.first.last.payloadsize)
219
+ assert_equal(dnssec, attempts.first.last.dnssec_ok)
220
+ assert_empty(observed)
221
+ end
222
+ end
223
+
224
+ def test_explicit_tcp_edns_retry_stays_on_tcp
225
+ server = TCPServer.new('127.0.0.1', 0)
226
+ @sockets << server
227
+ requests = Queue.new
228
+ worker do
229
+ 2.times do |index|
230
+ client = server.accept
231
+ begin
232
+ request = read_frame(client)
233
+ requests << request
234
+ reply = response(request, 'tcp')
235
+ reply.header.rcode = Dnsruby::RCode::FORMERR if index.zero?
236
+ write_frame(client, reply)
237
+ ensure
238
+ client.close
239
+ end
240
+ end
241
+ end
242
+ resolver = resolver_for(server.addr[1], use_tcp: true, do_caching: false)
243
+ message = Dnsruby::Message.new('review.example.', 'TXT')
244
+ message.do_caching = message.do_validation = false
245
+ message.add_additional(Dnsruby::RR::OPT.new(4096))
246
+ reply = Timeout.timeout(6) { resolver.send_message(message) }
247
+ assert_equal(['tcp'], reply.answer.first.strings)
248
+ assert_equal(2, requests.size)
249
+ assert(requests.pop.get_opt)
250
+ assert_nil(requests.pop.get_opt)
251
+ assert(message.get_opt, 'the caller retains the original EDNS request')
252
+ end
253
+
254
+ def test_udp_ignores_query_packets_and_wrong_opcodes_until_valid_reply
255
+ port, requests = udp_server do |request|
256
+ invalid_responses(request) + [response(request, 'valid')]
257
+ end
258
+ reply = query(resolver_for(port, do_caching: false))
259
+ assert_equal(['valid'], reply.answer.first.strings)
260
+ assert_equal(1, requests.size)
261
+ end
262
+
263
+ def test_tcp_ignores_query_frames_and_wrong_opcodes_until_valid_reply
264
+ server = TCPServer.new('127.0.0.1', 0)
265
+ @sockets << server
266
+ worker do
267
+ client = server.accept
268
+ begin
269
+ request = read_frame(client)
270
+ (invalid_responses(request) + [response(request, 'valid')]).each do |reply|
271
+ write_frame(client, reply)
272
+ end
273
+ ensure
274
+ client.close
275
+ end
276
+ end
277
+ reply = query(resolver_for(server.addr[1], use_tcp: true, do_caching: false))
278
+ assert_equal(['valid'], reply.answer.first.strings)
279
+ end
280
+
281
+ def test_udp_ignores_malformed_packets_with_unrelated_headers_without_retrying
282
+ port, requests = udp_server do |request|
283
+ malformed = invalid_responses(request).flat_map do |invalid|
284
+ [false, true].map do |truncated|
285
+ invalid.header.tc = truncated
286
+ bytes = invalid.encode
287
+ # Advertise an absent additional record, forcing Message.decode to
288
+ # expose its partially decoded header through DecodeError.
289
+ bytes[10, 2] = [1].pack('n')
290
+ bytes
291
+ end
292
+ end
293
+ malformed + [response(request, 'valid')]
294
+ end
295
+ reply = query(resolver_for(port, do_caching: false))
296
+ assert_equal(['valid'], reply.answer.first.strings)
297
+ assert_equal(1, requests.size)
298
+ end
299
+
300
+ def test_udp_ignores_incomplete_and_unrelated_malformed_questions_before_a_valid_reply
301
+ port, requests = udp_server do |request|
302
+ unrelated = Dnsruby::Message.new('unrelated.example.', 'AAAA', 'CH')
303
+ unrelated.header.id = request.header.id
304
+ unrelated.header.qr = true
305
+ malformed = [false, true].map do |truncated|
306
+ unrelated.header.tc = truncated
307
+ unrelated.encode.tap { |wire| wire[6, 2] = [1].pack('n') }
308
+ end
309
+ incomplete = [2, 11, 12, 15].map { |length| malformed.first.byteslice(0, length) }
310
+ incomplete + malformed + [response(request, 'valid')]
311
+ end
312
+ reply = query(resolver_for(port, do_caching: false))
313
+ assert_equal(['valid'], reply.answer.first.strings)
314
+ assert_equal(1, requests.size)
315
+ end
316
+
317
+ def test_short_deadline_query_on_busy_resolver_is_sent_before_its_deadline
318
+ port, requests = udp_server do |request|
319
+ request.question.first.qname.to_s == 'pending.example' ? [] : response(request, 'fast')
320
+ end
321
+ resolver = resolver_for(port, do_caching: false)
322
+ pending = Dnsruby::Message.new('pending.example.', 'TXT')
323
+ pending.do_caching = pending.do_validation = false
324
+ pending_results = Queue.new
325
+ resolver.send_async(pending, pending_results, :pending)
326
+ Timeout.timeout(2) { requests.pop }
327
+ sleep 0.03 # Start just after the selector's initial tick, while the first query remains pending.
328
+ resolver.query_timeout = 0.2
329
+ assert_equal(['fast'], query(resolver).answer.first.strings)
330
+ assert_equal(1, requests.size, 'the new query must reach the server while the first remains pending')
331
+ assert(pending_results.empty?)
332
+ end
333
+
334
+ private
335
+
336
+ def worker(&block)
337
+ thread = Thread.new(&block)
338
+ thread.report_on_exception = false
339
+ @workers << thread
340
+ thread
341
+ end
342
+
343
+ def udp_server(tcp: false, &handler)
344
+ if tcp
345
+ socket, listener = TestSockets.udp_tcp_pair
346
+ @sockets << listener
347
+ else
348
+ socket = UDPSocket.new
349
+ socket.bind('127.0.0.1', 0)
350
+ end
351
+ @sockets << socket
352
+ requests = Queue.new
353
+ worker do
354
+ loop do
355
+ bytes, peer = socket.recvfrom(65_535)
356
+ request = Dnsruby::Message.decode(bytes)
357
+ requests << request
358
+ replies = handler.call(request, bytes)
359
+ replies = [replies] unless replies.is_a?(Array)
360
+ replies.each do |reply|
361
+ bytes = reply.is_a?(String) ? reply : reply.encode
362
+ socket.send(bytes, 0, peer[3], peer[1])
363
+ end
364
+ end
365
+ end
366
+ [socket.addr[1], requests, listener]
367
+ end
368
+
369
+ def resolver_for(port, options = {})
370
+ resolver = Dnsruby::Resolver.new({nameserver: '127.0.0.1', port: port,
371
+ dnssec: false, do_validation: false,
372
+ packet_timeout: 1, query_timeout: 3, retry_times: 1}.merge(options))
373
+ @resolvers << resolver
374
+ resolver
375
+ end
376
+
377
+ def query(resolver, klass = 'IN')
378
+ Timeout.timeout(6) { resolver.query('review.example.', 'TXT', klass) }
379
+ end
380
+
381
+ def response(request, text)
382
+ question = request.question.first
383
+ reply = Dnsruby::Message.new
384
+ reply.header.id = request.header.id
385
+ reply.header.qr = reply.header.aa = reply.header.ra = true
386
+ reply.header.rd = request.header.rd
387
+ reply.header.opcode = request.header.opcode
388
+ reply.add_question(question.qname, question.qtype, question.qclass)
389
+ reply.add_answer(Dnsruby::RR.create("#{question.qname} 60 #{question.qclass} TXT \"#{text}\""))
390
+ reply
391
+ end
392
+
393
+ def invalid_responses(request)
394
+ query_packet = response(request, 'invalid-query')
395
+ query_packet.header.qr = false
396
+ wrong_opcode = response(request, 'invalid-opcode')
397
+ wrong_opcode.header.opcode = 2 # STATUS
398
+ [query_packet, wrong_opcode]
399
+ end
400
+
401
+ def tsig_key
402
+ Dnsruby::RR.create(name: 'review-key.example.', type: 'TSIG', klass: 'ANY',
403
+ key: 'c2VjcmV0', algorithm: 'hmac-sha256')
404
+ end
405
+
406
+ def read_frame(socket)
407
+ length = socket.read(2).unpack1('n')
408
+ Dnsruby::Message.decode(socket.read(length))
409
+ end
410
+
411
+ def write_frame(socket, reply)
412
+ bytes = reply.encode
413
+ socket.write([bytes.bytesize].pack('n') + bytes)
414
+ end
415
+ end