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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
|
|
4
|
+
# Functional policy and cache contracts; all discovery uses local doubles.
|
|
5
|
+
class ReviewValidationPrimingTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@klass = Dnsruby::Recursor
|
|
8
|
+
@saved = %i[@@hints @@stale_hints @@hints_generation @@nameservers
|
|
9
|
+
@@zones_cache @@zone_expirations @@authority_cache].to_h do |field|
|
|
10
|
+
[field, @klass.class_variable_get(field)]
|
|
11
|
+
end
|
|
12
|
+
%i[@@hints @@zones_cache @@zone_expirations @@authority_cache].each do |field|
|
|
13
|
+
@klass.class_variable_set(field, {})
|
|
14
|
+
end
|
|
15
|
+
@klass.class_variable_set(:@@stale_hints, nil)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def teardown
|
|
19
|
+
@saved.each { |field, value| @klass.class_variable_set(field, value) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_required_verification_errors_survive_provisional_classification
|
|
23
|
+
verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
|
|
24
|
+
message = Dnsruby::Message.new('ordinary.example.')
|
|
25
|
+
expected = Dnsruby::VerifyError.new('Verification service did not complete')
|
|
26
|
+
provisional = ->(_anchor, packet, _name) { packet.security_level = Dnsruby::Message::SecurityLevel.INSECURE; nil }
|
|
27
|
+
# Model the verification service's result independently of wire records.
|
|
28
|
+
message.stub(:rrsets, [:present]) do
|
|
29
|
+
verifier.stub(:find_closest_anchor_for, nil) do
|
|
30
|
+
verifier.stub(:try_to_follow_from_anchor, provisional) do
|
|
31
|
+
verifier.stub(:verify_result, ->(*) { raise expected }) do
|
|
32
|
+
verifier.stub(:discover_signer_keys, false) do
|
|
33
|
+
actual = assert_raises(Dnsruby::VerifyError) { verifier.validate(message, nil) }
|
|
34
|
+
assert_same(expected, actual)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_unsigned_alias_classification_visits_each_required_name
|
|
43
|
+
verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
|
|
44
|
+
message = Dnsruby::Message.new('alias.example.')
|
|
45
|
+
message.add_answer(Dnsruby::RR.create('alias.example. 60 IN CNAME target.other.'))
|
|
46
|
+
message.add_answer(Dnsruby::RR.create('target.other. 60 IN A 192.0.2.1'))
|
|
47
|
+
names = []
|
|
48
|
+
verifier.stub(:discover_insecure_name, ->(name, _klass) { names << name.to_s(true); true }) do
|
|
49
|
+
assert(verifier.validate(message, nil))
|
|
50
|
+
end
|
|
51
|
+
assert_equal(%w[alias.example. target.other.], names.sort)
|
|
52
|
+
assert_equal(Dnsruby::Message::SecurityLevel.INSECURE, message.security_level)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_escaped_dots_are_not_ancestor_boundaries
|
|
56
|
+
@klass.class_variable_set(:@@zones_cache, {'.' => {}, 'example.' => {}, 'b.example.' => {}})
|
|
57
|
+
recursor = @klass.allocate
|
|
58
|
+
assert_equal('example.', recursor.get_closest_known_zone_for('a\\.b.example.'))
|
|
59
|
+
assert_equal('example.', recursor.get_closest_known_zone_for('a\\046b.example.'))
|
|
60
|
+
assert_equal('example.', recursor.get_closest_known_zone_for('www.example'))
|
|
61
|
+
relative = Dnsruby::Name.create('a\\.b.example')
|
|
62
|
+
assert_equal('example.', recursor.get_closest_known_zone_for(relative))
|
|
63
|
+
refute(relative.absolute?)
|
|
64
|
+
assert_equal('.', recursor.get_closest_known_zone_for('other'))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_descendant_deadline_survives_parent_eviction_without_extending_it
|
|
68
|
+
clock = Time.utc(2026, 11, 15)
|
|
69
|
+
Time.stub(:now, -> { clock }) do
|
|
70
|
+
root = {'ns.root.' => addresses(300)}
|
|
71
|
+
@klass.class_variable_set(:@@hints, root)
|
|
72
|
+
@klass.class_variable_set(:@@zones_cache, {'.' => root})
|
|
73
|
+
@klass.class_variable_set(:@@zone_expirations, {'.' => [root, clock + 90]})
|
|
74
|
+
@klass.cache_zone('example', {'ns.example.' => addresses(300)}, 20, parent: '.')
|
|
75
|
+
@klass.cache_zone(Dnsruby::Name.create('child.example'), {'ns.child.example.' => addresses(300)}, 60, parent: 'example')
|
|
76
|
+
@klass.class_variable_get(:@@zones_cache).delete('example.')
|
|
77
|
+
@klass.class_variable_get(:@@zone_expirations).delete('example.')
|
|
78
|
+
recursor = @klass.allocate
|
|
79
|
+
clock += 19
|
|
80
|
+
assert_equal('child.example.', recursor.get_closest_known_zone_for('www.child.example.'))
|
|
81
|
+
clock += 1
|
|
82
|
+
assert_equal('.', recursor.get_closest_known_zone_for('www.child.example.'))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_partial_priming_fills_missing_root_names
|
|
87
|
+
packet = root_packet(ns_ttl: 60, address_ttl: 300)
|
|
88
|
+
packet.add_answer(Dnsruby::RR.create('. 60 IN NS b.root.example.'))
|
|
89
|
+
resolver, lookups = priming_resolver(packet)
|
|
90
|
+
@klass.set_hints([], resolver)
|
|
91
|
+
assert_equal([['b.root.example.', 'A'], ['b.root.example.', 'AAAA']], lookups)
|
|
92
|
+
assert_equal(%w[a.root.example b.root.example], @klass.class_variable_get(:@@hints).keys.sort)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_root_ns_expiration_is_independent_of_address_expiration
|
|
96
|
+
clock = Time.utc(2026, 11, 15)
|
|
97
|
+
Time.stub(:now, -> { clock }) do
|
|
98
|
+
resolver, = priming_resolver(root_packet(ns_ttl: 5, address_ttl: 300))
|
|
99
|
+
@klass.set_hints([], resolver)
|
|
100
|
+
assert(@klass.send(:hints_ready?))
|
|
101
|
+
clock += 5
|
|
102
|
+
refute(@klass.send(:hints_ready?))
|
|
103
|
+
assert_equal(['192.0.2.53'], @klass.class_variable_get(:@@hints).values.first.values)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_zero_ttl_priming_addresses_belong_to_the_current_lookup
|
|
108
|
+
resolver, = priming_resolver(root_packet(ns_ttl: 0, address_ttl: 0))
|
|
109
|
+
@klass.send(:with_limits, resolver) do
|
|
110
|
+
@klass.set_hints([], resolver)
|
|
111
|
+
refute(@klass.send(:hints_ready?))
|
|
112
|
+
generation = @klass.class_variable_get(:@@hints_generation)
|
|
113
|
+
assert_equal(generation, @klass.send(:ensure_hints, resolver))
|
|
114
|
+
zone, authorities = @klass.allocate.get_closest_known_zone_authorities_for('www.example.')
|
|
115
|
+
assert_equal('.', zone)
|
|
116
|
+
assert_equal(['192.0.2.53'], authorities.values.first.current_values)
|
|
117
|
+
assert_empty(@klass.class_variable_get(:@@hints).values.first.values)
|
|
118
|
+
end
|
|
119
|
+
refute(@klass.send(:hints_ready?))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_supported_name_classification_is_memoized_within_one_operation
|
|
123
|
+
verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
|
|
124
|
+
anchor = Struct.new(:klass).new(Dnsruby::Classes.IN)
|
|
125
|
+
calls = 0
|
|
126
|
+
verifier.stub(:find_closest_anchor_for, anchor) do
|
|
127
|
+
verifier.stub(:follow_chain, ->(*) { calls += 1; anchor }) do
|
|
128
|
+
verifier.send(:with_operational_validation, Dnsruby::Message.new('example.')) do
|
|
129
|
+
2.times { refute(verifier.send(:discover_insecure_name, Dnsruby::Name.create('example.'), Dnsruby::Classes.IN)) }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
assert_equal(1, calls)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_root_ns_deadline_starts_before_address_discovery
|
|
137
|
+
clock = Time.utc(2026, 11, 15)
|
|
138
|
+
Time.stub(:now, -> { clock }) do
|
|
139
|
+
packet = root_packet(ns_ttl: 5, address_ttl: 300)
|
|
140
|
+
packet.add_answer(Dnsruby::RR.create('. 5 IN NS b.root.example.'))
|
|
141
|
+
resolver, = priming_resolver(packet) { clock += 3 }
|
|
142
|
+
@klass.set_hints([], resolver)
|
|
143
|
+
refute(@klass.send(:hints_ready?))
|
|
144
|
+
assert_equal(Time.utc(2026, 11, 15) + 5, @klass.class_variable_get(:@@zone_expirations)['.'][1])
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def addresses(ttl)
|
|
151
|
+
@klass::AddressCache.new.tap { |cache| cache.push(['192.0.2.53', ttl]) }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def root_packet(ns_ttl:, address_ttl:)
|
|
155
|
+
packet = Dnsruby::Message.new('.', 'NS')
|
|
156
|
+
packet.add_answer(Dnsruby::RR.create(". #{ns_ttl} IN NS a.root.example."))
|
|
157
|
+
packet.add_additional(Dnsruby::RR.create("a.root.example. #{address_ttl} IN A 192.0.2.53"))
|
|
158
|
+
packet
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def priming_resolver(packet, &before_send)
|
|
162
|
+
lookups = []
|
|
163
|
+
resolver = Struct.new(:query_timeout, :recurse).new(2, false)
|
|
164
|
+
resolver.define_singleton_method(:single_resolvers) { [Object.new] }
|
|
165
|
+
resolver.define_singleton_method(:query_no_validation_or_recursion) { |*| packet }
|
|
166
|
+
resolver.define_singleton_method(:send_async) do |query, queue|
|
|
167
|
+
before_send&.call
|
|
168
|
+
name, type = query.question.first.qname.to_s(true), query.question.first.qtype.to_s
|
|
169
|
+
lookups << [name, type]
|
|
170
|
+
response = Dnsruby::Message.new(name, type)
|
|
171
|
+
value = type == 'A' ? '192.0.2.54' : '2001:db8::54'
|
|
172
|
+
response.add_answer(Dnsruby::RR.create("#{name} 60 IN #{type} #{value}"))
|
|
173
|
+
queue << [lookups.length, response, nil]
|
|
174
|
+
lookups.length
|
|
175
|
+
end
|
|
176
|
+
[resolver, lookups]
|
|
177
|
+
end
|
|
178
|
+
end
|
data/test/tc_rr-opt.rb
CHANGED
|
@@ -97,7 +97,9 @@ class TestRrOpt < Minitest::Test
|
|
|
97
97
|
received_query = s[0]
|
|
98
98
|
socket.connect(s[1][2], s[1][1])
|
|
99
99
|
q.push(Message.decode(received_query))
|
|
100
|
-
|
|
100
|
+
response = Message.decode(received_query)
|
|
101
|
+
response.header.qr = true
|
|
102
|
+
socket.send(response.encode,0)
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
# Now send query
|
|
@@ -168,12 +170,13 @@ class TestRrOpt < Minitest::Test
|
|
|
168
170
|
received_query = s[0]
|
|
169
171
|
m = Message.decode(received_query)
|
|
170
172
|
q.push(m)
|
|
173
|
+
m.header.qr = true
|
|
171
174
|
if (m.header.arcount > 0)
|
|
172
175
|
# send back FORMERR
|
|
173
176
|
m.header.rcode = RCode.FORMERR
|
|
174
177
|
socket.send(m.encode,0,s[1][2], s[1][1])
|
|
175
178
|
else
|
|
176
|
-
socket.send(
|
|
179
|
+
socket.send(m.encode,0,s[1][2], s[1][1])
|
|
177
180
|
end
|
|
178
181
|
}
|
|
179
182
|
|
data/test/tc_rr-unknown.rb
CHANGED
|
@@ -99,4 +99,23 @@ dd ee c0 11 00 02 00 01 00 00 03 84 00 05 02 6e
|
|
|
99
99
|
'Packet read from a packet dumped by bind...'
|
|
100
100
|
)
|
|
101
101
|
end
|
|
102
|
+
|
|
103
|
+
def test_empty_rdata_round_trips_through_rfc3597_presentation
|
|
104
|
+
%w[IN CH CLASS65000].each do |klass|
|
|
105
|
+
rr = RR.create("empty.example. 300 #{klass} TYPE65001 \\# 0")
|
|
106
|
+
assert_equal('', rr.data)
|
|
107
|
+
assert_equal(rr, RR.create(rr.to_s))
|
|
108
|
+
assert_equal(rr, rr.clone)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_malformed_generic_presentation_raises_decode_error
|
|
113
|
+
['\\#', '\\# -1', '\\# 1', '\\# 0 ff', '\\# 1 zz', '\\# 1 f', '\\# 65536'].each do |rdata|
|
|
114
|
+
%w[TYPE65001 TYPE64 A].each do |type|
|
|
115
|
+
assert_raises(DecodeError, "#{type} #{rdata}") do
|
|
116
|
+
RR.create("example. 300 IN #{type} #{rdata}")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
102
121
|
end
|
data/test/tc_rr.rb
CHANGED
|
@@ -376,6 +376,20 @@ class TestRR < Minitest::Test
|
|
|
376
376
|
assert_equal a1.hash, a3.hash
|
|
377
377
|
end
|
|
378
378
|
|
|
379
|
+
# RR.create has long accepted incomplete grouping around otherwise complete
|
|
380
|
+
# RDATA (including the TLSA certificate fixtures). A zone file must balance
|
|
381
|
+
# its continuation lines, but this standalone constructor remains permissive.
|
|
382
|
+
def test_unbalanced_grouping_in_standalone_record_strings
|
|
383
|
+
prefix = '_443._tcp.example. 60 IN TLSA 3 0 1 '
|
|
384
|
+
digest = 'aa' * 32
|
|
385
|
+
expected = RR.create(prefix + digest)
|
|
386
|
+
["( #{digest}", "#{digest} )", "( #{digest} )"].each do |rdata|
|
|
387
|
+
rr = RR.create(prefix + rdata)
|
|
388
|
+
assert_equal(expected, rr, rdata)
|
|
389
|
+
assert_equal([digest].pack('H*'), rr.databin, rdata)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
379
393
|
def _test_duplicate_answer(method_as_symbol)
|
|
380
394
|
expected_count = case method_as_symbol
|
|
381
395
|
when :add_answer
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
|
|
3
|
+
class RRSetGroupingTest < Minitest::Test
|
|
4
|
+
def test_grouping_preserves_order_class_and_signature_membership
|
|
5
|
+
signature = Dnsruby::RR.create('Host.Example. 300 IN RRSIG A 8 2 300 20300101000000 20200101000000 123 example. AQID')
|
|
6
|
+
records = [signature,
|
|
7
|
+
Dnsruby::RR.create('other.example. 300 IN A 192.0.2.2'),
|
|
8
|
+
Dnsruby::RR.create('host.example. 200 IN A 192.0.2.1'),
|
|
9
|
+
Dnsruby::RR.create('host.example. 100 IN A 192.0.2.3'),
|
|
10
|
+
Dnsruby::RR.create('host.example. 300 CH TXT "other class"')]
|
|
11
|
+
section = Dnsruby::Section.new.concat(records)
|
|
12
|
+
sets = section.rrsets
|
|
13
|
+
assert_equal([Dnsruby::Types::A, Dnsruby::Types::A, Dnsruby::Types::TXT], sets.map(&:type).map(&:code))
|
|
14
|
+
assert_equal(1, sets.first.num_sigs)
|
|
15
|
+
assert_equal(2, sets.first.rrs.length)
|
|
16
|
+
assert_equal([100, 100], sets.first.rrs.map(&:ttl))
|
|
17
|
+
assert_equal([200, 100], records[2..3].map(&:ttl), 'Grouping must not mutate source records')
|
|
18
|
+
assert_equal(1, section.rrsets('RRSIG').length)
|
|
19
|
+
assert_equal(2, section.rrsets('A').length)
|
|
20
|
+
assert_equal(0, section.rrsets('AAAA').length)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_grouping_copies_each_record_once
|
|
24
|
+
records = Array.new(40) { |i| Dnsruby::RR.create("host#{i}.example. 300 IN A 192.0.2.1") }
|
|
25
|
+
copies = 0
|
|
26
|
+
records.each do |record|
|
|
27
|
+
original = record.method(:clone)
|
|
28
|
+
record.define_singleton_method(:clone) { copies += 1; original.call }
|
|
29
|
+
end
|
|
30
|
+
sets = Dnsruby::Section.new.concat(records).rrsets
|
|
31
|
+
assert_equal(40, sets.length)
|
|
32
|
+
assert_equal(40, copies)
|
|
33
|
+
assert_equal(records.map(&:name), sets.map(&:name))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_grouping_observes_validation_work_limits
|
|
37
|
+
section = Dnsruby::Section.new.concat(Array.new(3) { |i| Dnsruby::RR.create("host#{i}.example. 300 IN A 192.0.2.1") })
|
|
38
|
+
limits = Dnsruby::ValidationBudget::DEFAULT_LIMITS.merge(max_work: 2)
|
|
39
|
+
budget = Dnsruby::ValidationBudget.new(limits: limits)
|
|
40
|
+
assert_raises(Dnsruby::ValidationLimitError) { Dnsruby::ValidationBudget.with(budget) { section.rrsets } }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_rejected_rrset_add_does_not_clone_the_record
|
|
44
|
+
set = Dnsruby::RRSet.new(Dnsruby::RR.create('one.example. 300 IN A 192.0.2.1'))
|
|
45
|
+
other = Dnsruby::RR.create('two.example. 300 IN A 192.0.2.2')
|
|
46
|
+
other.define_singleton_method(:clone) { raise 'Unexpected clone' }
|
|
47
|
+
refute(set.add(other))
|
|
48
|
+
assert_equal(1, set.length)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
|
|
3
|
+
class RRSetReviewFixesTest < Minitest::Test
|
|
4
|
+
def record(address = '192.0.2.1')
|
|
5
|
+
Dnsruby::RR.create("host.example. 300 IN A #{address}")
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def signature
|
|
9
|
+
Dnsruby::RR.create('host.example. 300 IN RRSIG A 8 2 300 20300101000000 20200101000000 123 example. AQID')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_add_set_and_delete_signature_preserve_membership
|
|
13
|
+
source = Dnsruby::RRSet.new([record, signature])
|
|
14
|
+
target = Dnsruby::RRSet.new(record('192.0.2.2'))
|
|
15
|
+
assert(target.add(source))
|
|
16
|
+
assert_equal(3, target.length)
|
|
17
|
+
assert_equal(1, target.num_sigs)
|
|
18
|
+
assert_equal(2, target.rrs.length)
|
|
19
|
+
assert(target.delete(target.sigs.first))
|
|
20
|
+
assert_equal(0, target.num_sigs)
|
|
21
|
+
assert_empty(target.sigs)
|
|
22
|
+
assert_equal(2, target.rrs.length)
|
|
23
|
+
refute(target.add(Dnsruby::RRSet.new))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_bulk_membership_index_is_discarded_before_records_escape
|
|
27
|
+
original = record
|
|
28
|
+
set = Dnsruby::RRSet.new([original, record('192.0.2.2'), original])
|
|
29
|
+
assert_equal(2, set.length)
|
|
30
|
+
assert_nil(set.instance_variable_get(:@membership_index))
|
|
31
|
+
set[0].address = Dnsruby::IPv4.create('192.0.2.3')
|
|
32
|
+
assert(set.add(record))
|
|
33
|
+
assert_equal(3, set.length)
|
|
34
|
+
assert(set.add(record('192.0.2.3')))
|
|
35
|
+
assert_equal(3, set.length)
|
|
36
|
+
assert_equal('192.0.2.1', original.address.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_add_set_without_cloning_keeps_the_requested_aliasing
|
|
40
|
+
source = Dnsruby::RRSet.new(record)
|
|
41
|
+
target = Dnsruby::RRSet.new
|
|
42
|
+
target.add(source, false)
|
|
43
|
+
assert_same(source[0], target[0])
|
|
44
|
+
source[0].address = Dnsruby::IPv4.create('192.0.2.2')
|
|
45
|
+
target.add(record)
|
|
46
|
+
assert_equal(2, target.length)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_bulk_grouping_deduplicates_and_normalizes_ttls
|
|
50
|
+
records = [record, record('192.0.2.2'), record]
|
|
51
|
+
records.last.ttl = 100
|
|
52
|
+
sets = Dnsruby::Section.new.concat(records).rrsets
|
|
53
|
+
assert_equal(1, sets.length)
|
|
54
|
+
assert_equal(2, sets.first.length)
|
|
55
|
+
assert_equal([100, 100], sets.first.rrs.map(&:ttl))
|
|
56
|
+
assert_equal([300, 300, 100], records.map(&:ttl))
|
|
57
|
+
end
|
|
58
|
+
end
|
data/test/tc_rrsig.rb
CHANGED
|
@@ -50,8 +50,28 @@ class RrsigTest < Minitest::Test
|
|
|
50
50
|
assert(rr.type_covered == Dnsruby::Types.TYPE731)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def test_type_covered_rejects_out_of_range_before_mapping_or_wire_truncation
|
|
54
|
+
table = Dnsruby::CodeMapper.class_variable_get(:@@arrays).fetch(Dnsruby::Types)
|
|
55
|
+
before = [table.strings.dup, table.stringsdown.dup, table.values.dup, table.maxcode]
|
|
56
|
+
[65_536, 65_538, 2**128].each do |code|
|
|
57
|
+
text = "x. 60 IN RRSIG TYPE#{code} 8 1 60 20300101000000 20200101000000 12345 x. AA=="
|
|
58
|
+
assert_raises(Dnsruby::DecodeError) { Dnsruby::RR.create(text) }
|
|
59
|
+
assert_raises(ArgumentError) { Dnsruby::ZoneReader.new('x.').process_line(text) }
|
|
60
|
+
end
|
|
61
|
+
assert_equal(before, [table.strings, table.stringsdown, table.values, table.maxcode])
|
|
62
|
+
record = Dnsruby::RR.create(INPUT)
|
|
63
|
+
[-1, 65_536].each do |code|
|
|
64
|
+
assert_raises(Dnsruby::DecodeError) { record.type_covered = code }
|
|
65
|
+
assert_equal(Dnsruby::Types.A, record.type_covered)
|
|
66
|
+
end
|
|
67
|
+
record.type_covered = 'TYPE65535'
|
|
68
|
+
message = Dnsruby::Message.new
|
|
69
|
+
message.add_answer(record)
|
|
70
|
+
assert_equal(65_535, Dnsruby::Message.decode(message.encode).answer.first.type_covered.code)
|
|
71
|
+
end
|
|
72
|
+
|
|
53
73
|
def test_string_with_comments
|
|
54
74
|
r = Dnsruby::RR.create("tjeb.nl. 3600 IN RRSIG NSEC3PARAM 7 2 3600 20090630164649 20090602164649 53177 tjeb.nl. Fw70WQMviRFGyeze3MUpfafaAcWIvHRpnq4ZK3lxexrR1p+rLxK5C4qVKU71XYrPYR7XEBxgUG1oyKNOhFOVyx31EjC462dz7Vxn6UDpD1LIwNnD28+oHfS9AFzGKcn4zUZqT+8IvOO1jiS9c3Y8WAkOloN9AwGIIKWU8zAp1n4= ;{id = 53177}")
|
|
55
75
|
assert_equal("Fw70WQMviRFGyeze3MUpfafaAcWIvHRpnq4ZK3lxexrR1p+rLxK5C4qVKU71XYrPYR7XEBxgUG1oyKNOhFOVyx31EjC462dz7Vxn6UDpD1LIwNnD28+oHfS9AFzGKcn4zUZqT+8IvOO1jiS9c3Y8WAkOloN9AwGIIKWU8zAp1n4=", ([r.signature].pack("m*")).gsub(/\n/,"").chomp)
|
|
56
76
|
end
|
|
57
|
-
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
5
|
+
class TestScopedIPv6 < Minitest::Test
|
|
6
|
+
SCOPED_SERVERS = %w[
|
|
7
|
+
fe80::feed:face:c0ff:ee00%en0
|
|
8
|
+
fe80::5054:ff:fe8b:6576%br0
|
|
9
|
+
fe80::1%3
|
|
10
|
+
FE80:0:0:0:0:0:0:ABCD%Eth0
|
|
11
|
+
fe80::1%eth0.100
|
|
12
|
+
fe80::1%veth-1
|
|
13
|
+
fe80::1%interface_1
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
def without_hostname_lookup
|
|
17
|
+
unexpected = ->(*) { flunk('A scoped literal must not trigger DNS resolution') }
|
|
18
|
+
Dnsruby::Resolver.stub(:new, unexpected) do
|
|
19
|
+
Dnsruby::Recursor.stub(:new, unexpected) { yield }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_scoped_nameservers_are_validated_without_dns_and_preserved
|
|
24
|
+
without_hostname_lookup do
|
|
25
|
+
SCOPED_SERVERS.each do |server|
|
|
26
|
+
assert_equal(server, Dnsruby::Config.resolve_server(server))
|
|
27
|
+
config = Dnsruby::Config.new
|
|
28
|
+
config.nameserver = [server]
|
|
29
|
+
assert_equal([server], config.nameserver)
|
|
30
|
+
assert_equal(Dnsruby::IPv6.create(server.split('%').first),
|
|
31
|
+
Dnsruby::Config.ipv6_server_address(server))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_explicit_ipv6_endpoint_validation_rejects_malformed_literals
|
|
37
|
+
invalid = [
|
|
38
|
+
'fe80::1%', 'fe80::1%en0%en1', 'fe80::1%en 0',
|
|
39
|
+
"fe80::1%en0\n", "fe80::1%en0\0", 'fe80::1%en0/64',
|
|
40
|
+
'fe80::1/64%en0', '[fe80::1%en0]', '192.0.2.1%en0',
|
|
41
|
+
'example.test%en0', '%en0', 'fe80::gggg%en0',
|
|
42
|
+
'1:2:3:4:5:6:7::8%en0', '1:2:3:4:5:6::192.0.2.1%3',
|
|
43
|
+
]
|
|
44
|
+
without_hostname_lookup do
|
|
45
|
+
invalid.each do |server|
|
|
46
|
+
assert_raises(ArgumentError, server) { Dnsruby::Config.ipv6_server_address(server) }
|
|
47
|
+
refute(Dnsruby::Config.scoped_ipv6_server?(server), server)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_percent_containing_dns_names_remain_nameserver_names
|
|
53
|
+
queried = []
|
|
54
|
+
lookup = Object.new
|
|
55
|
+
lookup.define_singleton_method(:query) do |name|
|
|
56
|
+
queried << name.to_s
|
|
57
|
+
response = Dnsruby::Message.new(name)
|
|
58
|
+
response.add_answer(Dnsruby::RR.create(name: name, type: 'A', address: '192.0.2.53'))
|
|
59
|
+
Dnsruby::Message.decode(response.encode)
|
|
60
|
+
end
|
|
61
|
+
lookup.define_singleton_method(:close) { }
|
|
62
|
+
Dnsruby::Config.stub(:nameserver_resolver, lookup) do
|
|
63
|
+
['ns%blue.example', 'ns:blue%en0.example', 'example.test%en0', '%en0',
|
|
64
|
+
'192.0.2.1%en0', 'fe80::1%', 'fe80::1%en0/64', 'fe80::gggg%en0'].each do |name|
|
|
65
|
+
[name, Dnsruby::Name.create(name)].each do |input|
|
|
66
|
+
config = Dnsruby::Config.new
|
|
67
|
+
config.nameserver = [input]
|
|
68
|
+
assert_equal([input], config.nameserver)
|
|
69
|
+
assert_equal('192.0.2.53', Dnsruby::Config.resolve_server(input))
|
|
70
|
+
assert_equal(name, queried.last)
|
|
71
|
+
resolver = Dnsruby::Resolver.new(nameserver: [input])
|
|
72
|
+
assert_equal(['192.0.2.53'], resolver.single_resolvers.map(&:server))
|
|
73
|
+
resolver.close
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_resolver_constructor_forms_keep_scoped_nameserver
|
|
80
|
+
server = SCOPED_SERVERS.first
|
|
81
|
+
[server, {nameserver: server}, {nameserver: [server, '192.0.2.1']}].each do |options|
|
|
82
|
+
resolver = Dnsruby::Resolver.new(options)
|
|
83
|
+
expected = options.is_a?(Hash) ? Array(options[:nameserver]) : [server]
|
|
84
|
+
assert_equal(expected, resolver.single_resolvers.map(&:server))
|
|
85
|
+
ensure
|
|
86
|
+
resolver.close if resolver
|
|
87
|
+
end
|
|
88
|
+
single = Dnsruby::SingleResolver.new(server)
|
|
89
|
+
assert_equal(server, single.server)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_resolv_conf_retains_scoped_server_and_ipv4_fallback
|
|
93
|
+
Tempfile.create('dnsruby-scoped-resolv') do |file|
|
|
94
|
+
file.write("nameserver #{SCOPED_SERVERS.first}\nnameserver 192.168.0.1\n")
|
|
95
|
+
file.flush
|
|
96
|
+
resolver = Dnsruby::Resolver.new(config_info: file.path)
|
|
97
|
+
expected = [SCOPED_SERVERS.first, '192.168.0.1']
|
|
98
|
+
assert_equal(expected, resolver.config.nameserver)
|
|
99
|
+
assert_equal(expected, resolver.single_resolvers.map(&:server))
|
|
100
|
+
ensure
|
|
101
|
+
resolver.close if resolver
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_server_setter_switches_address_family
|
|
106
|
+
sender = Dnsruby::PacketSender.new('192.0.2.1')
|
|
107
|
+
refute(sender.instance_variable_get(:@ipv6))
|
|
108
|
+
SCOPED_SERVERS.each do |server|
|
|
109
|
+
sender.server = server
|
|
110
|
+
assert_equal(server, sender.server)
|
|
111
|
+
assert(sender.instance_variable_get(:@ipv6))
|
|
112
|
+
end
|
|
113
|
+
sender.server = '192.0.2.2'
|
|
114
|
+
refute(sender.instance_variable_get(:@ipv6))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_unscoped_addresses_remain_supported
|
|
118
|
+
['192.0.2.1', '2001:db8::1', 'fe80::1', '::1'].each do |server|
|
|
119
|
+
assert_equal(server, Dnsruby::Config.resolve_server(server))
|
|
120
|
+
end
|
|
121
|
+
[Dnsruby::IPv4.create('192.0.2.1'), Dnsruby::IPv6.create('2001:db8::1')].each do |server|
|
|
122
|
+
assert_same(server, Dnsruby::Config.resolve_server(server))
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_config_accepts_mixed_plain_and_scoped_literals_without_dns
|
|
127
|
+
servers = ['192.0.2.1', '2001:DB8:0:0:0:0:0:53', '::1',
|
|
128
|
+
'::ffff:192.0.2.53', SCOPED_SERVERS.first, 'fe80::1%3']
|
|
129
|
+
without_hostname_lookup do
|
|
130
|
+
config = Dnsruby::Config.new
|
|
131
|
+
config.nameserver = servers
|
|
132
|
+
assert_equal(servers, config.nameserver)
|
|
133
|
+
Tempfile.create('dnsruby-mixed-ip-resolv') do |file|
|
|
134
|
+
servers.each { |server| file.puts("nameserver #{server}") }
|
|
135
|
+
file.flush
|
|
136
|
+
assert_equal(servers, Dnsruby::Config.parse_resolv_conf(file.path)[:nameserver])
|
|
137
|
+
config = Dnsruby::Config.new
|
|
138
|
+
config.set_config_info(file.path)
|
|
139
|
+
assert_equal(servers, config.nameserver)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_scope_does_not_become_dns_record_data
|
|
145
|
+
assert_raises(ArgumentError) { Dnsruby::IPv6.create('fe80::1%en0') }
|
|
146
|
+
assert_raises(ArgumentError) { Dnsruby::RR.create('host.test. 60 IN AAAA fe80::1%en0') }
|
|
147
|
+
message = Dnsruby::Message.new
|
|
148
|
+
message.add_answer(Dnsruby::RR.create('host.test. 60 IN AAAA fe80::1'))
|
|
149
|
+
answer = Dnsruby::Message.decode(message.encode).answer.first
|
|
150
|
+
assert_equal('FE80::1', answer.address.to_s)
|
|
151
|
+
assert_equal(16, answer.address.address.bytesize)
|
|
152
|
+
end
|
|
153
|
+
end
|