From f1a9ed0e1ec64abc7c4a7f3e43d44faf78b58ef7 Mon Sep 17 00:00:00 2001 From: TheRealAlexxx Date: Sat, 24 Oct 2026 09:11:49 +0100 Subject: [PATCH] Resolv: continue the search list on NODATA replies Fixes [Bug #18842]. When a name is resolved through the search list, `Resolv::DNS#fetch_resource` stops at the first candidate whose reply has rcode NoError, even when the answer section is empty (a NODATA reply: the name exists, but has no records of the requested type). The remaining search domains, and the name as given, are never tried, so `getaddress`/`getresources` report no information even though a later search domain holds the record. NXDomain replies already continue to the next candidate via `Config#resolv`. This change treats NODATA the same way: a NoError reply with an empty answer section raises a new `Config::NODATA`, which `Config#resolv` rescues alongside `NXDomain` before moving on to the next candidate. A NODATA reply for the final candidate is swallowed as before, so the all-NODATA outcome is unchanged. The new test runs a stub DNS server that answers NODATA for the first two search-list candidates and an A record for the third; `getaddress` now returns the third candidate's address instead of raising `ResolvError`. --- lib/resolv.rb | 13 ++++++++++++- test/resolv/test_dns.rb | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index bac338c..4a2be33 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -581,6 +581,11 @@ def fetch_resource(name, typeclass) # Retry via TCP: truncated[candidate] = true redo + elsif reply.answer.empty? + # NODATA: the name exists but has no records of the + # requested type. Continue with the next candidate in the + # search list, if any. + raise Config::NODATA.new(reply_name.to_s) else yield(reply, reply_name) end @@ -1293,7 +1298,7 @@ def resolv(name) } timeout_error = true raise ResolvError.new("DNS resolv timeout: #{name}") - rescue NXDomain + rescue NXDomain, NODATA end } rescue ResolvError @@ -1307,6 +1312,12 @@ def resolv(name) class NXDomain < ResolvError end + ## + # Indicates the name was found but has no data for the requested type. + + class NODATA < ResolvError + end + ## # Indicates some other unhandled resolver error was encountered. diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index b8ecef2..5a36b08 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -542,6 +542,39 @@ def test_query_ipv4_address_timeout } end + def test_search_list_continues_on_nodata + bug18842 = '[Bug #18842]' + with_udp('127.0.0.1', 0) {|u| + _, server_port, _, server_address = u.addr + client_thread = Thread.new { + Resolv::DNS.open(:nameserver_port => [[server_address, server_port]], :search => ['bad1.com', 'bad2.com', 'good.com'], :ndots => 5) {|dns| + dns.getaddress("example") + } + } + server_thread = Thread.new { + 3.times do + msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)} + query = Resolv::DNS::Message.decode(msg) + reply = Resolv::DNS::Message.new(query.id) + reply.qr = 1 + reply.rd = query.rd + reply.ra = 1 + query.each_question {|name, typeclass| + reply.add_question(name, typeclass) + if name.to_s == "example.good.com" + reply.add_answer(name, 3600, Resolv::DNS::Resource::IN::A.new("192.0.2.1")) + end + } + # No answers for the other candidates: a NODATA reply. + u.send(reply.encode, 0, client_address, client_port) + end + } + address = client_thread.value + server_thread.join + assert_equal(Resolv::IPv4.create("192.0.2.1"), address, bug18842) + } + end + def test_no_server omit if /mswin/ =~ RUBY_PLATFORM && ENV.key?('GITHUB_ACTIONS') # not working from the beginning u = UDPSocket.new