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
data/lib/dnsruby/cache.rb CHANGED
@@ -26,12 +26,18 @@
26
26
 
27
27
 
28
28
 
29
- # @TODO@ Max size for cache?
30
29
  module Dnsruby
31
30
  class Cache # :nodoc: all
32
- def initialize()
31
+ @@max_size = 16 * 1024
32
+ @@resolution_generation = 0
33
+ @@generation_mutex = Mutex.new
34
+
35
+ def self.invalidate_resolutions
36
+ @@generation_mutex.synchronize { @@resolution_generation += 1 }
37
+ end
38
+ def initialize(preserve_security: false)
39
+ @preserve_security = preserve_security
33
40
  @cache = Hash.new
34
- @@max_size = 16*1024 # Get this right...
35
41
  @mutex = Mutex.new
36
42
  end
37
43
  def cache
@@ -42,40 +48,89 @@ module Dnsruby
42
48
  @cache = Hash.new
43
49
  }
44
50
  end
51
+ def delete(qname, qtype, qclass = Classes.IN, scope: nil)
52
+ qname = Name.create(qname)
53
+ qname.absolute = true
54
+ keys = [CacheKey.new(qname, qtype, qclass).to_s,
55
+ CacheKey.new(qname, Types::ANY, qclass).to_s + ' NXDOMAIN']
56
+ keys.map! { |key| [scope, key] } if scope
57
+ @mutex.synchronize { keys.each { |key| @cache.delete(key) } }
58
+ end
45
59
  def length
46
- return @cache.length
60
+ @mutex.synchronize { @cache.length }
47
61
  end
48
62
  def Cache.max_size=(length)
49
63
  @@max_size = length
50
64
  end
51
- def add(message)
52
- q = message.question[0]
53
- key = CacheKey.new(q.qname, q.qtype, q.qclass).to_s
54
- data = CacheData.new(message)
55
- @mutex.synchronize {
56
- if (@cache[key])
57
- TheLog.debug("CACHE REPLACE : #{q.qname}, #{q.qtype}\n")
65
+ # A negative response requires an enclosing SOA. Following aliases here
66
+ # keeps a CNAME-only positive answer distinct from a NODATA response.
67
+ def self.negative_soa(message)
68
+ q = message.question.first
69
+ return unless q && [RCode::NOERROR, RCode::NXDOMAIN].include?(message.rcode)
70
+ terminal = q.qname
71
+ seen = {}
72
+ loop do
73
+ return if seen[terminal]
74
+ seen[terminal] = true
75
+ return if message.rcode == RCode::NOERROR && message.answer.any? { |rr|
76
+ rr.name == terminal && rr.klass == q.qclass &&
77
+ (rr.type == q.qtype || (q.qtype == Types::ANY && rr.type != Types::RRSIG)) }
78
+ alias_rr = message.answer.find { |rr| rr.name == terminal && rr.klass == q.qclass && rr.type == Types::CNAME }
79
+ if alias_rr
80
+ terminal = alias_rr.cname
58
81
  else
59
- TheLog.debug("CACHE ADD : #{q.qname}, #{q.qtype}\n")
82
+ # RFC 6672 permits omission of the synthesized CNAME. Follow the
83
+ # closest enclosing DNAME using complete labels, just as validation
84
+ # does, before deciding whether this response is negative.
85
+ dname = message.answer.select do |rr|
86
+ rr.type == Types::DNAME && rr.klass == q.qclass && terminal.subdomain_of?(rr.name)
87
+ end.max_by { |rr| rr.name.labels.length }
88
+ break unless dname
89
+ terminal = Name.new(terminal.labels.first(terminal.labels.length - dname.name.labels.length) +
90
+ dname.domainname.labels, true)
60
91
  end
61
- @cache[key] = data
92
+ end
93
+ message.authority.select { |rr| rr.type == Types::SOA && rr.klass == q.qclass &&
94
+ (terminal == rr.name || terminal.subdomain_of?(rr.name)) }.max_by { |rr| rr.name.labels.length }
95
+ rescue ResolvError
96
+ nil
97
+ end
62
98
 
63
- while @cache.size > @@max_size # keep the cache size reasonable
64
- @cache.shift
65
- end
66
- }
99
+ def add(message, scope: nil, error: nil)
100
+ return if message.header.tc
101
+ q = message.question.first
102
+ return unless q
103
+ soa = Cache.negative_soa(message)
104
+ return if message.rcode == RCode::NXDOMAIN && !soa
105
+ return if message.rcode == RCode::NOERROR && message.answer.empty? && !soa
106
+ key = CacheKey.new(q.qname, q.qtype, q.qclass).to_s
107
+ # A bare NXDOMAIN applies to all types. With aliases, retain the full
108
+ # original question so a CNAME query never receives a false name error.
109
+ if soa && message.rcode == RCode::NXDOMAIN && message.answer.empty?
110
+ key = CacheKey.new(q.qname, Types::ANY, q.qclass).to_s + ' NXDOMAIN'
111
+ end
112
+ key = [scope, key] if scope
113
+ data = CacheData.new(message, preserve_security: @preserve_security, error: error)
114
+ return if data.expiration <= Time.now.to_i
115
+ @mutex.synchronize do
116
+ @cache.delete(key)
117
+ @cache[key] = data
118
+ @cache.shift while @cache.size > @@max_size
119
+ end
120
+ data
67
121
  end
