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)