-- SPDX-License-Identifier: GPL-3.0-or-later
local kres = require('kres')
local ffi = require('ffi')

local LOG_GRP_POLICY_TAG = ffi.string(ffi.C.kr_log_grp2name(ffi.C.LOG_GRP_POLICY))
local LOG_GRP_REQDBG_TAG = ffi.string(ffi.C.kr_log_grp2name(ffi.C.LOG_GRP_REQDBG))

-- Counter of unique rules
local nextid = 0
local function getruleid()
	local newid = nextid
	nextid = nextid + 1
	return newid
end

-- Support for client sockets from inside policy actions
local socket_client = function ()
	return error("missing lua-cqueues library, can't create socket client")
end
local has_socket, socket = pcall(require, 'cqueues.socket')
if has_socket then
	socket_client = function (host, port)
		local s, err, status

		s = socket.connect({ host = host, port = port, type = socket.SOCK_DGRAM })
		s:setmode('bn', 'bn')
		status, err = pcall(s.connect, s)

		if not status then
			return status, err
		end
		return s
	end
end

-- Split address and port from a combined string.
local function addr_split_port(target, default_port)
	assert(default_port and type(default_port) == 'number')
	local port = ffi.new('uint16_t[1]', default_port)
	local addr = ffi.new('char[47]') -- INET6_ADDRSTRLEN + 1
	local ret = ffi.C.kr_straddr_split(target, addr, port)
	if ret ~= 0 then
		error('failed to parse address ' .. target)
	end
	return addr, tonumber(port[0])
end

-- String address@port -> sockaddr.
local function addr2sock(target, default_port)
	local addr, port = addr_split_port(target, default_port)
	local sock = ffi.gc(ffi.C.kr_straddr_socket(addr, port, nil), ffi.C.free);
	if sock == nil then
		error("target '"..target..'" is not a valid IP address')
	end
	return sock
end

-- Debug logging for taken policy actions
local function log_policy_action(req, name)
	if ffi.C.kr_log_is_debug_fun(ffi.C.LOG_GRP_POLICY, req) then
		local qry = req:current()
		ffi.C.kr_log_req1(
			req, qry.uid, 2, ffi.C.LOG_GRP_POLICY, LOG_GRP_POLICY_TAG,
			"%s applied for %s %s\n",
			name, kres.dname2str(qry.sname), kres.tostring.type[qry.stype])
	end
end

-- policy functions are defined below
local policy = {}

function policy.PASS(state, req)
	policy.FLAGS('PASSTHRU_LEGACY')(state, req)
	return state
end

-- Mirror request elsewhere, and continue solving
function policy.MIRROR(target)
	local addr, port = addr_split_port(target, 53)
	local sink, err = socket_client(ffi.string(addr), port)
	if not sink then panic('MIRROR target %s is not a valid: %s', target, err) end
	return function(state, req)
		if state == kres.FAIL then return state end
		local query = req.qsource.packet
		if query ~= nil then
			sink:send(ffi.string(query.wire, query.size), 1, tonumber(query.size))
		end
		return -- Chain action to next
	end
end

-- Override the list of nameservers (forwarders)
local function set_nslist(req, list)
	local ns_i = 0
	for _, ns in ipairs(list) do
		if ffi.C.kr_forward_add_target(req, ns) == 0 then
			ns_i = ns_i + 1
		end
	end
	if ns_i == 0 then
		-- would use assert() but don't want to compose the message if not triggered
		error('no usable address in NS set (check net.ipv4 and '
		      .. 'net.ipv6 config):\n' .. table_print(list, 2))
	end
end

-- Forward request, and solve as stub query
function policy.STUB(target)
	local list = {}
	if type(target) == 'table' then
		for _, v in pairs(target) do
			table.insert(list, addr2sock(v, 53))
		end
	else
		table.insert(list, addr2sock(target, 53))
	end
	return function(state, req)
		local qry = req:current()
		-- Switch mode to stub resolver, do not track origin zone cut since it's not real authority NS
		qry.flags.STUB = true
		qry.flags.ALWAYS_CUT = false
		set_nslist(req, list)
		return state
	end
