diff --git a/modules/block/block.mk b/modules/block/block.mk deleted file mode 100644 index e0acb7fe124c8464bce21fb20096209663a3926a..0000000000000000000000000000000000000000 --- a/modules/block/block.mk +++ /dev/null @@ -1,2 +0,0 @@ -block_SOURCES := block.lua aho-corasick.lua -$(call make_lua_module,block) diff --git a/modules/modules.mk b/modules/modules.mk index de98b7b9e951d8fef2e962aed59a84a8427c1a89..7c628ea27d971bef839edbae20c3625023b6cba6 100644 --- a/modules/modules.mk +++ b/modules/modules.mk @@ -16,7 +16,7 @@ endif ifeq ($(HAS_lua),yes) modules_TARGETS += ketcd \ graphite \ - block \ + policy \ predict endif diff --git a/modules/block/README.rst b/modules/policy/README.rst similarity index 69% rename from modules/block/README.rst rename to modules/policy/README.rst index 0802ba38a6fc14970dc77f14074bf45b879ab03b..a6eb146e592301b8ef7023559ff43727bea4e135 100644 --- a/modules/block/README.rst +++ b/modules/policy/README.rst @@ -1,11 +1,12 @@ -.. _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: @@ -15,11 +16,12 @@ 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". @@ -28,54 +30,55 @@ Example configuration .. 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 @@ -86,4 +89,4 @@ Properties .. _`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/ diff --git a/modules/block/aho-corasick.lua b/modules/policy/aho-corasick.lua similarity index 100% rename from modules/block/aho-corasick.lua rename to modules/policy/aho-corasick.lua diff --git a/modules/block/block.lua b/modules/policy/policy.lua similarity index 66% rename from modules/block/block.lua rename to modules/policy/policy.lua index 399b1af4d12746c8092a873b3062ab6ebf62f0c6..b4b71231aa6cbaeb0c69ff732eb23c108fa4b900 100644 --- a/modules/block/block.lua +++ b/modules/policy/policy.lua @@ -1,13 +1,13 @@ 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) @@ -20,7 +20,7 @@ function block.suffix(action, zone_list) 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) @@ -40,8 +40,8 @@ function block.suffix_common(action, suffix_list, common_suffix) 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 @@ -50,44 +50,51 @@ function block.pattern(action, pattern) 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)) @@ -133,9 +140,9 @@ local private_zones = { '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 diff --git a/modules/policy/policy.mk b/modules/policy/policy.mk new file mode 100644 index 0000000000000000000000000000000000000000..f859ac9701d71dffc607e99c963e64a29d39d9d2 --- /dev/null +++ b/modules/policy/policy.mk @@ -0,0 +1,2 @@ +policy_SOURCES := policy.lua aho-corasick.lua +$(call make_lua_module,policy)