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/test/spec_helper.rb
CHANGED
|
@@ -5,27 +5,22 @@ if Warning.respond_to?(:[]=)
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
if ENV['RUN_EXTRA_TASK'] == 'TRUE'
|
|
8
|
-
unless "test".frozen?
|
|
9
|
-
# Coverall setup term-ansi-color which isn't yet frozen string literal compatible
|
|
10
|
-
# Ref: https://github.com/flori/term-ansicolor/pull/38
|
|
11
|
-
require 'coveralls'
|
|
12
|
-
Coveralls.wear!
|
|
13
|
-
end
|
|
14
|
-
|
|
15
8
|
require 'simplecov'
|
|
16
9
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# end
|
|
10
|
+
# Generate reports locally; CI uploads coverage after the tests complete.
|
|
11
|
+
SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
|
|
12
|
+
SimpleCov.start do
|
|
13
|
+
add_filter '/test/'
|
|
14
|
+
end
|
|
23
15
|
end
|
|
24
16
|
|
|
25
17
|
require 'minitest'
|
|
26
18
|
require 'minitest/autorun'
|
|
27
19
|
require 'minitest/display'
|
|
28
20
|
|
|
21
|
+
# CI prints the active test and fails with thread backtraces if it stalls.
|
|
22
|
+
require_relative 'ci_watchdog' if ENV.key?('DNSRUBY_TEST_TIMEOUT')
|
|
23
|
+
|
|
29
24
|
Minitest::Display.options = {
|
|
30
25
|
suite_names: true,
|
|
31
26
|
color: true,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require_relative 'tc_pipeline_send'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
|
|
4
|
+
class AttemptConfigurationTest < Minitest::Test
|
|
5
|
+
def in_memory(code)
|
|
6
|
+
out, err, status = Open3.capture3(RbConfig.ruby, '-Ilib', '-e', TestPipelineSend::FIXTURE + code,
|
|
7
|
+
chdir: File.expand_path('..', __dir__))
|
|
8
|
+
assert(status.success?, "#{out}\n#{err}")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_attempt_configuration_survives_tcp_connect_reconfiguration
|
|
12
|
+
in_memory(<<~'RUBY')
|
|
13
|
+
[false, true].each do |pipeline|
|
|
14
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1', port: 5301,
|
|
15
|
+
use_tcp: true, tcp_pipelining: pipeline, tcp_pipelining_max_queries: :infinite,
|
|
16
|
+
tcp_idle_timeout: 10, dnssec: false)
|
|
17
|
+
writer = resolver.single_resolvers.first
|
|
18
|
+
connections, sockets = [], []
|
|
19
|
+
writer.define_singleton_method(:connect_tcp) do |server, port, source, source_port, *_|
|
|
20
|
+
connections << [server, port, source, source_port]
|
|
21
|
+
if connections.length == 1
|
|
22
|
+
resolver.port = 5302
|
|
23
|
+
resolver.src_address = '127.0.0.2'
|
|
24
|
+
resolver.src_port = 55001
|
|
25
|
+
resolver.ignore_truncation = true
|
|
26
|
+
resolver.no_tcp = true
|
|
27
|
+
resolver.tcp_idle_timeout = 20
|
|
28
|
+
resolver.tcp_pipelining_max_queries = 3
|
|
29
|
+
end
|
|
30
|
+
FrameSocket.new.tap do |socket|
|
|
31
|
+
socket.define_singleton_method(:connect) { |_| raise Errno::EISCONN }
|
|
32
|
+
sockets << socket
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
message = query
|
|
36
|
+
writer.send_async(message, Queue.new, :first)
|
|
37
|
+
first = SELECTOR.query_settings_for_id(:first)
|
|
38
|
+
raise 'connected endpoint changed' unless connections.first[0, 3] == ['127.0.0.1', 5301, '0.0.0.0']
|
|
39
|
+
raise 'registered peer changed' unless [first.dest_server, first.dest_port] == connections.first[0, 2]
|
|
40
|
+
raise 'response policy changed' if first.ignore_truncation || first.no_tcp
|
|
41
|
+
raise 'idle policy changed' unless first.tcp_idle_timeout == 10
|
|
42
|
+
raise 'cache route changed' unless first.cache_scope[0, 3] == ['127.0.0.1', 5301, '0.0.0.0']
|
|
43
|
+
raise 'mutable attempt configuration' unless first.transport_configuration.frozen? && first.transport_configuration[:src_port].frozen?
|
|
44
|
+
if pipeline
|
|
45
|
+
raise 'pipeline capacity changed' unless first.tcp_pipelining_max_queries == :infinite
|
|
46
|
+
end
|
|
47
|
+
writer.send_async(query('next.example.'), Queue.new, :next)
|
|
48
|
+
following = SELECTOR.query_settings_for_id(:next)
|
|
49
|
+
raise 'later send missed new settings' unless connections.last == ['127.0.0.1', 5302, '127.0.0.2', 55001]
|
|
50
|
+
raise 'later policy missed new settings' unless following.ignore_truncation && following.no_tcp && following.tcp_idle_timeout == 20
|
|
51
|
+
SELECTOR.remove_id(:first)
|
|
52
|
+
SELECTOR.remove_id(:next)
|
|
53
|
+
resolver.close
|
|
54
|
+
sockets.each { |socket| assert_clean(socket) }
|
|
55
|
+
end
|
|
56
|
+
RUBY
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_attempt_configuration_survives_udp_bind_reconfiguration
|
|
60
|
+
in_memory(<<~'RUBY')
|
|
61
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', port: 5301, src_port: 55000, dnssec: false)
|
|
62
|
+
socket = FrameSocket.new
|
|
63
|
+
connected = nil
|
|
64
|
+
socket.define_singleton_method(:getsockopt) { |*| Struct.new(:data).new([Socket::SOCK_DGRAM].pack('i')) }
|
|
65
|
+
socket.define_singleton_method(:bind) do |address, port|
|
|
66
|
+
raise 'wrong source' unless [address, port] == ['0.0.0.0', 55000]
|
|
67
|
+
writer.server = '127.0.0.2'
|
|
68
|
+
writer.port = 5302
|
|
69
|
+
end
|
|
70
|
+
socket.define_singleton_method(:connect) { |*endpoint| connected = endpoint }
|
|
71
|
+
UDPSocket.define_singleton_method(:new) { |*| socket }
|
|
72
|
+
writer.send_async(query, Queue.new, :udp)
|
|
73
|
+
pending = SELECTOR.query_settings_for_id(:udp)
|
|
74
|
+
raise 'UDP connected to new endpoint' unless connected == ['127.0.0.1', 5301]
|
|
75
|
+
raise 'UDP registration changed endpoint' unless [pending.dest_server, pending.dest_port] == connected
|
|
76
|
+
SELECTOR.remove_id(:udp)
|
|
77
|
+
writer.close
|
|
78
|
+
assert_clean(socket)
|
|
79
|
+
RUBY
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_attempt_configuration_preserves_truncation_policy_until_response
|
|
83
|
+
in_memory(<<~'RUBY')
|
|
84
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', no_tcp: true, dnssec: false)
|
|
85
|
+
socket = FrameSocket.new
|
|
86
|
+
socket.define_singleton_method(:bind) { |*| }
|
|
87
|
+
socket.define_singleton_method(:connect) { |*| }
|
|
88
|
+
socket.define_singleton_method(:getsockopt) { |*| Struct.new(:data).new([Socket::SOCK_DGRAM].pack('i')) }
|
|
89
|
+
UDPSocket.define_singleton_method(:new) { |*| socket }
|
|
90
|
+
message = query
|
|
91
|
+
queue = Queue.new
|
|
92
|
+
writer.send_async(message, queue, :policy)
|
|
93
|
+
pending = SELECTOR.query_settings_for_id(:policy)
|
|
94
|
+
writer.no_tcp = false
|
|
95
|
+
writer.define_singleton_method(:send_async) { |*| raise 'response used changed no_tcp policy' }
|
|
96
|
+
response = message.clone
|
|
97
|
+
response.header.qr = response.header.tc = true
|
|
98
|
+
Thread.current[:dnsruby_response_query] = pending
|
|
99
|
+
raise 'ordinary truncated response rejected' unless writer.check_response(response, response.encode, message, queue, :policy, false)
|
|
100
|
+
Thread.current[:dnsruby_response_query] = nil
|
|
101
|
+
SELECTOR.remove_id(:policy)
|
|
102
|
+
writer.close
|
|
103
|
+
assert_clean(socket)
|
|
104
|
+
RUBY
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_attempt_configuration_explicit_ports_replace_default_random_pool
|
|
108
|
+
[Dnsruby::Resolver, Dnsruby::SingleResolver, Dnsruby::PacketSender].each do |klass|
|
|
109
|
+
resolver = klass.new(server: '127.0.0.1')
|
|
110
|
+
resolver.add_src_port(55000)
|
|
111
|
+
assert_equal(55000, resolver.src_port, klass)
|
|
112
|
+
resolver.add_src_port(55001..55002)
|
|
113
|
+
assert_equal([55000, 55001, 55002], resolver.src_port, klass)
|
|
114
|
+
[[0, 55003], [55003, 0]].each do |ports|
|
|
115
|
+
assert_raises(ArgumentError) { resolver.src_port = ports }
|
|
116
|
+
assert_equal([55000, 55001, 55002], resolver.src_port, klass)
|
|
117
|
+
end
|
|
118
|
+
assert_raises(ArgumentError) { resolver.add_src_port(0) }
|
|
119
|
+
assert_equal([55000, 55001, 55002], resolver.src_port, klass)
|
|
120
|
+
if resolver.respond_to?(:single_resolvers)
|
|
121
|
+
assert_equal(resolver.src_port, resolver.single_resolvers.first.src_port)
|
|
122
|
+
end
|
|
123
|
+
resolver.close
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_attempt_configuration_disabled_cache_skips_response_classification
|
|
128
|
+
request = Dnsruby::Message.new('cache.example.')
|
|
129
|
+
request.do_caching = false
|
|
130
|
+
response = request.clone
|
|
131
|
+
response.header.qr = true
|
|
132
|
+
response.add_answer(Dnsruby::RR.create('cache.example. 60 IN A 192.0.2.1'))
|
|
133
|
+
Dnsruby::Cache.stub(:negative_soa, ->(*) { raise 'disabled cache examined records' }) do
|
|
134
|
+
assert_nil(Dnsruby::PacketSender.cache(request, response))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
data/test/tc_caa.rb
CHANGED
|
@@ -27,6 +27,7 @@ class TestCAA < Minitest::Test
|
|
|
27
27
|
'foo.com. IN CAA 0 issuewild "ca.example.net"' => [0, 'issuewild', 'ca.example.net'],
|
|
28
28
|
'foo.com. IN CAA 0 issuemail "ca.example.net"' => [0, 'issuemail', 'ca.example.net'],
|
|
29
29
|
'foo.com. IN CAA 0 issuevmc "ca.example.net"' => [0, 'issuevmc', 'ca.example.net'],
|
|
30
|
+
'foo.com. IN CAA 0 Issue "ca.example.net"' => [0, 'Issue', 'ca.example.net'],
|
|
30
31
|
'foo.com. IN CAA 0 iodef "mailto:security@example.com"' => [0, 'iodef', 'mailto:security@example.com'],
|
|
31
32
|
'foo.com. IN CAA 0 issue "ca.example.net; account=230123"' => [0, 'issue', 'ca.example.net; account=230123']
|
|
32
33
|
}.each do |text, data|
|
|
@@ -48,8 +49,7 @@ class TestCAA < Minitest::Test
|
|
|
48
49
|
|
|
49
50
|
def test_caa_error
|
|
50
51
|
{
|
|
51
|
-
'foo.com. IN CAA 0 ca.example.net "issue"' => [0, 'ca.example.net', 'issue']
|
|
52
|
-
'foo.com. IN CAA 0 Issue "ca.example.net"' => [0, 'Issue', 'ca.example.net']
|
|
52
|
+
'foo.com. IN CAA 0 ca.example.net "issue"' => [0, 'ca.example.net', 'issue']
|
|
53
53
|
}.each do |text, data|
|
|
54
54
|
assert_raises DecodeError do
|
|
55
55
|
RR.create(text)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
|
|
3
|
+
class TestCacheKeys < Minitest::Test
|
|
4
|
+
include Dnsruby
|
|
5
|
+
|
|
6
|
+
def test_numeric_query_spellings_find_the_decoded_response
|
|
7
|
+
cache = Cache.new
|
|
8
|
+
query = Message.new('cache.example.', 'TYPE0001', 'CLASS0001')
|
|
9
|
+
response = Message.decode(query.encode)
|
|
10
|
+
response.header.qr = true
|
|
11
|
+
response.add_answer(RR.create('cache.example. 300 IN A 192.0.2.1'))
|
|
12
|
+
cache.add(response)
|
|
13
|
+
|
|
14
|
+
['A', 'TYPE1', 'TYPE0001', Types.new('TYPE01'), 1].each do |type|
|
|
15
|
+
['IN', 'CLASS1', 'CLASS0001', Classes.new('CLASS01'), 1].each do |klass|
|
|
16
|
+
found = cache.find('CACHE.example', type, klass)
|
|
17
|
+
refute_nil(found, "#{type}/#{klass}")
|
|
18
|
+
assert(found.cached)
|
|
19
|
+
assert_equal('192.0.2.1', found.answer.first.address.to_s)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
assert_nil(cache.find('cache.example.', 'AAAA', 'IN'))
|
|
23
|
+
assert_nil(cache.find('cache.example.', 'A', 'CH'))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_unknown_numeric_aliases_share_one_cache_entry
|
|
27
|
+
cache = Cache.new
|
|
28
|
+
[['TYPE65001', 'CLASS65000'], ['TYPE065001', 'CLASS065000']].each do |type, klass|
|
|
29
|
+
response = Message.new('cache.example.', type, klass)
|
|
30
|
+
response.add_answer(RR.create('cache.example. 300 CLASS65000 TYPE65001 \\# 1 01'))
|
|
31
|
+
cache.add(response)
|
|
32
|
+
end
|
|
33
|
+
assert_equal(1, cache.length)
|
|
34
|
+
assert_equal("\x01".b, cache.find('cache.example.', 'TYPE0065001', 'CLASS0065000').answer.first.data)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
require_relative 'tc_pipeline_send'
|
|
2
|
+
|
|
3
|
+
class CancelledSendCapacityTest < Minitest::Test
|
|
4
|
+
def test_cancelled_udp_writes_release_all_workers_and_unregistered_sockets
|
|
5
|
+
isolated(<<~'RUBY')
|
|
6
|
+
sockets = []
|
|
7
|
+
UDPSocket.define_singleton_method(:new) do |*|
|
|
8
|
+
socket = FrameSocket.new
|
|
9
|
+
socket.define_singleton_method(:bind) { |*| }
|
|
10
|
+
socket.define_singleton_method(:connect) { |*| }
|
|
11
|
+
socket.define_singleton_method(:write_nonblock) { |*| raise IO::EAGAINWaitWritable }
|
|
12
|
+
sockets << socket
|
|
13
|
+
socket
|
|
14
|
+
end
|
|
15
|
+
tasks, queues = [], []
|
|
16
|
+
4.times do |id|
|
|
17
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false, packet_timeout: 60)
|
|
18
|
+
q, queue = query, Queue.new
|
|
19
|
+
queues << queue
|
|
20
|
+
tasks << writer.send_dnsruby(q.encode, q, id, queue, false)
|
|
21
|
+
end
|
|
22
|
+
4.times { WAITING.pop }
|
|
23
|
+
4.times { |id| SELECTOR.remove_id(id) }
|
|
24
|
+
healthy = Queue.new
|
|
25
|
+
next_task = SELECTOR.defer_send { healthy << true }
|
|
26
|
+
Timeout.timeout(2) do
|
|
27
|
+
healthy.pop
|
|
28
|
+
(tasks + [next_task]).each(&:value)
|
|
29
|
+
end
|
|
30
|
+
drain_errors
|
|
31
|
+
raise 'cancelled UDP socket retained' unless sockets.all?(&:closed?)
|
|
32
|
+
raise 'cancelled attempt still registered' unless K.class_variable_get(:@@query_hash).empty?
|
|
33
|
+
raise 'cancelled UDP wrote bytes' unless sockets.all? { |socket| socket.written.empty? }
|
|
34
|
+
raise 'cancelled owner received another completion' unless queues.all?(&:empty?)
|
|
35
|
+
RUBY
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_cancelled_unwritten_tcp_frames_release_workers_without_failing_peers
|
|
39
|
+
tcp_cancellation_scenario(false)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_cancelled_partial_tcp_frames_release_workers_and_retire_corrupt_streams
|
|
43
|
+
tcp_cancellation_scenario(true)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_writable_wait_slices_retain_the_original_tcp_and_udp_deadline
|
|
47
|
+
isolated(<<~'RUBY')
|
|
48
|
+
[:tcp, :udp].each do |transport|
|
|
49
|
+
writer = Dnsruby::PacketSender.new(server: '127.0.0.1', dnssec: false)
|
|
50
|
+
socket = FrameSocket.new
|
|
51
|
+
socket.define_singleton_method(:write_nonblock) { |*| raise IO::EAGAINWaitWritable }
|
|
52
|
+
token = K::SendToken.new
|
|
53
|
+
started = Dnsruby::TransportClock.now
|
|
54
|
+
begin
|
|
55
|
+
if transport == :tcp
|
|
56
|
+
writer.write_tcp_frame(socket, query.encode, started + 0.15, token)
|
|
57
|
+
else
|
|
58
|
+
writer.write_udp_packet(socket, query.encode, started + 0.15, token, true)
|
|
59
|
+
end
|
|
60
|
+
raise 'congested write did not time out'
|
|
61
|
+
rescue Dnsruby::ResolvTimeout
|
|
62
|
+
elapsed = Dnsruby::TransportClock.now - started
|
|
63
|
+
raise 'select slice expired the entire attempt early' if elapsed < 0.14
|
|
64
|
+
raise 'wait extended the packet deadline' if elapsed > 2
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
RUBY
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def tcp_cancellation_scenario(partial)
|
|
73
|
+
isolated("partial = #{partial}\n" + <<~'RUBY')
|
|
74
|
+
sockets, tasks, peers, queues = [], [], [], []
|
|
75
|
+
4.times do |id|
|
|
76
|
+
socket = FrameSocket.new
|
|
77
|
+
writer = sender(socket)
|
|
78
|
+
writer.packet_timeout = 60
|
|
79
|
+
peer = settings(socket, [:peer, id], query("peer-#{id}.example."), writer)
|
|
80
|
+
SELECTOR.add_to_select(peer)
|
|
81
|
+
calls = 0
|
|
82
|
+
native_write = socket.method(:write_nonblock)
|
|
83
|
+
socket.define_singleton_method(:write_nonblock) do |bytes|
|
|
84
|
+
calls += 1
|
|
85
|
+
if partial && calls == 1
|
|
86
|
+
native_write.call(bytes.byteslice(0, 2))
|
|
87
|
+
else
|
|
88
|
+
raise IO::EAGAINWaitWritable
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
q, queue = query("cancelled-#{id}.example."), Queue.new
|
|
92
|
+
sockets << socket
|
|
93
|
+
peers << peer
|
|
94
|
+
queues << queue
|
|
95
|
+
tasks << writer.send_dnsruby(q.encode, q, id, queue, true)
|
|
96
|
+
end
|
|
97
|
+
4.times { WAITING.pop }
|
|
98
|
+
4.times { |id| SELECTOR.remove_id(id) }
|
|
99
|
+
healthy = Queue.new
|
|
100
|
+
next_task = SELECTOR.defer_send { healthy << true }
|
|
101
|
+
Timeout.timeout(2) do
|
|
102
|
+
healthy.pop
|
|
103
|
+
(tasks + [next_task]).each(&:value)
|
|
104
|
+
end
|
|
105
|
+
drain_errors
|
|
106
|
+
raise 'cancelled owner received another completion' unless queues.all?(&:empty?)
|
|
107
|
+
sockets.zip(peers).each do |socket, peer|
|
|
108
|
+
raise 'canceled frame continued writing' unless socket.written.bytesize == (partial ? 2 : 0)
|
|
109
|
+
if partial
|
|
110
|
+
raise 'corrupt pipeline remained open' unless socket.closed?
|
|
111
|
+
raise 'corrupt stream peer not failed exactly once' unless peer.client_queue.length == 1 &&
|
|
112
|
+
peer.client_queue.pop(true).last.is_a?(Dnsruby::SocketEofResolvError)
|
|
113
|
+
else
|
|
114
|
+
raise 'unwritten cancellation closed peer socket' if socket.closed?
|
|
115
|
+
raise 'unwritten cancellation lost peer' unless SELECTOR.query_settings_for_id(peer.client_query_id).equal?(peer)
|
|
116
|
+
raise 'unwritten cancellation failed peer' unless peer.client_queue.empty?
|
|
117
|
+
response = peer.query.clone
|
|
118
|
+
response.header.qr = true
|
|
119
|
+
SELECTOR.process_message(response, response.encode, socket)
|
|
120
|
+
SELECTOR.send_queued_validation_responses
|
|
121
|
+
events = 2.times.map { peer.client_queue.pop(true) }
|
|
122
|
+
raise 'peer did not complete once' unless peer.client_queue.empty? && events.all? { |event| event.last.nil? }
|
|
123
|
+
raise 'retiring pipeline retained after peer finished' unless socket.closed?
|
|
124
|
+
end
|
|
125
|
+
assert_clean(socket)
|
|
126
|
+
end
|
|
127
|
+
raise 'cancelled TCP attempt retained' unless K.class_variable_get(:@@query_hash).empty?
|
|
128
|
+
RUBY
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
WAIT_FIXTURE = <<~'RUBY'
|
|
132
|
+
WAITING = Queue.new
|
|
133
|
+
waiters, waiter_mutex = {}, Mutex.new
|
|
134
|
+
IO.define_singleton_method(:select) do |readers, writers, _, timeout|
|
|
135
|
+
raise 'unexpected writable wait' unless readers.nil? && writers.length == 1
|
|
136
|
+
waiter_mutex.synchronize do
|
|
137
|
+
unless waiters[Thread.current]
|
|
138
|
+
waiters[Thread.current] = true
|
|
139
|
+
WAITING << Thread.current
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
# Model a socket that stays congested: honor the supplied timeout rather
|
|
143
|
+
# than artificially waking a canceled writer, which masked the leak.
|
|
144
|
+
sleep(timeout)
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
K.class_variable_set(:@@select_thread, Thread.current)
|
|
148
|
+
RUBY
|
|
149
|
+
|
|
150
|
+
def isolated(body)
|
|
151
|
+
options = ''.frozen? ? ['--enable-frozen-string-literal'] : []
|
|
152
|
+
source = "#{TestPipelineSend::FIXTURE}\n#{WAIT_FIXTURE}\nTimeout.timeout(10) do\n#{body}\nend"
|
|
153
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, *options, '-I',
|
|
154
|
+
File.expand_path('../lib', __dir__), '-e', source)
|
|
155
|
+
assert(status.success?, "#{stdout}\n#{stderr}")
|
|
156
|
+
assert_empty(stderr)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
require_relative 'tc_key_cache_identity'
|
|
2
|
+
|
|
3
|
+
class DnssecAliasValidationTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@now = Time.now.to_i
|
|
6
|
+
@private_keys = KeyCacheIdentityTest::PRIVATE_KEYS.map { |pem| OpenSSL::PKey::EC.new(pem) }
|
|
7
|
+
@keys = %w[. left. right.].each_with_index.to_h do |zone, index|
|
|
8
|
+
private_key = @private_keys[index % @private_keys.length]
|
|
9
|
+
bytes = private_key.public_key.to_octet_string(:uncompressed).byteslice(1, 64)
|
|
10
|
+
[zone, [Dnsruby::RR.create("#{zone} 300 IN DNSKEY 257 3 13 #{Base64.strict_encode64(bytes)}"), private_key]]
|
|
11
|
+
end
|
|
12
|
+
@verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
|
|
13
|
+
@verifier.add_trust_anchor(@keys.fetch('.').first)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_cold_cross_zone_cname_discovers_target_signer_with_the_same_budget
|
|
17
|
+
message = cross_zone_message
|
|
18
|
+
budget = Dnsruby::ValidationBudget.new
|
|
19
|
+
with_chain_resolver do |queries, budgets|
|
|
20
|
+
Dnsruby::ValidationBudget.with(budget) do
|
|
21
|
+
assert(@verifier.validate(message, Dnsruby::Message.new('alias.left.', 'A')))
|
|
22
|
+
end
|
|
23
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
|
|
24
|
+
assert_equal(1, queries.count(['right.', 'DS']))
|
|
25
|
+
assert_equal(1, queries.count(['right.', 'DNSKEY']))
|
|
26
|
+
refute_includes(queries, ['target.right.', 'DS'])
|
|
27
|
+
assert(budgets.all? { |used| used.equal?(budget) })
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_cross_zone_negative_answer_discovers_authority_signer
|
|
32
|
+
message = message_with('alias.left.', 'A', [signed('alias.left. 300 IN CNAME target.right.', 'left.')])
|
|
33
|
+
signed('right. 300 IN SOA ns.right. hostmaster.right. 1 3600 600 86400 300', 'right.').
|
|
34
|
+
each { |rr| message.add_authority(rr) }
|
|
35
|
+
signed('target.right. 300 IN NSEC z.right. TXT NSEC RRSIG', 'right.').
|
|
36
|
+
each { |rr| message.add_authority(rr) }
|
|
37
|
+
with_chain_resolver do |queries, _budgets|
|
|
38
|
+
assert(@verifier.validate(message, Dnsruby::Message.new('alias.left.', 'A')))
|
|
39
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
|
|
40
|
+
assert_equal(1, queries.count(['right.', 'DNSKEY']))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_cached_target_key_avoids_repeating_signer_discovery
|
|
45
|
+
@verifier.add_trust_anchor(@keys.fetch('right.').first)
|
|
46
|
+
message = cross_zone_message
|
|
47
|
+
with_chain_resolver do |queries, _budgets|
|
|
48
|
+
assert(@verifier.validate(message, nil))
|
|
49
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
|
|
50
|
+
assert_empty(queries.select { |name, _type| name == 'right.' })
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_overlapping_rollover_refreshes_a_cached_key_with_the_same_tag
|
|
55
|
+
new_key = @keys.fetch('right.').first
|
|
56
|
+
old_private = @private_keys.fetch(1)
|
|
57
|
+
old_bytes = old_private.public_key.to_octet_string(:uncompressed).byteslice(1, 64)
|
|
58
|
+
old_key = Dnsruby::RR.create("right. 300 IN DNSKEY 257 3 13 #{Base64.strict_encode64(old_bytes)}")
|
|
59
|
+
refute_equal(old_key.key, new_key.key)
|
|
60
|
+
assert_equal(old_key.key_tag, new_key.key_tag)
|
|
61
|
+
@verifier.add_trust_anchor(old_key)
|
|
62
|
+
|
|
63
|
+
rollover = Dnsruby::RRSet.new([old_key, new_key])
|
|
64
|
+
signature = Dnsruby::RR.create(name: 'right.', type: 'RRSIG', ttl: 300,
|
|
65
|
+
type_covered: Dnsruby::Types.DNSKEY, algorithm: 13, labels: 1,
|
|
66
|
+
original_ttl: 300, expiration: @now + 3600, inception: @now - 60,
|
|
67
|
+
key_tag: old_key.key_tag, signers_name: 'right.')
|
|
68
|
+
bytes = @verifier.send(:canonical_rrset_data, rollover, signature, {})
|
|
69
|
+
der = old_private.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(signature.sig_data + bytes))
|
|
70
|
+
signature.signature = OpenSSL::ASN1.decode(der).value.map { |n| n.value.to_s(2).rjust(32, "\0") }.join
|
|
71
|
+
rollover.add(signature)
|
|
72
|
+
# Authenticate the ordinary two-key rollover independently, without
|
|
73
|
+
# teaching its new key to the verifier whose discovery is under test.
|
|
74
|
+
check = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
|
|
75
|
+
assert(check.verify(rollover, old_key))
|
|
76
|
+
|
|
77
|
+
responses = {['right.', 'DNSKEY'] => message_with('right.', 'DNSKEY', [rollover])}
|
|
78
|
+
message = cross_zone_message
|
|
79
|
+
with_chain_resolver(responses: responses) do |queries, _budgets|
|
|
80
|
+
assert(@verifier.validate(message, nil))
|
|
81
|
+
assert_equal(Dnsruby::Message::SecurityLevel.SECURE, message.security_level)
|
|
82
|
+
assert_equal(1, queries.count(['right.', 'DNSKEY']))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_signer_discovery_does_not_reset_the_shared_work_limit
|
|
87
|
+
budget = Dnsruby::ValidationBudget.new
|
|
88
|
+
before_response = lambda do |name, type|
|
|
89
|
+
if name.to_s(true) == 'right.' && type == Dnsruby::Types.DS
|
|
90
|
+
assert_same(budget, Dnsruby::ValidationBudget.current)
|
|
91
|
+
budget.check!(Dnsruby::Dnssec.validation_limits[:max_work] + 1)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
with_chain_resolver(before_response) do |_queries, _budgets|
|
|
95
|
+
assert_raises(Dnsruby::ValidationLimitError) do
|
|
96
|
+
Dnsruby::ValidationBudget.with(budget) { @verifier.validate(cross_zone_message, nil) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_additional_data_does_not_trigger_signer_discovery
|
|
103
|
+
message = cross_zone_message
|
|
104
|
+
signed('extra.right. 300 IN A 192.0.2.2', 'right.').each { |rr| message.add_additional(rr) }
|
|
105
|
+
requested = []
|
|
106
|
+
@verifier.stub(:find_closest_anchor_for, ->(name) { requested << name.to_s(true); false }) do
|
|
107
|
+
@verifier.send(:discover_signer_keys, message_with('left.', 'A', [], message.additional))
|
|
108
|
+
end
|
|
109
|
+
assert_empty(requested)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_optional_foreign_signed_data_is_removed_without_rejecting_the_answer
|
|
113
|
+
@verifier.add_trust_anchor(@keys.fetch('left.').first)
|
|
114
|
+
answer = signed('left. 300 IN MX 10 mail.right.', 'left.')
|
|
115
|
+
extra = signed('mail.right. 300 IN A 192.0.2.2', 'right.')
|
|
116
|
+
message = message_with('left.', 'MX', [answer], extra.rrs + extra.sigs)
|
|
117
|
+
assert(@verifier.verify(message))
|
|
118
|
+
assert_empty(message.additional)
|
|
119
|
+
assert_equal(0, message.header.arcount)
|
|
120
|
+
assert(message.answer.all? { |rr| rr.instance_variable_get(:@dnsruby_authenticated_expiration) })
|
|
121
|
+
refute(@verifier.get_keys_to_check.any? { |key| key.name == @keys.fetch('right.').first.name })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_optional_foreign_dnskey_and_ds_learning_is_opportunistic
|
|
125
|
+
@verifier.add_trust_anchor(@keys.fetch('left.').first)
|
|
126
|
+
answer = signed('left. 300 IN MX 10 mail.right.', 'left.')
|
|
127
|
+
key = @keys.fetch('right.').first
|
|
128
|
+
[signed(key, 'right.'), signed(Dnsruby::RR::DS.from_key(key, 2), 'right.')].each do |extra|
|
|
129
|
+
message = message_with('left.', 'MX', [answer], extra.rrs + extra.sigs)
|
|
130
|
+
assert(@verifier.verify(message))
|
|
131
|
+
assert_empty(message.additional)
|
|
132
|
+
refute(@verifier.get_keys_to_check.any? { |known| known.name == key.name })
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_verified_optional_data_is_retained_and_unsigned_glue_stays_unauthenticated
|
|
137
|
+
%w[left. right.].each { |zone| @verifier.add_trust_anchor(@keys.fetch(zone).first) }
|
|
138
|
+
extra = signed('mail.right. 300 IN A 192.0.2.2', 'right.')
|
|
139
|
+
glue = Dnsruby::RR.create('ns.right. 300 IN A 192.0.2.3')
|
|
140
|
+
message = message_with('left.', 'MX', [signed('left. 300 IN MX 10 mail.right.', 'left.')],
|
|
141
|
+
extra.rrs + extra.sigs + [glue])
|
|
142
|
+
assert(@verifier.verify(message))
|
|
143
|
+
assert_equal(3, message.additional.length)
|
|
144
|
+
message.additional.each do |record|
|
|
145
|
+
if record.name == glue.name
|
|
146
|
+
assert_nil(record.instance_variable_get(:@dnsruby_authenticated_expiration))
|
|
147
|
+
else
|
|
148
|
+
refute_nil(record.instance_variable_get(:@dnsruby_authenticated_expiration))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_unsigned_additional_copy_does_not_inherit_answer_authentication
|
|
154
|
+
@verifier.add_trust_anchor(@keys.fetch('left.').first)
|
|
155
|
+
answer = signed('mail.left. 300 IN A 192.0.2.2', 'left.')
|
|
156
|
+
extra = answer.rrs.first.clone
|
|
157
|
+
message = message_with('mail.left.', 'A', [answer], [extra])
|
|
158
|
+
assert(@verifier.verify(message))
|
|
159
|
+
refute_nil(message.answer.first.instance_variable_get(:@dnsruby_authenticated_expiration))
|
|
160
|
+
assert_nil(message.additional.first.instance_variable_get(:@dnsruby_authenticated_expiration))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_optional_verification_preserves_the_shared_signature_limit
|
|
164
|
+
%w[left. right.].each { |zone| @verifier.add_trust_anchor(@keys.fetch(zone).first) }
|
|
165
|
+
extra = signed('mail.right. 300 IN A 192.0.2.2', 'right.')
|
|
166
|
+
message = message_with('left.', 'MX', [signed('left. 300 IN MX 10 mail.right.', 'left.')], extra.rrs + extra.sigs)
|
|
167
|
+
limits = Dnsruby::Dnssec.validation_limits.merge(max_signature_verifications: 1)
|
|
168
|
+
Dnsruby::Dnssec.stub(:validation_limits, limits) do
|
|
169
|
+
assert_raises(Dnsruby::ValidationLimitError) { @verifier.verify(message) }
|
|
170
|
+
end
|
|
171
|
+
assert_nil(Dnsruby::ValidationBudget.current)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def test_dname_authenticates_synthesized_cname_in_either_record_order
|
|
175
|
+
@verifier.add_trust_anchor(@keys.fetch('left.').first)
|
|
176
|
+
[false, true].each do |cname_first|
|
|
177
|
+
message = dname_message(cname_first: cname_first)
|
|
178
|
+
assert(@verifier.verify(message))
|
|
179
|
+
assert_equal('www.new.left', message.answer.find { |rr| rr.type == Dnsruby::Types.CNAME }.domainname.to_s)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_dname_redirects_without_a_server_synthesized_cname
|
|
184
|
+
@verifier.add_trust_anchor(@keys.fetch('left.').first)
|
|
185
|
+
message = dname_message
|
|
186
|
+
message.answer.delete_if { |rr| rr.type == Dnsruby::Types.CNAME }
|
|
187
|
+
assert(@verifier.verify(message))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_synthesized_cname_inherits_dname_expiration_and_preserves_zero_ttl
|
|
191
|
+
@verifier.add_trust_anchor(@keys.fetch('left.').first)
|
|
192
|
+
Time.stub(:now, Time.at(@now)) do
|
|
193
|
+
[:expiration, :signature_ttl].each do |bound|
|
|
194
|
+
[0, 300].each do |ttl|
|
|
195
|
+
message = dname_message(cname_ttl: ttl, dname_lifetime: bound == :expiration ? 7 : 3600)
|
|
196
|
+
if bound == :signature_ttl
|
|
197
|
+
message.answer.find { |rr| rr.type == Dnsruby::Types.RRSIG && rr.type_covered == Dnsruby::Types.DNAME }.ttl = 7
|
|
198
|
+
end
|
|
199
|
+
assert(@verifier.verify(message))
|
|
200
|
+
cname = message.answer.find { |rr| rr.type == Dnsruby::Types.CNAME }
|
|
201
|
+
assert_equal([ttl, 7].min, cname.ttl)
|
|
202
|
+
assert_equal(@now + [ttl, 7].min, cname.instance_variable_get(:@dnsruby_authenticated_expiration))
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def test_synthesized_cname_uses_labels_and_case_insensitive_names
|
|
209
|
+
@verifier.add_trust_anchor(@keys.fetch('left.').first)
|
|
210
|
+
message = message_with('www\\.part.old.left.', 'A', [
|
|
211
|
+
signed('old.left. 300 IN DNAME new.left.', 'left.'),
|
|
212
|
+
Dnsruby::RRSet.new([Dnsruby::RR.create('www\\.part.OLD.LEFT. 300 IN CNAME www\\.part.NEW.LEFT.')]),
|
|
213
|
+
signed('www\\.part.new.left. 300 IN A 192.0.2.1', 'left.')])
|
|
214
|
+
assert(@verifier.verify(message))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def test_an_ordinary_unsigned_cname_still_requires_authentication
|
|
218
|
+
@verifier.add_trust_anchor(@keys.fetch('left.').first)
|
|
219
|
+
message = message_with('alias.left.', 'A', [
|
|
220
|
+
Dnsruby::RRSet.new([Dnsruby::RR.create('alias.left. 300 IN CNAME target.left.')]),
|
|
221
|
+
signed('target.left. 300 IN A 192.0.2.1', 'left.')])
|
|
222
|
+
assert_raises(Dnsruby::VerifyError) { @verifier.verify(message) }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def signed(record, zone, lifetime: 3600)
|
|
228
|
+
record = Dnsruby::RR.create(record) if record.is_a?(String)
|
|
229
|
+
key, private_key = @keys.fetch(zone)
|
|
230
|
+
signature = Dnsruby::RR.create(name: record.name, type: 'RRSIG', ttl: record.ttl,
|
|
231
|
+
type_covered: record.type, algorithm: 13, labels: record.name.labels.length,
|
|
232
|
+
original_ttl: record.ttl, expiration: @now + lifetime, inception: @now - 60,
|
|
233
|
+
key_tag: key.key_tag, signers_name: key.name)
|
|
234
|
+
bytes = Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(record, true) }.to_s
|
|
235
|
+
der = private_key.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(signature.sig_data + bytes))
|
|
236
|
+
signature.signature = OpenSSL::ASN1.decode(der).value.map { |n| n.value.to_s(2).rjust(32, "\0") }.join
|
|
237
|
+
Dnsruby::RRSet.new([record, signature])
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def message_with(name, type, answer, additional = [])
|
|
241
|
+
Dnsruby::Message.new(name, type).tap do |message|
|
|
242
|
+
answer.each { |set| set.each { |rr| message.add_answer(rr) } }
|
|
243
|
+
additional.each { |rr| message.add_additional(rr) }
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def cross_zone_message
|
|
248
|
+
message_with('alias.left.', 'A', [signed('alias.left. 300 IN CNAME target.right.', 'left.'),
|
|
249
|
+
signed('target.right. 300 IN A 192.0.2.1', 'right.')])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def dname_message(cname_first: false, cname_ttl: 300, dname_lifetime: 3600)
|
|
253
|
+
dname = signed('old.left. 300 IN DNAME new.left.', 'left.', lifetime: dname_lifetime)
|
|
254
|
+
cname = Dnsruby::RRSet.new([Dnsruby::RR.create("www.old.left. #{cname_ttl} IN CNAME www.new.left.")])
|
|
255
|
+
answer = cname_first ? [cname, dname] : [dname, cname]
|
|
256
|
+
answer << signed('www.new.left. 300 IN A 192.0.2.1', 'left.')
|
|
257
|
+
message_with('www.old.left.', 'A', answer)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def with_chain_resolver(before_response = nil, responses: {})
|
|
261
|
+
lookup = {}
|
|
262
|
+
@keys.each do |zone, (key, _)|
|
|
263
|
+
lookup[[zone, 'DNSKEY']] = message_with(zone, 'DNSKEY', [signed(key, zone)])
|
|
264
|
+
end
|
|
265
|
+
%w[left. right.].each do |zone|
|
|
266
|
+
ds = Dnsruby::RR::DS.from_key(@keys.fetch(zone).first, 2)
|
|
267
|
+
lookup[[zone, 'DS']] = message_with(zone, 'DS', [signed(ds, '.')])
|
|
268
|
+
end
|
|
269
|
+
lookup[['alias.left.', 'DS']] = message_with('alias.left.', 'DS', [
|
|
270
|
+
signed('alias.left. 300 IN CNAME target.right.', 'left.')])
|
|
271
|
+
lookup.merge!(responses)
|
|
272
|
+
queries, budgets = [], []
|
|
273
|
+
resolver = Object.new
|
|
274
|
+
resolver.define_singleton_method(:query_no_validation_or_recursion) do |name, type|
|
|
275
|
+
before_response&.call(name, type)
|
|
276
|
+
identity = [name.to_s(true), type.to_s]
|
|
277
|
+
queries << identity
|
|
278
|
+
budgets << Dnsruby::ValidationBudget.current
|
|
279
|
+
lookup.fetch(identity)
|
|
280
|
+
end
|
|
281
|
+
Dnsruby::Dnssec.stub(:do_validation_with_recursor?, false) do
|
|
282
|
+
Dnsruby::Dnssec.stub(:default_resolver, resolver) do
|
|
283
|
+
@verifier.stub(:get_nameservers_for, resolver) do
|
|
284
|
+
@verifier.stub(:get_nameservers_from_message, resolver) { yield queries, budgets }
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|