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/test/tc_name.rb
CHANGED
|
@@ -20,6 +20,11 @@ class TestName < Minitest::Test
|
|
|
20
20
|
|
|
21
21
|
include Dnsruby
|
|
22
22
|
|
|
23
|
+
# Names on either side of the 255-octet wire limit: 249 octets of label text
|
|
24
|
+
# in 5 labels, plus a length octet each and the root's zero octet, is 255.
|
|
25
|
+
AT_LIMIT_NAME = "#{'a' * 63}.#{'b' * 63}.#{'c' * 63}.#{'d' * 59}.e."
|
|
26
|
+
OVER_LIMIT_NAME = "#{'a' * 63}.#{'b' * 63}.#{'c' * 63}.#{'d' * 60}.e."
|
|
27
|
+
|
|
23
28
|
def test_label_length
|
|
24
29
|
Name::Label.set_max_length(Name::Label::MaxLabelLength) # Other tests may have changed this
|
|
25
30
|
# Test max label length = 63
|
|
@@ -30,13 +35,102 @@ class TestName < Minitest::Test
|
|
|
30
35
|
end
|
|
31
36
|
end
|
|
32
37
|
|
|
38
|
+
def wire_length(name)
|
|
39
|
+
MessageEncoder.new { |msg| msg.put_name(name, true) }.to_s.length
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Test max name length=255. The limit is on the wire form, two octets more
|
|
43
|
+
# than the dotted presentation form without its trailing dot.
|
|
33
44
|
def test_name_length
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
assert_equal(255, wire_length(Name.create(AT_LIMIT_NAME)))
|
|
46
|
+
|
|
47
|
+
error = assert_raises(ResolvError) { Name.create(OVER_LIMIT_NAME) }
|
|
48
|
+
assert_match(/Name length is 256,/, error.message)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# An oversized name must not reach the wire through Message either, and a
|
|
52
|
+
# name right at the limit must still survive the round trip.
|
|
53
|
+
def test_name_length_in_message
|
|
54
|
+
assert_raises(ResolvError) { Message.new(OVER_LIMIT_NAME, "A") }
|
|
55
|
+
|
|
56
|
+
message = Message.new(AT_LIMIT_NAME, "A")
|
|
57
|
+
message.add_answer(RR.create("#{AT_LIMIT_NAME} 3600 IN NS #{AT_LIMIT_NAME}"))
|
|
58
|
+
decoded = Message.decode(message.encode)
|
|
59
|
+
assert_equal(AT_LIMIT_NAME, decoded.question[0].qname.to_s + ".")
|
|
60
|
+
assert_equal(AT_LIMIT_NAME, decoded.answer[0].domainname.to_s + ".")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_deep_backward_compression_chain_is_iterative_and_preserves_position
|
|
64
|
+
wire = "\x07example\x03com\0".b
|
|
65
|
+
target = 0
|
|
66
|
+
8_000.times do
|
|
67
|
+
offset = wire.bytesize
|
|
68
|
+
wire << [0xc000 | target].pack('n')
|
|
69
|
+
target = offset
|
|
39
70
|
end
|
|
71
|
+
wire << "!"
|
|
72
|
+
decoder = MessageDecoder.new(wire)
|
|
73
|
+
decoder.get_bytes(target)
|
|
74
|
+
assert_equal('example.com.', decoder.get_name.to_s(true))
|
|
75
|
+
assert_equal(target + 2, decoder.index)
|
|
76
|
+
assert_equal('!', decoder.get_bytes)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_compressed_names_share_suffixes_and_chain_to_complete_names
|
|
80
|
+
wire = "\x07example\x03com\0".b
|
|
81
|
+
www = wire.bytesize
|
|
82
|
+
wire << "\x03www".b << [0xc000].pack('n')
|
|
83
|
+
wire << "\x04mail".b << [0xc000].pack('n')
|
|
84
|
+
wire << [0xc000 | www].pack('n')
|
|
85
|
+
decoder = MessageDecoder.new(wire)
|
|
86
|
+
assert_equal(['example.com.', 'www.example.com.', 'mail.example.com.', 'www.example.com.'],
|
|
87
|
+
Array.new(4) { decoder.get_name.to_s(true) })
|
|
88
|
+
assert_equal(wire.bytesize, decoder.index)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_compressed_rdata_can_reference_a_name_before_its_length_field
|
|
92
|
+
wire = "\x07example\x03com\0".b + [2, 0xc000].pack('nn') + '!'
|
|
93
|
+
decoder = MessageDecoder.new(wire)
|
|
94
|
+
expected = decoder.get_name
|
|
95
|
+
assert_equal(expected, decoder.get_length16 { decoder.get_name })
|
|
96
|
+
assert_equal('!', decoder.get_bytes)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_compression_rejects_loops_forward_pointers_and_reserved_labels
|
|
100
|
+
malformed = [
|
|
101
|
+
["\xc0\x00".b, 0], # Self pointer.
|
|
102
|
+
["\xc0\x02\0".b, 0], # Forward pointer.
|
|
103
|
+
["\xc0\x02\xc0\x00".b, 2], # Two-pointer cycle.
|
|
104
|
+
["\x01a\xc0\x00".b, 0], # Label followed by a pointer into itself.
|
|
105
|
+
["\xc0".b, 0], # Incomplete pointer.
|
|
106
|
+
["\x01a".b, 0], # Missing root label.
|
|
107
|
+
["\x02a".b, 0], # Truncated label.
|
|
108
|
+
["\x40".b + 'a' * 64 + "\0", 0],
|
|
109
|
+
["\x80\0".b, 0], # Reserved label types.
|
|
110
|
+
["\xbf\0".b, 0]
|
|
111
|
+
]
|
|
112
|
+
malformed.each do |wire, offset|
|
|
113
|
+
decoder = MessageDecoder.new(wire)
|
|
114
|
+
decoder.get_bytes(offset)
|
|
115
|
+
assert_raises(DecodeError, wire.unpack1('H*')) { decoder.get_name }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_expanded_compressed_names_obey_the_wire_length_limit
|
|
120
|
+
encoded = MessageEncoder.new { |encoder| encoder.put_name(Name.create(AT_LIMIT_NAME), true) }.to_s
|
|
121
|
+
decoder = MessageDecoder.new(encoded + [0xc000].pack('n') + "\x01x".b + [0xc000].pack('n'))
|
|
122
|
+
assert_equal(AT_LIMIT_NAME, decoder.get_name.to_s(true))
|
|
123
|
+
assert_equal(AT_LIMIT_NAME, decoder.get_name.to_s(true))
|
|
124
|
+
assert_raises(DecodeError) { decoder.get_name }
|
|
125
|
+
|
|
126
|
+
oversized = OVER_LIMIT_NAME.split('.').map { |label| [label.bytesize].pack('C') + label }.join + "\0"
|
|
127
|
+
assert_raises(DecodeError) { MessageDecoder.new(oversized).get_name }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test_name_cannot_read_a_root_label_beyond_its_rdata_boundary
|
|
131
|
+
decoder = MessageDecoder.new([2].pack('n') + "\x01a\0".b)
|
|
132
|
+
assert_raises(DecodeError) { decoder.get_length16 { decoder.get_name } }
|
|
133
|
+
assert_equal("\0", decoder.get_bytes)
|
|
40
134
|
end
|
|
41
135
|
|
|
42
136
|
def test_absolute
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'minitest/mock'
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
5
|
+
class TestNameserverBootstrap < Minitest::Test
|
|
6
|
+
def with_resolv_conf(contents)
|
|
7
|
+
Tempfile.create('dnsruby-nameserver-bootstrap') do |file|
|
|
8
|
+
file.write(contents)
|
|
9
|
+
file.flush
|
|
10
|
+
yield file.path
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def answer_for(hostname, address = nil)
|
|
15
|
+
message = Dnsruby::Message.new(hostname)
|
|
16
|
+
if address
|
|
17
|
+
type = address.include?(':') ? 'AAAA' : 'A'
|
|
18
|
+
message.add_answer(Dnsruby::RR.create("#{hostname} 60 IN #{type} #{address}"))
|
|
19
|
+
end
|
|
20
|
+
message
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Exercise real Resolver and PacketSender initialization, replacing only DNS
|
|
24
|
+
# queries. Bound configuration reads and construction so a regression cannot
|
|
25
|
+
# recreate issue #179's unbounded chain of resolver threads. Worker threads
|
|
26
|
+
# record their observations through queues instead of performing assertions.
|
|
27
|
+
def with_lookup(defaults, response, recursive_response: nil, lookup_error: nil)
|
|
28
|
+
original_new = Dnsruby::Resolver.method(:new)
|
|
29
|
+
state = { queries: Queue.new, recursors: Queue.new, closed: Queue.new,
|
|
30
|
+
resolvers: Queue.new, counts: Hash.new(0) }
|
|
31
|
+
mutex = Mutex.new
|
|
32
|
+
count = lambda do |kind|
|
|
33
|
+
mutex.synchronize do
|
|
34
|
+
state[:counts][kind] += 1
|
|
35
|
+
raise 'Nameserver bootstrap reentered too many times' if state[:counts][kind] > 12
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
default_config = lambda do
|
|
39
|
+
count.call(:config_reads)
|
|
40
|
+
config = defaults.respond_to?(:call) ? defaults.call : defaults
|
|
41
|
+
config.merge(nameserver: Array(config[:nameserver]).dup)
|
|
42
|
+
end
|
|
43
|
+
factory = lambda do |*args|
|
|
44
|
+
count.call(:resolver_constructions)
|
|
45
|
+
resolver = original_new.call(*args)
|
|
46
|
+
state[:resolvers] << resolver
|
|
47
|
+
original_close = resolver.method(:close)
|
|
48
|
+
resolver.define_singleton_method(:close) do
|
|
49
|
+
state[:closed] << self
|
|
50
|
+
original_close.call
|
|
51
|
+
end
|
|
52
|
+
resolver.define_singleton_method(:query) do |hostname, *|
|
|
53
|
+
servers = single_resolvers.map(&:server)
|
|
54
|
+
state[:queries] << [self, hostname.to_s, servers]
|
|
55
|
+
raise lookup_error if lookup_error
|
|
56
|
+
response
|
|
57
|
+
end
|
|
58
|
+
resolver
|
|
59
|
+
end
|
|
60
|
+
recursive_factory = lambda do |*args|
|
|
61
|
+
state[:recursors] << args
|
|
62
|
+
recursor = Object.new
|
|
63
|
+
recursor.define_singleton_method(:query) do |hostname, *|
|
|
64
|
+
recursive_response || Dnsruby::Message.new(hostname)
|
|
65
|
+
end
|
|
66
|
+
recursor
|
|
67
|
+
end
|
|
68
|
+
Dnsruby::Config.stub(:default_config_hash, default_config) do
|
|
69
|
+
Dnsruby::Resolver.stub(:new, factory) do
|
|
70
|
+
Dnsruby::Recursor.stub(:new, recursive_factory) do
|
|
71
|
+
yield state, original_new
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
ensure
|
|
76
|
+
unless state.nil?
|
|
77
|
+
state[:resolvers].pop(true).close until state[:resolvers].empty?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_resolv_conf_preserves_hostnames_literals_and_token_order
|
|
82
|
+
servers = %w[ns.example.test 192.0.2.1 localhost 2001:db8::1 fe80::1%en0 ns.example.test]
|
|
83
|
+
contents = "nameserver #{servers.first(3).join(' ')} # comment\n" \
|
|
84
|
+
"nameserver #{servers.drop(3).join(' ')} ; comment\n"
|
|
85
|
+
with_resolv_conf(contents) do |path|
|
|
86
|
+
assert_equal(servers, Dnsruby::Config.parse_resolv_conf(path)[:nameserver])
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_overlong_nameserver_from_file_does_not_hide_a_valid_server_or_override
|
|
91
|
+
previous_limit = Dnsruby::Name::Label.class_variable_get(:@@max_length)
|
|
92
|
+
Dnsruby::Name::Label.set_max_length(Dnsruby::Name::Label::MaxLabelLength)
|
|
93
|
+
invalid = ('a' * 64) + '.example.'
|
|
94
|
+
with_resolv_conf("nameserver #{invalid}\nnameserver 127.0.0.1\n") do |path|
|
|
95
|
+
defaults = Dnsruby::Config.parse_resolv_conf(path)
|
|
96
|
+
Dnsruby::Config.stub(:default_config_hash, defaults) do
|
|
97
|
+
config = Dnsruby::Config.new
|
|
98
|
+
config.get_ready
|
|
99
|
+
assert_equal(['127.0.0.1'], config.nameserver)
|
|
100
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.2')
|
|
101
|
+
assert_equal(['127.0.0.2'], resolver.config.nameserver)
|
|
102
|
+
assert_equal(['127.0.0.2'], resolver.single_resolvers.map(&:server))
|
|
103
|
+
assert_raises(Dnsruby::ResolvError) do
|
|
104
|
+
Dnsruby::Config.new.set_config_info(nameserver: [invalid])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
ensure
|
|
109
|
+
Dnsruby::Name::Label.set_max_length(previous_limit)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_failed_nameserver_replacement_preserves_config_and_active_senders
|
|
113
|
+
resolver = Dnsruby::Resolver.new(nameserver: '127.0.0.1')
|
|
114
|
+
original = resolver.single_resolvers.first
|
|
115
|
+
assert_raises(ArgumentError) { resolver.nameserver = [] }
|
|
116
|
+
assert_equal(['127.0.0.1'], resolver.config.nameserver)
|
|
117
|
+
assert_equal([original], resolver.single_resolvers)
|
|
118
|
+
resolver.nameserver = '127.0.0.2'
|
|
119
|
+
assert_equal(['127.0.0.2'], resolver.config.nameserver)
|
|
120
|
+
assert_equal(['127.0.0.2'], resolver.single_resolvers.map(&:server))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_invalid_default_nameserver_fails_without_recursive_initialization
|
|
124
|
+
with_resolv_conf("nameserver 8.8.8.8,\n") do |path|
|
|
125
|
+
defaults = -> { Dnsruby::Config.parse_resolv_conf(path) }
|
|
126
|
+
with_lookup(defaults, answer_for('unused.example.test')) do |state, original_new|
|
|
127
|
+
resolver = original_new.call
|
|
128
|
+
error = assert_raises(ArgumentError) { resolver.add_config_nameservers }
|
|
129
|
+
assert_equal('No valid nameservers found', error.message)
|
|
130
|
+
assert_equal(['8.8.8.8,'], resolver.config.nameserver)
|
|
131
|
+
assert_operator(state[:counts][:config_reads], :<=, 3)
|
|
132
|
+
assert_equal(0, state[:counts][:resolver_constructions])
|
|
133
|
+
assert(state[:queries].empty?)
|
|
134
|
+
assert(state[:recursors].empty?)
|
|
135
|
+
ensure
|
|
136
|
+
resolver.close if resolver
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_scoped_looking_dns_names_do_not_hide_valid_fallbacks
|
|
142
|
+
contents = "nameserver fe80::1%\nnameserver fe80::gggg%en0\n" \
|
|
143
|
+
"nameserver 192.0.2.1\nnameserver fe80::1%3\n"
|
|
144
|
+
with_resolv_conf(contents) do |path|
|
|
145
|
+
defaults = -> { Dnsruby::Config.parse_resolv_conf(path) }
|
|
146
|
+
[nil, {config_info: path}, {config_info: {search: ['example.test']}}].each do |options|
|
|
147
|
+
with_lookup(defaults, nil) do |state, original_new|
|
|
148
|
+
resolver = options ? original_new.call(options) : original_new.call
|
|
149
|
+
resolver.add_config_nameservers unless options
|
|
150
|
+
assert_equal(['fe80::1%', 'fe80::gggg%en0', '192.0.2.1', 'fe80::1%3'], resolver.config.nameserver)
|
|
151
|
+
assert_equal(['192.0.2.1', 'fe80::1%3'], resolver.single_resolvers.map(&:server))
|
|
152
|
+
queried = []
|
|
153
|
+
queried << state[:queries].pop(true)[1] until state[:queries].empty?
|
|
154
|
+
# PacketSender also retries with nil after a failed server setter.
|
|
155
|
+
assert_equal(['', '', 'fe80::1%', 'fe80::gggg%en0'].sort, queried.sort)
|
|
156
|
+
assert_equal(4, state[:counts][:resolver_constructions])
|
|
157
|
+
ensure
|
|
158
|
+
resolver.close if resolver
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_invalid_scoped_defaults_do_not_block_explicit_nameserver
|
|
165
|
+
with_resolv_conf("nameserver fe80::1%\n") do |path|
|
|
166
|
+
defaults = -> { Dnsruby::Config.parse_resolv_conf(path) }
|
|
167
|
+
with_lookup(defaults, nil) do |state, original_new|
|
|
168
|
+
['192.0.2.53', {nameserver: '2001:db8::53'}].each do |options|
|
|
169
|
+
resolver = original_new.call(options)
|
|
170
|
+
expected = options.is_a?(Hash) ? options[:nameserver] : options
|
|
171
|
+
assert_equal([expected], resolver.single_resolvers.map(&:server))
|
|
172
|
+
ensure
|
|
173
|
+
resolver.close if resolver
|
|
174
|
+
end
|
|
175
|
+
resolver = original_new.call
|
|
176
|
+
assert_raises(ArgumentError) { resolver.add_config_nameservers }
|
|
177
|
+
assert_equal(['fe80::1%'], resolver.config.nameserver)
|
|
178
|
+
assert(state[:queries].empty?)
|
|
179
|
+
assert_equal(0, state[:counts][:resolver_constructions])
|
|
180
|
+
ensure
|
|
181
|
+
resolver.close if resolver
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def test_explicit_config_hash_preserves_scoped_looking_dns_names
|
|
187
|
+
config = Dnsruby::Config.new
|
|
188
|
+
servers = ['fe80::1%', '192.0.2.1']
|
|
189
|
+
config.set_config_info(nameserver: servers)
|
|
190
|
+
assert_equal(servers, config.nameserver)
|
|
191
|
+
assert_raises(ArgumentError) { Dnsruby::Config.ipv6_server_address(servers.first) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_default_file_hostname_uses_literal_nameserver_without_changing_config
|
|
195
|
+
hostname = 'ns.example.test'
|
|
196
|
+
with_resolv_conf("nameserver #{hostname}\nnameserver 192.0.2.1\n") do |path|
|
|
197
|
+
defaults = -> { Dnsruby::Config.parse_resolv_conf(path) }
|
|
198
|
+
with_lookup(defaults, answer_for(hostname, '192.0.2.53')) do |state, original_new|
|
|
199
|
+
resolver = original_new.call
|
|
200
|
+
resolver.add_config_nameservers
|
|
201
|
+
assert_equal([hostname, '192.0.2.1'], resolver.config.nameserver)
|
|
202
|
+
assert_equal(['192.0.2.53', '192.0.2.1'], resolver.single_resolvers.map(&:server))
|
|
203
|
+
lookup = state[:queries].pop(true)
|
|
204
|
+
assert_equal(hostname, lookup[1])
|
|
205
|
+
assert_equal(['192.0.2.1'], lookup[2])
|
|
206
|
+
assert_same(lookup[0], state[:closed].pop(true))
|
|
207
|
+
assert(state[:queries].empty?)
|
|
208
|
+
assert_equal(1, state[:counts][:resolver_constructions])
|
|
209
|
+
ensure
|
|
210
|
+
resolver.close if resolver
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def test_custom_file_hostname_uses_system_nameserver
|
|
216
|
+
hostname = 'custom-ns.example.test'
|
|
217
|
+
with_resolv_conf("nameserver #{hostname}\n") do |path|
|
|
218
|
+
with_lookup({nameserver: ['192.0.2.1']}, answer_for(hostname, '192.0.2.53')) do |state, original_new|
|
|
219
|
+
resolver = original_new.call(config_info: path)
|
|
220
|
+
assert_equal([hostname], resolver.config.nameserver)
|
|
221
|
+
assert_equal(['192.0.2.53'], resolver.single_resolvers.map(&:server))
|
|
222
|
+
assert_equal([hostname, ['192.0.2.1']], state[:queries].pop(true).drop(1))
|
|
223
|
+
assert(state[:queries].empty?)
|
|
224
|
+
ensure
|
|
225
|
+
resolver.close if resolver
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_localhost_file_nameserver_does_not_require_dns
|
|
231
|
+
with_resolv_conf("nameserver localhost\n") do |path|
|
|
232
|
+
with_lookup(-> { Dnsruby::Config.parse_resolv_conf(path) }, nil) do |state, original_new|
|
|
233
|
+
resolver = original_new.call(config_info: path)
|
|
234
|
+
assert_equal(['localhost'], resolver.config.nameserver)
|
|
235
|
+
assert_equal(['127.0.0.1'], resolver.single_resolvers.map(&:server))
|
|
236
|
+
assert(state[:queries].empty?)
|
|
237
|
+
assert_equal(0, state[:counts][:resolver_constructions])
|
|
238
|
+
ensure
|
|
239
|
+
resolver.close if resolver
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_explicit_hostname_and_name_object_remain_supported
|
|
245
|
+
hostname = 'explicit-ns.example.test'
|
|
246
|
+
with_lookup({nameserver: ['192.0.2.1']}, answer_for(hostname, '192.0.2.53')) do |state, original_new|
|
|
247
|
+
resolver = original_new.call(nameserver: hostname)
|
|
248
|
+
assert_equal(['192.0.2.53'], resolver.single_resolvers.map(&:server))
|
|
249
|
+
assert_equal('192.0.2.53', Dnsruby::Config.resolve_server(Dnsruby::Name.create(hostname)))
|
|
250
|
+
2.times do
|
|
251
|
+
assert_equal([hostname, ['192.0.2.1']], state[:queries].pop(true).drop(1))
|
|
252
|
+
end
|
|
253
|
+
assert(state[:queries].empty?)
|
|
254
|
+
ensure
|
|
255
|
+
resolver.close if resolver
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def test_bootstrap_keeps_ipv6_scopes_and_localhost_but_excludes_hostnames
|
|
260
|
+
hostname = 'ns.example.test'
|
|
261
|
+
servers = [hostname, '192.0.2.1', '2001:db8::1', 'fe80::1%en0',
|
|
262
|
+
'fe80::1%3', 'localhost', '8.8.8.8,', 'fe80::1%']
|
|
263
|
+
with_lookup({nameserver: servers}, answer_for(hostname, '192.0.2.53')) do |state, _|
|
|
264
|
+
assert_equal('192.0.2.53', Dnsruby::Config.resolve_server(hostname))
|
|
265
|
+
assert_equal(['192.0.2.1', '2001:db8::1', 'fe80::1%en0', 'fe80::1%3', '127.0.0.1'],
|
|
266
|
+
state[:queries].pop(true)[2])
|
|
267
|
+
assert_equal([hostname, '192.0.2.1', '2001:db8::1', 'fe80::1%en0',
|
|
268
|
+
'fe80::1%3', 'localhost', '8.8.8.8,', 'fe80::1%'], servers)
|
|
269
|
+
assert(state[:queries].empty?)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def test_all_hostname_defaults_fail_before_creating_helper_resolver
|
|
274
|
+
hostname = 'ns.example.test'
|
|
275
|
+
with_lookup({nameserver: [hostname, 'other-ns.example.test']}, nil) do |state, _|
|
|
276
|
+
error = assert_raises(ArgumentError) { Dnsruby::Config.resolve_server(hostname) }
|
|
277
|
+
assert_match(/No nameserver addresses available/, error.message)
|
|
278
|
+
assert_equal(1, state[:counts][:config_reads])
|
|
279
|
+
assert_equal(0, state[:counts][:resolver_constructions])
|
|
280
|
+
assert(state[:queries].empty?)
|
|
281
|
+
assert(state[:recursors].empty?)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def test_recursive_fallback_accepts_addresses_and_reuses_safe_resolver
|
|
286
|
+
hostname = 'ns.example.test'
|
|
287
|
+
{ '192.0.2.53' => '192.0.2.53', '2001:db8::53' => '2001:DB8::53' }.each do |address, expected|
|
|
288
|
+
with_lookup({nameserver: [hostname, '192.0.2.1']}, answer_for(hostname),
|
|
289
|
+
recursive_response: answer_for(hostname, address)) do |state, _|
|
|
290
|
+
assert_equal(expected, Dnsruby::Config.resolve_server(hostname))
|
|
291
|
+
lookup = state[:queries].pop(true)
|
|
292
|
+
assert_equal(['192.0.2.1'], lookup[2])
|
|
293
|
+
assert_equal([lookup[0]], state[:recursors].pop(true))
|
|
294
|
+
assert_same(lookup[0], state[:closed].pop(true))
|
|
295
|
+
assert_equal(1, state[:counts][:resolver_constructions])
|
|
296
|
+
assert(state[:queries].empty?)
|
|
297
|
+
assert(state[:recursors].empty?)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def test_recursive_fallback_after_query_error_and_without_address
|
|
303
|
+
hostname = 'ns.example.test'
|
|
304
|
+
[answer_for(hostname, '192.0.2.53'), answer_for(hostname)].each do |response|
|
|
305
|
+
with_lookup({nameserver: [hostname, '192.0.2.1']}, nil,
|
|
306
|
+
lookup_error: Dnsruby::ResolvTimeout.new('simulated timeout'),
|
|
307
|
+
recursive_response: response) do |state, _|
|
|
308
|
+
if response.answer.empty?
|
|
309
|
+
error = assert_raises(ArgumentError) { Dnsruby::Config.resolve_server(hostname) }
|
|
310
|
+
assert_match(/Recursor can't locate/, error.message)
|
|
311
|
+
else
|
|
312
|
+
assert_equal('192.0.2.53', Dnsruby::Config.resolve_server(hostname))
|
|
313
|
+
end
|
|
314
|
+
lookup = state[:queries].pop(true)
|
|
315
|
+
assert_equal(['192.0.2.1'], lookup[2])
|
|
316
|
+
assert_equal([lookup[0]], state[:recursors].pop(true))
|
|
317
|
+
assert_same(lookup[0], state[:closed].pop(true))
|
|
318
|
+
assert_equal(1, state[:counts][:resolver_constructions])
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
data/test/tc_nsec.rb
CHANGED
|
@@ -126,50 +126,9 @@ class NsecTest < Minitest::Test
|
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
# and make sure that they fail verification for that reason
|
|
133
|
-
m = Message.new
|
|
134
|
-
m.header.rcode =
|
|
135
|
-
m.add_question(Question.new("a.z.w.example.", Types.MX))
|
|
136
|
-
m.add_answer(RR.create("a.z.w.example. 3600 IN MX 1 ai.example."))
|
|
137
|
-
m.add_answer(RR.create("a.z.w.example. 3600 RRSIG MX 5 4 3600 20040509183619 (
|
|
138
|
-
20040409183619 38519 example.
|
|
139
|
-
OMK8rAZlepfzLWW75Dxd63jy2wswESzxDKG2
|
|
140
|
-
f9AMN1CytCd10cYISAxfAdvXSZ7xujKAtPbc
|
|
141
|
-
tvOQ2ofO7AZJ+d01EeeQTVBPq4/6KCWhqe2X
|
|
142
|
-
TjnkVLNvvhnc0u28aoSsG0+4InvkkOHknKxw
|
|
143
|
-
4kX18MMR34i8lC36SR5xBni8vHI= )"))
|
|
144
|
-
m.add_authority(RR.create("x.y.w.example. 3600 NSEC xx.example. MX RRSIG NSEC"))
|
|
145
|
-
begin
|
|
146
|
-
Dnssec.anchor_verifier.verify_nsecs(m)
|
|
147
|
-
fail("Should have failed with bad number of labels in RRSIG")
|
|
148
|
-
rescue VerifyError
|
|
149
|
-
end
|
|
150
|
-
m.answer.delete(RR.create("a.z.w.example. 3600 RRSIG MX 5 4 3600 20040509183619 (
|
|
151
|
-
20040409183619 38519 example.
|
|
152
|
-
OMK8rAZlepfzLWW75Dxd63jy2wswESzxDKG2
|
|
153
|
-
f9AMN1CytCd10cYISAxfAdvXSZ7xujKAtPbc
|
|
154
|
-
tvOQ2ofO7AZJ+d01EeeQTVBPq4/6KCWhqe2X
|
|
155
|
-
TjnkVLNvvhnc0u28aoSsG0+4InvkkOHknKxw
|
|
156
|
-
4kX18MMR34i8lC36SR5xBni8vHI= )"))
|
|
157
|
-
m.add_answer(RR.create("a.z.w.example. 3600 RRSIG MX 5 2 3600 20040509183619 (
|
|
158
|
-
20040409183619 38519 example.
|
|
159
|
-
OMK8rAZlepfzLWW75Dxd63jy2wswESzxDKG2
|
|
160
|
-
f9AMN1CytCd10cYISAxfAdvXSZ7xujKAtPbc
|
|
161
|
-
tvOQ2ofO7AZJ+d01EeeQTVBPq4/6KCWhqe2X
|
|
162
|
-
TjnkVLNvvhnc0u28aoSsG0+4InvkkOHknKxw
|
|
163
|
-
4kX18MMR34i8lC36SR5xBni8vHI= )"))
|
|
164
|
-
Dnssec.anchor_verifier.verify_nsecs(m)
|
|
165
|
-
m.authority.delete(RR.create("x.y.w.example. 3600 NSEC xx.example. MX RRSIG NSEC"))
|
|
166
|
-
m.add_authority(RR.create("x.y.w.example. 3600 NSEC z.w.example. MX RRSIG NSEC"))
|
|
167
|
-
begin
|
|
168
|
-
Dnssec.anchor_verifier.verify_nsecs(m)
|
|
169
|
-
fail("Should have failed with bad NSEC")
|
|
170
|
-
rescue VerifyError
|
|
171
|
-
end
|
|
172
|
-
end
|
|
129
|
+
# Positive wildcard expansion from RFC 4035 is covered by
|
|
130
|
+
# DnssecWildcardProofsTest#test_examples_from_rfc_4035_wildcard_expansion,
|
|
131
|
+
# where both the answer and its denial proof undergo signature verification.
|
|
173
132
|
|
|
174
133
|
def test_examples_from_rfc_4035_wildcard_no_data
|
|
175
134
|
# Grab the example responses from RFC4035 and make sure that they pass.
|
|
@@ -223,4 +182,4 @@ class NsecTest < Minitest::Test
|
|
|
223
182
|
# fail
|
|
224
183
|
# end
|
|
225
184
|
#
|
|
226
|
-
end
|
|
185
|
+
end
|
data/test/tc_nsec3.rb
CHANGED
|
@@ -69,6 +69,22 @@ class Nsec3Test < Minitest::Test
|
|
|
69
69
|
assert_equal(nsec.to_s, nsec3.to_s)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
def test_future_flags_round_trip_but_are_unusable_for_denial
|
|
73
|
+
record = Dnsruby::RR.create(INPUT)
|
|
74
|
+
record.flags = 2
|
|
75
|
+
message = Dnsruby::Message.new
|
|
76
|
+
message.add_authority(record)
|
|
77
|
+
decoded = Dnsruby::Message.decode(message.encode).authority.first
|
|
78
|
+
assert_equal(2, decoded.flags)
|
|
79
|
+
assert_equal(record.to_s, decoded.to_s)
|
|
80
|
+
refute(decoded.supported_parameters?)
|
|
81
|
+
refute(decoded.usable_hash?)
|
|
82
|
+
[0, 1].each do |flags|
|
|
83
|
+
decoded.flags = flags
|
|
84
|
+
assert(decoded.usable_hash?)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
72
88
|
def test_calculate_hash
|
|
73
89
|
input = [
|
|
74
90
|
[ "example" , "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom"],
|
|
@@ -113,7 +129,7 @@ class Nsec3Test < Minitest::Test
|
|
|
113
129
|
# rescue DecodeError
|
|
114
130
|
# end
|
|
115
131
|
begin
|
|
116
|
-
nsec.flags =
|
|
132
|
+
nsec.flags = 256
|
|
117
133
|
fail
|
|
118
134
|
rescue DecodeError
|
|
119
135
|
end
|