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
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,36 +48,87 @@ 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)
124
+ def find(qname, qtype, qclass = Classes.IN, scope: nil)
71
125
  qn = Name.create(qname)
72
126
  qn.absolute = true
73
127
  key = CacheKey.new(qn, qtype, qclass).to_s
74
- @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]
75
132
  data = @cache[key]
76
133
  if (!data)
77
134
  return nil
@@ -81,10 +138,18 @@ module Dnsruby
81
138
  TheLog.debug("CACHE lookup stale\n")
82
139
  return nil
83
140
  end
84
- m = data.message
85
- TheLog.debug("CACHE found\n")
86
- return m
141
+ data
87
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
88
153
  end
89
154
  def Cache.delete(qname, qtype, qclass = Classes.IN)
90
155
  key = CacheKey.new(qname, qtype, qclass)
@@ -92,6 +157,132 @@ module Dnsruby
92
157
  @cache.delete(key)
93
158
  }
94
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
+
95
286
  class CacheKey # :nodoc: all
96
287
  attr_accessor :qname, :qtype, :qclass
97
288
  def initialize(*args)
@@ -108,52 +299,93 @@ module Dnsruby
108
299
  end
109
300
  end
110
301
  def to_s
111
- 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}"
112
306
  end
113
307
  end
114
308
  class CacheData # :nodoc: all
115
- 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
+
116
316
  def message=(m)
117
- @expiration = get_expiration(m)
118
- @message = Message.decode(m.encode(true))
119
- @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
120
335
  end
121
336
  def message
122
- m = Message.decode(@message.encode)
337
+ snapshot = @snapshot
338
+ m = Message.decode(snapshot.wire)
123
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
124
348
  # @TODO@ What do we do about answerfrom, answersize, etc.?
125
349
  m.header.aa = false # Anything else to do here?
126
350
  # Fix up TTLs!!
127
- offset = (Time.now - @time_stored).to_i
351
+ now = Time.now.to_i
352
+ index = -1
128
353
  m.each_resource {|rr|
354
+ index += 1
129
355
  next if rr.type == Types::OPT
130
- 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
131
360
  }
132
361
  return m
133
362
  end
134
363
  def get_expiration(m)
135
- # Find the minimum ttl of any of the rrsets
136
- min_ttl = 9999999
137
- m.each_section {|section|
138
- section.rrsets.each {|rrset|
139
- if (rrset.ttl < min_ttl)
140
- min_ttl = rrset.ttl
141
- end
142
- }
143
- }
144
- if (min_ttl == 9999999)
145
- 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
146
374
  end
147
- return (Time.now.to_i + min_ttl)
375
+ expirations.min || 0
148
376
  end
149
- def initialize(*args)
150
- @expiration = 0
151
- @time_stored = Time.now.to_i
152
- 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
153
385
  end
154
386
  def to_s
155
387
  return "#{self.message}"
156
388
  end
157
389
  end
158
390
  end
159
- 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