68
122
  # This method "fixes up" the response, so that the header and ttls are OK
69
123
  # The resolver will still need to copy the flags and ID across from the query
70
- def find(qname, qtype, qclass = Classes.IN)
71
- # print "CACHE find : #{qname}, #{qtype}\n"
124
+ def find(qname, qtype, qclass = Classes.IN, scope: nil)
72
125
  qn = Name.create(qname)
73
126
  qn.absolute = true
74
127
  key = CacheKey.new(qn, qtype, qclass).to_s
75
- @mutex.synchronize {
128
+ name_error_key = CacheKey.new(qn, Types::ANY, qclass).to_s + ' NXDOMAIN'
129
+ key, name_error_key = [scope, key], [scope, name_error_key] if scope
130
+ data = @mutex.synchronize {
131
+ key = name_error_key if !@cache[key] && @cache[name_error_key]
76
132
  data = @cache[key]
77
133
  if (!data)
78
- # print "CACHE lookup failed\n"
79
134
  return nil
80
135
  end
81
136
  if (data.expiration <= Time.now.to_i)
@@ -83,10 +138,18 @@ module Dnsruby
83
138
  TheLog.debug("CACHE lookup stale\n")
84
139
  return nil
85
140
  end
86
- m = data.message
87
- TheLog.debug("CACHE found\n")
88
- return m
141
+ data
89
142
  }
143
+ # CacheData owns an immutable wire snapshot, so decoding does not need to
144
+ # serialize other cache lookups, insertions or clears behind this lock.
145
+ m = data.message
146
+ if data.expiration <= Time.now.to_i
147
+ @mutex.synchronize { @cache.delete(key) if @cache[key].equal?(data) }
148
+ return nil
149
+ end
150
+ TheLog.debug("CACHE found\n")
151
+ m.question[0] = Question.new(qname, qtype, qclass)
152
+ m
90
153
  end
91
154
  def Cache.delete(qname, qtype, qclass = Classes.IN)
92
155
  key = CacheKey.new(qname, qtype, qclass)
@@ -94,6 +157,132 @@ module Dnsruby
94
157
  @cache.delete(key)
95
158
  }
96
159
  end
