diff --git a/modules/block/README.rst b/modules/block/README.rst index 9e52d11f8ec03f1112d3888382ffa389b5f14351..4e1c078bebe5b60ab311de8c0772a4daf629e217 100644 --- a/modules/block/README.rst +++ b/modules/block/README.rst @@ -3,9 +3,65 @@ Query blocking -------------- -This module can block queries (and subqueries) 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`. +This module can block queries (and subrequests) 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. + +There are two policies implemented: + +* ``pattern`` + - applies action if QNAME matches `regular expression <http://lua-users.org/wiki/PatternsTutorial>`_ +* ``suffix`` + - 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: + +* ``PASS`` - let the query pass through +* ``DENY`` - return NXDOMAIN answer +* ``DROP`` - terminate query resolution, returns SERVFAIL to requestor Example configuration ^^^^^^^^^^^^^^^^^^^^^ +.. code-block:: lua + + -- Load default block rules + modules = { 'block' } + -- Whitelist 'www[0-9].badboy.cz' + block:add(block.pattern(block.PASS, 'www[0-9].badboy.cz')) + -- Block all names below badboy.cz + block:add(block.suffix(block.DENY, {'badboy.cz'})) + +Properties +^^^^^^^^^^ + +.. envvar:: block.PASS (number) +.. envvar:: block.DENY (number) +.. envvar:: block.DROP (number) +.. envvar:: block.private_zones (table of private zones) + +.. function:: block:add(rule) + + :param rule: added rule, i.e. ``block.pattern(block.DENY, '[0-9]+.cz')`` + :param pattern: regular expression + + Policy to block queries based on the QNAME regex matching. + +.. function:: block.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) + + :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. + +.. _`Aho-Corasick`: https://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_string_matching_algorithm +.. _`@jgrahamc`: https://github.com/jgrahamc/aho-corasick-lua + diff --git a/modules/block/aho-corasick.lua b/modules/block/aho-corasick.lua new file mode 100644 index 0000000000000000000000000000000000000000..af4330bcc15e8b05eaaaec8fd81e80c1b0e6fa3f --- /dev/null +++ b/modules/block/aho-corasick.lua @@ -0,0 +1,159 @@ +-- A Lua implementation of the Aho-Corasick string matching algorithm +-- +-- Copyright (c) 2013 CloudFlare, Inc. All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above +-- copyright notice, this list of conditions and the following disclaimer +-- in the documentation and/or other materials provided with the +-- distribution. +-- * Neither the name of CloudFlare, Inc. nor the names of its +-- contributors may be used to endorse or promote products derived from +-- this software without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-- +-- Usage: +-- +-- local AC = require 'aho-corasick' +-- +-- t = AC.build({'words', 'to', 'find'}) +-- r = AC.match(t, 'try to find in this string') +-- r == {'to', 'find'} + +local M = {} +local byte = string.byte +local char = string.char + +local root = "" + +-- make: creates a new entry in t for the given string c with optional fail +-- state +local function make(t, c, f) + t[c] = {} + t[c].to = {} + t[c].fail = f + t[c].hit = root + t[c].word = false +end + +-- build: builds the Aho-Corasick data structure from an array of strings +function M.build(m) + local t = {} + make(t, root, root) + + for i = 1, #m do + local current = root + + -- Build the tos which capture the transitions within the tree + + for j = 1, m[i]:len() do + local c = byte(m[i], j) + local path = current .. char(c) + + if t[current].to[c] == nil then + t[current].to[c] = path + + if current == root then + make(t, path, root) + else + make(t, path) + end + end + + current = path + end + + t[m[i]].word = true + end + + -- Build the fails which show how to backtrack when a fail matches and + -- build the hits which connect nodes to suffixes that are words + + local q = {root} + + while #q > 0 do + local path = table.remove(q, 1) + + for c, p in pairs(t[path].to) do + table.insert(q, p) + + local fail = p:sub(2) + while fail ~= "" and t[fail] == nil do + fail = fail:sub(2) + end + if fail == "" then fail = root end + t[p].fail = fail + + local hit = p:sub(2) + while hit ~= "" and (t[hit] == nil or not t[hit].word) do + hit = hit:sub(2) + end + if hit == "" then hit = root end + t[p].hit = hit + end + end + + return t +end + +-- match: checks to see if the passed in string matches the passed in tree +-- created with build. If all is true (the default) an array of all matches is +-- returned. If all is false then only the first match is returned. +function M.match(t, s, all) + if all == nil then + all = true + end + + local path = root + local hits = {} + local hits_idx = 0 + + for i = 1,s:len() do + local c = byte(s, i) + + while t[path].to[c] == nil and path ~= root do + path = t[path].fail + end + + local n = t[path].to[c] + + if n ~= nil then + path = n + + if t[n].word then + hits_idx = hits_idx + 1 + hits[hits_idx] = n + end + + while t[n].hit ~= root do + n = t[n].hit + hits_idx = hits_idx + 1 + hits[hits_idx] = n + end + + if all == false and hits_idx > 0 then + return hits + end + end + end + + return hits +end + +return M \ No newline at end of file diff --git a/modules/block/block.lua b/modules/block/block.lua index 39e9470a288f8c2525d973c397200da09d4c1e58..24167985b92e1e672b7e06a9a9ea3547418c1676 100644 --- a/modules/block/block.lua +++ b/modules/block/block.lua @@ -44,14 +44,26 @@ local block = { } } --- @function Block requests which QNAME matches given zone list -function block.in_zone(zone_list) +-- @function Block requests which QNAME matches given zone list (i.e. suffix match) +function block.suffix(action, zone_list) + local AC = require('aho-corasick') + local tree = AC.build(zone_list) return function(pkt, qry) local qname = qry:qname() - for _,zone in pairs(zone_list) do - if qname:sub(-zone:len()) == zone then - return block.DENY - end + local match = AC.match(tree, qname, false) + if next(match) ~= nil then + return action, match[1] + end + return nil + end +end + +-- @function Block QNAME pattern +function block.pattern(action, pattern) + return function(pkt, qry) + local qname = qry:qname() + if string.find(qname, pattern) then + return action, qname end return nil end @@ -60,12 +72,12 @@ end -- @function Evaluate packet in given rules to determine block action function block.evaluate(block, pkt, qry) for _,rule in pairs(block.rules) do - local action = rule(pkt, qry) - if action then - return action + local action, authority = rule(pkt, qry) + if action ~= nil then + return action, authority end end - return block.PASS + return block.PASS, nil end -- @function Block layer implementation @@ -77,7 +89,7 @@ block.layer = { end -- Interpret packet in Lua and evaluate local qry = kres.query_current(req) - local action = block:evaluate(pkt, qry) + local action, authority = block:evaluate(pkt, qry) if action == block.DENY then -- Answer full question qry:flag(kres.query.NO_MINIMIZE) @@ -87,8 +99,8 @@ block.layer = { -- Write authority information pkt:rcode(kres.rcode.NXDOMAIN) pkt:begin(kres.AUTHORITY) - -- pkt:add(qry:qname(), qry:qclass(), 6, 900, - -- 'abcd\0efgh\0'..'\0\0\0\1'..'\0\0\0\0'..'\132\3\0\0'..'\132\3\0\0'..'\132\3\0\0') + pkt:add(authority, qry:qclass(), kres.rrtype.SOA, 900, + '\5block\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 return kres.FAIL @@ -99,6 +111,11 @@ block.layer = { } -- @var Default rules -block.rules = { block.in_zone(block.private_zones) } +block.rules = { block.suffix(block.DENY, block.private_zones) } + +-- @function Add rule to block list +function block.add(block, rule) + return table.insert(block.rules, rule) +end return block diff --git a/modules/block/block.mk b/modules/block/block.mk index fba7b340fefe3fcb38c88d09d36ab32070bd4315..e0acb7fe124c8464bce21fb20096209663a3926a 100644 --- a/modules/block/block.mk +++ b/modules/block/block.mk @@ -1,2 +1,2 @@ -block_SOURCES := block.lua +block_SOURCES := block.lua aho-corasick.lua $(call make_lua_module,block)