1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
|
--- 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
-- * <code>Comm</code>
-- ** A class with static functions that handle communication using the dns library
-- * <code>Helper</code>
-- ** The helper class wraps the <code>Comm</code> 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.
-- * <code>Util</code>
-- ** The <code>Util</code> 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:
-- <code>
-- local helper = dnssd.Helper:new( )
-- helper:setMulticast(true)
-- return stdnse.format_output(helper:queryServices())
-- </code>
--
-- This next snippet queries a specific host for the same information:
-- <code>
-- local helper = dnssd.Helper:new( host, port )
-- return stdnse.format_output(helper:queryServices())
-- </code>
--
-- In order to query for a specific service a string or table with service
-- names can be passed to the <code>Helper.queryServices</code> method.
--
-- @args dnssd.services string or table containing services to query
--
-- @author Patrik Karlsson <patrik@cqure.net>
-- @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"
_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 <code>dns.query</code>
-- @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
-- <code>dns.query</code>
-- @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 <code>dtype</code> 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 <code>queryService</code> function
--
-- @param response as returned by <code>queryService</code>
-- @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 = stringaux.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 = stringaux.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 <code>stdnse.format_output</code>
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;
|