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.
- 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 +302 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/demo/rubydig.rb +0 -2
- data/dnsruby.gemspec +5 -4
- data/lib/dnsruby/cache.rb +279 -47
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +75 -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 +645 -203
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +795 -339
- data/lib/dnsruby/resolver.rb +468 -186
- 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 +19 -0
- 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/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 +979 -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 +42 -20
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +21 -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 +105 -10
- data/.coveralls.yml +0 -2
data/lib/dnsruby/key_cache.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
160
|
+
@keys.values.map(&:dup)
|
|
90
161
|
end
|
|
91
162
|
def remove_expired_keys
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
+
return_index ||= @index
|
|
165
|
+
path << [offset, label_index, @index]
|
|
133
166
|
@index = idx
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
179
|
+
raise DecodeError, 'Reserved or invalid DNS label type'
|
|
141
180
|
end
|
|
142
181
|
end
|
|
143
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
28
|
+
data_start = @data.bytesize
|
|
29
29
|
yield
|
|
30
|
-
data_end = @data.
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 =
|
|
64
|
-
if
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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,
|
|
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
|