-.. _mod-block:
+.. _mod-policy:
-Query blocking
+Query policies
--------------
-This module can block queries (and subrequests) based on user-defined policies.
+This module can block, rewrite, or alter queries based on user-defined policies.
By default, it blocks queries to reverse lookups in private subnets as per :rfc:`1918`, :rfc:`5735` and :rfc:`5737`.
You can however extend it to deflect `Slow drip DNS attacks <https://blog.secure64.com/?p=377>`_ for example, or gray-list resolution of misbehaving zones.
+It supports a subset of the ISC RPZ_ format.
There are two policies implemented:
- applies action if QNAME suffix matches given list of suffixes (useful for "is domain in zone" rules),
uses `Aho-Corasick`_ string matching algorithm implemented by `@jgrahamc`_ (CloudFlare, Inc.) (BSD 3-clause)
-There are three action:
+There are several defined actions:
* ``PASS`` - let the query pass through
* ``DENY`` - return NXDOMAIN answer
* ``DROP`` - terminate query resolution, returns SERVFAIL to requestor
+* ``TC`` - set TC=1 if the request came through UDP, forcing client to retry with TCP
.. note:: The module (and ``kres``) treats domain names as wire, not textual representation. So each label in name is prefixed with its length, e.g. "example.com" equals to "\7example\3com".
.. code-block:: lua
- -- Load default block rules
- modules = { 'block' }
+ -- Load default policies
+ modules = { 'policy' }
-- Whitelist 'www[0-9].badboy.cz'
- block:add(block.pattern(block.PASS, '\4www[0-9]\6badboy\2cz'))
+ policy:add(policy.pattern(policy.PASS, '\4www[0-9]\6badboy\2cz'))
-- Block all names below badboy.cz
- block:add(block.suffix(block.DENY, {'\6badboy\2cz'}))
+ policy:add(policy.suffix(policy.DENY, {'\6badboy\2cz'}))
-- Custom rule
- block:add(function (req, query)
+ policy:add(function (req, query)
if query:qname():find('%d.%d.%d.224\7in-addr\4arpa') then
- return block.DENY
+ return policy.DENY
end
end)
-- Disallow ANY queries
- block:add(function (req, query)
+ policy:add(function (req, query)
if query.type == kres.type.ANY then
- return block.DROP
+ return policy.DROP
end
end)
Properties
^^^^^^^^^^
-.. envvar:: block.PASS (number)
-.. envvar:: block.DENY (number)
-.. envvar:: block.DROP (number)
+.. envvar:: policy.PASS (number)
+.. envvar:: policy.DENY (number)
+.. envvar:: policy.DROP (number)
+.. envvar:: policy.TC (number)
-.. function:: block:add(rule)
+.. function:: policy:add(rule)
- :param rule: added rule, i.e. ``block.pattern(block.DENY, '[0-9]+\2cz')``
+ :param rule: added rule, i.e. ``policy.pattern(policy.DENY, '[0-9]+\2cz')``
:param pattern: regular expression
Policy to block queries based on the QNAME regex matching.
-.. function:: block.pattern(action, pattern)
+.. function:: policy.pattern(action, pattern)
:param action: action if the pattern matches QNAME
:param pattern: regular expression
Policy to block queries based on the QNAME regex matching.
-.. function:: block.suffix(action, suffix_table)
+.. function:: policy.suffix(action, suffix_table)
:param action: action if the pattern matches QNAME
:param suffix_table: table of valid suffixes
Policy to block queries based on the QNAME suffix match.
-.. function:: block.suffix_common(action, suffix_table[, common_suffix])
+.. function:: policy.suffix_common(action, suffix_table[, common_suffix])
:param action: action if the pattern matches QNAME
:param suffix_table: table of valid suffixes
.. _`Aho-Corasick`: https://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_string_matching_algorithm
.. _`@jgrahamc`: https://github.com/jgrahamc/aho-corasick-lua
-
+.. _RPZ: https://dnsrpz.info/
local kres = require('kres')
-local block = {
+local policy = {
-- Policies
- PASS = 1, DENY = 2, DROP = 3,
+ PASS = 1, DENY = 2, DROP = 3, TC = 4,
-- Special values
ANY = 0,
}
--- @function Block requests which QNAME matches given zone list (i.e. suffix match)
-function block.suffix(action, zone_list)
+-- @function Requests which QNAME matches given zone list (i.e. suffix match)
+function policy.suffix(action, zone_list)
local AC = require('aho-corasick')
local tree = AC.build(zone_list)
return function(req, query)
end
-- @function Check for common suffix first, then suffix match (specialized version of suffix match)
-function block.suffix_common(action, suffix_list, common_suffix)
+function policy.suffix_common(action, suffix_list, common_suffix)
local common_len = string.len(common_suffix)
local suffix_count = #suffix_list
return function(req, query)
end
end
--- @function Block QNAME pattern
-function block.pattern(action, pattern)
+-- @function policy QNAME pattern
+function policy.pattern(action, pattern)
return function(req, query)
if string.find(query:name(), pattern) then
return action
end
end
--- @function Evaluate packet in given rules to determine block action
-function block.evaluate(block, req, query)
- for i = 1, #block.rules do
- local action = block.rules[i](req, query)
+-- @function Evaluate packet in given rules to determine policy action
+function policy.evaluate(policy, req, query)
+ for i = 1, #policy.rules do
+ local action = policy.rules[i](req, query)
if action ~= nil then
return action
end
end
- return block.PASS
+ return policy.PASS
end
--- @function Block layer implementation
-block.layer = {
+-- @function policy layer implementation
+policy.layer = {
begin = function(state, req)
req = kres.request_t(req)
- local action = block:evaluate(req, req:current())
- if action == block.DENY then
+ local action = policy:evaluate(req, req:current())
+ if action == policy.DENY then
-- Write authority information
local answer = req.answer
answer:rcode(kres.rcode.NXDOMAIN)
answer:begin(kres.section.AUTHORITY)
- answer:put('\5block', 900, answer:qclass(), kres.type.SOA,
- '\5block\0\0\0\0\0\0\0\0\14\16\0\0\3\132\0\9\58\128\0\0\3\132')
+ answer:put('\7blocked', 900, answer:qclass(), kres.type.SOA,
+ '\7blocked\0\0\0\0\0\0\0\0\14\16\0\0\3\132\0\9\58\128\0\0\3\132')
return kres.DONE
- elseif action == block.DROP then
+ elseif action == policy.DROP then
return kres.FAIL
+ elseif action == policy.TC then
+ local answer = req.answer
+ print(answer.max_size)
+ if answer.max_size ~= 65535 then
+ answer:tc(1) -- ^ Only UDP queries
+ return kres.DONE
+ end
end
return state
end
}
--- @function Add rule to block list
-function block.add(block, rule)
- return table.insert(block.rules, rule)
+-- @function Add rule to policy list
+function policy.add(policy, rule)
+ return table.insert(policy.rules, rule)
end
-- @function Convert list of string names to domain names
-function block.to_domains(names)
+function policy.to_domains(names)
for i, v in ipairs(names) do
names[i] = v:gsub('([^.]*%.)', function (x)
return string.format('%s%s', string.char(x:len()-1), x:sub(1,-2))
'b.e.f.ip6.arpa.',
'8.b.d.0.1.0.0.2.ip6.arpa',
}
-block.to_domains(private_zones)
+policy.to_domains(private_zones)
-- @var Default rules
-block.rules = { block.suffix_common(block.DENY, private_zones, '\4arpa') }
+policy.rules = { policy.suffix_common(policy.DENY, private_zones, '\4arpa') }
-return block
+return policy