* ``DROP`` - terminate query resolution, returns SERVFAIL to requestor
* ``TC`` - set TC=1 if the request came through UDP, forcing client to retry with TCP
* ``FORWARD(ip)`` - forward query to given IP and proxy back response (stub mode)
+* ``MIRROR(ip)`` - mirror query to given IP and continue solving it (useful for partial snooping)
* ``REROUTE({{subnet,target}, ...})`` - reroute addresses in response matching given subnet to given target, e.g. ``{'192.0.2.0/24', '127.0.0.0'}`` will rewrite '192.0.2.55' to '127.0.0.55', see :ref:`renumber module <mod-renumber>` for more information.
.. note:: The module (and ``kres``) expects domain names in wire format, not textual representation. So each label in name is prefixed with its length, e.g. "example.com" equals to ``"\7example\3com"``. You can use convenience function ``todname('example.com')`` for automatic conversion.
policy:add(policy.pattern(policy.FORWARD('2001:DB8::1'), '\4bad[0-9]\2cz'))
-- Forward all queries (complete stub mode)
policy:add(policy.all(policy.FORWARD('2001:DB8::1')))
+ -- Mirror all queries and retrieve information
+ local rule = policy:add(policy.all(policy.MIRROR('127.0.0.2')))
+ -- Print information about the rule
+ print(string.format('id: %d, matched queries: %d', rule.id, rule.count)
+ -- Reroute all addresses found in answer from 192.0.2.0/24 to 127.0.0.x
+ -- this policy is enforced on answers, therefore 'postrule'
+ local rule = policy:add(policy.REROUTE({'192.0.2.0/24', '127.0.0.0'}), true)
+ -- Delete rule that we just created
+ policy:del(rule.id)
Properties
^^^^^^^^^^
Forward query to given IP address.
-.. function:: policy:add(rule)
+.. envvar:: policy.MIRROR (address)
+
+ Forward query to given IP address.
+
+.. envvar:: policy.REROUTE({{subnet,target}, ...})
+
+ Reroute addresses in response matching given subnet to given target, e.g. ``{'192.0.2.0/24', '127.0.0.0'}`` will rewrite '192.0.2.55' to '127.0.0.55'.
+
+.. function:: policy:add(rule, postrule)
:param rule: added rule, i.e. ``policy.pattern(policy.DENY, '[0-9]+\2cz')``
- :param pattern: regular expression
+ :param postrule: boolean, if true the rule will be evaluated on answer instead of query
+ :return: rule description
- Policy to block queries based on the QNAME regex matching.
+ Add a new policy rule that is executed either or queries or answers, depending on the ``postrule`` parameter. You can then use the returned rule description to get information and unique identifier for the rule, as well as match count.
+
+.. function:: policy:del(id)
+
+ :param id: identifier of a given rule
+ :return: boolean
+
+ Remove a rule from policy list.
.. function:: policy.all(action)
return newid
end
+-- Support for client sockets from inside policy actions
+local socket_client = function () return error("missing luasocket, can't create socket client") end
+local has_socket, socket = pcall(require, 'socket')
+if has_socket then
+ socket_client = function (host, port)
+ local s, err, status
+ if host:find(':') then
+ s, err = socket.udp6()
+ else
+ s, err = socket.udp()
+ end
+ if not s then
+ return nil, err
+ end
+ status, err = s:setpeername(host, port)
+ if not status then
+ return nil, err
+ end
+ return s
+ end
+end
+local has_ffi, ffi = pcall(require, 'ffi')
+if not has_ffi then
+ socket_client = function () return error("missing ffi library, required for this policy") end
+end
+
+-- Mirror request elsewhere, and continue solving
+local function mirror(target)
+ local addr, port = target:match '([^@]*)@?(.*)'
+ if not port or #port == 0 then port = 53 end
+ local sink, err = socket_client(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
+ req = kres.request_t(req)
+ local query = req.qsource.packet
+ if query ~= nil then
+ sink:send(ffi.string(query.wire, query.size))
+ end
+ return -- Chain action to next
+ end
+end
+
-- Forward request, and solve as stub query
local function forward(target)
local dst_ip = kres.str2ip(target)
local policy = {
-- Policies
- PASS = 1, DENY = 2, DROP = 3, TC = 4, FORWARD = forward, REROUTE = reroute,
+ PASS = 1, DENY = 2, DROP = 3, TC = 4, FORWARD = forward, REROUTE = reroute, MIRROR = mirror,
-- Special values
ANY = 0,
}
end
-- Evaluate packet in given rules to determine policy action
-function policy.evaluate(rules, req, query)
+function policy.evaluate(rules, req, query, state)
for i = 1, #rules do
local rule = rules[i]
- local action = rule.cb(req, query)
- if action ~= nil then
- rule.count = rule.count + 1
- return action
+ if not rule.suspended then
+ local action = rule.cb(req, query)
+ if action ~= nil then
+ rule.count = rule.count + 1
+ local next_state = policy.enforce(state, req, action)
+ if next_state then -- Not a chain rule,
+ return next_state -- stop on first match
+ end
+ end
end
end
- return policy.PASS
+ return state
end
-- Enforce policy action
return state
end
--- Capture queries before processing
+-- 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)
req = kres.request_t(req)
- local action = policy.evaluate(policy.rules, req, req:current())
- return policy.enforce(state, req, action)
+ return policy.evaluate(policy.rules, req, req:current(), state)
end,
finish = function(state, req)
req = kres.request_t(req)
- local action = policy.evaluate(policy.postrules, req, req:current())
- return policy.enforce(state, req, action)
+ return policy.evaluate(policy.postrules, req, req:current(), state)
end
}
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(policy, 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
end
end
end
- if not changed then return state end
+ -- If not rewritten, chain action
+ if not changed then return end
-- Replace section if renumbering
local qname = pkt:qname()
local qclass = pkt:qclass()
-- Module declaration
local view = {
key = {},
- subnet = {},
+ src = {},
+ dst = {},
}
-- @function View based on TSIG key name.
end
-- @function View based on source IP subnet.
-function view.addr(view, subnet, policy)
+function view.addr(view, subnet, policy, dst)
local subnet_cd = ffi.new('char[16]')
local family = C.kr_straddr_family(subnet)
local bitlen = C.kr_straddr_subnet(subnet_cd, subnet)
local t = {family, subnet_cd, bitlen, policy}
- table.insert(view.subnet, t)
+ table.insert(dst and view.dst or view.src, t)
return t
end
local client_key = req.qsource.key
local match_cb = (client_key ~= nil) and view.key[client_key:owner()] or nil
-- Search subnets otherwise
- if match_cb == nil and req.qsource.addr ~= nil then
- for i = 1, #view.subnet do
- local pair = view.subnet[i]
- if match_subnet(pair[1], pair[2], pair[3], req.qsource.addr) then
- match_cb = pair[4]
- break
+ if match_cb == nil then
+ if req.qsource.addr ~= nil then
+ for i = 1, #view.src do
+ local pair = view.src[i]
+ if match_subnet(pair[1], pair[2], pair[3], req.qsource.addr) then
+ match_cb = pair[4]
+ break
+ end
+ end
+ elseif req.qsource.dst_addr ~= nil then
+ for i = 1, #view.dst do
+ local pair = view.dst[i]
+ if match_subnet(pair[1], pair[2], pair[3], req.qsource.dst_addr) then
+ match_cb = pair[4]
+ break
+ end
end
end
end
return match_cb
end
--- @function Return view policy rule
-function view.rule(action, subnet)
+-- @function Return policy based on source address
+function view.rule_src(action, subnet)
+ local subnet_cd = ffi.new('char[16]')
+ local family = C.kr_straddr_family(subnet)
+ local bitlen = C.kr_straddr_subnet(subnet_cd, subnet)
+ return function(req, _)
+ local addr = req.qsource.addr
+ if addr ~= nil and match_subnet(family, subnet_cd, bitlen, addr) then
+ return action
+ end
+ end
+end
+
+-- @function Return policy based on destination address
+function view.rule_dst(action, subnet)
local subnet_cd = ffi.new('char[16]')
local family = C.kr_straddr_family(subnet)
local bitlen = C.kr_straddr_subnet(subnet_cd, subnet)
return function(req, _)
- local src_addr = req.qsource.addr
- if src_addr ~= nil and match_subnet(family, subnet_cd, bitlen, src_addr) then
+ local addr = req.qsource.dst_addr
+ if addr ~= nil and match_subnet(family, subnet_cd, bitlen, addr) then
return action
end
end
local match_cb = evaluate(view, req)
if match_cb ~= nil then
local action = match_cb(req, req:current())
- return policy.enforce(state, req, action)
+ return policy.enforce(state, req, action) or state
end
return state
end