160
+ def self.validation_scope
161
+ [@@generation_mutex.synchronize { @@resolution_generation },
162
+ Dnssec.validation_policy, Dnssec.validation_limits,
163
+ [Dnssec.root_verifier, Dnssec.anchor_verifier, Dnssec.dlv_verifier].map(&:validation_cache_identity),
164
+ validation_discovery_scope]
165
+ end
166
+
167
+ DISCOVERY_SCOPE_FIELDS = %i[port use_tcp no_tcp tcp_pipelining tcp_pipelining_max_queries
168
+ tcp_idle_timeout packet_timeout query_timeout retry_times retry_delay dnssec do_validation
169
+ do_caching recurse udp_size ignore_truncation src_address src_address6 src_port tsig].freeze
170
+ private_constant :DISCOVERY_SCOPE_FIELDS
171
+
172
+ def self.validation_discovery_scope
173
+ resolver = Dnssec.default_resolver
174
+ # This snapshot may run while the caller owns this resolver's packet
175
+ # mutex. Read the existing configuration without invoking lazy setup or
176
+ # acquiring resolver locks; insertion rechecks the complete snapshot.
177
+ snapshot = lambda do |object, fields|
178
+ fields.map { |field| immutable_scope_value(object.instance_variable_get("@#{field}")) }
179
+ end
180
+ configuration = if resolver
181
+ senders = resolver.instance_variable_get(:@single_resolvers) || []
182
+ config = resolver.instance_variable_get(:@config)
183
+ [snapshot.call(resolver, DISCOVERY_SCOPE_FIELDS),
184
+ immutable_scope_value(config&.instance_variable_get(:@nameserver)),
185
+ senders.map { |sender| [sender.object_id, snapshot.call(sender, [:server] + DISCOVERY_SCOPE_FIELDS)] }.
186
+ sort_by(&:inspect)]
187
+ end
188
+ [Dnssec.do_validation_with_recursor?, resolver&.object_id, configuration]
189
+ end
190
+
191
+ def self.immutable_scope_value(value)
192
+ case value
193
+ when String then value.dup.freeze
194
+ when Array then value.map { |entry| immutable_scope_value(entry) }.freeze
195
+ when Hash then value.to_h { |key, entry| [immutable_scope_value(key), immutable_scope_value(entry)] }.freeze
196
+ when Numeric, Symbol, NilClass, TrueClass, FalseClass then value
197
+ else
198
+ if value.is_a?(RR::TSIG)
199
+ [value.name.to_s, value.algorithm.to_s, value.key.to_s, value.fudge].map { |entry| immutable_scope_value(entry) }
200
+ else
201
+ value.to_s.dup.freeze
202
+ end
203
+ end
204
+ end
205
+ private_class_method :validation_discovery_scope, :immutable_scope_value
206
+
207
+ # A resolver's completed negative answers and resolution failures. Failure
208
+ # retention is deliberately short (RFC 9520 requires at least one second).
209
+ # The caller supplies a scope captured before the resolution begins.
210
+ class ResolutionCache # :nodoc:
211
+ FAILURE_LIFETIME = 1
212
+ FAILURE_LIMIT = 1024
213
+
214
+ def initialize
215
+ @negative = Cache.new(preserve_security: true)
216
+ @failures = {}
217
+ @mutex = Mutex.new
218
+ end
219
+
220
+ def find(query, scope)
221
+ return unless scope
222
+ question = query.question.first
223
+ @mutex.synchronize do
224
+ response = @negative.find(question.qname, question.qtype, question.qclass, scope: scope)
225
+ if response
226
+ response.header.id = query.header.id
227
+ failure = response.instance_variable_get(:@dnsruby_completion_error)
228
+ return [response, failure && failure[0].new(failure[1])]
229
+ end
230
+ key = [scope, CacheKey.new(question.qname, question.qtype, question.qclass).to_s]
231
+ entry = @failures[key]
232
+ return unless entry
233
+ if entry[0] <= TransportClock.now
234
+ @failures.delete(key)
235
+ return
236
+ end
237
+ response = entry[1]&.message
238
+ response.header.id = query.header.id if response
239
+ [response, entry[2] && entry[2].new(entry[3])]
240
+ end
241
+ end
242
+
243
+ def add(query, scope, response, error, control: nil, no_response: false)
244
+ return unless scope
245
+ begin
246
+ control&.check!
247
+ rescue QueryControl::Stopped
248
+ return
249
+ end
250
+ return if response && (response.tsig || response.signed? || response.header.tc)
251
+ @mutex.synchronize do
252
+ question = query.question.first
253
+ key = [scope, CacheKey.new(question.qname, question.qtype, question.qclass).to_s]
254
+ if response && (!error || error.is_a?(NXDomain)) &&
255
+ [RCode::NOERROR, RCode::NXDOMAIN].include?(response.rcode) &&
256
+ response.security_level != Message::SecurityLevel::BOGUS
257
+ # A later successful completion supersedes earlier failures and
258
+ # contradictory negatives, even when the success itself has TTL 0.
259
+ @failures.delete(key)
260
+ @negative.delete(question.qname, question.qtype, question.qclass, scope: scope)
261
+ end
262
+ if response && (!error || error.is_a?(NXDomain)) && Cache.negative_soa(response)
263
+ if query.do_validation && response.security_level == Message::SecurityLevel::INSECURE
264
+ proof_expiration = response.instance_variable_get(:@dnsruby_authenticated_expiration)
265
+ return unless proof_expiration && proof_expiration > Time.now.to_i
266
+ end
267
+ return if response.security_level == Message::SecurityLevel::BOGUS
268
+ @negative.add(response, scope: scope, error: error)
269
+ # Nested lookups may inherit an already-consumed validation budget;
270
+ # its exhaustion does not establish that this question itself fails.
271
+ elsif no_response || (error && !error.is_a?(ValidationCancelled) &&
272
+ !(control && error.is_a?(ValidationLimitError)) &&
273
+ [ResolvTimeout, ServFail, Refused, FormErr, VerifyError, SocketEofResolvError,
274
+ IOError, SystemCallError].any? { |klass| error.is_a?(klass) })
275
+ @negative.delete(question.qname, question.qtype, question.qclass, scope: scope)
276
+ snapshot = response && CacheData.new(response, lifetime: FAILURE_LIFETIME, preserve_security: true)
277
+ @failures.delete(key)
278
+ @failures[key] = [TransportClock.now + FAILURE_LIFETIME,
279
+ snapshot, error&.class, error&.message&.dup]
280
+ @failures.shift while @failures.length > FAILURE_LIMIT
281
+ end
282
+ end
283
+ end
284
+ end
285
+
97
286
  class CacheKey # :nodoc: all
