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.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -4
  3. data/DNSSEC +3 -2
  4. data/EXAMPLES +1 -1
  5. data/README.md +370 -64
  6. data/RELEASE_NOTES.md +314 -1
  7. data/Rakefile +0 -3
  8. data/demo/axfr.rb +24 -6
  9. data/dnsruby.gemspec +6 -4
  10. data/lib/dnsruby/cache.rb +279 -49
  11. data/lib/dnsruby/code_mapper.rb +35 -8
  12. data/lib/dnsruby/code_mappers.rb +63 -49
  13. data/lib/dnsruby/config.rb +77 -4
  14. data/lib/dnsruby/deadline_queue.rb +75 -0
  15. data/lib/dnsruby/dnssec.rb +148 -45
  16. data/lib/dnsruby/ipv6.rb +45 -5
  17. data/lib/dnsruby/key_cache.rb +202 -35
  18. data/lib/dnsruby/message/decoder.rb +85 -19
  19. data/lib/dnsruby/message/encoder.rb +27 -16
  20. data/lib/dnsruby/message/header.rb +8 -12
  21. data/lib/dnsruby/message/message.rb +53 -22
  22. data/lib/dnsruby/message/section.rb +12 -34
  23. data/lib/dnsruby/name.rb +5 -2
  24. data/lib/dnsruby/packet_sender.rb +664 -207
  25. data/lib/dnsruby/presentation.rb +147 -0
  26. data/lib/dnsruby/query_control.rb +132 -0
  27. data/lib/dnsruby/recursor.rb +798 -341
  28. data/lib/dnsruby/resolver.rb +472 -176
  29. data/lib/dnsruby/resource/CAA.rb +67 -16
  30. data/lib/dnsruby/resource/DLV.rb +6 -0
  31. data/lib/dnsruby/resource/DNSKEY.rb +108 -16
  32. data/lib/dnsruby/resource/DS.rb +27 -28
  33. data/lib/dnsruby/resource/HTTPS.rb +16 -0
  34. data/lib/dnsruby/resource/IN.rb +21 -2
  35. data/lib/dnsruby/resource/MX.rb +2 -2
  36. data/lib/dnsruby/resource/NSEC.rb +6 -10
  37. data/lib/dnsruby/resource/NSEC3.rb +55 -19
  38. data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
  39. data/lib/dnsruby/resource/OPT.rb +2 -3
  40. data/lib/dnsruby/resource/RR.rb +108 -65
  41. data/lib/dnsruby/resource/RRSIG.rb +5 -5
  42. data/lib/dnsruby/resource/RRSet.rb +38 -22
  43. data/lib/dnsruby/resource/SRV.rb +2 -2
  44. data/lib/dnsruby/resource/SVCB.rb +775 -0
  45. data/lib/dnsruby/resource/TSIG.rb +266 -231
  46. data/lib/dnsruby/resource/TXT.rb +34 -42
  47. data/lib/dnsruby/resource/domain_name.rb +1 -1
  48. data/lib/dnsruby/resource/generic.rb +31 -0
  49. data/lib/dnsruby/resource/generic_cache.rb +384 -0
  50. data/lib/dnsruby/resource/generic_constants.rb +79 -0
  51. data/lib/dnsruby/select_thread.rb +986 -291
  52. data/lib/dnsruby/serial_time.rb +25 -0
  53. data/lib/dnsruby/single_resolver.rb +25 -6
  54. data/lib/dnsruby/single_verifier.rb +1620 -530
  55. data/lib/dnsruby/transport_clock.rb +9 -0
  56. data/lib/dnsruby/update.rb +8 -6
  57. data/lib/dnsruby/validation_budget.rb +143 -0
  58. data/lib/dnsruby/validator_thread.rb +139 -58
  59. data/lib/dnsruby/version.rb +1 -1
  60. data/lib/dnsruby/zone_reader.rb +196 -129
  61. data/lib/dnsruby/zone_transfer.rb +157 -25
  62. data/lib/dnsruby.rb +7 -2
  63. data/test/ci_watchdog.rb +33 -0
  64. data/test/fixtures/org-ds-2026-11-05.zone +5 -0
  65. data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
  66. data/test/fork_test_helper.rb +37 -0
  67. data/test/local_dns_server.rb +57 -0
  68. data/test/soak_server.rb +152 -0
  69. data/test/socket_helpers.rb +26 -0
  70. data/test/spec_helper.rb +8 -13
  71. data/test/tc_attempt_configuration.rb +137 -0
  72. data/test/tc_caa.rb +2 -2
  73. data/test/tc_cache_keys.rb +36 -0
  74. data/test/tc_cancelled_send_capacity.rb +158 -0
  75. data/test/tc_dnssec_alias_validation.rb +289 -0
  76. data/test/tc_dnssec_budget.rb +489 -0
  77. data/test/tc_dnssec_cache_bounds.rb +197 -0
  78. data/test/tc_dnssec_cache_lifetime.rb +615 -0
  79. data/test/tc_dnssec_denial_proofs.rb +285 -0
  80. data/test/tc_dnssec_recursion_limits.rb +186 -0
  81. data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
  82. data/test/tc_dnssec_trust_anchors.rb +286 -0
  83. data/test/tc_dnssec_trust_races.rb +147 -0
  84. data/test/tc_dnssec_wildcard_proofs.rb +249 -0
  85. data/test/tc_ds.rb +5 -5
  86. data/test/tc_edns_retry_regressions.rb +255 -0
  87. data/test/tc_escapedchars.rb +8 -0
  88. data/test/tc_final_review_fixes.rb +167 -0
  89. data/test/tc_final_transport_contracts.rb +104 -0
  90. data/test/tc_generic_cache.rb +852 -0
  91. data/test/tc_header.rb +26 -0
  92. data/test/tc_ipv6.rb +120 -0
  93. data/test/tc_key_cache_identity.rb +382 -0
  94. data/test/tc_malformed_tcp.rb +712 -0
  95. data/test/tc_name.rb +99 -5
  96. data/test/tc_nameserver_bootstrap.rb +322 -0
  97. data/test/tc_nsec.rb +4 -45
  98. data/test/tc_nsec3.rb +17 -1
  99. data/test/tc_pipeline_retirement.rb +358 -0
  100. data/test/tc_pipeline_retry.rb +462 -0
  101. data/test/tc_pipeline_send.rb +663 -0
  102. data/test/tc_protocol_review_fixes.rb +95 -0
  103. data/test/tc_query_control.rb +90 -0
  104. data/test/tc_record_codec_contracts.rb +107 -0
  105. data/test/tc_record_regressions.rb +225 -0
  106. data/test/tc_recur.rb +1 -0
  107. data/test/tc_recursor_address_fallback.rb +169 -0
  108. data/test/tc_recursor_limits.rb +139 -0
  109. data/test/tc_release_auxiliary_fixes.rb +288 -0
  110. data/test/tc_release_configuration_fixes.rb +612 -0
  111. data/test/tc_release_dnssec_completion.rb +405 -0
  112. data/test/tc_release_dnssec_fixes.rb +305 -0
  113. data/test/tc_release_negative_cache.rb +327 -0
  114. data/test/tc_release_recursor_cache.rb +212 -0
  115. data/test/tc_release_review_transport.rb +372 -0
  116. data/test/tc_release_review_transport_native.rb +56 -0
  117. data/test/tc_release_transport.rb +906 -0
  118. data/test/tc_release_transport_fixes.rb +196 -0
  119. data/test/tc_release_zone_defaults.rb +157 -0
  120. data/test/tc_release_zone_ttls.rb +39 -0
  121. data/test/tc_res_config.rb +53 -2
  122. data/test/tc_res_opt.rb +21 -1
  123. data/test/tc_resolver.rb +34 -19
  124. data/test/tc_resolver_concurrency.rb +383 -0
  125. data/test/tc_resolver_configuration_contracts.rb +31 -0
  126. data/test/tc_resolver_validation.rb +211 -0
  127. data/test/tc_review_cache.rb +163 -0
  128. data/test/tc_review_code_mapper.rb +279 -0
  129. data/test/tc_review_control_deadlines.rb +169 -0
  130. data/test/tc_review_followup_transport.rb +583 -0
  131. data/test/tc_review_packet_contracts.rb +369 -0
  132. data/test/tc_review_presentation.rb +213 -0
  133. data/test/tc_review_record_compatibility.rb +101 -0
  134. data/test/tc_review_transport_fixes.rb +195 -0
  135. data/test/tc_review_transport_native.rb +415 -0
  136. data/test/tc_review_validation_priming.rb +178 -0
  137. data/test/tc_rr-opt.rb +5 -2
  138. data/test/tc_rr-unknown.rb +19 -0
  139. data/test/tc_rr.rb +14 -0
  140. data/test/tc_rrset_grouping.rb +50 -0
  141. data/test/tc_rrset_review_fixes.rb +58 -0
  142. data/test/tc_rrsig.rb +21 -1
  143. data/test/tc_scoped_ipv6.rb +153 -0
  144. data/test/tc_scoped_ipv6_select.rb +157 -0
  145. data/test/tc_scoped_ipv6_transport.rb +329 -0
  146. data/test/tc_selector_wakeup.rb +42 -0
  147. data/test/tc_soak.rb +118 -196
  148. data/test/tc_socket_helpers.rb +90 -0
  149. data/test/tc_supporting_lookup_limits.rb +252 -0
  150. data/test/tc_svcb.rb +1043 -0
  151. data/test/tc_svcb_signed_target.rb +145 -0
  152. data/test/tc_svcb_transport.rb +120 -0
  153. data/test/tc_tcp.rb +69 -43
  154. data/test/tc_tcp_pipelining.rb +95 -204
  155. data/test/tc_transport_fork.rb +179 -0
  156. data/test/tc_tsig.rb +2 -2
  157. data/test/tc_tsig_api.rb +251 -0
  158. data/test/tc_tsig_rfc8945.rb +357 -0
  159. data/test/tc_v2_release_dnssec.rb +442 -0
  160. data/test/tc_v2_release_transport.rb +304 -0
  161. data/test/tc_v2_zone_transfer_native.rb +58 -0
  162. data/test/tc_zone_reader.rb +217 -1
  163. data/test/tcp_pipelining_server.rb +150 -0
  164. data/test/ts_offline.rb +77 -6
  165. data/test/ts_online.rb +2 -1
  166. metadata +127 -14
  167. 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
- res = Dnsruby::Resolver.new()
23
- res.use_tcp = true
24
- ret=res.query("example.com")
25
- assert(ret.is_a?(Dnsruby::Message))
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
- # Need a test server so we can tell what port this message was actually sent on!
29
- port = nil
30
- src_port = 57923
31
- Dnsruby::PacketSender.clear_caches
32
- received_port = nil
33
- server_thread = Thread.new {
34
- ts = TCPServer.new(0)
35
- port = ts.addr[1]
36
- t = ts.accept
37
- # Check that the source port was src_port
38
- received_port = t.peeraddr()[1]
39
- packet = t.recvfrom(2)[0]
40
-
41
- len = (packet[0]<<8)+packet[1]
42
- if (RUBY_VERSION >= "1.9")
43
- len = (packet[0].getbyte(0)<<8)+packet[1].getbyte(0)# Ruby 1.9
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
- packet = t.recvfrom(len)[0]
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}"))