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
@@ -14,28 +14,40 @@
14
14
  # limitations under the License.
15
15
  # ++
16
16
 
17
+ require 'dnsruby/serial_time'
18
+
17
19
  module Dnsruby
18
20
  class KeyCache #:nodoc: all
19
21
  # Cache includes expiration time for keys
20
22
  # Cache removes expired records
21
- def initialize(keys = nil)
22
- # Store key tag against [expiry, key]
23
+ def initialize(keys = nil, max_keys: nil)
24
+ # Store DNSKEY identity against [expiry, key]. A 16-bit key tag can collide.
23
25
  @keys = {}
26
+ @candidate_index = {}
27
+ @key_candidates = {}
28
+ @name_index = {}
29
+ raise ArgumentError, 'max_keys must be a positive integer' if max_keys && (!max_keys.is_a?(Integer) || max_keys <= 0)
30
+ @max_keys = max_keys
24
31
  add(keys)
25
32
  end
33
+
34
+ # TTL and cached OpenSSL objects are not part of a DNSKEY's identity.
35
+ # Copy mutable fields so later changes cannot corrupt the hash index.
36
+ def self.key_identity(key)
37
+ [key.name.canonical.freeze, key.klass.code, key.type.code,
38
+ key.flags, key.protocol, key.algorithm.code, key.key.dup.freeze].freeze
39
+ end
26
40
  def add_key_with_expiration(k, expiration)
27
41
  priv_add_key(k, expiration)
28
42
  end
29
- def add(k)
43
+ def add(k, expiration = nil)
30
44
  if (k == nil)
31
45
  return false
32
46
  elsif (k.instance_of?RRSet)
33
- add_rrset(k)
47
+ add_rrset(k, expiration)
34
48
  elsif (k.kind_of?KeyCache)
