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
+
--- /dev/null
+-- 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
}
}
--- @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
-- @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
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)
-- 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
}
-- @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
-block_SOURCES := block.lua
+block_SOURCES := block.lua aho-corasick.lua
$(call make_lua_module,block)