summaryrefslogtreecommitdiffstats
path: root/nselib/dnssd.lua
blob: b4aadcc6ee756395c916acc395b0171a03f1ca33 (plain)
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;