35
- kaes = k.keys_and_expirations
36
- kaes.keys.each { |keykey|
37
- # priv_add_key(keykey, kaes[keykey])
38
- priv_add_key(keykey[1], keykey[0])
49
+ k.keys_and_expirations.each { |expiration, key|
50
+ priv_add_key(key, expiration)
39
51
  }
40
52
  else
41
53
  raise ArgumentError.new("Expected an RRSet or KeyCache! Got #{k.class}")
@@ -43,39 +55,98 @@ module Dnsruby
43
55
  return true
44
56
  end
45
57
 
46
- def add_rrset(k)
47
- # Get expiration from the RRSIG
48
- # There can be several RRSIGs here, one for each key which has signed the RRSet
49
- # We want to choose the one with the most secure signing algorithm, key length,
50
- # and the longest expiration time - not easy!
51
- # for now, we simply accept all signed keys
52
- k.sigs.each { |sig|
53
- if (sig.type_covered = Types.DNSKEY)
54
- if (sig.inception <= Time.now.to_i)
55
- # Check sig.expiration, sig.algorithm
56
- if (sig.expiration > Time.now.to_i)
57
- # add the keys to the store
58
- k.rrs.each {|rr| priv_add_key(rr, sig.expiration)}
59
- end
60
- end
61
- end
62
- }
58
+ def add_rrset(k, expiration = nil)
59
+ return unless k.type == Types.DNSKEY
60
+ now = Time.now.to_i
61
+ # Verification supplies an absolute deadline derived from the signature
62
+ # that actually verified. Never extend it using other supplied RRSIGs.
63
+ # Explicit callers without that deadline get a conservative TTL bound.
64
+ unless expiration
65
+ signatures = k.sigs.select { |sig| SerialTime.valid?(sig.inception, sig.expiration, now) }
66
+ return if signatures.empty?
67
+ expiration = ([now + k.ttl] + signatures.map { |sig| SerialTime.absolute(sig.expiration, now) }).min
68
+ end
69
+ return if expiration <= now
70
+ k.rrs.each { |rr| priv_add_key(rr, expiration) }
71
+ end
72
+
73
+ # A tag identifies a bucket, not a unique key. Keep every colliding key.
74
+ def self.candidate_identity(name, klass, algorithm, tag)
75
+ [name.canonical.freeze, klass.code, algorithm.code, tag].freeze
63
76
  end
64
77
 
65
78
  def priv_add_key(k, exp)
66
- # Check that the key does not already exist with a longer expiration!
67
- if (@keys[k] == nil)
68
- @keys[k.key_tag] = [exp,k]
69
- elsif ((@keys[k])[0] < exp)
70
- @keys[k.key_tag] = [exp,k]
79
+ identity = self.class.key_identity(k)
80
+ previous = @keys[identity]
81
+ return if previous && previous[0] >= exp
82
+ if @max_keys && !previous && @keys.length >= @max_keys
83
+ remove_expired_keys
84
+ delete_identity(@keys.each_key.first) while @keys.length >= @max_keys
85
+ end
86
+ @keys[identity] = [exp, k]
87
+ candidate = self.class.candidate_identity(k.name, k.klass, k.algorithm, k.key_tag)
88
+ (@candidate_index[candidate] ||= {})[identity] = true
89
+ @key_candidates[identity] = candidate
90
+ (@name_index[identity.first] ||= {})[identity] = true
91
+ end
92
+
93
+ def matching_keys(signatures)
94
+ identities = signatures.flat_map do |sig|
95
+ candidate = self.class.candidate_identity(sig.signers_name, sig.klass, sig.algorithm, sig.key_tag)
96
+ (@candidate_index[candidate] || {}).keys
97
+ end.uniq
98
+ now = Time.now.to_i
99
+ identities.filter_map do |identity|
100
+ expiration, key = @keys.fetch(identity)
101
+ if expiration <= now
102
+ delete_identity(identity)
103
+ nil
104
+ else
105
+ key
106
+ end
107
+ end
108
+ end
109
+
110
+ def keys_for_name(name)
111
+ now = Time.now.to_i
112
+ (@name_index[name.canonical] || {}).keys.filter_map do |identity|
113
+ expiration, key = @keys.fetch(identity)
114
+ if expiration <= now
115
+ delete_identity(identity)
116
+ nil
117
+ else
118
+ key
119
+ end
71
120
  end
72
121
  end
73
122
 
123
+ def include_identity?(identity)
124
+ @keys.key?(identity)
125
+ end
126
+
74
127
  def each
75
128
  # Only offer currently-valid keys here
76
129
  remove_expired_keys
77
130
  @keys.values.each {|v| yield v[1]}
78
131
  end
132
+ def delete(key)
133
+ delete_identity(self.class.key_identity(key))
134
+ end
135
+ def delete_identity(identity)
136
+ cached = @keys.delete(identity)
137
+ return unless cached
138
+ key = cached[1]
139
+ names = @name_index.fetch(identity.first)
140
+ names.delete(identity)
141
+ @name_index.delete(identity.first) if names.empty?
142
+ candidate = @key_candidates.delete(identity)
143
+ bucket = @candidate_index[candidate]
144
+ if bucket
145
+ bucket.delete(identity)
146
+ @candidate_index.delete(candidate) if bucket.empty?
147
+ end
148
+ key
149
+ end
79
150
  def keys
80
151
  # Only offer currently-valid keys here
81
152
  remove_expired_keys
@@ -86,16 +157,112 @@ module Dnsruby
86
157
  end
87
158
  def keys_and_expirations
88
159
  remove_expired_keys
89
- return keys.values
160
+ @keys.values.map(&:dup)
90
161
  end
91
162
  def remove_expired_keys
92
- @keys.delete_if {|k,v|
93
- v[0] < Time.now.to_i
94
- }
163
+ now = Time.now.to_i
164
+ @keys.each { |identity, (expiration, _key)| delete_identity(identity) if expiration <= now }
95
165
  end
96
166
  def find_key_for(name)
97
167
  each {|key| return key if key.name == name}
98
168
  return false
99
169
  end
100
170
  end
101
- end
171
+
172
+ # Learned DS/DLV RRsets are evicted as a whole. The record limit bounds both
173
+ # retained records and index entries, even when an RRset contains many DSes.
174
+ class DSCache # :nodoc: all
175
+ def initialize(max_records:)
176
+ if max_records && (!max_records.is_a?(Integer) || max_records <= 0)
177
+ raise ArgumentError, 'max_records must be a positive integer'
178
+ end
179
+ @max_records = max_records
180
+ @record_count = 0
181
+ @sets = {}
182
+ @candidate_index = {}
183
+ @name_index = {}
184
+ end
185
+
186
+ def self.set_identity(rrset)
187
+ [rrset.name.canonical.freeze, rrset.klass.code, rrset.type.code].freeze
188
+ end
189
+
190
+ def self.candidate_identity(record)
191
+ # DLV owners belong to a lookaside registry, not the DNSKEY's zone.
192
+ [record.type == Types.DLV ? nil : record.name.canonical.freeze,
193
+ record.klass.code, record.algorithm.code, record.key_tag].freeze
194
+ end
195
+
196
+ def add(rrset, expiration)
197
+ identity = self.class.set_identity(rrset)
198
+ delete(identity)
199
+ count = rrset.rrs.length + rrset.sigs.length
200
+ return if expiration <= Time.now.to_i || (@max_records && count > @max_records)
201
+ remove_expired if @max_records && @record_count + count > @max_records
202
+ delete(@sets.each_key.first) while @max_records && @record_count + count > @max_records
203
+ candidates = rrset.rrs.map { |record| self.class.candidate_identity(record) }.uniq
204
+ @sets[identity] = [expiration, rrset, count, candidates]
205
+ @record_count += count
206
+ (@name_index[identity.first] ||= {})[identity] = true
207
+ candidates.each { |candidate| (@candidate_index[candidate] ||= {})[identity] = true }
208
+ end
209
+
210
+ def matching_rrsets(keys)
211
+ identities = keys.flat_map do |key|
212
+ [key.name.canonical, nil].flat_map do |owner|
213
+ (@candidate_index[[owner, key.klass.code, key.algorithm.code, key.key_tag]] || {}).keys
214
+ end
215
+ end.uniq
216
+ now = Time.now.to_i
217
+ identities.filter_map do |identity|
218
+ expiration, rrset = @sets.fetch(identity)
219
+ if expiration <= now
220
+ delete(identity)
221
+ nil
222
+ else
223
+ rrset
224
+ end
225
+ end
226
+ end
227
+
228
+ def rrsets_for_name(name)
229
+ now = Time.now.to_i
230
+ (@name_index[name.canonical] || {}).keys.filter_map do |identity|
231
+ expiration, rrset = @sets.fetch(identity)
232
+ if expiration <= now
233
+ delete(identity)
234
+ nil
235
+ else
236
+ rrset
237
+ end
238
+ end
239
+ end
240
+
241
+ def rrsets
242
+ remove_expired
243
+ @sets.values.map { |entry| entry[1] }
244
+ end
245
+
246
+ private
247
+
248
+ def remove_expired
249
+ now = Time.now.to_i
250
+ @sets.each { |identity, entry| delete(identity) if entry[0] <= now }
251
+ end
252
+
253
+ def delete(identity)
254
+ entry = @sets.delete(identity)
255
+ return unless entry
256
+ @record_count -= entry[2]
257
+ names = @name_index.fetch(identity.first)
258
+ names.delete(identity)
259
+ @name_index.delete(identity.first) if names.empty?
260
+ entry[3].each do |candidate|
261
+ bucket = @candidate_index.fetch(candidate)
262
+ bucket.delete(identity)
263
+ @candidate_index.delete(candidate) if bucket.empty?
264
+ end
265
+ end
266
+ end
267
+
268
+ end
@@ -17,6 +17,9 @@ class MessageDecoder #:nodoc: all
17
17
  @data = data
18
18
  @index = 0
19
19
  @limit = data.length
20
+ # Compression addresses have 14 bits. Cache only validated suffixes at
21
+ # addressable offsets, so repeated names do not repeat pointer traversal.
22
+ @name_suffixes = {}
20
23
  yield self if block_given?
21
24
  end
22
25
 
@@ -36,19 +39,28 @@ class MessageDecoder #:nodoc: all
36
39
  # Gets the byte value at the specified position
37
40
  def get_byte_at(position)
38
41
  assert_buffer_position_valid(position)
39
- return nil if @data[position].nil?
40
- @data[position].getbyte(0)
42
+ # Wire strings have byte offsets. Avoid allocating a one-character
43
+ # String for every label octet, retaining historical character indexing
44
+ # for direct callers that supply a multibyte text encoding.
45
+ if @data.encoding == Encoding::BINARY || @data.ascii_only?
46
+ @data.getbyte(position)
47
+ else
48
+ @data[position]&.getbyte(0)
49
+ end
41
50
  end
42
51
 
43
52
  # Gets a 16-bit length field from the binary string and yields to the block.
44
53
  # This will be the length of the next item to parse in the binary string.
45
54
  # Returns the object returned from that block.
46
55
  #
47
- # When this method returns, @index will point to the byte after the
48
- # 16-bit length field.
56
+ # When this method returns, @index will point just after the parsed item.
49
57
  def get_length16
50
- len, = self.get_unpack('n')
51
58
  save_limit = @limit
59
+ len, = self.get_unpack('n')
60
+ # A nested length may narrow its enclosing buffer, never extend it. In
61
+ # particular, truncated UDP RDATA must raise DecodeError before a typed
62
+ # decoder reads beyond the physical packet and raises an unrelated error.
63
+ assert_buffer_position_valid(@index + len)
52
64
  @limit = @index + len
53
65
  parsed_data = yield(len)
54
66
  if @index < @limit
@@ -56,8 +68,9 @@ class MessageDecoder #:nodoc: all
56
68
  raise DecodeError.new(message)
57
69
  end
58
70
  assert_buffer_position_valid(@index)
59
- @limit = save_limit
60
71
  parsed_data
72
+ ensure
73
+ @limit = save_limit
61
74
  end
62
75
 
63
76
  # Returns the specified number of bytes from the binary string.
@@ -118,29 +131,68 @@ class MessageDecoder #:nodoc: all
118
131
  def get_labels(limit = nil)
119
132
  limit = @index if limit.nil? || (@index < limit)
120
133
  labels = []
121
- while true
134
+ return_index = nil
135
+ expanded_length = 1 # The terminating root label.
136
+ path = []
137
+ suffix_end = 0
138
+
139
+ loop do
140
+ raise DecodeError, 'Truncated DNS name' unless has_remaining?
141
+ if return_index && (suffix = @name_suffixes[@index])
142
+ # Previously decoded suffixes must also fit the current RDATA bound.
143
+ # The pointer's own two octets were checked before this lookup.
144
+ assert_buffer_position_valid(suffix[2])
145
+ expanded_length += suffix[1] - 1
146
+ raise DecodeError, "DNS name exceeds #{Name::MaxNameLength} octets" if expanded_length > Name::MaxNameLength
147
+ labels.concat(suffix[0])
148
+ suffix_end = suffix[2]
149
+ break
150
+ end
151
+ offset = @index
152
+ label_index = labels.length
122
153
  temp = get_byte_at(@index)
123
154
  case temp
124
155
  when 0
125
156
  @index += 1
126
- return labels
157
+ path << [offset, label_index, @index]
158
+ break
127
159
  when 192..255
128
160
  idx = get_unpack('n')[0] & 0x3fff
129
161
  if limit <= idx
130
162
  raise DecodeError.new('non-backward name pointer')
131
163
  end
132
- save_index = @index
164
+ return_index ||= @index
165
+ path << [offset, label_index, @index]
133
166
  @index = idx
134
- labels += self.get_labels(limit)
135
- @index = save_index
136
- return labels
137
- when nil
138
- return labels
167
+ # Each target is below the preceding target (or original name).
168
+ # The 14-bit pointer address therefore bounds traversal without
169
+ # recursion, an arbitrary hop limit, or a growing visited set.
170
+ limit = idx
171
+ when 1..63
172
+ expanded_length += 1 + temp
173
+ if expanded_length > Name::MaxNameLength
174
+ raise DecodeError, "DNS name exceeds #{Name::MaxNameLength} octets"
175
+ end
176
+ labels << get_label.string.dup.freeze
177
+ path << [offset, label_index, @index]
139
178
  else
140
- labels << self.get_label
179
+ raise DecodeError, 'Reserved or invalid DNS label type'
141
180
  end
142
181
  end
143
- labels
182
+ # Pointer-only aliases share one immutable array. Label suffix arrays are
183
+ # bounded by the 255-octet expanded name limit, not by packet record count.
184
+ previous_index = previous_suffix = nil
185
+ path.reverse_each do |offset, label_index, wire_end|
186
+ suffix_end = [suffix_end, wire_end].max
187
+ next if offset >= 0x4000
188
+ value = label_index == previous_index ? previous_suffix : labels[label_index..-1].freeze
189
+ @name_suffixes[offset] ||= [value, 1 + value.sum { |label| label.bytesize + 1 }, suffix_end].freeze
190
+ previous_index, previous_suffix = label_index, value
191
+ end
192
+ @index = return_index if return_index
193
+ # Name and Label expose mutable strings; cached data must never be shared
194
+ # with callers or between separately decoded names.
195
+ labels.map { |label| Name::Label.new(label.dup) }
144
196
  end
145
197
 
146
198
  # Gets a single label.
@@ -161,18 +213,32 @@ class MessageDecoder #:nodoc: all
161
213
  end
162
214
 
163
215
  # Gets a resource record.
164
- def get_rr
216
+ def get_rr(update_section: nil)
165
217
  name = get_name
166
218
  type, klass, ttl = get_unpack('nnN')
219
+ if type == Types::TSIG && (klass != Classes::ANY || ttl != 0)
220
+ raise DecodeError.new('TSIG requires class ANY and TTL zero (RFC 8945 section 4.2)')
221
+ end
167
222
  klass = Classes.new(klass)
168
223
  typeclass = RR.get_class(type, klass)
169
224
  # @TODO@ Trap decode errors here, and somehow mark the record as bad.
170
225
  # Need some way to represent raw data only
171
- record = get_length16 { typeclass.decode_rdata(self) }
226
+ record = get_length16 do |length|
227
+ marker_context = ttl == 0 && (update_section.nil? ||
228
+ (update_section == :prerequisite ||
229
+ (update_section == :update && klass == Classes::ANY)))
230
+ if length == 0 && marker_context && RR.empty_update_type?(type, klass)
231
+ RR.empty_update_record(name, type, klass)
232
+ else
233
+ typeclass.decode_rdata(self)
234
+ end
235
+ end
172
236
  record.name = name
173
- record.ttl = ttl
237
+ # OPT overloads this field with EDNS flags and the extended RCODE.
238
+ record.ttl = type != Types::OPT && ttl >= 0x80000000 ? 0 : ttl
174
239
  record.type = type
175
240
  record.klass = klass
241
+ record.validate_decoded_rdata
176
242
  record
177
243
  end
178
244
  end
@@ -1,7 +1,7 @@
1
1
  module Dnsruby
2
2
  class MessageEncoder #:nodoc: all
3
3
  def initialize
4
- @data = +''
4
+ @data = ''.b
5
5
  @names = {}
6
6
  yield self if block_given?
7
7
  end
@@ -11,7 +11,7 @@ class MessageEncoder #:nodoc: all
11
11
  end
12
12
 
13
13
  def put_bytes(d)
14
- @data << d
14
+ @data << d.b
15
15
  end
16
16
 
17
17
  def put_pack(template, *d)
@@ -25,19 +25,20 @@ class MessageEncoder #:nodoc: all
25
25
  def put_length16
26
26
  length_index = @data.length
27
27
  @data << "\0\0"
28
- data_start = @data.length
28
+ data_start = @data.bytesize
29
29
  yield
30
- data_end = @data.length
31
- @data[length_index, 2] = [data_end - data_start].pack("n")
30
+ data_end = @data.bytesize
31
+ length = data_end - data_start
32
+ if length > 0xffff
33
+ raise EncodeError.new("Length-prefixed data exceeds 65535 octets: #{length}")
34
+ end
35
+ @data[length_index, 2] = [length].pack("n")
32
36
  end
33
37
 
34
38
  def put_string(d)
35
- begin
36
- self.put_pack("C", d.length)
37
- @data << d
38
- rescue Encoding::CompatibilityError
39
- raise Dnsruby::EncodeError.new("IDN support currently requires punycode string")
40
- end
39
+ raise EncodeError, "Character-string exceeds 255 octets: #{d.bytesize}" if d.bytesize > 255
40
+ self.put_pack("C", d.bytesize)
41
+ @data << d.b
41
42
  end
42
43
 
43
44
  def put_string_list(ds)
@@ -59,13 +60,23 @@ class MessageEncoder #:nodoc: all
59
60
  end
60
61
 
61
62
  def put_labels(d, do_canonical)
63
+ if do_canonical
64
+ d.each { |label| put_label(label) }
65
+ @data << "\0"
66
+ return
67
+ end
68
+ # Snapshot each label once. Suffix keys share these immutable strings,
69
+ # while retaining label boundaries and case for lossless compression.
70
+ labels = d.map { |label| label.string.b.freeze }
62
71
  d.each_index do |i|
63
- domain = d[i..-1].join('.')
64
- if !do_canonical && (idx = @names[domain])
72
+ domain = labels[i..-1].freeze
73
+ if (idx = @names[domain]) && idx < 0x4000
65
74
  self.put_pack('n', 0xc000 | idx)
66
75
  return
67
76
  else
68
- @names[domain] = @data.length
77
+ # Compression pointers have only 14 bits for the target offset.
78
+ # Later names can still use an earlier eligible suffix.
79
+ @names[domain] ||= @data.bytesize if @data.bytesize < 0x4000
69
80
  self.put_label(d[i])
70
81
  end
71
82
  end
@@ -76,8 +87,8 @@ class MessageEncoder #:nodoc: all
76
87
  def put_label(d)
77
88
  # s, = Name.encode(d)
78
89
  s = d
79
- raise RuntimeError, "length of #{s} is #{s.string.length} (larger than 63 octets)" if s.string.length > 63
90
+ raise EncodeError, "Label exceeds 63 octets: #{s.string.bytesize}" if s.string.bytesize > 63
80
91
  self.put_string(s.string)
81
92
  end
82
93
  end
83
- end
94
+ end
@@ -1,3 +1,5 @@
1
+ require 'securerandom'
2
+
1
3
  module Dnsruby
2
4
 
3
5
  # The header portion of a DNS packet
@@ -60,7 +62,7 @@ class Header
60
62
 
61
63
  def initialize(*args)
62
64
  if (args.length == 0)
63
- @id = rand(MAX_ID)
65
+ @id = SecureRandom.random_number(MAX_ID + 1)
64
66
  @qr = false
65
67
  @opcode = OpCode.Query
66
68
  @aa = false
@@ -88,9 +90,7 @@ class Header
88
90
  end
89
91
 
90
92
  def Header.new_from_data(data)
91
- header = Header.new
92
- MessageDecoder.new(data) { |msg| header.decode(msg) }
93
- header
93
+ Header.new(MessageDecoder.new(data))
94
94
  end
95
95
 
96
96
  def data
@@ -116,14 +116,10 @@ class Header
116
116
  end
117
117
 
118
118
  def Header.decrement_arcount_encoded(bytes)
119
- header = Header.new
120
- header_end = 0
121
- MessageDecoder.new(bytes) do |msg|
122
- header.decode(msg)
123
- header_end = msg.index
124
- end
119
+ decoder = MessageDecoder.new(bytes)
120
+ header = Header.new(decoder)
125
121
  header.arcount -= 1
126
- bytes[0, header_end] = MessageEncoder.new { |msg| header.encode(msg) }.to_s
122
+ bytes[0, decoder.index] = MessageEncoder.new { |msg| header.encode(msg) }.to_s
127
123
  bytes
128
124
  end
129
125
 
@@ -243,4 +239,4 @@ class Header
243
239
  alias adcount= arcount=
244
240
 
245
241
  end
246
- end
242
+ end