end

-- Forward request and all subrequests to upstream; validate answers
function policy.FORWARD(target)
	local list = {}
	if type(target) == 'table' then
		for _, v in pairs(target) do
			table.insert(list, addr2sock(v, 53))
		end
	else
		table.insert(list, addr2sock(target, 53))
	end
	return function(state, req)
		local qry = req:current()
		req.options.FORWARD = true
		req.options.NO_MINIMIZE = true
		qry.flags.FORWARD = true
		qry.flags.ALWAYS_CUT = false
		qry.flags.NO_MINIMIZE = true
		qry.flags.AWAIT_CUT = true
		set_nslist(req, list)
		return state
	end
end

-- Forward request and all subrequests to upstream over TLS; validate answers
function policy.TLS_FORWARD(targets)
	if type(targets) ~= 'table' or #targets < 1 then
		error('TLS_FORWARD argument must be a non-empty table')
	end

	local sockaddr_c_set = {}
	local nslist = {} -- to persist in closure of the returned function
	for idx, target in pairs(targets) do
		if type(target) ~= 'table' or type(target[1]) ~= 'string' then
			error(string.format('TLS_FORWARD configuration at position ' ..
			'%d must be a table starting with an IP address', idx))
		end
		-- Note: some functions have checks with error() calls inside.
		local sockaddr_c = addr2sock(target[1], 853)

		-- Refuse repeated addresses in the same set.
		local sockaddr_lua = ffi.string(sockaddr_c, ffi.C.kr_sockaddr_len(sockaddr_c))
		if sockaddr_c_set[sockaddr_lua] then
			error('TLS_FORWARD configuration cannot declare two configs for IP address '
					.. target[1])
		else
			sockaddr_c_set[sockaddr_lua] = true;
		end

		table.insert(nslist, sockaddr_c)
		net.tls_client(target)
	end

	return function(state, req)
		local qry = req:current()
		req.options.FORWARD = true
		req.options.NO_MINIMIZE = true
		qry.flags.FORWARD = true
		qry.flags.ALWAYS_CUT = false
		qry.flags.NO_MINIMIZE = true
		qry.flags.AWAIT_CUT = true
		req.options.TCP = true
		qry.flags.TCP = true
		set_nslist(req, nslist)
		return state
	end
end

-- Rewrite records in packet
function policy.REROUTE(tbl, names)
	-- Import renumbering rules
	local ren = require('kres_modules.renumber')
	local prefixes = {}
	for from, to in pairs(tbl) do
		local prefix = names and ren.name(from, to) or ren.prefix(from, to)
		table.insert(prefixes, prefix)
	end
	-- Return rule closure
	return ren.rule(prefixes)
end

-- Set and clear some query flags
function policy.FLAGS(opts_set, opts_clear)
	return function(_, req)
		-- We assume to be running in the begin phase, so to truly apply
		-- to the whole request we need to change both kr_request and kr_query.
		local qry = req:current()
		for _, flags in pairs({qry.flags, req.options}) do
			ffi.C.kr_qflags_set  (flags, kres.mk_qflags(opts_set   or {}))
			ffi.C.kr_qflags_clear(flags, kres.mk_qflags(opts_clear or {}))
		end
		return nil -- chain rule
	end
end

local function mkauth_soa(answer, dname, mname, ttl)
	if mname == nil then
		mname = dname
	end
	return answer:put(dname, ttl or 10800, answer:qclass(), kres.type.SOA,
		mname .. '\6nobody\7invalid\0\0\0\0\1\0\0\14\16\0\0\4\176\0\9\58\128\0\0\42\48')
end

