This module can block, rewrite, or alter queries 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.
-It supports a subset of the ISC RPZ_ format.
-There are two policies implemented:
+There are several 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)
+* ``rpz``
+ - implementes a subset of the RPZ_ format. Currently it can be used with a zonefile, a binary database support is on the way. Binary database can be updated by an external process on the fly.
+* custom filter function
There are several defined actions:
* ``DROP`` - terminate query resolution, returns SERVFAIL to requestor
* ``TC`` - set TC=1 if the request came through UDP, forcing client to retry with TCP
-.. 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".
+.. 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
^^^^^^^^^^^^^^^^^^^^^
return policy.DROP
end
end)
+ -- Enforce local RPZ
+ policy:add(policy.rpz(policy.DENY, 'blacklist.rpz'))
Properties
^^^^^^^^^^
Like suffix match, but you can also provide a common suffix of all matches for faster processing (nil otherwise).
This function is faster for small suffix tables (in the order of "hundreds").
+.. function:: policy.rpz(action, path[, format])
+
+ :param action: the default action for match in the zone (e.g. RH-value `.`)
+ :param path: path to zone file | database
+ :param format: set to `'lmdb'` for binary DB, currently NYI
+
+ Enforce RPZ_ rules. This can be used in conjunction with published blocklist feeds.
+ The RPZ_ operation is well described in this `Jan-Piet Mens's post`_,
+ or the `Pro DNS and BIND`_ book. Here's compatibility table:
+
+ .. csv-table::
+ :header: "Policy Action", "RH Value", "Support"
+
+ "NXDOMAIN", "``.``", "**yes**"
+ "NODATA", "``*.``", "*partial*, implemented as NXDOMAIN"
+ "Unchanged", "``rpz-passthru.``", "**yes**"
+ "Nothing", "``rpz-drop.``", "**yes**"
+ "Truncated", "``rpz-tcp-only.``", "**yes**"
+ "Modified", "anything", "no"
+
+ .. csv-table::
+ :header: "Policy Trigger", "Support"
+
+ "QNAME", "**yes**"
+ "CLIENT-IP", "*partial*, may be done with :ref:`views <mod-view>`"
+ "IP", "no"
+ "NSDNAME", "no"
+ "NS-IP", "no"
+
.. _`Aho-Corasick`: https://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_string_matching_algorithm
.. _`@jgrahamc`: https://github.com/jgrahamc/aho-corasick-lua
.. _RPZ: https://dnsrpz.info/
+.. _`Pro DNS and BIND`: http://www.zytrax.com/books/dns/ch7/rpz.html
+.. _`Jan-Piet Mens's post`: http://jpmens.net/2011/04/26/how-to-configure-your-bind-resolvers-to-lie-using-response-policy-zones-rpz/
\ No newline at end of file
ANY = 0,
}
--- @function Requests which QNAME matches given zone list (i.e. suffix match)
+-- Requests which QNAME matches given zone list (i.e. suffix match)
function policy.suffix(action, zone_list)
local AC = require('aho-corasick')
local tree = AC.build(zone_list)
end
end
--- @function Check for common suffix first, then suffix match (specialized version of suffix match)
+-- Check for common suffix first, then suffix match (specialized version of suffix match)
function policy.suffix_common(action, suffix_list, common_suffix)
local common_len = string.len(common_suffix)
local suffix_count = #suffix_list
end
end
--- @function policy QNAME pattern
+-- Filter QNAME pattern
function policy.pattern(action, pattern)
return function(req, query)
if string.find(query:name(), pattern) then
end
end
--- @function Evaluate packet in given rules to determine policy action
+local function rpz_parse(action, path)
+ local rules = {}
+ local ffi = require('ffi')
+ local action_map = {
+ -- RPZ Policy Actions
+ ['\0'] = action,
+ ['\1*\0'] = action, -- deviates from RPZ spec
+ ['\012rpz-passthru\0'] = policy.PASS, -- the grammar...
+ ['\008rpz-drop\0'] = policy.DROP,
+ ['\012rpz-tcp-only\0'] = policy.TC,
+ -- Policy triggers @NYI@
+ }
+ local parser = require('zonefile').parser(function (p)
+ local name = ffi.string(p.r_owner, p.r_owner_length - 1)
+ local action = ffi.string(p.r_data, p.r_data_length)
+ rules[name] = action_map[action]
+ end, function (p)
+ print(string.format('[policy.rpz] %s: line %d: %s', path,
+ tonumber(p.line_counter), p:last_error()))
+ end)
+ parser:parse_file(path)
+ return rules
+end
+
+-- Create RPZ from zone file
+local function rpz_zonefile(action, path)
+ local rules = rpz_parse(action, path)
+ collectgarbage()
+ return function(req, query)
+ local label = query:name()
+ local action = rules[label]
+ while action == nil and string.len(label) > 0 do
+ label = string.sub(label, string.byte(label) + 2)
+ action = rules['\1*'..label]
+ end
+ return action
+ end
+end
+
+-- RPZ policy set
+function policy.rpz(action, path, format)
+ if format == 'lmdb' then
+ error('lmdb zone format is NYI')
+ else
+ return rpz_zonefile(action, path)
+ end
+end
+
+-- Evaluate packet in given rules to determine policy action
function policy.evaluate(policy, req, query)
for i = 1, #policy.rules do
local action = policy.rules[i](req, query)
return policy.PASS
end
--- @function Enforce policy action
+-- Enforce policy action
function policy.enforce(state, req, action)
if action == policy.DENY then
-- Write authority information
return state
end
--- @function policy layer implementation
+-- Capture queries before processing
policy.layer = {
begin = function(state, req)
req = kres.request_t(req)
end
}
--- @function Add rule to policy list
+-- Add rule to policy list
function policy.add(policy, rule)
return table.insert(policy.rules, rule)
end
--- @function Convert list of string names to domain names
+-- Convert list of string names to domain names
function policy.to_domains(names)
for i, v in ipairs(names) do
names[i] = v:gsub('([^.]*%.)', function (x)
-policy_SOURCES := policy.lua aho-corasick.lua
+policy_SOURCES := policy.lua aho-corasick.lua zonefile.lua
$(call make_lua_module,policy)
--- /dev/null
+-- LuaJIT ffi bindings for zscanner, a DNS zone parser.
+-- Author: Marek Vavrusa <marek.vavrusa@nic.cz>
+--
+
+local ffi = require('ffi')
+local libzscanner = ffi.load('zscanner')
+ffi.cdef[[
+
+/*
+ * Data structures
+ */
+
+enum {
+ MAX_RDATA_LENGTH = 65535,
+ MAX_ITEM_LENGTH = 255,
+ MAX_DNAME_LENGTH = 255,
+ MAX_LABEL_LENGTH = 63,
+ MAX_RDATA_ITEMS = 64,
+ BITMAP_WINDOWS = 256,
+ INET4_ADDR_LENGTH = 4,
+ INET6_ADDR_LENGTH = 16,
+ RAGEL_STACK_SIZE = 16,
+};
+typedef struct {
+ uint8_t bitmap[32];
+ uint8_t length;
+} window_t;
+typedef struct {
+ uint8_t excl_flag;
+ uint16_t addr_family;
+ uint8_t prefix_length;
+} apl_t;
+typedef struct {
+ uint32_t d1, d2;
+ uint32_t m1, m2;
+ uint32_t s1, s2;
+ uint32_t alt;
+ uint64_t siz, hp, vp;
+ int8_t lat_sign, long_sign, alt_sign;
+} loc_t;
+
+typedef struct scanner zs_scanner_t;
+struct scanner {
+ int cs;
+ int top;
+ int stack[RAGEL_STACK_SIZE];
+ bool multiline;
+ uint64_t number64;
+ uint64_t number64_tmp;
+ uint32_t decimals;
+ uint32_t decimal_counter;
+ uint32_t item_length;
+ uint32_t item_length_position;
+ uint8_t *item_length_location;
+ uint32_t buffer_length;
+ uint8_t buffer[MAX_RDATA_LENGTH];
+ char include_filename[MAX_RDATA_LENGTH + 1];
+ window_t windows[BITMAP_WINDOWS];
+ int16_t last_window;
+ apl_t apl;
+ loc_t loc;
+ bool long_string;
+ uint8_t *dname;
+ uint32_t *dname_length;
+ uint32_t dname_tmp_length;
+ uint32_t r_data_tail;
+ uint32_t zone_origin_length;
+ uint8_t zone_origin[MAX_DNAME_LENGTH + MAX_LABEL_LENGTH];
+ uint16_t default_class;
+ uint32_t default_ttl;
+ void (*process_record)(zs_scanner_t *);
+ void (*process_error)(zs_scanner_t *);
+ void *data;
+ char *path;
+ uint64_t line_counter;
+ int error_code;
+ uint64_t error_counter;
+ bool stop;
+ struct {
+ char *name;
+ int descriptor;
+ } file;
+ uint32_t r_owner_length;
+ uint8_t r_owner[MAX_DNAME_LENGTH + MAX_LABEL_LENGTH];
+ uint16_t r_class;
+ uint32_t r_ttl;
+ uint16_t r_type;
+ uint32_t r_data_length;
+ uint8_t r_data[MAX_RDATA_LENGTH];
+};
+
+/*
+ * Function signatures
+ */
+
+zs_scanner_t* zs_scanner_create(const char *origin,
+ const uint16_t rclass,
+ const uint32_t ttl,
+ void (*process_record)(zs_scanner_t *),
+ void (*process_error)(zs_scanner_t *),
+ void *data);
+void zs_scanner_free(zs_scanner_t *scanner);
+int zs_scanner_parse(zs_scanner_t *scanner,
+ const char *start,
+ const char *end,
+ const bool final_block);
+int zs_scanner_parse_file(zs_scanner_t *scanner,
+ const char *file_name);
+const char* zs_strerror(const int code);
+]]
+
+-- Wrap scanner context
+local zs_scanner_t = ffi.typeof('zs_scanner_t')
+ffi.metatype( zs_scanner_t, {
+ __new = function(zs, on_record, on_error)
+ return ffi.gc(libzscanner.zs_scanner_create('.', 1, 3600, on_record, on_error, nil),
+ libzscanner.zs_scanner_free)
+ end,
+ __index = {
+ parse_file = function(zs, file)
+ return libzscanner.zs_scanner_parse_file(zs, file)
+ end,
+ read = function(zs, str)
+ local buf = ffi.cast(ffi.typeof('const char *'), str)
+ return libzscanner.zs_scanner_parse(zs, buf, buf + #str, false)
+ end,
+ current_rr = function(zs)
+ return {owner = ffi.string(zs.r_owner, zs.r_owner_length),
+ ttl = tonumber(zs.r_ttl),
+ class = tonumber(zs.r_class),
+ type = tonumber(zs.r_type),
+ rdata = ffi.string(zs.r_data, zs.r_data_length)}
+ end,
+ last_error = function(zs)
+ return ffi.string(libzscanner.zs_strerror(zs.error_code))
+ end,
+ },
+})
+
+-- Module API
+local zonefile = {}
+
+function zonefile.parser(on_record, on_error)
+ return zs_scanner_t(on_record, on_error)
+end
+
+function zonefile.parse_file(file)
+ local records = {}
+ local context = zonefile.parser(function (parser)
+ table.insert(records, parser:current_rr())
+ end)
+ if context:parse_file(file) ~= 0 then
+ return nil
+ end
+ return records
+end
+
+return zonefile
view:addr('127.0.0.1', function (req, qry) return policy.DENY end))
-- Drop queries with suffix match for remote client
view:addr('10.0.0.0/8', policy.suffix(policy.DROP, {'\3xxx'}))
+ -- RPZ for subset of clients
+ view:addr('192.168.1.0/24', policy.rpz(policy.PASS, 'whitelist.rpz'))
Properties
^^^^^^^^^^