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,145 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require 'openssl'
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
require 'tempfile'
|
|
6
|
+
|
|
7
|
+
class SvcbSignedTargetTest < Minitest::Test
|
|
8
|
+
MIXED_CASE_RDATA = ['000103466f6f074578616d706c6503434f4d00'].pack('H*').freeze
|
|
9
|
+
|
|
10
|
+
def setup
|
|
11
|
+
@rsa = OpenSSL::PKey::RSA.generate(2048)
|
|
12
|
+
exponent = @rsa.e.to_s(2)
|
|
13
|
+
key_bytes = [exponent.bytesize].pack('C') + exponent + @rsa.n.to_s(2)
|
|
14
|
+
@key = Dnsruby::RR.create("example. 300 IN DNSKEY 256 3 8 #{Base64.strict_encode64(key_bytes)}")
|
|
15
|
+
@verifier = Dnsruby::SingleVerifier.new(Dnsruby::SingleVerifier::VerifierType::ANCHOR)
|
|
16
|
+
@verifier.add_trust_anchor(@key)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_signed_text_roundtrip_preserves_target_case
|
|
20
|
+
[64, 65].each do |type|
|
|
21
|
+
original, signature = signed_record(type)
|
|
22
|
+
assert_signed_copy(original, Dnsruby::RR.create(original.to_s), signature)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_case_distinct_targets_remain_separate_in_signed_rrsets
|
|
27
|
+
[64, 65].each do |type|
|
|
28
|
+
records = ['Foo.Example.COM.', 'foo.example.com.'].map do |target|
|
|
29
|
+
Dnsruby::RR.create(name: 'example.', type: type, ttl: 300,
|
|
30
|
+
priority: 1, target: target)
|
|
31
|
+
end
|
|
32
|
+
refute_equal(records.first, records.last)
|
|
33
|
+
refute(records.first.eql?(records.last))
|
|
34
|
+
assert_equal(2, records.to_h { |record| [record, true] }.length)
|
|
35
|
+
|
|
36
|
+
same_record = records.first.clone
|
|
37
|
+
same_record.ttl = 60
|
|
38
|
+
assert_equal(records.first, same_record, 'TTL must remain irrelevant to RR equality')
|
|
39
|
+
assert(records.first.eql?(same_record))
|
|
40
|
+
assert_equal(records.first.hash, same_record.hash)
|
|
41
|
+
|
|
42
|
+
# Sign both original records before any RRSet or Message can deduplicate
|
|
43
|
+
# them. The signature must cover both case-distinct RDATA byte strings.
|
|
44
|
+
signature = signature_for(records)
|
|
45
|
+
rrset = Dnsruby::RRSet.new(records + [signature])
|
|
46
|
+
assert_equal(2, rrset.rrs.length)
|
|
47
|
+
assert(@verifier.verify(rrset))
|
|
48
|
+
|
|
49
|
+
message = Dnsruby::Message.new('example.', type)
|
|
50
|
+
(records + [signature]).each { |record| message.add_answer(record) }
|
|
51
|
+
decoded = Dnsruby::Message.decode(message.encode)
|
|
52
|
+
assert_equal(3, decoded.answer.length)
|
|
53
|
+
assert(@verifier.verify(Dnsruby::RRSet.new(decoded.answer.to_a)))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_signed_hash_constructor_preserves_string_and_name_targets
|
|
58
|
+
[64, 65].each do |type|
|
|
59
|
+
original, signature = signed_record(type)
|
|
60
|
+
['Foo.Example.COM.', original.target].each do |target|
|
|
61
|
+
copy = Dnsruby::RR.create(name: original.name, type: type, ttl: 300,
|
|
62
|
+
priority: 1, target: target)
|
|
63
|
+
assert_signed_copy(original, copy, signature)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_signed_saved_zone_roundtrip_preserves_target_case
|
|
69
|
+
[64, 65].each do |type|
|
|
70
|
+
original, signature = signed_record(type)
|
|
71
|
+
Tempfile.create(['dnsruby-signed-target', '.zone']) do |zone|
|
|
72
|
+
zone.write("#{original}\n#{signature}\n")
|
|
73
|
+
zone.flush
|
|
74
|
+
records = Dnsruby::ZoneReader.new('example.').process_file(zone.path)
|
|
75
|
+
assert_signed_copy(original, records.first, records.last)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_signed_relative_zone_target_preserves_target_and_origin_case
|
|
81
|
+
[64, 65].each do |type|
|
|
82
|
+
original, signature = signed_record(type)
|
|
83
|
+
text = "example. 300 IN TYPE#{type} 1 Foo\n#{signature}\n"
|
|
84
|
+
records = Dnsruby::ZoneReader.new('Example.COM.').process_file(StringIO.new(text))
|
|
85
|
+
assert_signed_copy(original, records.first, records.last)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_ascii_escaped_target_preserves_wire_octets
|
|
90
|
+
[64, 65].each do |type|
|
|
91
|
+
target = 'F\\111o\\.Bar.\\255.Example.COM.'
|
|
92
|
+
expected = [1].pack('n') + "\x07Foo.Bar\x01\xff\x07Example\x03COM\0".b
|
|
93
|
+
string = Dnsruby::RR.create("example. IN TYPE#{type} 1 #{target}")
|
|
94
|
+
hash = Dnsruby::RR.create(name: 'example.', type: type, priority: 1, target: target)
|
|
95
|
+
[string, hash, Dnsruby::RR.create(string.to_s)].each do |record|
|
|
96
|
+
assert_equal(expected, record.rdata)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_unicode_targets_retain_idna_conversion
|
|
102
|
+
[64, 65].each do |type|
|
|
103
|
+
target = 'フガフガ。hogehoge.エグザンプル.JP.'
|
|
104
|
+
string = Dnsruby::RR.create("example. IN TYPE#{type} 1 #{target}")
|
|
105
|
+
hash = Dnsruby::RR.create(name: 'example.', type: type, priority: 1, target: target)
|
|
106
|
+
[string, hash].each do |record|
|
|
107
|
+
assert_equal('xn--mcka5jb.hogehoge.xn--ickqs6k2dyb.jp.', record.target.to_s(true))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
text = "example. IN TYPE#{type} 1 Foo\n"
|
|
111
|
+
record = Dnsruby::ZoneReader.new('møllerriis.com.').process_file(StringIO.new(text)).first
|
|
112
|
+
assert_equal('Foo.xn--mllerriis-l8a.com.', record.target.to_s(true))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def signed_record(type)
|
|
119
|
+
record = Dnsruby::RR.new_from_data('example.', type, Dnsruby::Classes::IN, 300,
|
|
120
|
+
MIXED_CASE_RDATA.bytesize, MIXED_CASE_RDATA, 0)
|
|
121
|
+
signature = signature_for([record])
|
|
122
|
+
assert(@verifier.verify(Dnsruby::RRSet.new([record, signature])), 'original wire record must verify')
|
|
123
|
+
[record, signature]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def signature_for(records)
|
|
127
|
+
record = records.first
|
|
128
|
+
now = Time.now.to_i
|
|
129
|
+
signature = Dnsruby::RR.create(name: record.name, type: 'RRSIG', ttl: 300,
|
|
130
|
+
type_covered: record.type, algorithm: 8, labels: 1, original_ttl: 300,
|
|
131
|
+
expiration: now + 3600, inception: now - 60, key_tag: @key.key_tag,
|
|
132
|
+
signers_name: 'example.')
|
|
133
|
+
canonical = records.sort_by(&:rdata).map do |rr|
|
|
134
|
+
Dnsruby::MessageEncoder.new { |encoder| encoder.put_rr(rr, true) }.to_s
|
|
135
|
+
end.join
|
|
136
|
+
signature.signature = @rsa.sign(OpenSSL::Digest::SHA256.new, signature.sig_data + canonical)
|
|
137
|
+
signature
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def assert_signed_copy(original, copy, signature)
|
|
141
|
+
assert_equal(original.rdata, copy.rdata)
|
|
142
|
+
assert_equal('Foo.Example.COM.', copy.target.to_s(true))
|
|
143
|
+
assert(@verifier.verify(Dnsruby::RRSet.new([copy, signature])), 'reconstructed record must retain its signature')
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require_relative 'spec_helper'
|
|
2
|
+
require_relative 'socket_helpers'
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
class TestSVCBTransport < Minitest::Test
|
|
7
|
+
RDATA = ['0001000003000201bb'].pack('H*').freeze
|
|
8
|
+
|
|
9
|
+
def test_every_truncated_svcb_and_https_rdata_boundary_retries_over_tcp
|
|
10
|
+
truncated = ->(query) { response(query, truncated_at: query.question.first.qname.to_s[/\Acut(\d+)/, 1].to_i) }
|
|
11
|
+
with_server(truncated, ->(query) { response(query) }) do |port, events|
|
|
12
|
+
resolver = resolver_for(port)
|
|
13
|
+
begin
|
|
14
|
+
[64, 65].each do |type|
|
|
15
|
+
RDATA.bytesize.times do |length|
|
|
16
|
+
answer = Timeout.timeout(3) { resolver.query("cut#{length}.example.", type) }.answer.first
|
|
17
|
+
assert_equal(type, answer.type.code)
|
|
18
|
+
assert_equal(RDATA, answer.data)
|
|
19
|
+
assert_equal('443', answer.params_to_hash['port'])
|
|
20
|
+
assert_equal(:udp, events.pop(true))
|
|
21
|
+
assert_equal(:tcp, events.pop(true))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
assert(events.empty?)
|
|
25
|
+
ensure
|
|
26
|
+
resolver.close
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_malformed_svcb_does_not_bypass_transport_policy_or_retry_tcp
|
|
32
|
+
truncated = ->(query) { response(query, truncated_at: 3) }
|
|
33
|
+
malformed = ->(query) { response(query, data: ['0001000003000301bb'].pack('H*')) }
|
|
34
|
+
[
|
|
35
|
+
[{no_tcp: true}, truncated, :udp],
|
|
36
|
+
[{ignore_truncation: true}, truncated, :udp],
|
|
37
|
+
[{no_tcp: true, ignore_truncation: true}, truncated, :udp],
|
|
38
|
+
[{use_tcp: true}, truncated, :tcp],
|
|
39
|
+
[{}, malformed, :udp],
|
|
40
|
+
].each do |options, reply, expected_transport|
|
|
41
|
+
with_server(reply, reply) do |port, events|
|
|
42
|
+
resolver = resolver_for(port, options)
|
|
43
|
+
begin
|
|
44
|
+
assert_raises(Dnsruby::DecodeError, options.inspect) do
|
|
45
|
+
Timeout.timeout(3) { resolver.query('invalid.example.', 65) }
|
|
46
|
+
end
|
|
47
|
+
assert_equal(expected_transport, events.pop(true))
|
|
48
|
+
assert(events.empty?, 'terminal malformed response must not start another attempt')
|
|
49
|
+
ensure
|
|
50
|
+
resolver.close
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def resolver_for(port, options = {})
|
|
59
|
+
Dnsruby::Resolver.new({nameserver: '127.0.0.1', port: port,
|
|
60
|
+
dnssec: false, do_validation: false, do_caching: false,
|
|
61
|
+
packet_timeout: 1, query_timeout: 2, retry_times: 1}.merge(options))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The answer's declared length always describes the complete RDATA, even
|
|
65
|
+
# when the UDP packet ends early. Build that length independently of the
|
|
66
|
+
# production RR encoder so the regression cannot repair its own fixture.
|
|
67
|
+
def response(query, truncated_at: nil, data: RDATA)
|
|
68
|
+
question = query.question.first
|
|
69
|
+
encoded_question = Dnsruby::MessageEncoder.new do |encoder|
|
|
70
|
+
encoder.put_name(question.qname)
|
|
71
|
+
encoder.put_pack('nn', question.qtype.code, question.qclass.code)
|
|
72
|
+
end.to_s
|
|
73
|
+
flags = truncated_at ? 0x8380 : 0x8180
|
|
74
|
+
header = [query.header.id, flags, 1, 1, 0, 0].pack('n6')
|
|
75
|
+
rr = "\xc0\x0c".b + [question.qtype.code, 1, 60, data.bytesize].pack('nnNn')
|
|
76
|
+
header + encoded_question + rr + (truncated_at ? data.byteslice(0, truncated_at) : data)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def with_server(udp_reply, tcp_reply)
|
|
80
|
+
udp, tcp = TestSockets.udp_tcp_pair
|
|
81
|
+
events, errors = Queue.new, Queue.new
|
|
82
|
+
running = true
|
|
83
|
+
workers = [[udp, :udp, udp_reply], [tcp, :tcp, tcp_reply]].map do |listener, transport, reply|
|
|
84
|
+
Thread.new do
|
|
85
|
+
while running
|
|
86
|
+
next unless IO.select([listener], nil, nil, 0.05)
|
|
87
|
+
events << transport
|
|
88
|
+
if transport == :udp
|
|
89
|
+
bytes, peer = listener.recvfrom(4096)
|
|
90
|
+
listener.send(reply.call(Dnsruby::Message.decode(bytes)), 0, peer[3], peer[1])
|
|
91
|
+
else
|
|
92
|
+
socket = listener.accept
|
|
93
|
+
begin
|
|
94
|
+
Timeout.timeout(2) do
|
|
95
|
+
length = socket.read(2).unpack1('n')
|
|
96
|
+
bytes = reply.call(Dnsruby::Message.decode(socket.read(length)))
|
|
97
|
+
socket.write([bytes.bytesize].pack('n') + bytes)
|
|
98
|
+
end
|
|
99
|
+
ensure
|
|
100
|
+
socket.close
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
rescue StandardError => error
|
|
105
|
+
errors << error
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
yield udp.addr[1], events
|
|
109
|
+
assert(errors.empty?, errors.empty? ? nil : errors.pop.full_message)
|
|
110
|
+
ensure
|
|
111
|
+
running = false
|
|
112
|
+
(workers || []).each do |worker|
|
|
113
|
+
worker.join(0.2)
|
|
114
|
+
worker.kill if worker.alive?
|
|
115
|
+
worker.join
|
|
116
|
+
end
|
|
117
|
+
tcp.close if tcp && !tcp.closed?
|
|
118
|
+
udp.close if udp && !udp.closed?
|
|
119
|
+
end
|
|
120
|
+
end
|
data/test/tc_tcp.rb
CHANGED
|
@@ -15,56 +15,80 @@
|
|
|
15
15
|
# ++
|
|
16
16
|
|
|
17
17
|
require_relative 'spec_helper'
|
|
18
|
+
require_relative 'local_dns_server'
|
|
18
19
|
|
|
19
20
|
require 'socket'
|
|
20
21
|
class TestTcp < Minitest::Test
|
|
22
|
+
include LocalDnsServer
|
|
23
|
+
|
|
21
24
|
def test_TCP
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
with_local_dns_server(:tcp, answer: 'example.com 60 IN A 192.0.2.1') do |resolver|
|
|
26
|
+
response = resolver.query('example.com')
|
|
27
|
+
assert_kind_of(Dnsruby::Message, response)
|
|
28
|
+
assert_equal(Dnsruby::RCode.NOERROR, response.rcode)
|
|
29
|
+
assert_equal(['192.0.2.1'], response.answer.map { |rr| rr.address.to_s })
|
|
30
|
+
end
|
|
26
31
|
end
|
|
27
32
|
def test_TCP_port
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
3.times do |attempt|
|
|
34
|
+
listener = reservation = server = resolver = nil
|
|
35
|
+
begin
|
|
36
|
+
listener = TCPServer.new('127.0.0.1', 0)
|
|
37
|
+
# OS-selected ephemeral ports can be below the resolver's permitted
|
|
38
|
+
# source-port range. Reserve an allowed port without SO_REUSEADDR.
|
|
39
|
+
reservation = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
|
40
|
+
src_port = (50_000..65_535).to_a.shuffle.find do |port|
|
|
41
|
+
begin
|
|
42
|
+
reservation.bind(Socket.sockaddr_in(port, '127.0.0.1'))
|
|
43
|
+
true
|
|
44
|
+
rescue Errno::EADDRINUSE
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
raise IOError, 'No available resolver source port' unless src_port
|
|
49
|
+
server = Thread.new do
|
|
50
|
+
begin
|
|
51
|
+
client = listener.accept
|
|
52
|
+
received_port = client.peeraddr[1]
|
|
53
|
+
length = client.read(2).unpack1('n')
|
|
54
|
+
response = Dnsruby::Message.decode(client.read(length))
|
|
55
|
+
response.header.qr = true
|
|
56
|
+
wire = response.encode
|
|
57
|
+
client.write([wire.bytesize].pack('n') + wire)
|
|
58
|
+
received_port
|
|
59
|
+
rescue StandardError => error
|
|
60
|
+
error
|
|
61
|
+
ensure
|
|
62
|
+
client.close if client && !client.closed?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
resolver = Dnsruby::SingleResolver.new(
|
|
66
|
+
server: '127.0.0.1', port: listener.addr[1], use_tcp: true,
|
|
67
|
+
src_address: '127.0.0.1', src_port: src_port, do_caching: false,
|
|
68
|
+
dnssec: false, packet_timeout: 2
|
|
69
|
+
)
|
|
70
|
+
assert_equal(src_port, resolver.src_port)
|
|
71
|
+
Timeout.timeout(5) do
|
|
72
|
+
reservation.close
|
|
73
|
+
response = resolver.query('example.com')
|
|
74
|
+
received_port = server.value
|
|
75
|
+
raise received_port if received_port.is_a?(StandardError)
|
|
76
|
+
assert_equal(src_port, received_port)
|
|
77
|
+
assert_kind_of(Dnsruby::Message, response)
|
|
78
|
+
end
|
|
79
|
+
return
|
|
80
|
+
rescue IOError => error
|
|
81
|
+
# Another process can claim the port between release and bind.
|
|
82
|
+
# PacketSender wraps that specific bind failure in IOError.
|
|
83
|
+
port_in_use = error.message.match?(/\Adnsruby can't connect to [^,\n]+, use_tcp=true, exception = Errno::EADDRINUSE,/)
|
|
84
|
+
raise unless attempt < 2 && port_in_use
|
|
85
|
+
ensure
|
|
86
|
+
resolver.close if resolver
|
|
87
|
+
server.kill.join if server
|
|
88
|
+
reservation.close if reservation && !reservation.closed?
|
|
89
|
+
listener.close if listener && !listener.closed?
|
|
44
90
|
end
|
|
45
|
-
|
|
46
|
-
tcpPacket = Dnsruby::Message.decode(packet)
|
|
47
|
-
tcpPacket.header.tc = true
|
|
48
|
-
lenmsg = [tcpPacket.encode.length].pack('n')
|
|
49
|
-
t.send(lenmsg, 0)
|
|
50
|
-
t.write(tcpPacket.encode)
|
|
51
|
-
t.close
|
|
52
|
-
ts.close
|
|
53
|
-
}
|
|
54
|
-
ret = nil
|
|
55
|
-
sleep(1)
|
|
56
|
-
client_thread = Thread.new {
|
|
57
|
-
# res = Dnsruby::SingleResolver.new("127.0.0.1")
|
|
58
|
-
res = Dnsruby::SingleResolver.new("localhost")
|
|
59
|
-
res.port = port
|
|
60
|
-
res.use_tcp = true
|
|
61
|
-
res.src_port=src_port
|
|
62
|
-
ret=res.query("example.com")
|
|
63
|
-
}
|
|
64
|
-
server_thread.join
|
|
65
|
-
client_thread.join
|
|
66
|
-
assert(received_port == src_port)
|
|
67
|
-
assert(ret.is_a?(Dnsruby::Message))
|
|
91
|
+
end
|
|
68
92
|
end
|
|
69
93
|
|
|
70
94
|
# def test_no_tcp
|
|
@@ -140,6 +164,7 @@ class TestTcp < Minitest::Test
|
|
|
140
164
|
received_query = s[0]
|
|
141
165
|
socket.connect(s[1][2], s[1][1])
|
|
142
166
|
ans = HackMessage.decode(received_query)
|
|
167
|
+
ans.header.qr = true
|
|
143
168
|
ans.wipe_additional
|
|
144
169
|
100.times {|i|
|
|
145
170
|
ans.add_additional(Dnsruby::RR.create("example.com 3600 IN A 1.2.3.#{i}"))
|
|
@@ -160,6 +185,7 @@ class TestTcp < Minitest::Test
|
|
|
160
185
|
end
|
|
161
186
|
packet = t.recvfrom(len)[0]
|
|
162
187
|
tcpPacket = HackMessage.decode(packet)
|
|
188
|
+
tcpPacket.header.qr = true
|
|
163
189
|
tcpPacket.wipe_additional
|
|
164
190
|
110.times {|i|
|
|
165
191
|
tcpPacket.add_additional(Dnsruby::RR.create("example.com 3600 IN A 1.2.3.#{i}"))
|