1 # frozen_string_literal: false
8 def stub name, val_or_callable, &block
9 new_name = "__minitest_stub__#{name}"
11 metaclass = class << self; self; end
13 if respond_to? name and not methods.map(&:to_s).include? name.to_s then
14 metaclass.send :define_method, name do |*args|
19 metaclass.send :alias_method, new_name, name
21 metaclass.send :define_method, name do |*args|
22 if val_or_callable.respond_to? :call then
23 val_or_callable.call(*args)
31 metaclass.send :undef_method, name
32 metaclass.send :alias_method, name, new_name
33 metaclass.send :undef_method, new_name
34 end unless method_defined?(:stub) # lib/rubygems/test_case.rb also has the same method definition
37 class TestResolvDNS < Test::Unit::TestCase
39 @save_do_not_reverse_lookup = BasicSocket.do_not_reverse_lookup
40 BasicSocket.do_not_reverse_lookup = true
44 BasicSocket.do_not_reverse_lookup = @save_do_not_reverse_lookup
47 def with_tcp(host, port)
48 t = TCPServer.new(host, port)
57 def with_udp(host, port)
67 def with_udp_and_tcp(host, port)
69 # Automatic port; we might need to retry until we find a port which is free on both UDP _and_ TCP.
77 _, udp_port, _, _ = u.addr
78 t = TCPServer.new(host, udp_port)
80 rescue Errno::EADDRINUSE, Errno::EACCES
81 # ADDRINUSE is what should get thrown if we try and bind a port which is already bound on UNIXen,
82 # but windows can sometimes throw EACCESS.
83 # See: https://stackoverflow.com/questions/48478869/cannot-bind-to-some-ports-due-to-permission-denied
84 retries_remaining -= 1
85 if retries_remaining > 0
95 # If we get to this point, we have a valid t & u socket
102 # Explicitly specified port, don't retry the bind.
103 with_udp(host, port) do |u|
104 with_tcp(host, port) do |t|
112 def test_resolve_with_2_ndots
113 conf = Resolv::DNS::Config.new :nameserver => ['127.0.0.1'], :ndots => 2
117 conf.resolv('example.com') { |candidate, *args|
118 candidates << candidate
119 raise Resolv::DNS::Config::NXDomain
121 n = Resolv::DNS::Name.create 'example.com.'
122 assert_equal n, candidates.last
125 def test_query_ipv4_address
129 omit 'autoload problem. see [ruby-dev:45021][Bug #5786]'
130 end if defined?(OpenSSL)
132 with_udp('127.0.0.1', 0) {|u|
133 _, server_port, _, server_address = u.addr
135 client_thread = Thread.new {
136 Resolv::DNS.open(:nameserver_port => [[server_address, server_port]]) {|dns|
137 dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A)
140 server_thread = Thread.new {
141 msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)}
142 id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
143 qr = (word2 & 0x8000) >> 15
144 opcode = (word2 & 0x7800) >> 11
145 aa = (word2 & 0x0400) >> 10
146 tc = (word2 & 0x0200) >> 9
147 rd = (word2 & 0x0100) >> 8
148 ra = (word2 & 0x0080) >> 7
149 z = (word2 & 0x0070) >> 4
150 rcode = word2 & 0x000f
152 assert_equal(0, qr) # 0:query 1:response
153 assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS
154 assert_equal(0, aa) # Authoritative Answer
155 assert_equal(0, tc) # TrunCation
156 assert_equal(1, rd) # Recursion Desired
157 assert_equal(0, ra) # Recursion Available
158 assert_equal(0, z) # Reserved for future use
159 assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused
160 assert_equal(1, qdcount) # number of entries in the question section.
161 assert_equal(0, ancount) # number of entries in the answer section.
162 assert_equal(0, nscount) # number of entries in the authority records section.
163 assert_equal(0, arcount) # number of entries in the additional records section.
164 name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C")
165 assert_operator(rest, :start_with?, name)
166 rest = rest[name.length..-1]
167 assert_equal(4, rest.length)
168 qtype, _ = rest.unpack("nn")
169 assert_equal(1, qtype) # A
170 assert_equal(1, qtype) # IN
192 msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
197 rdata = [192,0,2,1].pack("CCCC") # 192.0.2.1 (TEST-NET address) RFC 3330
198 rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*")
200 u.send(msg, 0, client_address, client_port)
202 result, _ = assert_join_threads([client_thread, server_thread])
203 assert_instance_of(Array, result)
204 assert_equal(1, result.length)
206 assert_instance_of(Resolv::DNS::Resource::IN::A, rr)
207 assert_instance_of(Resolv::IPv4, rr.address)
208 assert_equal("192.0.2.1", rr.address.to_s)
209 assert_equal(3600, rr.ttl)
214 def test_query_ipv4_address_truncated_tcp_fallback
218 skip 'autoload problem. see [ruby-dev:45021][Bug #5786]'
219 end if defined?(OpenSSL)
223 with_udp_and_tcp('127.0.0.1', 0) {|u, t|
224 _, server_port, _, server_address = u.addr
225 client_thread = Thread.new {
226 Resolv::DNS.open(:nameserver_port => [[server_address, server_port]]) {|dns|
227 dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A)
230 udp_server_thread = Thread.new {
231 msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)}
232 id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
233 qr = (word2 & 0x8000) >> 15
234 opcode = (word2 & 0x7800) >> 11
235 aa = (word2 & 0x0400) >> 10
236 tc = (word2 & 0x0200) >> 9
237 rd = (word2 & 0x0100) >> 8
238 ra = (word2 & 0x0080) >> 7
239 z = (word2 & 0x0070) >> 4
240 rcode = word2 & 0x000f
242 assert_equal(0, qr) # 0:query 1:response
243 assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS
244 assert_equal(0, aa) # Authoritative Answer
245 assert_equal(0, tc) # TrunCation
246 assert_equal(1, rd) # Recursion Desired
247 assert_equal(0, ra) # Recursion Available
248 assert_equal(0, z) # Reserved for future use
249 assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused
250 assert_equal(1, qdcount) # number of entries in the question section.
251 assert_equal(0, ancount) # number of entries in the answer section.
252 assert_equal(0, nscount) # number of entries in the authority records section.
253 assert_equal(0, arcount) # number of entries in the additional records section.
254 name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C")
255 assert_operator(rest, :start_with?, name)
256 rest = rest[name.length..-1]
257 assert_equal(4, rest.length)
258 qtype, _ = rest.unpack("nn")
259 assert_equal(1, qtype) # A
260 assert_equal(1, qtype) # IN
271 ancount = num_records
282 msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
287 num_records.times do |i|
288 rdata = [192,0,2,i].pack("CCCC") # 192.0.2.x (TEST-NET address) RFC 3330
289 rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*")
292 u.send(msg[0...512], 0, client_address, client_port)
294 tcp_server_thread = Thread.new {
297 msg.slice!(0..1) # Size (only for TCP)
298 id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
299 qr = (word2 & 0x8000) >> 15
300 opcode = (word2 & 0x7800) >> 11
301 aa = (word2 & 0x0400) >> 10
302 tc = (word2 & 0x0200) >> 9
303 rd = (word2 & 0x0100) >> 8
304 ra = (word2 & 0x0080) >> 7
305 z = (word2 & 0x0070) >> 4
306 rcode = word2 & 0x000f
308 assert_equal(0, qr) # 0:query 1:response
309 assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS
310 assert_equal(0, aa) # Authoritative Answer
311 assert_equal(0, tc) # TrunCation
312 assert_equal(1, rd) # Recursion Desired
313 assert_equal(0, ra) # Recursion Available
314 assert_equal(0, z) # Reserved for future use
315 assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused
316 assert_equal(1, qdcount) # number of entries in the question section.
317 assert_equal(0, ancount) # number of entries in the answer section.
318 assert_equal(0, nscount) # number of entries in the authority records section.
319 assert_equal(0, arcount) # number of entries in the additional records section.
320 name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C")
321 assert_operator(rest, :start_with?, name)
322 rest = rest[name.length..-1]
323 assert_equal(4, rest.length)
324 qtype, _ = rest.unpack("nn")
325 assert_equal(1, qtype) # A
326 assert_equal(1, qtype) # IN
337 ancount = num_records
348 msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
353 num_records.times do |i|
354 rdata = [192,0,2,i].pack("CCCC") # 192.0.2.x (TEST-NET address) RFC 3330
355 rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*")
358 msg = "#{[msg.bytesize].pack("n")}#{msg}" # Prefix with size
362 result, _ = assert_join_threads([client_thread, udp_server_thread, tcp_server_thread])
363 assert_instance_of(Array, result)
364 assert_equal(50, result.length)
365 result.each_with_index do |rr, i|
366 assert_instance_of(Resolv::DNS::Resource::IN::A, rr)
367 assert_instance_of(Resolv::IPv4, rr.address)
368 assert_equal("192.0.2.#{i}", rr.address.to_s)
369 assert_equal(3600, rr.ttl)
374 def test_query_ipv4_duplicate_responses
378 omit 'autoload problem. see [ruby-dev:45021][Bug #5786]'
379 end if defined?(OpenSSL)
381 with_udp('127.0.0.1', 0) {|u|
382 _, server_port, _, server_address = u.addr
384 client_thread = Thread.new {
385 Resolv::DNS.open(:nameserver_port => [[server_address, server_port]], :search => ['bad1.com', 'bad2.com', 'good.com'], ndots: 5) {|dns|
386 dns.getaddress("example")
389 server_thread = Thread.new {
391 msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)}
392 id, flags, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
394 qr = (flags & 0x8000) >> 15
395 opcode = (flags & 0x7800) >> 11
396 aa = (flags & 0x0400) >> 10
397 tc = (flags & 0x0200) >> 9
398 rd = (flags & 0x0100) >> 8
399 ra = (flags & 0x0080) >> 7
400 z = (flags & 0x0070) >> 4
401 rcode = flags & 0x000f
404 questions = msg.bytes[12..-1]
407 while idx < questions.length-5
408 size = questions[idx]
409 labels << questions[idx+1..idx+size].pack('c*')
412 hostname = labels.join('.')
414 if hostname == "example.good.com"
436 msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
437 msg << questions.pack('c*')
442 rdata = [52,0,2,1].pack("CCCC")
443 rr = [0xc00c, type, klass, ttl, rdlength, rdata].pack("nnnNna*")
445 rdata = [52,0,2,2].pack("CCCC")
446 rr = [0xc00c, type, klass, ttl, rdlength, rdata].pack("nnnNna*")
449 u.send(msg, 0, client_address, client_port)
472 msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
473 msg << questions.pack('c*')
475 u.send(msg, 0, client_address, client_port)
476 u.send(msg, 0, client_address, client_port)
480 result, _ = assert_join_threads([client_thread, server_thread])
481 assert_instance_of(Resolv::IPv4, result)
482 assert_equal("52.0.2.1", result.to_s)
487 def test_query_ipv4_address_timeout
488 with_udp('127.0.0.1', 0) {|u|
489 _, port , _, host = u.addr
491 rv = Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns|
494 dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A)
498 assert rv.empty?, "unexpected: #{rv.inspect} (expected empty)"
499 assert_operator 0.1, :<=, diff
501 rv = Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns|
502 dns.timeouts = [ 0.1, 0.2 ]
504 dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A)
508 assert rv.empty?, "unexpected: #{rv.inspect} (expected empty)"
509 assert_operator 0.3, :<=, diff
514 omit if /mswin/ =~ RUBY_PLATFORM && ENV.key?('GITHUB_ACTIONS') # not working from the beginning
516 u.bind("127.0.0.1", 0)
517 _, port, _, host = u.addr
519 # A race condition here.
520 # Another program may use the port.
521 # But no way to prevent it.
523 Timeout.timeout(5) do
524 Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns|
525 assert_equal([], dns.getresources("test-no-server.example.org", Resolv::DNS::Resource::IN::A))
528 rescue Timeout::Error
529 if RUBY_PLATFORM.match?(/mingw/)
530 # cannot repo locally
531 omit 'Timeout Error on MinGW CI'
538 def test_invalid_byte_comment
539 bug9273 = '[ruby-core:59239] [Bug #9273]'
540 Tempfile.create('resolv_test_dns_') do |tmpfile|
541 tmpfile.print("\xff\x00\x40")
543 assert_nothing_raised(ArgumentError, bug9273) do
544 Resolv::DNS::Config.parse_resolv_conf(tmpfile.path)
549 def test_resolv_conf_by_command
550 Dir.mktmpdir do |dir|
552 assert_raise(Errno::ENOENT, Errno::EINVAL) do
553 Resolv::DNS::Config.parse_resolv_conf("|echo foo")
559 def test_dots_diffences
560 name1 = Resolv::DNS::Name.create("example.org")
561 name2 = Resolv::DNS::Name.create("ex.ampl.eo.rg")
562 assert_not_equal(name1, name2, "different dots")
565 def test_case_insensitive_name
566 bug10550 = '[ruby-core:66498] [Bug #10550]'
567 lower = Resolv::DNS::Name.create("ruby-lang.org")
568 upper = Resolv::DNS::Name.create("Ruby-Lang.org")
569 assert_equal(lower, upper, bug10550)
573 addr = Resolv::IPv6.new("\0"*16)
574 labels = addr.to_name.to_a
575 expected = (['0'] * 32 + ['ip6', 'arpa']).map {|label| Resolv::DNS::Label::Str.new(label) }
576 assert_equal(expected, labels)
580 ref = '[Bug #11910] [ruby-core:72559]'
581 assert_instance_of Resolv::IPv6, Resolv::IPv6.create('::1'), ref
582 assert_instance_of Resolv::IPv6, Resolv::IPv6.create('::1:127.0.0.1'), ref
587 ["2001::abcd:abcd:abcd", "2001::ABcd:abcd:ABCD"],
588 ["2001:db8::1", "2001:db8::0:1"],
589 ["::", "0:0:0:0:0:0:0:0"],
590 ["2001::", "2001::0"],
591 ["2001:db8:0:1:1:1:1:1", "2001:db8:0:1:1:1:1:1"], # RFC 5952 Section 4.2.2.
592 ["2001:db8::1:1:1:1", "2001:db8:0:0:1:1:1:1"],
593 ["1::1:0:0:0:1", "1:0:0:1:0:0:0:1"],
594 ["1::1:0:0:1", "1:0:0:0:1:0:0:1"],
597 test_cases.each do |expected, ipv6|
598 assert_equal expected, Resolv::IPv6.create(ipv6).to_s
602 def test_ipv6_should_be_16
603 ref = '[rubygems:1626]'
606 "\0\0\0\0\0\0\0\0\0\0\0\1" \
607 "\x03ns2\bdnsimple\x03com\x00" \
608 "\x00\x1C\x00\x01\x00\x02OD" \
609 "\x00\x10$\x00\xCB\x00 I\x00\x01\x00\x00\x00\x00"
611 e = assert_raise_with_message(Resolv::DNS::DecodeError, /IPv6 address must be 16 bytes/, ref) do
612 Resolv::DNS::Message.decode broken_message
614 assert_kind_of(ArgumentError, e.cause)
617 def test_too_big_label_address
619 m = Resolv::DNS::Message::MessageEncoder.new {|msg|
621 n.times {|i| msg.put_labels(["foo#{i}"]) }
624 Resolv::DNS::Message::MessageDecoder.new(m.to_s) {|msg|
627 assert_equal(["foo#{i}"], msg.get_labels.map {|label| label.to_s })
631 assert_operator(2**14, :<, m.to_s.length)
634 def assert_no_fd_leak
635 socket = assert_throw(self) do |tag|
636 Resolv::DNS.stub(:bind_random_port, ->(s, *) {throw(tag, s)}) do
637 yield.getname("8.8.8.8")
641 assert_predicate(socket, :closed?, "file descriptor leaked")
644 def test_no_fd_leak_connected
645 assert_no_fd_leak {Resolv::DNS.new(nameserver_port: [['127.0.0.1', 53]])}
648 def test_no_fd_leak_unconnected
649 assert_no_fd_leak {Resolv::DNS.new}
653 dns = Resolv::DNS.new
654 def dns.each_resource(name, typeclass)
655 yield typeclass.new(name)
658 dns.each_name('127.0.0.1') do |ptr|
659 assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s)
661 dns.each_name(Resolv::IPv4.create('127.0.0.1')) do |ptr|
662 assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s)
664 dns.each_name('::1') do |ptr|
665 assert_equal('1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', ptr.to_s)
667 dns.each_name(Resolv::IPv6.create('::1')) do |ptr|
668 assert_equal('1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', ptr.to_s)
670 dns.each_name(Resolv::DNS::Name.create('1.0.0.127.in-addr.arpa.')) do |ptr|
671 assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s)
673 assert_raise(Resolv::ResolvError) { dns.each_name('example.com') }
676 def test_unreachable_server
677 unreachable_ip = '127.0.0.1'
679 sock.connect(unreachable_ip, 53)
682 rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH
684 omit('cannot test unreachable server, as IP used is reachable')
688 :nameserver => [unreachable_ip],
692 r = Resolv.new([Resolv::DNS.new(config)])
693 assert_equal([], r.getaddresses('www.google.com'))
695 config[:raise_timeout_errors] = true
696 r = Resolv.new([Resolv::DNS.new(config)])
697 assert_raise(Resolv::ResolvError) { r.getaddresses('www.google.com') }