summaryrefslogtreecommitdiffstats
path: root/nselib/multicast.lua
blob: ccdb72a329411ee828621f5a66ddf9bf5be6eab1 (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
---
-- Utility functions for sending MLD requests and parsing reports.
--
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html

local nmap = require "nmap"
local ipOps = require "ipOps"
local packet = require "packet"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"

_ENV = stdnse.module("multicast", stdnse.seeall)

---
-- Performs an MLD general query on the selected interface and caches the results such that
-- subsequent calls to this function do not generate additional traffic.
--
-- @param if_nfo A table containing information about the interface to send the request on.
-- Can be one of those returned by nmap.list_interfaces().
-- @param arg_timeout The amount of time to wait for reports.
--
-- @return A list of tables, each table containing three items, namely device, layer 2 reply and layer 3 reply.
--
mld_query = function( if_nfo, arg_timeout )
  -- check if the interface name is valid or if nmap can find one
  if if_nfo == nil then
    return nil
  end

  -- we need some ID for this interface & address combination to use as the
  -- registry key and the object to lock the mutex on
  local reg_entry = "mld_reports_" .. if_nfo.device .. "_" .. if_nfo.address
  local mutex = nmap.mutex( reg_entry )
  mutex('lock')

  -- first check if nmap.registry contains reports for this interface from a previous call of this function
  if nmap.registry[reg_entry] ~= nil then
    mutex('done')
    return nmap.registry[reg_entry]
  end

  if not ipOps.ip_in_range(if_nfo.address, "fe80::/10")  -- link local address
    or if_nfo.link ~= "ethernet" then                 -- not the loopback interface
    mutex('done')
    return nil
  end

  -- create the query packet
  local src_mac = if_nfo.mac
  local src_ip6 = ipOps.ip_to_str(if_nfo.address)
  local dst_mac = packet.mactobin("33:33:00:00:00:01")
  local dst_ip6 = ipOps.ip_to_str("ff02::1")
  local general_qry = ipOps.ip_to_str("::")

  local dnet = nmap.new_dnet()
  local pcap = nmap.new_socket()

  dnet:ethernet_open(if_nfo.device)
  pcap:pcap_open(if_nfo.device, 1500, false, "ip6[40:1] == 58")

  local probe = packet.Frame:new()
  probe.mac_src = src_mac
  probe.mac_dst = dst_mac
  probe.ip_bin_src = src_ip6
  probe.ip_bin_dst = dst_ip6

  probe.ip6_tc = 0
  probe.ip6_fl = 0
  probe.ip6_hlimit = 1

  probe.icmpv6_type = packet.MLD_LISTENER_QUERY
  probe.icmpv6_code = 0

  -- Add a non-empty payload too.
  probe.icmpv6_payload = (
    "\x00\x01" ..              -- maximum response delay 1 millisecond (if 0, virtualbox TCP/IP stack crashes)
    "\x00\x00" ..              -- reserved
    ipOps.ip_to_str("::")   -- empty address - general MLD query
  )
  probe:build_icmpv6_header()
  probe.exheader = string.pack(">BBBB I2 BB",
    packet.IPPROTO_ICMPV6,  -- next header
    0x00, -- length not including first 8 octets
    0x05, -- type is router alert
    0x02, -- length 2 bytes
    0x00, -- router alert MLD
    0x01, -- padding type PadN
    0x00  -- padding length 0
  )
  probe.ip6_nhdr = packet.IPPROTO_HOPOPTS
  probe:build_ipv6_packet()
  probe:build_ether_frame()

  -- send the query packet
  dnet:ethernet_send(probe.frame_buf)

  -- wait for responses to the query packet
  pcap:set_timeout(1000)
  local pcap_timeout_count = 0
  local nse_timeout = arg_timeout or 10
  local start_time = nmap:clock()
  local addrs = {}
  nmap.registry[reg_entry] = {}

  repeat
    local status, length, layer2, layer3 = pcap:pcap_receive()
    local cur_time = nmap:clock()
    if status then
      local l2reply = packet.Frame:new(layer2)
      local l3reply = packet.Packet:new(layer3, length, true)
      local target_ip = l3reply.ip_src
      if l3reply.ip6_nhdr == packet.MLD_LISTENER_REPORT or l3reply.ip6_nhdr == packet.MLDV2_LISTENER_REPORT then
        table.insert(
          nmap.registry[reg_entry],
          { if_nfo.device, l2reply, l3reply }
        )
      end
    end
  until ( cur_time - start_time >= nse_timeout )

  -- clean up
  dnet:ethernet_close()
  pcap:pcap_close()

  mutex('done')
  return nmap.registry[reg_entry]
end

---
-- Extracts IP addresses from MLD reports captured by the mld_query function.
--
-- @param reports The output of the mld_query function.
--
-- @return A list of tables, each table containing three items, namely device, mac and a list of addresses.
--
mld_report_addresses = function(reports)
  local rep_addresses = {}
  for _, report in pairs(reports) do
    local device = report[1]
    local l2reply = report[2]
    local l3reply = report[3]

    local target_ip = l3reply.ip_src
    if l3reply.ip6_nhdr == packet.MLD_LISTENER_REPORT or l3reply.ip6_nhdr == packet.MLDV2_LISTENER_REPORT then

      -- if this is the first reply from the target, make an entry for it
      if not rep_addresses[target_ip] then
        rep_addresses[target_ip] = stdnse.output_table()
      end
      local rep = rep_addresses[target_ip]
      rep.device = device
      rep.mac = stdnse.format_mac(l2reply.mac_src)
      rep.multicast_ips = rep.multicast_ips or {}

      -- depending on the MLD version of the report, add appropriate IP addresses
      if l3reply.ip6_nhdr == packet.MLD_LISTENER_REPORT then
        local multicast_ip = ipOps.str_to_ip( l3reply:raw(0x38, 16) ) -- IP starts at byte 0x38 and is 16 bytes long
        table.insert(rep.multicast_ips, multicast_ip)
      elseif l3reply.ip6_nhdr == packet.MLDV2_LISTENER_REPORT then
        local no_records = l3reply:u16(0x36)
        local record_offset = 0
        local records_start = 0x38
        for i = 1, no_records do
          -- for the format description, see RFC3810 (ch. 5.2)
          local aux_data_len = l3reply:u8(records_start + record_offset + 1)
          local no_sources = l3reply:u16(records_start + record_offset + 2)
          local multicast_ip = ipOps.str_to_ip(l3reply:raw(records_start + record_offset + 4, 16))
          table.insert(rep.multicast_ips, multicast_ip)
          record_offset = record_offset + 4 + 16 + no_sources * 16 + aux_data_len * 4
        end
      end

    end
  end
  return rep_addresses
end

return _ENV