* ``DENY`` - return NXDOMAIN answer
* ``DROP`` - terminate query resolution, returns SERVFAIL to requestor
+.. note:: The module (and ``kres``) treats domain names as wire, not textual representation. So each label in name is prefixed with its length, e.g. "example.com" equals to "\7example\3com".
+
Example configuration
^^^^^^^^^^^^^^^^^^^^^
-- Load default block rules
modules = { 'block' }
-- Whitelist 'www[0-9].badboy.cz'
- block:add(block.pattern(block.PASS, 'www[0-9].badboy.cz'))
+ block:add(block.pattern(block.PASS, '\4www[0-9]\6badboy\2cz'))
-- Block all names below badboy.cz
- block:add(block.suffix(block.DENY, {'badboy.cz'}))
+ block:add(block.suffix(block.DENY, {'\6badboy\2cz'}))
-- Custom rule
- block:add(function (pkt, qname)
- if qname:find('%d.%d.%d.224.in-addr.arpa.') then
- return block.DENY, '224.in-addr.arpa.'
+ block:add(function (req, query)
+ if query:qname():find('%d.%d.%d.224\7in-addr\4arpa') then
+ return block.DENY
end
end)
-- Disallow ANY queries
- block:add(function (pkt, qname)
- if pkt:qtype() == kres.rrtype.ANY then
+ block:add(function (req, query)
+ if query.type == kres.type.ANY then
return block.DROP
end
end)
.. 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 rule: added rule, i.e. ``block.pattern(block.DENY, '[0-9]+\2cz')``
:param pattern: regular expression
Policy to block queries based on the QNAME regex matching.
:param common_suffix: common suffix of entries in suffix_table
Like suffix match, but you can also provide a common suffix of all matches for faster processing (nil otherwise).
-
-.. tip:: If you want to match suffixes only, prefix the strings with `.`, e.g. `.127.in-addr.arpa.` instead of `127.in-addr.arpa`.
+ This function is faster for small suffix tables (in the order of "hundreds").
.. _`Aho-Corasick`: https://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_string_matching_algorithm
.. _`@jgrahamc`: https://github.com/jgrahamc/aho-corasick-lua
+local kres = require('kres')
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 (i.e. suffix match)
function block.suffix(action, zone_list)
local AC = require('aho-corasick')
local tree = AC.build(zone_list)
- return function(pkt, qname)
- local match = AC.match(tree, qname, false)
+ return function(req, query)
+ local match = AC.match(tree, query:name(), false)
if match[1] ~= nil then
return action
end
function block.suffix_common(action, suffix_list, common_suffix)
local common_len = string.len(common_suffix)
local suffix_count = #suffix_list
- return function(pkt, qname)
+ return function(req, query)
-- Preliminary check
+ local qname = query:name()
if not string.find(qname, common_suffix, -common_len, true) then
return nil
end
-- String match
- local zone = nil
for i = 1, suffix_count do
- zone = suffix_list[i]
+ local zone = suffix_list[i]
if string.find(qname, zone, -string.len(zone), true) then
return action
end
-- @function Block QNAME pattern
function block.pattern(action, pattern)
- return function(pkt, qname)
- if string.find(qname, pattern) then
+ return function(req, query)
+ if string.find(query:name(), pattern) then
return action
end
return nil
end
-- @function Evaluate packet in given rules to determine block action
-function block.evaluate(block, pkt, qname)
- for i = 1, block.rules_count do
- local action = block.rules[i](pkt, qname)
+function block.evaluate(block, req, query)
+ for i = 1, #block.rules do
+ local action = block.rules[i](req, query)
if action ~= nil then
return action
end
-- @function Block layer implementation
block.layer = {
- produce = function(state, req, pkt)
- -- Check only for first iteration of a query
- if state == kres.DONE then
- return state
- end
- local qry = kres.query_current(req)
- if kres.query.flag(qry, kres.query.AWAIT_CUT) then
- return state
- end
- local qname = kres.query.qname(qry)
- local action = block:evaluate(pkt, qname)
+ begin = function(state, req)
+ req = kres.request_t(req)
+ local action = block:evaluate(req, req:current())
if action == block.DENY then
- -- Answer full question
- local qclass = kres.query.qclass(qry)
- local qtype = kres.query.qtype(qry)
- kres.query.flag(qry, kres.query.NO_MINIMIZE + kres.query.CACHED)
- pkt:question(qname, qtype, qclass)
- pkt:flag(kres.wire.QR)
-- Write authority information
- pkt:rcode(kres.rcode.NXDOMAIN)
- pkt:begin(kres.AUTHORITY)
- pkt:add('block.', qclass, kres.type.SOA, 900,
+ local answer = req.answer
+ answer:rcode(kres.rcode.NXDOMAIN)
+ answer:begin(kres.section.AUTHORITY)
+ answer:put('\5block', 900, answer:qclass(), kres.type.SOA,
'\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
- else
- return state
end
+ return state
end
}
--- @var Default rules
-block.rules_count = 1
-block.rules = { block.suffix_common(block.DENY, block.private_zones, 'arpa.') }
-
-- @function Add rule to block list
function block.add(block, rule)
- block.rules_count = block.rules_count + 1
return table.insert(block.rules, rule)
end
+-- @function Convert list of string names to domain names
+function block.to_domains(names)
+ for i, v in ipairs(names) do
+ names[i] = v:gsub('([^.]*%.)', function (x)
+ return string.format('%s%s', string.char(x:len()-1), x:sub(1,-2))
+ end)
+ end
+end
+
+-- RFC1918 Private, local, broadcast, test and special zones
+local private_zones = {
+ '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',
+}
+block.to_domains(private_zones)
+
+-- @var Default rules
+block.rules = { block.suffix_common(block.DENY, private_zones, '\4arpa') }
+
return block