Skip to content

Commit 7d524df

Browse files
optijmehnle
andcommitted
Fix TCP fallback with multiple nameservers
Under the following conditions the exception `Resolv::DNS::Requester::RequestError: host/port don't match` is raised: - Multiple nameservers are configured for Resolv::DNS - A nameserver falls back from UDP to TCP - TCP request hits Resolv::DNS timeout - Resolv::DNS retries the next nameserver More details here https://bugs.ruby-lang.org/issues/8285 Co-authored-by: Julian Mehnle <julian@mehnle.net>
1 parent b25c63c commit 7d524df

File tree

2 files changed

+140
-20
lines changed

2 files changed

+140
-20
lines changed

lib/resolv.rb

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -513,35 +513,40 @@ def each_resource(name, typeclass, &proc)
513513

514514
def fetch_resource(name, typeclass)
515515
lazy_initialize
516-
begin
517-
requester = make_udp_requester
518-
rescue Errno::EACCES
519-
# fall back to TCP
520-
end
516+
protocols = {}
517+
requesters = {}
521518
senders = {}
522519
begin
523-
@config.resolv(name) {|candidate, tout, nameserver, port|
524-
requester ||= make_tcp_requester(nameserver, port)
520+
@config.resolv(name) do |candidate, tout, nameserver, port|
525521
msg = Message.new
526522
msg.rd = 1
527523
msg.add_question(candidate, typeclass)
528-
unless sender = senders[[candidate, nameserver, port]]
524+
525+
protocol = protocols[candidate] ||= :udp
526+
requester = requesters[[protocol, nameserver]] ||=
527+
case protocol
528+
when :udp
529+
begin
530+
make_udp_requester
531+
rescue Errno::EACCES
532+
make_tcp_requester(nameserver, port)
533+
end
534+
when :tcp
535+
make_tcp_requester(nameserver, port)
536+
end
537+
538+
unless sender = senders[[candidate, requester, nameserver, port]]
529539
sender = requester.sender(msg, candidate, nameserver, port)
530540
next if !sender
531-
senders[[candidate, nameserver, port]] = sender
541+
senders[[candidate, requester, nameserver, port]] = sender
532542
end
533543
reply, reply_name = requester.request(sender, tout)
534544
case reply.rcode
535545
when RCode::NoError
536546
if reply.tc == 1 and not Requester::TCP === requester
537547
requester.close
538548
# Retry via TCP:
539-
requester = make_tcp_requester(nameserver, port)
540-
senders = {}
541-
# This will use TCP for all remaining candidates (assuming the
542-
# current candidate does not already respond successfully via
543-
# TCP). This makes sense because we already know the full
544-
# response will not fit in an untruncated UDP packet.
549+
protocols[candidate] = :tcp
545550
redo
546551
else
547552
yield(reply, reply_name)
@@ -552,9 +557,9 @@ def fetch_resource(name, typeclass)
552557
else
553558
raise Config::OtherResolvError.new(reply_name.to_s)
554559
end
555-
}
560+
end
556561
ensure
557-
requester&.close
562+
requesters.each_value { |requester| requester&.close }
558563
end
559564
end
560565

@@ -569,6 +574,11 @@ def make_udp_requester # :nodoc:
569574

570575
def make_tcp_requester(host, port) # :nodoc:
571576
return Requester::TCP.new(host, port)
577+
rescue Errno::ECONNREFUSED
578+
# Treat a refused TCP connection attempt to a nameserver like a timeout,
579+
# as Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a
580+
# hint to try the next nameserver:
581+
raise ResolvTimeout
572582
end
573583

574584
def extract_resources(msg, name, typeclass) # :nodoc:
@@ -1800,7 +1810,6 @@ def canonical_key(key) # :nodoc:
18001810
end
18011811
end
18021812

1803-
18041813
##
18051814
# Base class for SvcParam. [RFC9460]
18061815

