local dns = require "dns"
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 rand = require "rand"

description = [[
Obtains hostnames, IPv4 and IPv6 addresses through IPv6 Node Information Queries.

IPv6 Node Information Queries are defined in RFC 4620. There are three
useful types of queries:
* qtype=2: Node Name
* qtype=3: Node Addresses
* qtype=4: IPv4 Addresses

Some operating systems (Mac OS X and OpenBSD) return hostnames in
response to qtype=4, IPv4 Addresses. In this case, the hostnames are still
shown in the "IPv4 addresses" output row, but are prefixed by "(actually
hostnames)".
]]

---
-- @usage nmap -6 <target>
--
-- @output
-- | ipv6-node-info:
-- |   Hostnames: mac-mini.local
-- |   IPv6 addresses: fe80::a8bb:ccff:fedd:eeff, 2001:db8:1234:1234::3
-- |_  IPv4 addresses: mac-mini.local
--
-- @xmloutput
-- <elem key="Hostnames">mac-mini.local</elem>
-- <table key="IPv6 addresses">
--   <elem>fe80::a8bb:ccff:fedd:eeff</elem>
--   <elem>2001:db8:1234:1234::3</elem>
-- </table>
-- <table key="IPv4 addresses">
--   <elem>mac-mini.local</elem>
-- </table>

categories = {"default", "discovery", "safe"}

author = "David Fifield"


local ICMPv6_NODEINFOQUERY = 139
local   ICMPv6_NODEINFOQUERY_IPv6ADDR = 0
local   ICMPv6_NODEINFOQUERY_NAME = 1
local   ICMPv6_NODEINFOQUERY_IPv4ADDR = 1
local ICMPv6_NODEINFORESP = 140
local   ICMPv6_NODEINFORESP_SUCCESS = 0
local   ICMPv6_NODEINFORESP_REFUSED = 1
local   ICMPv6_NODEINFORESP_UNKNOWN = 2

local QTYPE_NOOP = 0
local QTYPE_NODENAME = 2
local QTYPE_NODEADDRESSES = 3
local QTYPE_NODEIPV4ADDRESSES = 4

local QTYPE_STRINGS = {
  [QTYPE_NOOP] = "NOOP",
  [QTYPE_NODENAME] = "Hostnames",
  [QTYPE_NODEADDRESSES] = "IPv6 addresses",
  [QTYPE_NODEIPV4ADDRESSES] = "IPv4 addresses",
}

local function build_ni_query(src, dst, qtype)
  local flags
  local nonce = rand.random_string(8)
  if qtype == QTYPE_NODENAME then
    flags = 0x0000
  elseif qtype == QTYPE_NODEADDRESSES then
    -- Set all the flags GSLCA (see RFC 4620, Figure 3).
    flags = 0x003E
  elseif qtype == QTYPE_NODEIPV4ADDRESSES then
    -- Set the A flag (see RFC 4620, Figure 4).
    flags = 0x0002
  else
    error("Unknown qtype " .. qtype)
  end
  local payload = string.pack(">I2 I2", qtype, flags) .. nonce .. dst
  local p = packet.Packet:new()
  p:build_icmpv6_header(ICMPv6_NODEINFOQUERY, ICMPv6_NODEINFOQUERY_IPv6ADDR, payload, src, dst)
  p:build_ipv6_packet(src, dst, packet.IPPROTO_ICMPV6)

  return p.buf
end

function hostrule(host)
  return nmap.is_privileged() and #host.bin_ip == 16 and host.interface
end

local function open_sniffer(host)
  local bpf
  local s

  s = nmap.new_socket()
  bpf = string.format("ip6 and src host %s", host.ip)
  s:pcap_open(host.interface, 1500, false, bpf)

  return s
end

local function send_queries(host)
  local dnet

  dnet = nmap.new_dnet()
  dnet:ip_open()
  local p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODEADDRESSES)
  dnet:ip_send(p, host)
  p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODENAME)
  dnet:ip_send(p, host)
  p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODEIPV4ADDRESSES)
  dnet:ip_send(p, host)
  dnet:ip_close()
end

local function empty(t)
  return not next(t)
end

