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

-- -*- mode: lua -*-:
-- vim: set filetype=lua :

description = [[
Sniffs the local network for a configurable amount of time (10 seconds
by default) and prints discovered addresses. If the
<code>newtargets</code> script argument is set, discovered addresses
are added to the scan queue.

Requires root privileges. Either the <code>targets-sniffer.interface</code> script
argument or <code>-e</code> Nmap option to define which interface to use.
]]

---
-- @usage
-- nmap -sL --script=targets-sniffer --script-args=newtargets,targets-sniffer.timeout=5s,targets-sniffer.interface=eth0
-- @args targets-sniffer.timeout  The amount of time to listen for packets. Default <code>10s</code>.
-- @args targets-sniffer.interface  The interface to use for sniffing.
-- @output
-- Pre-scan script results:
-- | targets-sniffer:
-- | 192.168.0.1
-- | 192.168.0.3
-- | 192.168.0.35
-- |_192.168.0.100


-- Thanks to everyone for the feedback and especially Henri Doreau for his detailed feedback and suggestions

author = "Nick Nikolaou"
categories = {"broadcast", "discovery", "safe"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"


local interface_info
local all_addresses= {}
local unique_addresses = {}

--Make sure the IP is not a broadcast or the local address
local function check_if_valid(address)
  local broadcast = interface_info.broadcast
  local local_address = interface_info.address

  if address == local_address
    or address == broadcast or address == "255.255.255.255"
    or address:match('^ff') --IPv6 Multicast addrs
    then
    return false
  else
    return true end
end

-- Returns an array of address strings.
local function get_ip_addresses(layer3)
  local ip = packet.Packet:new(layer3, layer3:len())
  if not ip then return {} end
  return { ipOps.str_to_ip(ip.ip_bin_src), ipOps.str_to_ip(ip.ip_bin_dst) }
end

prerule =  function()
  return nmap.is_privileged()
end

local function collect_interface(if_table)
  if not interface_info and if_table.up == "up" and if_table.link ~= "loopback" then
    interface_info = if_table
  end
end

action = function()

  local sock = nmap.new_socket()
  local packet_counter = 0
  local ip_counter = 0
  local timeout = stdnse.parse_timespec(stdnse.get_script_args("targets-sniffer.timeout"))
  timeout = (timeout or 10) * 1000
  -- TODO: sniff on all interfaces
  -- NOTE: targets-sniffer.iface script-arg name is non-standard, but left for compatibility.
  local interface = stdnse.get_script_args("targets-sniffer.iface")
  if interface then
    interface_info = nmap.get_interface_info(interface)
  else
    stdnse.get_script_interfaces(collect_interface)
  end

  if not interface_info then -- Check if we have the interface information
    stdnse.debug1("Error: Unable to get interface info. Did you specify the correct interface using 'targets-sniffer.interface=<interface>' or '-e <interface>'?")
    return
  elseif not interface then
    interface = interface_info.shortname
  end


  if sock==nil then
    stdnse.debug1("Error - unable to open socket using interface %s",interface)
    return
  else
    sock:pcap_open(interface, 104, true, "ip or ip6")
    stdnse.debug1("Will sniff for %s seconds on interface %s.", (timeout/1000),interface)

    repeat

      local start_time = nmap.clock_ms() -- Used for script timeout
      sock:set_timeout(timeout)
      local status, _, _, layer3 = sock:pcap_receive()

      if status then
        local addresses

        packet_counter=packet_counter+1
        addresses = get_ip_addresses(layer3)
        stdnse.debug1("Got IP addresses %s", table.concat(addresses, " "))

        for _, addr in ipairs(addresses) do
          if check_if_valid(addr) then
            if not unique_addresses[addr] then
              unique_addresses[addr] = true
              table.insert(all_addresses, addr)
            end
          end
        end

      end
      -- Update timeout
      timeout = timeout - (nmap.clock_ms() - start_time)

    until timeout <= 0

    sock:pcap_close()
  end

  if target.ALLOW_NEW_TARGETS then
    if nmap.address_family() == 'inet6' then
      for _,v in pairs(all_addresses) do
        if v:match(':') then
          target.add(v)
        end
      end
    else
      for _,v in pairs(all_addresses) do
        if not v:match(':') then
          target.add(v)
        end
      end
    end
  else
    stdnse.debug1("Not adding targets to newtargets. If you want to do that use the 'newtargets' script argument.")
  end

  if #all_addresses>0 then
    stdnse.debug1("Added %s address(es) to newtargets", #all_addresses)
  end

  return string.format("Sniffed %s address(es). \n", #all_addresses) .. table.concat(all_addresses, "\n")
end