98
287
  attr_accessor :qname, :qtype, :qclass
99
288
  def initialize(*args)
@@ -110,52 +299,93 @@ module Dnsruby
110
299
  end
111
300
  end
112
301
  def to_s
113
- return "#{qname.inspect.downcase} #{qclass} #{qtype}"
302
+ # Numeric presentation spellings (TYPE1, TYPE0001, CLASS1, ...) are
303
+ # aliases of the same wire values, including when a decoded answer
304
+ # uses the registered mnemonics instead of the query's spelling.
305
+ return "#{qname.inspect.downcase} #{qclass.code} #{qtype.code}"
114
306
  end
115
307
  end
116
308
  class CacheData # :nodoc: all
117
- attr_reader :expiration
309
+ Snapshot = Struct.new(:wire, :expiration, :record_expirations, :authenticated_expirations)
310
+ private_constant :Snapshot
311
+
312
+ def expiration
313
+ @snapshot.expiration
314
+ end
315
+
118
316
  def message=(m)
119
- @expiration = get_expiration(m)
120
- @message = Message.decode(m.encode(true))
121
- @message.cached = true
317
+ now = Time.now.to_i
318
+ record_expirations = []
319
+ authenticated_expirations = []
320
+ @authenticated_expiration = m.instance_variable_get(:@dnsruby_authenticated_expiration)
321
+ soa = Cache.negative_soa(m)
322
+ m.each_resource do |rr|
323
+ authenticated = [rr.instance_variable_get(:@dnsruby_authenticated_expiration),
324
+ @authenticated_expiration].compact.min
325
+ authenticated_expirations << authenticated
326
+ record_expirations << if rr.type != Types::OPT
327
+ received_at = rr.instance_variable_get(:@dnsruby_received_at) || now
328
+ ttl = rr.equal?(soa) ? [rr.ttl, soa.minimum].min : rr.ttl
329
+ [received_at + ttl, authenticated].compact.min
330
+ end
331
+ end
332
+ @snapshot = Snapshot.new(m.encode(true).freeze,
333
+ @lifetime ? now + @lifetime : (record_expirations.compact.min || 0), record_expirations.freeze,
334
+ authenticated_expirations.freeze).freeze
122
335
  end