@@ -2499,7 +2508,6 @@ def initialize(version, ssize, hprecision, vprecision, latitude, longitude, alti
24992508

25002509
attr_reader :altitude
25012510

2502-
25032511
def encode_rdata(msg) # :nodoc:
25042512
msg.put_bytes(@version)
25052513
msg.put_bytes(@ssize.scalar)
@@ -3439,4 +3447,3 @@ def DefaultResolver.replace_resolvers new_resolvers
34393447
AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/
34403448

34413449
end
3442-

test/resolv/test_dns.rb

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,4 +698,117 @@ def test_unreachable_server
698698
ensure
699699
sock&.close
700700
end
701+
702+
def test_multiple_servers_with_timeout_and_truncated_tcp_fallback
703+
begin
704+
OpenSSL
705+
rescue LoadError
706+
skip 'autoload problem. see [ruby-dev:45021][Bug #5786]'
707+
end if defined?(OpenSSL)
708+
709+
num_records = 50
710+
711+
with_udp_and_tcp('127.0.0.1', 0) do |u1, t1|
712+
with_tcp('0.0.0.0', 0) do |t2|
713+
_, server1_port, _, server1_address = u1.addr
714+
_, server2_port, _, server2_address = t2.addr
715+
716+
client_thread = Thread.new do
717+
Resolv::DNS.open(nameserver_port: [[server1_address, server1_port], [server2_address, server2_port]]) do |dns|
718+
dns.timeouts = [0.1, 0.2]
719+
dns.getresources('foo.example.org', Resolv::DNS::Resource::IN::A)
720+
end
721+
end
722+
723+
udp_server1_thread = Thread.new do
724+
msg, (_, client_port, _, client_address) = Timeout.timeout(5) { u1.recvfrom(4096) }
725+
id, word2, _qdcount, _ancount, _nscount, _arcount = msg.unpack('nnnnnn')
726+
opcode = (word2 & 0x7800) >> 11
727+
rd = (word2 & 0x0100) >> 8
728+
name = [3, 'foo', 7, 'example', 3, 'org', 0].pack('Ca*Ca*Ca*C')
729+
qr = 1
730+
aa = 0
731+
tc = 1
732+
ra = 1
733+
z = 0
734+
rcode = 0
735+
qdcount = 0
736+
ancount = num_records
737+
nscount = 0
738+
arcount = 0
739+
word2 = (qr << 15) |
740+
(opcode << 11) |
741+
(aa << 10) |
742+
(tc << 9) |
743+
(rd << 8) |
744+
(ra << 7) |
745+
(z << 4) |
746+
rcode
747+
msg = [id, word2, qdcount, ancount, nscount, arcount].pack('nnnnnn')
748+
type = 1
749+
klass = 1
750+
ttl = 3600
751+
rdlength = 4
752+
num_records.times do |i|
753+
rdata = [192, 0, 2, i].pack('CCCC') # 192.0.2.x (TEST-NET address) RFC 3330
754+
rr = [name, type, klass, ttl, rdlength, rdata].pack('a*nnNna*')
755+
msg << rr
756+
end
757+
u1.send(msg[0...512], 0, client_address, client_port)
758+
end
759+
760+
tcp_server1_thread = Thread.new { t1.accept }
761+
762+
tcp_server2_thread = Thread.new do
763+
ct = t2.accept
764+
msg = ct.recv(512)
765+
msg.slice!(0..1) # Size (only for TCP)
766+
id, word2, _qdcount, _ancount, _nscount, _arcount = msg.unpack('nnnnnn')
767+
rd = (word2 & 0x0100) >> 8
768+
opcode = (word2 & 0x7800) >> 11
769+
name = [3, 'foo', 7, 'example', 3, 'org', 0].pack('Ca*Ca*Ca*C')
770+
qr = 1
771+
aa = 0
772+
tc = 0
773+
ra = 1
774+
z = 0
775+
rcode = 0
776+
qdcount = 0
777+
ancount = num_records
778+
nscount = 0
779+
arcount = 0
780+
word2 = (qr << 15) |
781+
(opcode << 11) |
782+
(aa << 10) |
783+
(tc << 9) |
784+
(rd << 8) |
785+
(ra << 7) |
786+
(z << 4) |
787+
rcode
788+
msg = [id, word2, qdcount, ancount, nscount, arcount].pack('nnnnnn')
789+
type = 1
790+
klass = 1
791+
ttl = 3600
792+
rdlength = 4
793+
num_records.times do |i|
794+
rdata = [192, 0, 2, i].pack('CCCC') # 192.0.2.x (TEST-NET address) RFC 3330
795+
rr = [name, type, klass, ttl, rdlength, rdata].pack('a*nnNna*')
796+
msg << rr
797+
end
798+
msg = "#{[msg.bytesize].pack('n')}#{msg}" # Prefix with size
799+
ct.send(msg, 0)
800+
ct.close
801+
end
802+
result, = assert_join_threads([client_thread, udp_server1_thread, tcp_server1_thread, tcp_server2_thread])
803+
assert_instance_of(Array, result)
804+
assert_equal(50, result.length)
805+
result.each_with_index do |rr, i|
806+
assert_instance_of(Resolv::DNS::Resource::IN::A, rr)
807+
assert_instance_of(Resolv::IPv4, rr.address)
808+
assert_equal("192.0.2.#{i}", rr.address.to_s)
809+
assert_equal(3600, rr.ttl)
810+
end
811+
end
812+
end
813+
end
701814
end

0 commit comments

Comments
 (0)