--- 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 table = require "table"
local target = require "target"
_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.name:match("^(%d+)") or 999999
local port_b = b.name: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, deviceinfo = {}, {}
local txt = {}
local ipv6, srv, address, port, proto
local record = ( #response.questions > 0 and response.questions[1].dname ) and response.questions[1].dname or ""
local status, ip = Comm.getRecordType( dns.types.A, response, false )
if status then address = ip end
status, ipv6 = Comm.getRecordType( dns.types.AAAA, response, false )
if status then
address = address or ""
address = address .. " " .. ipv6
end
status, txt = Comm.getRecordType( dns.types.TXT, response, true )
if status then
for _, v in ipairs(txt) do
if v:len() > 0 then
table.insert(service, v)
end
end
end
status, srv = Comm.getRecordType( dns.types.SRV, response, false )
if status then
local srvparams = stdnse.strsplit( ":", srv )
if #srvparams > 3 then
port = srvparams[3]
end
end
if address then
table.insert( service, ("Address=%s"):format( address ) )
end
if record == "_device-info._tcp.local" then
service.name = "Device Information"
deviceinfo = service
table.insert(result, deviceinfo)
else
local serviceparams = stdnse.strsplit("[.]", record)
if #serviceparams > 2 then
local servicename = serviceparams[1]:sub(2)
local proto = serviceparams[2]:sub(2)
if port == nil or proto == nil or servicename == nil then
service.name = record
else
service.name = string.format( "%s/%s %s", port, proto, servicename)
end
end
table.insert( result, 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
local ipsvctbl = {}
if ( mcast ) then
-- Process all records that were returned
for svcname, response in pairs(svcresponse) do
for _, r in ipairs( response ) do
ipsvctbl[r.peer] = ipsvctbl[r.peer] or {}
Comm.decodeRecords( r.output, ipsvctbl[r.peer] )
end
end
else
-- Process all records that were returned
for svcname, response in pairs(svcresponse) do
Comm.decodeRecords( response, result )
end
end
if ( mcast ) then
-- Restructure and build our output table
for ip, svctbl in pairs( ipsvctbl ) do
table.sort(svctbl, Util.serviceCompare)
svctbl.name = ip
if target.ALLOW_NEW_TARGETS then target.add(ip) end
table.insert( result, svctbl )
end
table.sort( result, Util.ipCompare )
else
-- sort the tables per port
table.sort( result, Util.serviceCompare )
end
return true, result
end,
}
return _ENV;