local bin = require "bin"
local bit = require "bit"
local dns = require "dns"
local nmap = require "nmap"
local packet = require "packet"
local stdnse = require "stdnse"
local string = require "string"

local openssl = stdnse.silent_require "openssl"

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: (actually hostnames) mac-mini.local

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 payload, p, flags
	local nonce

	nonce = openssl.rand_pseudo_bytes(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
	payload = bin.pack(">SSAA", qtype, flags, nonce, dst)
	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)
	p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODENAME)
	dnet:ip_send(p)
	p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODEIPV4ADDRESSES)
	dnet:ip_send(p)
	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 ttl
	local names = {}
	local pos = nil

	pos, ttl = bin.unpack(">I", data, pos)
	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

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

	status, names = try_decode_nodenames(data)
	if empty(names) then
		return
	end
	text = stdnse.strjoin(", ", names)
	if not status then
		text = text .. " (parsing error)"
	end

	return text
end

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

	while true do
		pos, ttl, binaddr = bin.unpack(">IA16", data, pos)
		if not ttl then
			break
		end
		addrs[#addrs + 1] = packet.toipv6(binaddr)
	end
	if empty(addrs) then
		return
	end

	text = stdnse.strjoin(", ", addrs)
	if bit.band(flags, 0x01) ~= 0 then
		text = text .. " (more omitted for space reasons)"
	end

	return text
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 text
	local addrs = {}
	local pos = nil

	-- Check for DNS names.
	status, names = try_decode_nodenames(data .. "\0\0")
	if status then
		return "(actually hostnames) " .. stdnse.strjoin(", ", names)
	end

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

	text = stdnse.strjoin(", ", addrs)
	if bit.band(flags, 0x01) ~= 0 then
		text = text .. " (more omitted for space reasons)"
	end

	return text
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.print_debug(1, "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)
	local QTYPE_ORDER = {
		QTYPE_NOOP,
		QTYPE_NODENAME,
		QTYPE_NODEADDRESSES,
		QTYPE_NODEIPV4ADDRESSES,
	}
	local output

	output = {}
	for _, qtype in ipairs(QTYPE_ORDER) do
		if results[qtype] then
			output[#output + 1] = QTYPE_STRINGS[qtype] .. ": " .. results[qtype]
		end
	end

	return stdnse.format_output(true, 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