-- Create answer with passed arguments
function policy.ANSWER(rtable, nodata)
	return function(_, req)
		local qry = req:current()
		local data = rtable[qry.stype]
		if data == nil and nodata ~= true then
			return nil
		end
		-- now we're certain we want to generate an answer

		local answer = req:ensure_answer()
		if answer == nil then return nil end
		ffi.C.kr_pkt_make_auth_header(answer)
		local ttl = (data or {}).ttl or 1
		answer:rcode(kres.rcode.NOERROR)
		req:set_extended_error(kres.extended_error.FORGED, "5DO5")

		if data == nil then -- want NODATA, i.e. just a SOA
			answer:begin(kres.section.AUTHORITY)
			local soa = rtable[kres.type.SOA]
			if soa ~= nil then
				answer:put(qry.sname, soa.ttl or ttl, qry.sclass, kres.type.SOA,
							soa.rdata[1] or soa.rdata)
			else
				mkauth_soa(answer, kres.dname2wire(qry.sname), nil, ttl)
			end
			log_policy_action(req, 'ANSWER (nodata)')
		else
			answer:begin(kres.section.ANSWER)
			if type(data.rdata) == 'table' then
				for _, entry in ipairs(data.rdata) do
					answer:put(qry.sname, ttl, qry.sclass, qry.stype, entry)
				end
			else
				answer:put(qry.sname, ttl, qry.sclass, qry.stype, data.rdata)
			end
			log_policy_action(req, 'ANSWER (forged)')
		end
		return kres.DONE
	end
end

-- All requests
function policy.all(action)
	return function(_, _) return action end
end

-- Requests whose QNAME is exactly the provided domain
function policy.domains(action, dname_list)
	return function(_, query)
		local qname = query:name()
		for _, dname in ipairs(dname_list) do
			if ffi.C.knot_dname_is_equal(qname, dname) then
				return action
			end
		end
		return nil
	end
end

-- Requests whose QNAME matches given zone list (i.e. suffix match)
function policy.suffix(action, zone_list)
	local AC = require('ahocorasick')
	local tree = AC.create(zone_list)
	return function(_, query)
		local match = AC.match(tree, query:name(), false)
		if match ~= nil then
			return action
		end
		return nil
	end
end

-- Check for common suffix first, then suffix match (specialized version of suffix match)
function policy.suffix_common(action, suffix_list, common_suffix)
	local common_len = string.len(common_suffix)
	local suffix_count = #suffix_list
	return function(_, query)
		-- Preliminary check
		local qname = query:name()
		if not string.find(qname, common_suffix, -common_len, true) then
			return nil
		end
		-- String match
		for i = 1, suffix_count do
			local zone = suffix_list[i]
			if string.find(qname, zone, -string.len(zone), true) then
				return action
			end
		end
		return nil
	end
end

-- Filter QNAME pattern
function policy.pattern(action, pattern)
	return function(_, query)
		if string.find(query:name(), pattern) then
			return action
		end
		return nil
	end
end

