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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +314 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/dnsruby.gemspec +6 -4
- data/lib/dnsruby/cache.rb +279 -49
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +77 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +664 -207
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +798 -341
- data/lib/dnsruby/resolver.rb +472 -176
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +21 -2
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/OPT.rb +2 -3
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +986 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +53 -2
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +34 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +127 -14
- 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
|
-
|
|
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
|
-
|
|
60
|
+
@mutex.synchronize { @cache.length }
|
|
47
61
|
end
|
|
48
62
|
def Cache.max_size=(length)
|
|
49
63
|
@@max_size = length
|
|
50
64
|
end
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
m
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
375
|
+
expirations.min || 0
|
|
150
376
|
end
|
|
151
|
-
def initialize(
|
|
152
|
-
@
|
|
153
|
-
@
|
|
154
|
-
|
|
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
|
data/lib/dnsruby/code_mapper.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|