From b04432bfc35915c7378aa1ab7f59ea2a3e8c6012 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Petr=20=C5=A0pa=C4=8Dek?= <petr.spacek@nic.cz>
Date: Thu, 25 Jan 2018 12:26:50 +0100
Subject: [PATCH] policy: add explanatory TXT record to zones blocked by
 default

---
 modules/policy/README.rst |  1 +
 modules/policy/policy.lua | 49 ++++++++++++++++++++++++++++-----------
 2 files changed, 36 insertions(+), 14 deletions(-)

diff --git a/modules/policy/README.rst b/modules/policy/README.rst
index 499d15f15..934604ee4 100644
--- a/modules/policy/README.rst
+++ b/modules/policy/README.rst
@@ -26,6 +26,7 @@ There are several actions available in the ``policy.`` table:
 
 * ``PASS`` - let the query pass through; it's useful to make exceptions before wider rules
 * ``DENY`` - reply NXDOMAIN authoritatively
+* ``DENY_MSG(msg)`` - reply NXDOMAIN authoritatively and add explanatory message to additional section
 * ``DROP`` - terminate query resolution and return SERVFAIL to the requestor
 * ``TC`` - set TC=1 if the request came through UDP, forcing client to retry with TCP
 * ``FORWARD(ip)`` - resolve a query via forwarding to an IP while validating and caching locally;
diff --git a/modules/policy/policy.lua b/modules/policy/policy.lua
index cd32181d2..552731ed0 100644
--- a/modules/policy/policy.lua
+++ b/modules/policy/policy.lua
@@ -464,15 +464,28 @@ function policy.rpz(action, path)
 	end
 end
 
-function policy.DENY(_, req)
-	-- Write authority information
-	local answer = req.answer
-	ffi.C.kr_pkt_make_auth_header(answer)
-	answer:rcode(kres.rcode.NXDOMAIN)
-	answer:begin(kres.section.AUTHORITY)
-	mkauth_soa(answer, '\7blocked\0')
-	return kres.DONE
+function policy.DENY_MSG(msg)
+	if msg and (type(msg) ~= 'string' or #msg >= 255) then
+		error('DENY_MSG: optional msg must be string shorter than 256 characters')
+        end
+
+	return function (_, req)
+		-- Write authority information
+		local answer = req.answer
+		ffi.C.kr_pkt_make_auth_header(answer)
+		answer:rcode(kres.rcode.NXDOMAIN)
+		answer:begin(kres.section.AUTHORITY)
+		mkauth_soa(answer, '\7blocked\0')
+		if msg then
+			answer:begin(kres.section.ADDITIONAL)
+			answer:put('\11explanation\7invalid', 900, answer:qclass(), kres.type.TXT,
+				   string.char(#msg) .. msg)
+
+		end
+		return kres.DONE
+	end
 end
+policy.DENY = policy.DENY_MSG() -- compatibility with < 2.0
 
 function policy.DROP(_, _)
 	return kres.FAIL
@@ -603,7 +616,7 @@ local private_zones = {
 	'100.51.198.in-addr.arpa.',
 	'113.0.203.in-addr.arpa.',
 	'255.255.255.255.in-addr.arpa.',
-	-- RFC7796
+	-- RFC7793
 	'64.100.in-addr.arpa.',
 	'65.100.in-addr.arpa.',
 	'66.100.in-addr.arpa.',
@@ -686,14 +699,22 @@ policy.rules = {}
 policy.postrules = {}
 policy.special_names = {
 	{
-		cb=policy.suffix_common(policy.DENY, private_zones, todname('arpa.')),
+		cb=policy.suffix_common(policy.DENY_MSG(
+			'Blocking is mandated by standards, see references on '
+			.. 'https://www.iana.org/assignments/'
+			.. 'locally-served-dns-zones/locally-served-dns-zones.xhtml'),
+			private_zones, todname('arpa.')),
 		count=0
 	},
 	{
-		cb=policy.suffix(policy.DENY, {
-			todname('test.'),
-			todname('invalid.'),
-			todname('onion.'), -- RFC7686, 2.4
+		cb=policy.suffix(policy.DENY_MSG(
+			'Blocking is mandated by standards, see references on '
+			.. 'https://www.iana.org/assignments/'
+			.. 'special-use-domain-names/special-use-domain-names.xhtml'),
+			{
+				todname('test.'),
+				todname('onion.'),
+				todname('invalid.'),
 			}),
 		count=0
 	},
-- 
GitLab