Nmap Development mailing list archives

[NSE] - dns.query() return types


From: jah <jah () zadkiel plus com>
Date: Mon, 20 Jul 2009 23:14:11 +0100

Hi folks,

I've looked a little closer to the types of return values from
dns.query() and seen that there's room for some improvement.  Here's a
run-down of the conditions under which each type is returned:

false, 9
9 is err.NoServers - can't find any DNS servers to query

table, nil
table is the decoded dns response - we got a dns response and we
requested that the decoded response be returned with options.retPkt or
we don't have an "answer fetcher" for the dns response type

false, 3
3 is errNoSuchName - the decode dns response contains zero answers and
the flags indicate a reply code of No Such Name

false, table
table is the decoded dns response - No sure of the exact conditions, but
gotAnswer() returned false and we don't know of an authoritative
nameserver to query or otherwise can't ask one we do know of

false, nil
we didn't get a response from any of the dns servers we tried.

The following are specific for query types AAAA, MX, A:

string, nil
string is the first answer found in the decoded dns response -
options.retAll was not requested

table, nil
table is zero or more string answers found in the decoded dns response
if we requested options.retAll

table, nil
table is the decoded dns packet - found zero answers in the decoded
response and options.retAll was not requested

The following are specific for query types CNAME, NS, PTR, TXT:

string, nil
string is the first answer found in the decoded dns response -
options.retAll was not requested

table, nil
table is zero or more string answers found in the decoded dns response
if we requested options.retAll

nil, nil
unlikely to happen, but it could for instance if findNiceAnswer found no
answers in a decoded response and flags indicate a reply code of
something other than No Such Name

In summary, the return values from dns.query() can be

1st return value: nil, false, string answer, table of 0 or more string
answers, table decoded response
2nd return value: nil, 3, 9, table decoded response

In order to improve the ease of use, I propose that query() should return:
false, error
true, answer
true, decoded_response

where:
answer is string or, if the caller sets options.retAll, a table of 1 or
more answer strings
error is one of something like: "No Such Name", "No Servers", "No
Answers", "Unable to handle response"
decoded_response is returned instead of answer when the caller sets
options.retPkt

So other than streamlining the variations in return values, we would now
never return the decoded_response if we don't have an 'answer fetcher'
for the response type - or for any other reason except when the caller
requests it instead of an answer.

Attached is a patch which implements the changes plus fixes a small bug
when requesting MX records.
Three scripts html-title, asn-query and socks-open-proxy would be
affected and I'll check the changes for those in separately if we go
ahead with this.

Regards,

jah
--- dns.lua.orig        2009-07-20 23:05:30.937500000 +0100
+++ dns.lua     2009-07-20 23:02:09.609375000 +0100
@@ -10,6 +10,10 @@
 get_servers = nmap.get_dns_servers
 
 