123
336
  def message
124
- m = Message.decode(@message.encode)
337
+ snapshot = @snapshot
338
+ m = Message.decode(snapshot.wire)
125
339
  m.cached = true
340
+ m.instance_variable_set(:@dnsruby_authenticated_expiration, @authenticated_expiration) if @authenticated_expiration
341
+ m.instance_variable_set(:@dnsruby_completion_error, @completion_error) if @completion_error
342
+ m.security_level = @security_level if @security_level
343
+ m.security_error = if @security_error.is_a?(Exception)
344
+ @security_error.exception(@security_error.message)
345
+ elsif @security_error
346
+ @security_error.dup
347
+ end
126
348
  # @TODO@ What do we do about answerfrom, answersize, etc.?
127
349
  m.header.aa = false # Anything else to do here?
128
350
  # Fix up TTLs!!
129
- offset = (Time.now - @time_stored).to_i
351
+ now = Time.now.to_i
352
+ index = -1
130
353
  m.each_resource {|rr|
354
+ index += 1
131
355
  next if rr.type == Types::OPT
132
- rr.ttl = rr.ttl - offset
356
+ rr.ttl = [[snapshot.record_expirations[index] - now, rr.ttl].min, 0].max
357
+ rr.instance_variable_set(:@dnsruby_received_at, now)
358
+ authenticated = snapshot.authenticated_expirations[index]
359
+ rr.instance_variable_set(:@dnsruby_authenticated_expiration, authenticated) if authenticated
133
360
  }
134
361
  return m
135
362
  end
136
363
  def get_expiration(m)
137
- # Find the minimum ttl of any of the rrsets
138
- min_ttl = 9999999
139
- m.each_section {|section|
140
- section.rrsets.each {|rrset|
141
- if (rrset.ttl < min_ttl)
142
- min_ttl = rrset.ttl
143
- end
144
- }
145
- }
146
- if (min_ttl == 9999999)
147
- return 0
364
+ now = Time.now.to_i
365
+ expirations = []
366
+ soa = Cache.negative_soa(m)
367
+ m.each_resource do |rr|
368
+ next if rr.type == Types::OPT
369
+ received_at = rr.instance_variable_get(:@dnsruby_received_at) || now
370
+ ttl = rr.equal?(soa) ? [rr.ttl, soa.minimum].min : rr.ttl
371
+ expirations << [received_at + ttl,
372
+ rr.instance_variable_get(:@dnsruby_authenticated_expiration),
373
+ m.instance_variable_get(:@dnsruby_authenticated_expiration)].compact.min
148
374
  end
149
- return (Time.now.to_i + min_ttl)
375
+ expirations.min || 0
150
376
  end
151
- def initialize(*args)
152
- @expiration = 0
153
- @time_stored = Time.now.to_i
154
- self.message=(args[0])
377
+ def initialize(message, lifetime: nil, preserve_security: false, error: nil)
378
+ @lifetime = lifetime
379
+ @completion_error = error && [error.class, error.message.dup.freeze].freeze
380
+ if preserve_security
381
+ @security_level = message.security_level
382
+ @security_error = message.security_error
383
+ end
384
+ self.message = message
155
385
  end
156
386
  def to_s
