diff --git a/doc/modules.rst b/doc/modules.rst
index 2bfcc3eeac14c672cf0781b2d6744eb70dd0150c..2a1e4cfd89d73ed1018fa0ae227d5d750e7d9623 100644
--- a/doc/modules.rst
+++ b/doc/modules.rst
@@ -10,6 +10,7 @@ Implemented modules
    :local:
 
 .. include:: ../modules/hints/README.rst
+.. include:: ../modules/block/README.rst
 .. include:: ../modules/stats/README.rst
 .. include:: ../modules/cachectl/README.rst
 .. include:: ../modules/graphite/README.rst
diff --git a/modules/block/README.rst b/modules/block/README.rst
new file mode 100644
index 0000000000000000000000000000000000000000..9e52d11f8ec03f1112d3888382ffa389b5f14351
--- /dev/null
+++ b/modules/block/README.rst
@@ -0,0 +1,11 @@
+.. _mod-block:
+
+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`.
+
+Example configuration
+^^^^^^^^^^^^^^^^^^^^^
+
diff --git a/modules/block/block.lua b/modules/block/block.lua
new file mode 100644
index 0000000000000000000000000000000000000000..002ebc5b2de520616777f8fddf4e12e45a6b5ddb
--- /dev/null
+++ b/modules/block/block.lua
@@ -0,0 +1,102 @@
+local block = {
+	-- Policies
+	PASS = 1, DENY = 2, DROP = 3,
+	-- Special values
+	ANY = 0,
+	-- Private, local, broadcast, test and special zones 
+	private_zones = {
+		-- RFC1918
+		'10.in-addr.arpa.',
+		'16.172.in-addr.arpa.',
+		'17.172.in-addr.arpa.',
+		'18.172.in-addr.arpa.',
+		'19.172.in-addr.arpa.',
+		'20.172.in-addr.arpa.',
+		'21.172.in-addr.arpa.',
+		'22.172.in-addr.arpa.',
+		'23.172.in-addr.arpa.',
+		'24.172.in-addr.arpa.',
+		'25.172.in-addr.arpa.',
+		'26.172.in-addr.arpa.',
+		'27.172.in-addr.arpa.',
+		'28.172.in-addr.arpa.',
+		'29.172.in-addr.arpa.',
+		'30.172.in-addr.arpa.',
+		'31.172.in-addr.arpa.',
+		'168.192.in-addr.arpa.',
+		-- RFC5735, RFC5737
+		'0.in-addr.arpa.',
+		'127.in-addr.arpa.',
+		'254.169.in-addr.arpa.',
+		'2.0.192.in-addr.arpa.',
+		'100.51.198.in-addr.arpa.',
+		'113.0.203.in-addr.arpa.',
+		'255.255.255.255.in-addr.arpa.',
+		-- IPv6 local, example
+		'0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.',
+		'1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.',
+		'd.f.ip6.arpa.',
+		'8.e.f.ip6.arpa.',
+		'9.e.f.ip6.arpa.',
+		'a.e.f.ip6.arpa.',
+		'b.e.f.ip6.arpa.',
+		'8.b.d.0.1.0.0.2.ip6.arpa',
+	}
+}
+
+-- @function Block requests which QNAME matches given zone list
+function block.in_zone(zone_list)
+	return function(pkt, qry)
+		local qname = pkt:qname()
+		for _,zone in pairs(zone_list) do
+			if qname:sub(-zone:len()) == zone then
+				return block.DENY
+			end
+		end
+		return nil
+	end
+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
+		end
+	end
+	return block.PASS
+end
+
+-- @function Block layer implementation
+block.layer = {
+	produce = function(state, data, pkt)
+		-- Only when a query isn't already answered
+		if state ~= kres.CONSUME then
+			return state
+		end
+		-- @todo Interpret QUERY (as it has final name)
+		-- Interpret packet in Lua and evaluate
+		local pkt = kres.packet(pkt)
+		local action = block:evaluate(pkt, nil)
+		if action == block.DENY then
+			pkt:flag(kres.wire.QR)
+			pkt:flag(kres.wire.AA)
+			pkt:flag(kres.wire.CD)
+			pkt:rcode(kres.rcode.NXDOMAIN)
+			-- @todo add SOA record
+			return kres.DONE
+		elseif action == block.DROP then
+			return kres.FAIL
+		else
+			return state
+		end
+
+		
+	end
+}
+
+-- @var Default rules
+block.rules = { block.in_zone(block.private_zones) }
+
+return block
diff --git a/modules/block/block.mk b/modules/block/block.mk
new file mode 100644
index 0000000000000000000000000000000000000000..fba7b340fefe3fcb38c88d09d36ab32070bd4315
--- /dev/null
+++ b/modules/block/block.mk
@@ -0,0 +1,2 @@
+block_SOURCES := block.lua
+$(call make_lua_module,block)
diff --git a/modules/modules.mk b/modules/modules.mk
index 41bbb8c90fabd3901ce38ced548c5f16c943439c..1717714590fca54672094a6ee07cce5aae36e9a0 100644
--- a/modules/modules.mk
+++ b/modules/modules.mk
@@ -15,7 +15,8 @@ endif
 # List of Lua modules
 ifeq ($(HAS_lua),yes)
 modules_TARGETS += ketcd \
-                   graphite
+                   graphite \
+                   block
 endif
 
 # List of Golang modules