-- SPDX-License-Identifier: GPL-3.0-or-later
-- Module interface
local ffi = require('ffi')
local M = {}
local addr_buf = ffi.new('char[16]')

--[[
Missing parts of the RFC:
	> The implementation SHOULD support mapping of separate IPv4 address
	> ranges to separate IPv6 prefixes for AAAA record synthesis.  This
	> allows handling of special use IPv4 addresses [RFC5735].

	Also the exclusion prefixes are not implemented, sec. 5.1.4 (MUST).

	TODO: support different prefix lengths, defaulting to /96 if not specified
	https://tools.ietf.org/html/rfc6052#section-2.2

	PTR queries aren't supported (MUST), sec. 5.3.1.2
]]

-- Config
function M.config (confstr)
	M.proxy = kres.str2ip(confstr or '64:ff9b::')
	if M.proxy == nil then error('[dns64] "'..confstr..'" is not a valid address') end
end

-- Layers
M.layer = { }
function M.layer.consume(state, req, pkt)
	if state == kres.FAIL then return state end
	local qry = req:current()
	-- Observe only final answers in IN class where request has no CD flag.
	if M.proxy == nil or not qry.flags.RESOLVED
			or pkt:qclass() ~= kres.class.IN or req.qsource.packet:cd() then
		return state
	end
	-- Synthetic AAAA from marked A responses
	local answer = pkt:section(kres.section.ANSWER)

	-- Observe final AAAA NODATA responses to the current SNAME.
	local is_nodata = pkt:rcode() == kres.rcode.NOERROR and #answer == 0
	if pkt:qtype() == kres.type.AAAA and is_nodata and pkt:qname() == qry:name()
			and qry.flags.RESOLVED and not qry.flags.CNAME and qry.parent == nil then
		-- Start a *marked* corresponding A sub-query.
		local extraFlags = kres.mk_qflags({})
		extraFlags.DNSSEC_WANT = qry.flags.DNSSEC_WANT
		extraFlags.AWAIT_CUT = true
		extraFlags.DNS64_MARK = true
		req:push(pkt:qname(), kres.type.A, kres.class.IN, extraFlags, qry)
		return state
	end


	-- Observe answer to the marked sub-query, and convert all A records in ANSWER
	-- to corresponding AAAA records to be put into the request's answer.
	if not qry.flags.DNS64_MARK then return state end
	-- Find rank for the NODATA answer.
	-- That will result into corresponding AD flag.  See RFC 6147 5.5.2.
	local neg_rank
	if qry.parent.flags.DNSSEC_WANT and not qry.parent.flags.DNSSEC_INSECURE
		then neg_rank = ffi.C.KR_RANK_SECURE
		else neg_rank = ffi.C.KR_RANK_INSECURE
	end
	-- Find TTL bound from SOA, according to RFC 6147 5.1.7.4.
	local max_ttl = 600
	for i = 1, tonumber(req.auth_selected.len) do
		local entry = req.auth_selected.at[i - 1]
		if entry.qry_uid == qry.parent.uid and entry.rr
				and entry.rr.type == kres.type.SOA
				and entry.rr.rclass == kres.class.IN then
			max_ttl = entry.rr:ttl()
		end
	end
	-- Find the As and do the conversion itself.
	for i = 1, tonumber(req.answ_selected.len) do
		local orig = req.answ_selected.at[i - 1]
		if orig.qry_uid == qry.uid and orig.rr.type == kres.type.A then
			local rank = neg_rank
			if orig.rank < rank then rank = orig.rank end
			-- Disable GC, as this object doesn't own owner or RDATA, it's just a reference
			local ttl = orig.rr:ttl()
			if ttl > max_ttl then ttl = max_ttl end
			local rrs = ffi.gc(kres.rrset(nil, kres.type.AAAA, orig.rr.rclass, ttl), nil)
			rrs._owner = orig.rr._owner
			for k = 1, orig.rr.rrs.count do
				local rdata = orig.rr:rdata( k - 1 )
				ffi.copy(addr_buf, M.proxy, 12)
				ffi.copy(addr_buf + 12, rdata, 4)
				ffi.C.knot_rrset_add_rdata(rrs, ffi.string(addr_buf, 16), 16, req.pool)
			end
			ffi.C.kr_ranked_rrarray_add(
				req.answ_selected,
				rrs,
				rank,
				true,
				qry.uid,
				req.pool)
		end
	end
	ffi.C.kr_ranked_rrarray_finalize(req.answ_selected, qry.uid, req.pool);
end

return M