local function rpz_parse(action, path)
	local rules = {}
	local new_actions = {}
	local action_map = {
		-- RPZ Policy Actions
		['\0'] = action,
		['\1*\0'] = policy.ANSWER({}, true),
		['\012rpz-passthru\0'] = policy.PASS, -- the grammar...
		['\008rpz-drop\0'] = policy.DROP,
		['\012rpz-tcp-only\0'] = policy.TC,
		-- Policy triggers @NYI@
	}
	-- RR types to be skipped; boolean denoting whether to throw a warning even for RPZ apex.
	local rrtype_bad = {
		[kres.type.DNAME]  = true,
		[kres.type.NS]     = false,
		[kres.type.DNSKEY] = true,
		[kres.type.DS]     = true,
		[kres.type.RRSIG]  = true,
		[kres.type.NSEC]   = true,
		[kres.type.NSEC3]  = true,
	}

	-- We generally don't know what zone should be in the file; we try to detect it.
	-- Fortunately, it's typical that SOA is the first record, even required for AXFR.
	local origin_soa = nil
	local warned_soa, warned_bailiwick

	local parser = require('zonefile').new()
	local ok, errstr = parser:open(path)
	if not ok then
		error(string.format('failed to parse "%s": %s', path, errstr or "unknown error"))
	end
	while true do
		ok, errstr = parser:parse()
		if errstr then
			log_warn(ffi.C.LOG_GRP_POLICY, 'RPZ %s:%d: %s',
				path, tonumber(parser.line_counter), errstr)
		end
		if not ok then break end

		local full_name = ffi.gc(ffi.C.knot_dname_copy(parser.r_owner, nil), ffi.C.free)
		local rdata = ffi.string(parser.r_data, parser.r_data_length)
		ffi.C.knot_dname_to_lower(full_name)

		local origin = origin_soa or parser.zone_origin
		local prefix_labels = ffi.C.knot_dname_in_bailiwick(full_name, origin)
		if prefix_labels < 0 then
			if not warned_bailiwick then
				warned_bailiwick = true
				log_warn(ffi.C.LOG_GRP_POLICY,
					'RPZ %s:%d: RR owner "%s" outside the zone (ignored; reported once per file)',
					path, tonumber(parser.line_counter), kres.dname2str(full_name))
			end
			goto continue
		end

		local bytes = ffi.C.knot_dname_size(full_name) - ffi.C.knot_dname_size(origin)
		local name = ffi.string(full_name, bytes) .. '\0'

		if parser.r_type == kres.type.CNAME then
			if action_map[rdata] then
				rules[name] = action_map[rdata]
			else
				log_warn(ffi.C.LOG_GRP_POLICY,
					'RPZ %s:%d: CNAME with custom target in RPZ is not supported yet (ignored)',
					path, tonumber(parser.line_counter))
			end
		else
			if #name then
				local is_bad = rrtype_bad[parser.r_type]

				if parser.r_type == kres.type.SOA then
					if origin_soa == nil then
						origin_soa = ffi.gc(ffi.C.knot_dname_copy(parser.r_owner, nil), ffi.C.free)
						goto continue -- we don't want to modify `new_actions`
					else
						is_bad = true -- maybe provide more info, but it seems rare
					end
				elseif origin_soa == nil and not warned_soa then
					warned_soa = true
					log_warn(ffi.C.LOG_GRP_POLICY,
						'RPZ %s:%d warning: SOA missing as the first record',
						path, tonumber(parser.line_counter))
				end

				if is_bad == true or (is_bad == false and prefix_labels ~= 0) then
					log_warn(ffi.C.LOG_GRP_POLICY, 'RPZ %s:%d warning: RR type %s is not allowed in RPZ (ignored)',
						path, tonumber(parser.line_counter), kres.tostring.type[parser.r_type])
				elseif is_bad == nil then
					if new_actions[name] == nil then new_actions[name] = {} end
					local act = new_actions[name][parser.r_type]
					if act == nil then
						new_actions[name][parser.r_type] = { ttl=parser.r_ttl, rdata=rdata }
					else -- multiple RRs: no reordering or deduplication
						if type(act.rdata) ~= 'table' then
							act.rdata = { act.rdata }
						end
						table.insert(act.rdata, rdata)
						if parser.r_ttl ~= act.ttl then -- be conservative
							log_warn(ffi.C.LOG_GRP_POLICY, 'RPZ %s:%d warning: different TTLs in a set (minimum taken)',
								path, tonumber(parser.line_counter))
							act.ttl = math.min(act.ttl, parser.r_ttl)
						end
					end
				else
					assert(is_bad == false and prefix_labels == 0)
				end
			end
		end

		::continue::
	end
	collectgarbage()
	for qname, rrsets in pairs(new_actions) do
		rules[qname] = policy.ANSWER(rrsets, true)
	end
	return rules
end