157
387
  return "#{self.message}"
158
388
  end
159
389
  end
160
390
  end
161
- end
391
+ end
@@ -27,6 +27,9 @@ module Dnsruby
27
27
  include Comparable
28
28
 
29
29
  @@arrays = {}
30
+ # Serialize native numeric inserts without holding a lock across
31
+ # application-defined string/hash methods.
32
+ @@lock = Mutex.new
30
33
 
31
34
  attr_accessor :string, :code
32
35
  alias to_code code
@@ -35,12 +38,13 @@ module Dnsruby
35
38
  alias to_s string
36
39
 
37
40
  class Arrays
38
- attr_accessor :strings, :stringsdown, :values, :maxcode
41
+ attr_accessor :strings, :stringsdown, :values, :maxcode, :incremental
39
42
  def initialize
40
43
  @strings = {}
41
44
  @stringsdown = {}
42
45
  @values = {}
43
46
  @maxcode = 0
47
+ @incremental = true
44
48
  end
45
49
  end
46
50
 
@@ -54,10 +58,11 @@ module Dnsruby
54
58
  def CodeMapper.update
55
59
 
56
60
  @@arrays[self] = Arrays.new
57
-
58
61
  constants = self.constants - CodeMapper.constants
59
62
  constants.each do |i|
60
- @@arrays[self].strings.store(i.to_s, const_get(i))
63
+ code = const_get(i)
64
+ @@arrays[self].incremental &&= code.instance_of?(Integer)
65
+ @@arrays[self].strings.store(i.to_s, code)
61
66
  end
62
67
  @@arrays[self].maxcode = constants.length
63
68
  @@arrays[self].values = @@arrays[self].strings.invert
@@ -70,10 +75,32 @@ module Dnsruby
70
75
  # Add new a code to the CodeMapper
71
76
  def CodeMapper.add_pair(string, code)
72
77
  array = @@arrays[self]
73
- array.strings.store(string, code)
74
- array.values=array.strings.invert
78
+ native = string.instance_of?(String) && code.instance_of?(Integer)
79
+ # Use native frozen? semantics even if the caller overrides the method.
80
+ key = Object.instance_method(:frozen?).bind_call(string) ? string : String.new(string).freeze if native
81
+ handled = @@lock.synchronize do
82
+ array.incremental &&= native
83
+ if array.incremental && !array.strings.key?(string) && !array.values.key?(code)
84
+ # Hash stores a frozen copy of mutable String keys. Keep that same
85
+ # immutable name in the reverse index without inverting the table.
86
+ array.strings.store(key, code)
87
+ array.values.store(code, key)
88
+ true
89
+ elsif array.incremental
90
+ # Aliases and reassignment follow Hash#invert's insertion order.
91
+ array.strings.store(string, code)
92
+ array.values = array.strings.invert
93
+ true
94
+ end
95
+ end
96
+ unless handled
97
+ # Keep custom keys/values on the original path: inversion rehashes
98
+ # mutable values, and hash/eql? callbacks must run outside the lock.
99
+ array.strings.store(string, code)
100
+ array.values = array.strings.invert
101
+ end
75
102
  array.stringsdown.store(string.downcase, code)
76
- array.maxcode+=1
103
+ @@lock.synchronize { array.maxcode += 1 }
77
104
  end
78
105
 
79
106
  def unknown_string(arg) #:nodoc: all
@@ -83,7 +110,7 @@ module Dnsruby
83
110
  def unknown_code(arg) #:nodoc: all
84
111
  # Be liberal in what you accept...
85
112
  # raise ArgumentError.new("Code #{arg} not a member of #{self.class}")
86
- Classes.add_pair(arg.to_s, arg)
113
+ self.class.add_pair(arg.to_s, arg)
87
114
  set_code(arg)
88
115
  end
89
116
 
@@ -177,4 +204,4 @@ module Dnsruby
177
204
  end
178
205
 
179
206
  end
180
- end
207
+ end