Newer
Older
-- SPDX-License-Identifier: GPL-3.0-or-later
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)
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)
return addr, tonumber(port[0])
-- 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);
error("target '"..target..'" is not a valid IP address')
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)
-- Mirror request elsewhere, and continue solving
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
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))
-- Forward request, and solve as stub query
local list = {}
if type(target) == 'table' then
for _, v in pairs(target) do
table.insert(list, addr2sock(v, 53))
table.insert(list, addr2sock(target, 53))
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
return state
end
end
-- Forward request and all subrequests to upstream; validate answers
local list = {}
if type(target) == 'table' then
for _, v in pairs(target) do
table.insert(list, addr2sock(v, 53))
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
-- 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))

Grigorii Demidov
committed
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])
sockaddr_c_set[sockaddr_lua] = true;
table.insert(nslist, sockaddr_c)
net.tls_client(target)

Grigorii Demidov
committed
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

Grigorii Demidov
committed
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
function policy.FLAGS(opts_set, opts_clear)
-- 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
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
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
else
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
return kres.DONE
-- All requests
function policy.all(action)
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 match = AC.match(tree, query:name(), false)
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
if not string.find(qname, common_suffix, -common_len, true) then
return nil
local zone = suffix_list[i]
if string.find(qname, zone, -string.len(zone), true) then
end
end
return nil
end
end
-- Filter QNAME pattern
function policy.pattern(action, pattern)
if string.find(query:name(), pattern) then
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.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 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))
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])
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 }
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
assert(is_bad == false and prefix_labels == 0)
end
::continue::
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
-- 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')
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)
-- 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)
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
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
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
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")
return kres.DONE
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
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',
log_notrace(req, 'answer packet:\n%s\n', req.answer)
log_notrace(req, 'answer packet DROPPED\n')
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)
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()
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")
function policy.NO_ANSWER(_, req)
req.options.NO_ANSWER = true
log_policy_action(req, 'NO_ANSWER')
return kres.FAIL
end
local answer = answer_clear(req)
if answer == nil then return nil end
answer:ad(false)
req:set_extended_error(kres.extended_error.PROHIBITED, "EIM4")
if req.qsource.addr == nil or req.qsource.flags.tcp then
local answer = answer_clear(req)
if answer == nil then return nil end
answer:tc(1)
answer:ad(false)
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)
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
-- 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
-- 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
for i, v in ipairs(names) do
end
-- @var Default rules
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