From: Marek VavruĊĦa Date: Tue, 11 Aug 2015 11:57:10 +0000 (+0200) Subject: modules/policy: added support for a subset of RPZ X-Git-Tag: v1.0.0-beta1~59^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e5008a8fffb5ea2f87e16f34abaa51abd64074ab;p=thirdparty%2Fknot-resolver.git modules/policy: added support for a subset of RPZ the module can enforce RPZ from zone file, later a LMDB binary database is going to come to solve following: - updating zones on the fly - instant startup (although it loads 1M blocklist in a fraction of second) - no extra memory usage between multiple processes the compatibility notes are in the documentation --- diff --git a/modules/policy/README.rst b/modules/policy/README.rst index a6eb146e5..40edc49c1 100644 --- a/modules/policy/README.rst +++ b/modules/policy/README.rst @@ -6,15 +6,17 @@ Query policies 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 `_ 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 `_ * ``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: @@ -23,7 +25,7 @@ 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 ^^^^^^^^^^^^^^^^^^^^^ @@ -48,6 +50,8 @@ Example configuration return policy.DROP end end) + -- Enforce local RPZ + policy:add(policy.rpz(policy.DENY, 'blacklist.rpz')) Properties ^^^^^^^^^^ @@ -87,6 +91,37 @@ 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 `" + "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 diff --git a/modules/policy/policy.lua b/modules/policy/policy.lua index adf7aa3c6..84276cb6e 100644 --- a/modules/policy/policy.lua +++ b/modules/policy/policy.lua @@ -6,7 +6,7 @@ local policy = { 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) @@ -19,7 +19,7 @@ function policy.suffix(action, 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 @@ -40,7 +40,7 @@ function policy.suffix_common(action, suffix_list, common_suffix) 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 @@ -50,7 +50,55 @@ function policy.pattern(action, pattern) 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) @@ -61,7 +109,7 @@ function policy.evaluate(policy, 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 @@ -83,7 +131,7 @@ function policy.enforce(state, req, action) return state end --- @function policy layer implementation +-- Capture queries before processing policy.layer = { begin = function(state, req) req = kres.request_t(req) @@ -92,12 +140,12 @@ policy.layer = { 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) diff --git a/modules/policy/policy.mk b/modules/policy/policy.mk index f859ac970..63ca42848 100644 --- a/modules/policy/policy.mk +++ b/modules/policy/policy.mk @@ -1,2 +1,2 @@ -policy_SOURCES := policy.lua aho-corasick.lua +policy_SOURCES := policy.lua aho-corasick.lua zonefile.lua $(call make_lua_module,policy) diff --git a/modules/policy/zonefile.lua b/modules/policy/zonefile.lua new file mode 100644 index 000000000..815acae45 --- /dev/null +++ b/modules/policy/zonefile.lua @@ -0,0 +1,158 @@ +-- LuaJIT ffi bindings for zscanner, a DNS zone parser. +-- Author: Marek Vavrusa +-- + +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 diff --git a/modules/view/README.rst b/modules/view/README.rst index b5e38169e..0aebb6825 100644 --- a/modules/view/README.rst +++ b/modules/view/README.rst @@ -36,6 +36,8 @@ Example configuration 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 ^^^^^^^^^^