-- Try to decode a Node Name reply data field. If successful, returns true and
-- a list of DNS names. In case of a parsing error, returns false and the
-- partial list of names that were parsed prior to the error.
local function try_decode_nodenames(data)
  local names = {}

  local ttl, pos = string.unpack(">I4", data)
  if not ttl then
    return false, names
  end
  while pos <= #data do
    local name

    pos, name = dns.decStr(data, pos)
    if not name then
      return false, names
    end
    -- Ignore empty names, such as those at the end.
    if name ~= "" then
      names[#names + 1] = name
    end
  end

  return true, names
end

local function stringify_noop(flags, data)
  return "replied"
end

local commasep = {
  __tostring = function (t)
    return table.concat(t, ", ")
  end
}

-- RFC 4620, section 6.3.
local function stringify_nodename(flags, data)
  local status, names

  status, names = try_decode_nodenames(data)
  if empty(names) then
    return
  end
  if not status then
    names[#names+1] = "(parsing error)"
  end

  setmetatable(names, commasep)
  return names
end

-- RFC 4620, section 6.3.
local function stringify_nodeaddresses(flags, data)
  local ttl, binaddr
  local addrs = {}
  local pos = nil

  while true do
    ttl, binaddr, pos = string.unpack(">I4 c16", data, pos)
    if not ttl then
      break
    end
    addrs[#addrs + 1] = ipOps.str_to_ip(binaddr)
  end
  if empty(addrs) then
    return
  end

  if (flags & 0x01) ~= 0 then
    addrs[#addrs+1] = "(more omitted for space reasons)"
  end

  setmetatable(addrs, commasep)
  return addrs
end

-- RFC 4620, section 6.4.
-- But Mac OS X puts DNS names in here instead of IPv4 addresses, but it
-- doesn't include the two empty labels at the end as it does with a Node Name
-- response. For example, here is a Node Name reply:
-- 00 00 00 00 0e 6d 61 63  2d 6d 69 6e 69 2e 6c 6f    .....mac -mini.lo
-- 63 61 6c 00 00                                      cal..
-- And here is a Node Addresses reply:
-- 00 00 00 00 0e 6d 61 63  2d 6d 69 6e 69 2e 6c 6f    .....mac -mini.lo
-- 63 61 6c                                            cal
local function stringify_nodeipv4addresses(flags, data)
  local status, names
  local ttl, binaddr
  local addrs = {}
  local pos = nil

  -- Check for DNS names.
  status, names = try_decode_nodenames(data .. "\0\0")
  if status then
    setmetatable(names, commasep)
    return names
  end

  -- Okay, looks like it's really IP addresses.
  while true do
    ttl, binaddr, pos = string.unpack(">I4 c4", data, pos)
    if not ttl then
      break
    end
    addrs[#addrs + 1] = ipOps.str_to_ip(binaddr)
  end
  if empty(addrs) then
    return
  end

  if (flags & 0x01) ~= 0 then
    addrs[#addrs+1] = "(more omitted for space reasons)"
  end

  setmetatable(addrs, commasep)
  return addrs
end

local STRINGIFY = {
  [QTYPE_NOOP] = stringify_noop,
  [QTYPE_NODENAME] = stringify_nodename,
  [QTYPE_NODEADDRESSES] = stringify_nodeaddresses,
  [QTYPE_NODEIPV4ADDRESSES] = stringify_nodeipv4addresses,
}

local function handle_received_packet(buf)
  local p, qtype, flags, data
  local text

  p = packet.Packet:new(buf)
  if p.icmpv6_type ~= ICMPv6_NODEINFORESP then
    return
  end
  qtype = packet.u16(p.buf, p.icmpv6_offset + 4)
  flags = packet.u16(p.buf, p.icmpv6_offset + 6)
  data = string.sub(p.buf, p.icmpv6_offset + 16 + 1)

  if not STRINGIFY[qtype] then
    -- This is a not a qtype we sent or know about.
    stdnse.debug1("Got NI reply with unknown qtype %d from %s", qtype, p.ip6_src)
    return
  end

  if p.icmpv6_code == ICMPv6_NODEINFORESP_SUCCESS then
    text = STRINGIFY[qtype](flags, data)
  elseif p.icmpv6_code == ICMPv6_NODEINFORESP_REFUSED then
    text = "refused"
  elseif p.icmpv6_code == ICMPv6_NODEINFORESP_UNKNOWN then
    text = string.format("target said: qtype %d is unknown", qtype)
  else
    text = string.format("unknown ICMPv6 code %d for qtype %d", p.icmpv6_code, qtype)
  end

  return qtype, text
end

local function format_results(results)
  if empty(results) then
    return nil
  end
  local QTYPE_ORDER = {
    QTYPE_NOOP,
    QTYPE_NODENAME,
    QTYPE_NODEADDRESSES,
    QTYPE_NODEIPV4ADDRESSES,
  }
  local output

  output = stdnse.output_table()
  for _, qtype in ipairs(QTYPE_ORDER) do
    if results[qtype] then
      output[QTYPE_STRINGS[qtype]] = results[qtype]
    end
  end

  return output
end

function action(host)
  local s
  local timeout, end_time, now
  local pending, results

  timeout = host.times.timeout * 10

  s = open_sniffer(host)

  send_queries(host)

  pending = {
    [QTYPE_NODENAME] = true,
    [QTYPE_NODEADDRESSES] = true,
    [QTYPE_NODEIPV4ADDRESSES] = true,
  }
  results = {}

  now = nmap.clock_ms()
  end_time = now + timeout
  repeat
    local _, status, buf

    s:set_timeout((end_time - now) * 1000)

    status, _, _, buf = s:pcap_receive()
    if status then
      local qtype, text = handle_received_packet(buf)
      if qtype then
        results[qtype] = text
        pending[qtype] = nil
      end
    end

    now = nmap.clock_ms()
  until empty(pending) or now > end_time

  s:pcap_close()

  return format_results(results)
end
