--- Library for supporting DNS Service Discovery
--
-- The library supports
-- * Unicast and Multicast requests
-- * Decoding responses
-- * Running requests in parallel using Lua coroutines
--
-- The library contains the following classes
-- * Comm
-- ** A class with static functions that handle communication using the dns library
-- * Helper
-- ** The helper class wraps the Comm
class using functions with a more descriptive name.
-- ** The purpose of this class is to give developers easy access to some of the common DNS-SD tasks.
-- * Util
-- ** The Util
class contains a number of static functions mainly used to convert data.
--
-- The following code snippet queries all mDNS resolvers on the network for a
-- full list of their supported services and returns the formatted output:
--
-- local helper = dnssd.Helper:new( )
-- helper:setMulticast(true)
-- return stdnse.format_output(helper:queryServices())
--
--
-- This next snippet queries a specific host for the same information:
--
-- local helper = dnssd.Helper:new( host, port )
-- return stdnse.format_output(helper:queryServices())
--
--
-- In order to query for a specific service a string or table with service
-- names can be passed to the Helper.queryServices
method.
--
-- @args dnssd.services string or table containing services to query
--
-- @author Patrik Karlsson
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
--
local coroutine = require "coroutine"
local dns = require "dns"
local ipOps = require "ipOps"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local stringaux = require "stringaux"
local table = require "table"
local target = require "target"
local outlib = require "outlib"
_ENV = stdnse.module("dnssd", stdnse.seeall)
Util = {
--- Compare function used for sorting IP-addresses
--
-- @param a table containing first item
-- @param b table containing second item
-- @return true if a is less than b
ipCompare = function(a, b)
return ipOps.compare_ip(a, "lt", b)
end,
--- Function used to compare discovered DNS services so they can be sorted
--
-- @param a table containing first item
-- @param b table containing second item
-- @return true if the port of a is less than the port of b
serviceCompare = function(a, b)
-- if no port is found use 999999 for comparing, this way all services
-- without ports and device information gets printed at the end
local port_a = a:match("^(%d+)") or 999999
local port_b = b:match("^(%d+)") or 999999
if ( tonumber(port_a) < tonumber(port_b) ) then
return true
end
return false
end,
--- Creates a service host table
--
-- ['_ftp._tcp.local'] = {10.10.10.10,20.20.20.20}
-- ['_http._tcp.local'] = {30.30.30.30,40.40.40.40}
--
-- @param response containing multiple responses from dns.query
-- @return services table containing the service name as a key and all host addresses as value
createSvcHostTbl = function( response )
local services = {}
-- Create unique table of services
for _, r in ipairs( response ) do
-- do we really have multiple responses?
if ( not(r.output) ) then return end
for _, svc in ipairs(r.output ) do
services[svc] = services[svc] or {}
table.insert(services[svc], r.peer)
end
end
return services
end,
--- Creates a unique list of services
--
-- @param response containing a single or multiple responses from
-- dns.query
-- @return array of strings containing service names
getUniqueServices = function( response )
local services = {}
for _, r in ipairs(response) do
if ( r.output ) then
for _, svc in ipairs(r.output) do services[svc] = true end
else
services[r] = true
end
end
return services
end,
--- Returns the amount of currently active threads
--
-- @param threads table containing the list of threads
-- @return count number containing the number of non-dead threads
threadCount = function( threads )
local count = 0
for thread in pairs(threads) do
if ( coroutine.status(thread) == "dead" ) then
threads[thread] = nil
else
count = count + 1
end
end
return count
end
}
Comm = {
--- Gets a record from both the Answer and Additional section
--
-- @param dtype DNS resource record type.
-- @param response Decoded DNS response.
-- @param retAll If true, return all entries, not just the first.
-- @return True if one or more answers of the required type were found - otherwise false.
-- @return Answer according to the answer fetcher for dtype
or an Error message.
getRecordType = function( dtype, response, retAll )
local result = {}
local status1, answers = dns.findNiceAnswer( dtype, response, retAll )
if status1 then
if retAll then
for _, v in ipairs(answers) do
table.insert(result, string.format("%s", v) )
end
else
return true, answers
end
end
local status2, answers = dns.findNiceAdditional( dtype, response, retAll )
if status2 then
if retAll then
for _, v in ipairs(answers) do
table.insert(result, v)
end
else
return true, answers
end
end
if not status1 and not status2 then
return false, answers
end
return true, result
end,
--- Send a query for a particular service and store the response in a table
--
-- @param host string containing the ip to connect to
-- @param port number containing the port to connect to
-- @param svc the service record to retrieve
-- @param multiple true if responses from multiple hosts are expected
-- @param svcresponse table to which results are stored
queryService = function( host, port, svc, multiple, svcresponse )
local condvar = nmap.condvar(svcresponse)
local status, response = dns.query( svc, { port = port, host = host, dtype="PTR", retPkt=true, retAll=true, multiple=multiple, sendCount=1, timeout=2000} )
if not status then
stdnse.debug1("Failed to query service: %s; Error: %s", svc, response)
return
end
svcresponse[svc] = svcresponse[svc] or {}
if ( multiple ) then
for _, r in ipairs(response) do
table.insert( svcresponse[svc], r )
end
else
svcresponse[svc] = response
end
condvar("broadcast")
end,
--- Decodes a record received from the queryService
function
--
-- @param response as returned by queryService
-- @param result table into which the decoded output should be stored
decodeRecords = function( response, result )
local service = stdnse.output_table()
local txt = {}
local record = ( #response.questions > 0 and response.questions[1].dname ) and response.questions[1].dname or ""
local status, ipv4 = Comm.getRecordType( dns.types.A, response, false )
if status then service.ipv4 = ipv4 end
local status, ipv6 = Comm.getRecordType( dns.types.AAAA, response, false )
if status then service.ipv6 = ipv6 end
local status, name = Comm.getRecordType( dns.types.PTR, response, false )
if status then
local i = name:find("." .. record, 1, true)
if i then
name = name:sub(1, i - 1)
end
service.name = name
end
local name, port
local status, srv = Comm.getRecordType( dns.types.SRV, response, false )
if status then
local srvparams = stringaux.strsplit( ":", srv )
if #srvparams > 3 then
name = srvparams[4]:gsub("%.local$", "")
port = srvparams[3]
if service.name ~= name then
service.hostname = name
end
end
end
status, txt = Comm.getRecordType( dns.types.TXT, response, true )
if status then
for _, t in ipairs(txt) do
if #t > 0 then
service.TXT = txt
break
end
end
end
if record == "_device-info._tcp.local" then
result["Device Information"] = service
else
local heading
local servicename, proto = record:match("^_([^.]+)%._([^.]+)%.")
if port and servicename then
heading = string.format( "%s/%s %s", port, proto, servicename)
else
heading = record
end
result[heading] = service
end
end,
--- Query the mDNS resolvers for a list of their services
--
-- @param host table as received by the action function
-- @param port number specifying the port to connect to
-- @param multiple receive multiple responses (multicast)
-- @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"
queryAllServices = function( host, port, multiple )
local sendCount, timeout = 1, 2000
if ( multiple ) then
sendCount, timeout = 2, 5000
end
return dns.query( "_services._dns-sd._udp.local", { port = port, host = ( host.ip or host ), dtype="PTR", retAll=true, multiple=multiple, sendCount=sendCount, timeout=timeout } )
end,
}
Helper = {
--- Creates a new helper instance
--
-- @param host string containing the host name or ip
-- @param port number containing the port to connect to
-- @return o a new instance of Helper
new = function( self, host, port )
local o = {}
setmetatable(o, self)
self.__index = self
o.host = host
o.port = port
o.mcast = false
return o
end,
--- Instructs the helper to use unconnected sockets supporting multicast
--
-- @param mcast boolean true if multicast is to be used, false otherwise
setMulticast = function( self, mcast )
assert( type(mcast)=="boolean", "mcast has to be either true or false")
self.mcast = mcast
end,
--- Performs a DNS-SD query against a host
--
-- @param host table as received by the action function
-- @param port number specifying the port to connect to
-- @param service string or table with the service(s) to query eg.
-- _ssh._tcp.local, _afpovertcp._tcp.local
-- if nil defaults to _services._dns-sd._udp.local (all)
-- @param mcast boolean true if a multicast query is to be done
-- @return status true on success, false on failure
-- @return response table suitable for stdnse.format_output
queryServices = function( self, service )
local result = {}
local status, response
local mcast = self.mcast
local port = self.port or 5353
local family = nmap.address_family()
local host = mcast and (family=="inet6" and "ff02::fb" or "224.0.0.251") or self.host
local service = service or stdnse.get_script_args('dnssd.services')
if ( not(service) ) then
status, response = Comm.queryAllServices( host, port, mcast )
if ( not(status) ) then return status, response end
else
if ( 'string' == type(service) ) then
response = { service }
elseif ( 'table' == type(service) ) then
response = service
end
end
response = Util.getUniqueServices(response)
local svcresponse = {}
local condvar = nmap.condvar( svcresponse )
local threads = {}
for svc in pairs(response) do
local co = stdnse.new_thread( Comm.queryService, (host.ip or host), port, svc, mcast, svcresponse )
threads[co] = true
end
-- Wait for all threads to finish running
while Util.threadCount(threads)>0 do condvar("wait") end
if ( mcast ) then
-- Process all records that were returned
local addr1, addr2
if nmap.address_family() == "inet" then
addr1 = "ipv4"
addr2 = "ipv6"
else
addr1 = "ipv6"
addr2 = "ipv4"
end
local ipsvctbl = {}
for svcname, response in pairs(svcresponse) do
for _, r in ipairs( response ) do
local key = r[addr1]
if key then
target.add(key)
else
key = r[addr2] or r.peer
end
ipsvctbl[key] = ipsvctbl[key] or {}
Comm.decodeRecords( r.output, ipsvctbl[key] )
end
end
for k, svc in pairs(ipsvctbl) do
ipsvctbl[k] = outlib.sorted_by_key(svc, Util.serviceCompare)
end
return true, outlib.sorted_by_key(ipsvctbl, Util.ipCompare)
else
-- Process all records that were returned
for svcname, response in pairs(svcresponse) do
Comm.decodeRecords( response, result )
end
return true, outlib.sorted_by_key(result, Util.serviceCompare)
end
return false
end,
}
return _ENV;