-- Split path into dirname and basename (like the shell utilities)
local function get_dir_and_file(path)
	local dir, file = string.match(path, "(.*)/([^/]+)")

	-- If regex doesn't match then path must be the file directly (i.e. doesn't contain '/')
	-- This assumes that the file exists (rpz_parse() would fail if it doesn't)
	if not dir and not file then
		dir = '.'
		file = path
	end

	return dir, file
end

-- RPZ policy set
-- Create RPZ from zone file and optionally watch the file for changes
function policy.rpz(action, path, watch)
	local rules = rpz_parse(action, path)

	if watch ~= false then
		local has_notify, notify  = pcall(require, 'cqueues.notify')
		if has_notify then
			local bit = require('bit')

			local dir, file = get_dir_and_file(path)
			local watcher = notify.opendir(dir)
			watcher:add(file, bit.bxor(notify.CREATE, notify.MODIFY))

			worker.coroutine(function ()
				for _, name in watcher:changes() do
					-- Limit to changes on file we're interested in
					-- Watcher will also fire for changes to the directory itself
					if name == file then
						-- If the file changes then reparse and replace the existing ruleset
						log_info(ffi.C.LOG_GRP_POLICY, 'RPZ reloading: ' .. name)
						rules = rpz_parse(action, path)
					end
				end
			end)
		elseif watch then -- explicitly requested and failed
			error('[poli] lua-cqueues required to watch and reload RPZ file')
		else
			log_info(ffi.C.LOG_GRP_POLICY, 'lua-cqueues required to watch and reload RPZ file, continuing without watching')
		end
	end

	return function(_, query)
		local label = query:name()
		local rule = rules[label]
		while rule == nil and string.len(label) > 0 do
			label = string.sub(label, string.byte(label) + 2)
			rule = rules['\1*'..label]
		end
		return rule
	end
end

-- Apply an action when query belongs to a slice (determined by slice_func())
function policy.slice(slice_func, ...)
	local actions = {...}
	if #actions <= 0 then
		error('[poli] at least one action must be provided to policy.slice()')
	end

	return function(_, query)
		local index = slice_func(query, #actions)
		return actions[index]
	end
end

-- Initializes slicing function that randomly assigns queries to a slice based on their registrable domain
function policy.slice_randomize_psl(seed)
	local has_psl, psl_lib = pcall(require, 'psl')
	if not has_psl then
		error('[poli] lua-psl is required for policy.slice_randomize_psl()')
	end
	-- load psl
	local has_latest, psl = pcall(psl_lib.latest)
	if not has_latest then -- compatibility with lua-psl < 0.15
		psl = psl_lib.builtin()
	end

	if seed == nil then
		seed = os.time() / (3600 * 24 * 7)
	end
	seed = math.floor(seed)  -- convert to int

	return function(query, length)
		assert(length > 0)

		local domain = kres.dname2str(query:name())
		if domain == nil then -- invalid data: auto-select first action
			return 1
		end
		if domain:len() > 1 then  --remove trailing dot
			domain = domain:sub(0, -2)
		end

		-- do psl lookup for registrable domain
		local reg_domain = psl:registrable_domain(domain)
		if reg_domain == nil then  -- fallback to unreg. domain
			reg_domain = psl:unregistrable_domain(domain)
			if reg_domain == nil then  -- shouldn't happen: safe fallback
				return 1
			end
		end

		local rand_seed = seed
		-- create deterministic seed for pseudo-random slice assignment
		for i = 1, #reg_domain do
			rand_seed = rand_seed + reg_domain:byte(i)
		end

		-- use linear congruential generator with values from ANSI C
		rand_seed = rand_seed % 0x80000000  -- ensure seed is positive 32b int
		local rand = (1103515245 * rand_seed + 12345) % 0x10000
		return 1 + rand % length
	end
end

-- Prepare for making an answer from scratch.  (Return the packet for convenience.)
local function answer_clear(req)
	-- If we're in postrules, previous resolving might have chosen some RRs
	-- for inclusion in the answer, so we need to avoid those.
	-- *_selected arrays are in mempool, so explicit deallocation is not necessary.
	req.answ_selected.len = 0
	req.auth_selected.len = 0
	req.add_selected.len = 0

	-- Let's be defensive and clear the answer, too.
	local pkt = req:ensure_answer()
	if pkt == nil then return nil end
	pkt:clear_payload()
	req:ensure_edns()
	return pkt
end

function policy.DENY_MSG(msg, extended_error)
	if msg and (type(msg) ~= 'string' or #msg >= 255) then
		error('DENY_MSG: optional msg must be string shorter than 256 characters')
        end
	if extended_error == nil then
		extended_error = kres.extended_error.BLOCKED
	end
	local action_name = msg and 'DENY_MSG' or 'DENY'

	return function (_, req)
		-- Write authority information
		local answer = answer_clear(req)
		if answer == nil then return nil end
		ffi.C.kr_pkt_make_auth_header(answer)
		answer:rcode(kres.rcode.NXDOMAIN)
		answer:begin(kres.section.AUTHORITY)
		mkauth_soa(answer, answer:qname())
		if msg then
			answer:begin(kres.section.ADDITIONAL)
			answer:put('\11explanation\7invalid', 10800, answer:qclass(), kres.type.TXT,
				   string.char(#msg) .. msg)

		end
		req:set_extended_error(extended_error, "CR36")
		log_policy_action(req, action_name)
		return kres.DONE
	end
end

local function free_cb(func)
	func:free()
end

local debug_logline_cb = ffi.cast('trace_log_f', function (_, msg)
	jit.off(true, true) -- JIT for (C -> lua)^2 nesting isn't allowed
	ffi.C.kr_log_fmt(
		ffi.C.LOG_GRP_REQDBG, -- but the original [group] tag also remains in the string
		LOG_DEBUG,
		'CODE_FILE=policy.lua', 'CODE_LINE=', 'CODE_FUNC=policy.DEBUG_ALWAYS', -- no meaningful locations
		'[%-6s]%s', LOG_GRP_REQDBG_TAG, msg) -- msg should end with newline already
end)
ffi.gc(debug_logline_cb, free_cb)

-- LOG_DEBUG without log_trace and without code locations
local function log_notrace(req, fmt, ...)
	ffi.C.kr_log_fmt(ffi.C.LOG_GRP_REQDBG, LOG_DEBUG,
		'CODE_FILE=policy.lua', 'CODE_LINE=', 'CODE_FUNC=', -- no meaningful locations
		'%s', string.format( -- convert in lua, as integers in C varargs would pass as double
			'[%-6s][%-6s][%05u.00] ' .. fmt,
			LOG_GRP_REQDBG_TAG, LOG_GRP_POLICY_TAG, req.uid, ...)
	)
end

local debug_logfinish_cb = ffi.cast('trace_callback_f', function (req)
	jit.off(true, true) -- JIT for (C -> lua)^2 nesting isn't allowed
	log_notrace(req, 'following rrsets were marked as interesting:\n%s\n',
		req:selected_tostring())
	if req.answer ~= nil then
		log_notrace(req, 'answer packet:\n%s\n', req.answer)
	else
		log_notrace(req, 'answer packet DROPPED\n')
	end
end)
ffi.gc(debug_logfinish_cb, free_cb)

-- log request packet
function policy.REQTRACE(_, req)
	log_notrace(req, 'request packet:\n%s', req.qsource.packet)
end

-- log how the request arrived, notably the client's IP
function policy.IPTRACE(_, req)
	if req.qsource.addr == nil then
		log_notrace(req, 'request packet arrived internally\n')
	else
		-- stringify transport flags: struct kr_request_qsource_flags
		local qf = req.qsource.flags
		local qf_str = qf.tcp and 'TCP' or 'UDP'
		if qf.tls  then qf_str = qf_str .. ' + TLS'  end
		if qf.http then qf_str = qf_str .. ' + HTTP' end
		if qf.xdp  then qf_str = qf_str .. ' + XDP'  end

		log_notrace(req, 'request packet arrived from %s to %s (%s)\n',
			req.qsource.addr, req.qsource.dst_addr, qf_str)
	end
	return nil -- chain rule
end

function policy.DEBUG_ALWAYS(state, req)
	policy.QTRACE(state, req)
	req:trace_chain_callbacks(debug_logline_cb, debug_logfinish_cb)
	policy.REQTRACE(state, req)
end

local debug_stashlog_cb = ffi.cast('trace_log_f', function (req, msg)
	jit.off(true, true) -- JIT for (C -> lua)^2 nesting isn't allowed

	-- stash messages for conditional logging in trace_finish
	local stash = req:vars()['policy_debug_stash']
	table.insert(stash, ffi.string(msg))
end)
ffi.gc(debug_stashlog_cb, free_cb)

-- buffer debug logs and print then only if test() returns a truthy value
function policy.DEBUG_IF(test)
	local debug_finish_cb = ffi.cast('trace_callback_f', function (cbreq)
		jit.off(true, true) -- JIT for (C -> lua)^2 nesting isn't allowed
		if test(cbreq) then
			policy.REQTRACE(nil, cbreq)
			debug_logfinish_cb(cbreq)  -- unconditional version

			local stash = cbreq:vars()['policy_debug_stash']
			for _, line in ipairs(stash) do -- don't want one huge entry
				ffi.C.kr_log_fmt(ffi.C.LOG_GRP_REQDBG, LOG_DEBUG,
					'CODE_FILE=policy.lua', 'CODE_LINE=', 'CODE_FUNC=', -- no meaningful locations
					'[%-6s]%s', LOG_GRP_REQDBG_TAG, line)
			end
		end
	end)
	ffi.gc(debug_finish_cb, function (func) func:free() end)

	return function (state, req)
		req:vars()['policy_debug_stash'] = {}
		policy.QTRACE(state, req)
		req:trace_chain_callbacks(debug_stashlog_cb, debug_finish_cb)
		return
	end
end

policy.DEBUG_CACHE_MISS = policy.DEBUG_IF(
	function(req)
		return not req:all_from_cache()
	end
)

policy.DENY = policy.DENY_MSG() -- compatibility with < 2.0

function policy.DROP(_, req)
	local answer = answer_clear(req)
	if answer == nil then return nil end
	req:set_extended_error(kres.extended_error.PROHIBITED, "U5KL")
	log_policy_action(req, 'DROP')
	return kres.FAIL
end

function policy.NO_ANSWER(_, req)
	req.options.NO_ANSWER = true
	log_policy_action(req, 'NO_ANSWER')
	return kres.FAIL
end

function policy.REFUSE(_, req)
	local answer = answer_clear(req)
	if answer == nil then return nil end
	answer:rcode(kres.rcode.REFUSED)
	answer:ad(false)
	req:set_extended_error(kres.extended_error.PROHIBITED, "EIM4")
	log_policy_action(req, 'REFUSE')
	return kres.DONE
end

function policy.TC(state, req)
	-- Avoid non-UDP queries
	if req.qsource.addr == nil or req.qsource.flags.tcp then
		return state
	end

	local answer = answer_clear(req)
	if answer == nil then return nil end
	answer:tc(1)
	answer:ad(false)
	log_policy_action(req, 'TC')
	return kres.DONE
end

function policy.QTRACE(_, req)
	local qry = req:current()
	req.options.TRACE = true
	qry.flags.TRACE = true
	return -- this allows to continue iterating over policy list
end

-- Evaluate packet in given rules to determine policy action
function policy.evaluate(rules, req, query, state)
	for i = 1, #rules do
		local rule = rules[i]
		if not rule.suspended then
			local action = rule.cb(req, query)
			if action ~= nil then
				rule.count = rule.count + 1
				local next_state = action(state, req)
				if next_state then    -- Not a chain rule,
					return next_state -- stop on first match
				end
			end
		end
	end
	return
end

-- Add rule to policy list
function policy.add(rule, postrule)
	-- Compatibility with 1.0.0 API
	-- it will be dropped in 1.2.0
	if rule == policy then
		rule = postrule
		postrule = nil
	end
	-- End of compatibility shim
	local desc = {id=getruleid(), cb=rule, count=0}
	table.insert(postrule and policy.postrules or policy.rules, desc)
	return desc
end

-- Remove rule from a list
local function delrule(rules, id)
	for i, r in ipairs(rules) do
		if r.id == id then
			table.remove(rules, i)
			return true
		end
	end
	return false
end

-- Delete rule from policy list
function policy.del(id)
	if not delrule(policy.rules, id) then
		if not delrule(policy.postrules, id) then
			return false
		end
	end
	return true
end

-- Convert list of string names to domain names
function policy.todnames(names)
	for i, v in ipairs(names) do
		names[i] = kres.str2dname(v)
	end
	return names
end

-- @var Default rules
policy.rules = {}
policy.postrules = {}

-- This certainly isn't perfect, but it allows lua config like:
-- kr_view_insert_action('127.0.0.0/24', policy.TAGS_ASSIGN({'t01', 't02'}))
local kr_rule_tags_t = ffi.typeof('kr_rule_tags_t[1]')
function policy.get_tagset(names)
	local result = ffi.new(kr_rule_tags_t, 0)
	for _, name in pairs(names) do
		if ffi.C.kr_rule_tag_add(name, result) ~= 0 then
			error('converting tagset failed')
		end
	end
	return result[0] -- it's atomic value fortunately
end
function policy.tags_assign_bitmap(bitmap)
	return function (_, req)
		req.rule_tags = bitmap
	end
end
function policy.TAGS_ASSIGN(names)
	local bitmap = policy.get_tagset(names)
	return 'policy.tags_assign_bitmap(' .. tostring(bitmap) .. ')'
end

-- Perform a list of actions sequentially; meant for kr_view_insert_action().
function policy.COMBINE(list)
	if #list == 1 then return list[1] end
	local r = 'function(state,req) '
		for _, item in ipairs(list) do
			r = r .. item .. '(state,req); '
		end
	return r .. 'end'
end

--[[ Insert a forwarding rule, i.e. override upstream for one DNS subtree.

Throws lua exceptions when detecting something fishy.

\param subtree plain string
\param options
  .auth targets are authoritative (false by default = resolver)
  .dnssec if overridden to false, don't validate DNSSEC locally
    - for resolvers we still do *not* send CD=1 upstream,
      i.e. we trust their DNSSEC validation.
    - for auths this inserts a negative trust anchor
      Beware that setting .set_insecure() *later* would override that.
\param targets same format as policy.TLS_FORWARD() except that `tls = true`
               can be specified for each address (defaults to false)
--]]
function policy.rule_forward_add(subtree, options, targets)
	local targets_2 = {}
	for _, target in ipairs(targets) do
		local port_default = 53
		if target.tls or false then
			port_default = 853
			-- lots of code; easiest to just call it this way; checks and throws
			-- The extra .tls field gets ignored.
			policy.TLS_FORWARD({target})
		end

		-- this also throws on failure
		local sock = addr2sock(target[1], port_default)
		if options.auth then
			local port = ffi.C.kr_inaddr_port(sock)
			assert(not options.tls and port == port_default)
		end
		table.insert(targets_2, sock)
	end
	local targets_3 = ffi.new('const struct sockaddr * [?]', #targets_2 + 1, targets_2)
	targets_3[#targets_2] = nil

	local subtree_dname = todname(subtree)
	assert(ffi.C.kr_rule_forward(subtree_dname,
			{
				is_nods = options.dnssec == false,
				is_auth = options.auth,
			},
			targets_3
		) == 0)

	-- Probably the best way to turn off DNSSEC validation for auth is negative TA.
	if options.auth and options.dnssec == false then
		local ntas = kres.context().negative_anchors
		assert(ffi.C.kr_ta_add(ntas, subtree_dname, kres.type.DS, 0, nil, 0) == 0)
	end
end


local view_action_buf = ffi.new('knot_db_val_t[1]')

-- Top-down policy list walk until we hit a match
-- the caller is responsible for reordering policy list
-- from most specific to least specific.
-- Some rules may be chained, in this case they are evaluated
-- as a dependency chain, e.g. r1,r2,r3 -> r3(r2(r1(state)))
policy.layer = {
	begin = function(state, req)
		-- Don't act on "finished" cases.
		if bit.band(state, bit.bor(kres.FAIL, kres.DONE)) ~= 0 then return state end

		if ffi.C.kr_view_select_action(req, view_action_buf) == 0 then
			local act_str = ffi.string(view_action_buf[0].data, view_action_buf[0].len)
			return loadstring('return '..act_str)()(state, req)
		end

		local qry = req:initial() -- same as :current() but more descriptive
		return policy.evaluate(policy.rules, req, qry, state)
			or state
	end,
	finish = function(state, req)
		-- Optimization for the typical case
		if #policy.postrules == 0 then return state end
		-- Don't act on failed cases.
		if bit.band(state, kres.FAIL) ~= 0 then return state end
		return policy.evaluate(policy.postrules, req, req:initial(), state) or state
	end
}

return policy