+---
+-- Table of DNS resource types.
+-- @name types
+-- @class table
 types = {
    A = 1,
    AAAA = 28,
@@ -28,16 +32,6 @@
 
 
 ---
--- Table of error codes.
--- @name err
--- @class table
-err = {
-   noSuchName = 3,
-   noServers = 9
-}
-
-
----
 -- Repeatedly sends UDP packets to host, waiting for an answer.
 -- @param data Data to be sent.
 -- @param host Host to connect to.
@@ -139,9 +133,10 @@
 -- * <code>retAll</code>: Return all answers, not just the first.
 -- * <code>retPkt</code>: Return the packet instead of using the answer-fetching mechanism.
 -- * <code>norecurse</code> If true, do not set the recursion (RD) flag.
--- @return Nice answer string by an answer fetcher on success or false on error.
--- @return An error code on error.
--- @see dns.err
+-- @return True if a dns response was received and contained an answer of the requested type,
+--  or the decoded dns response was requested (retPkt) and is being returned - or False otherwise.
+-- @return String answer of the requested type, Table of answers or a String error message of one of the following:
+--  "No Such Name", "No Servers", "No Answers", "Unable to handle response"
 function query(dname, options)
    if not options then options = {} end
 
@@ -162,16 +157,11 @@
    local srvI = 1
    if not port then port = 53 end
    if not host then 
-      -- IF PATCH NOT APPLIED!
-      if type(get_servers) == "function" then
-         srv = get_servers()
-      end
-      -- !
-      
+      srv = get_servers()
       if srv and srv[1] then 
          host = srv[1]
       else
-         return false, err.noServers
+         return false, "No Servers"
       end
    elseif type(host) == "table" then
       srv = host
@@ -200,7 +190,7 @@
       -- is it a real answer?
       if gotAnswer(rPkt) then
          if (options.retPkt) then 
-            return rPkt 
+            return true, rPkt
          else
             return findNiceAnswer(dtype, rPkt, options.retAll)
          end
@@ -224,10 +214,12 @@
          end
       end
       
-      -- nothing worked, maybe user finds decoded packet useful
-      return false, rPkt 
+      -- nothing worked
+      stdnse.print_debug(1, "dns.query() failed to resolve the requested query%s%s", dname and ": " or ".", dname or 
"")
+      return false, "No Answers" 
    else
-      return false
+      stdnse.print_debug(1, "dns.query() got zero responses attempting to resolve query%s%s", dname and ": " or ".", 
dname or "")
+      return false, "No Answers"
    end
 end
 
@@ -277,37 +269,48 @@
 -- Answer fetcher for TXT records.
 -- @param dec Decoded DNS response.
 -- @param retAll If true, return all entries, not just the first.
--- @return First entry (or all of them), treated as TXT.
+-- @return True if one or more answers of the required type were found - otherwise false.
+-- @return String first dns TXT record or Table of TXT records or String Error message.
 answerFetcher[types.TXT] = function(dec, retAll)
-   if not retAll then
+   local answers = {}
+   if not retAll and dec.answers[1].data then
       return string.sub(dec.answers[1].data, 2)
+   elseif not retAll then
+      stdnse.print_debug(1, "dns.answerFetcher found no records of the required type: TXT")
+      return false, "No Answers"   
    else
-      local answers = {}
       for _, v in ipairs(dec.answers) do
          if v.data then table.insert(answers, string.sub(v.data, 2)) end
       end
-      return answers
    end
+   if #answers == 0 then
+      stdnse.print_debug(1, "dns.answerFetcher found no records of the required type: TXT")
+      return false, "No Answers"
+   end
+   return true, answers  
 end
 
 ---
 -- Answer fetcher for A records
 -- @param dec Decoded DNS response.
 -- @param retAll If true, return all entries, not just the first.
--- @return First IP (or all of them) of response packet.
+-- @return True if one or more answers of the required type were found - otherwise false.
+-- @return String first dns A record or Table of A records or String Error message.
 answerFetcher[types.A] = function(dec, retAll)
    local answers = {}
    for _, ans in ipairs(dec.answers) do
       if ans.dtype == types.A then
          if not retAll then
-            return ans.ip
-         else
-            table.insert(answers, ans.ip)
+            return true, ans.ip
          end
+         table.insert(answers, ans.ip)
       end
    end
-   if retAll then return answers end
-   return dec
+   if not retAll or #answers == 0 then
+      stdnse.print_debug(1, "dns.answerFetcher found no records of the required type: A")
+      return false, "No Answers"
+   end
+   return true, answers
 end
 
 
@@ -315,38 +318,58 @@
 -- Answer fetcher for CNAME records.
 -- @param dec Decoded DNS response.
 -- @param retAll If true, return all entries, not just the first.
--- @return Domain entry of first answer RR (or all of them) in response packet.
+-- @return True if one or more answers of the required type were found - otherwise false.
+-- @return String first Domain entry or Table of domain entries or String Error message.
 answerFetcher[types.CNAME] = function(dec, retAll)
-   if not retAll then
-      return dec.answers[1].domain
+   local answers = {}
+   if not retAll and dec.answers[1].domain then
+      return true, dec.answers[1].domain
+   elseif not retAll then
+      stdnse.print_debug(1, "dns.answerFetcher found no records of the required type: NS, PTR or CNAME")
+      return false, "No Answers"
    else
-      local answers = {}
       for _, v in ipairs(dec.answers) do
          if v.domain then table.insert(answers, v.domain) end
       end
-      return answers
    end
+   if #answers == 0 then
+      stdnse.print_debug(1, "dns.answerFetcher found no records of the required type: NS, PTR or CNAME")
+      return false, "No Answers"
+   end
+   return true, answers        
 end
 
----
 -- Answer fetcher for MX records.
 -- @param dec Decoded DNS response.
 -- @param retAll If true, return all entries, not just the first.
--- @return Domain entry of first answer RR (or all of them) in response packet.
+-- @return True if one or more answers of the required type were found - otherwise false.
+-- @return String first dns MX record or Table of MX records or String Error message.
+--  Note that zero or more IP addresses may be added to MX records for your convenience.
 answerFetcher[types.MX] = function(dec, retAll)
-   if not retAll then
-      if dec.answers[1] then
-         return dec.answers[1].MX.pref .. ":" .. dec.answers[1].MX.server .. ":" .. dec.add[1].ip
+   local mx, ip, answers = {}, {}, {}
+   for _, ans in ipairs(dec.answers) do
+      if ans.MX then mx[#mx+1] = ans.MX end
+      if not retAll then break end
+   end
+   if #mx == 0 then
+      stdnse.print_debug(1, "dns.answerFetcher found no records of the required type: MX")
+      return false, "No Answers"
+   end
+   for _, add in ipairs(dec.add) do
+      if ip[add.dname] then table.insert(ip[add.dname], add.ip)
+      else ip[add.dname] = {add.ip} end
+   end
+   for _, mxrec in ipairs(mx) do
+      if ip[mxrec.server] then
+         table.insert( answers, ("%s:%s:%s"):format(mxrec.pref or "-", mxrec.server or "-", 
table.concat(ip[mxrec.server], ":")) )
+         if not retAll then return true, answers[1] end
       else
-         return dec
+         -- no IP ?
+         table.insert( answers, ("%s:%s"):format(mxrec.pref or "-", mxrec.server or "-") )
+         if not retAll then return true, answers[1] end
       end
-   else
-      local answers = {}
-      for _, v in ipairs(dec.answers) do
-         if v.MX then table.insert(answers, v.MX.pref .. ":" .. v.MX.server .. ":" .. v.MX.ip) end
-      end
-      return answers
    end
+   return true, answers
 end
 
 
@@ -355,8 +378,8 @@
 -- @name answerFetcher[types.NS]
 -- @class function
 -- @param dec Decoded DNS response.
--- @param retAll If true, return all entries, not just the first.
--- @return Domain entry of first answer RR (or all of them) in response packet.
+-- @return True if one or more answers of the required type were found - otherwise false.
+-- @return String first Domain entry or Table of domain entries or String Error message.
 answerFetcher[types.NS] = answerFetcher[types.CNAME]
 
 ---
@@ -365,27 +388,31 @@
 -- @class function
 -- @param dec Decoded DNS response.
 -- @param retAll If true, return all entries, not just the first.
--- @return Domain entry of first answer RR (or all of them) in response packet.
+-- @return True if one or more answers of the required type were found - otherwise false.
+-- @return String first Domain entry or Table of domain entries or String Error message.
 answerFetcher[types.PTR] = answerFetcher[types.CNAME]
 
 ---
 -- Answer fetcher for AAAA records.
 -- @param dec Decoded DNS response.
 -- @param retAll If true, return all entries, not just the first.
--- @return First IPv6 (or all of them) of response packet.
+-- @return True if one or more answers of the required type were found - otherwise false.
+-- @return String first dns AAAA record or Table of AAAA records or String Error message.
 answerFetcher[types.AAAA] = function(dec, retAll)
    local answers = {}
    for _, ans in ipairs(dec.answers) do
       if ans.dtype == types.AAAA then
          if not retAll then
-            return ans.ipv6
-         else
-            table.insert(answers, ans.ipv6)
+            return true, ans.ipv6
          end
+         table.insert(answers, ans.ipv6)
       end
    end
-   if retAll then return answers end
-   return dec
+   if not retAll or #answers == 0 then
+      stdnse.print_debug(1, "dns.answerFetcher found no records of the required type: AAAA")
+      return false, "No Answers"
+   end
+   return true, answers
 end
 
 
@@ -395,20 +422,22 @@
 -- @param dtype DNS resource record type.
 -- @param dec Decoded DNS response.
 -- @param retAll If true, return all entries, not just the first.
--- @return Answer according to the answer fetcher for <code>dtype</code>,
--- <code>dec</code> if no answer fetcher is known for <code>dtype</code>, or
--- false if flags indicate an error.
--- @return Error code on error.
+-- @return True if one or more answers of the required type were found - otherwise false.
+-- @return Answer according to the answer fetcher for <code>dtype</code> or an Error message.
 function findNiceAnswer(dtype, dec, retAll) 
    if (#dec.answers > 0) then
       if answerFetcher[dtype] then 
          return answerFetcher[dtype](dec, retAll)
       else 
-         return dec
+         stdnse.print_debug(1, "dns.findNiceAnswer() does not have an answerFetcher for dtype %s",
+            (type(dtype) == 'string' and dtype) or type(dtype) or "nil")
+         return false, "Unable to handle response"
       end
+   elseif (dec.flags.RC3 and dec.flags.RC4) then
+      return false, "No Such Name"
    else
-      if (dec.flags.RC3 and dec.flags.RC4) then return false, err.noSuchName
-      end
+      stdnse.print_debug(1, "dns.findNiceAnswer() found zero answers in a response, but got an unexpected 
flags.replycode")
+      return false, "No Answers"
    end
 end
 

_______________________________________________
Sent through the nmap-dev mailing list
http://cgi.insecure.org/mailman/listinfo/nmap-dev
Archived at http://SecLists.Org

Current thread: