]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
modules/policy: added support for a subset of RPZ
authorMarek Vavruša <marek.vavrusa@nic.cz>
Tue, 11 Aug 2015 11:57:10 +0000 (13:57 +0200)
committerMarek Vavruša <marek.vavrusa@nic.cz>
Tue, 11 Aug 2015 11:57:10 +0000 (13:57 +0200)
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

modules/policy/README.rst
modules/policy/policy.lua
modules/policy/policy.mk
modules/policy/zonefile.lua [new file with mode: 0644]
modules/view/README.rst

index a6eb146e592301b8ef7023559ff43727bea4e135..40edc49c1ab5f686c2b65490933d21466d6e0590 100644 (file)
@@ -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 <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:
 
@@ -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 <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
index adf7aa3c6e8217e04acb143cd8ce11364c66f9a1..84276cb6ee8a2bf250beaf291767baea28ced431 100644 (file)
@@ -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)
index f859ac9701d71dffc607e99c963e64a29d39d9d2..63ca42848bca40f188153f38f9c7b6ad139c1e67 100644 (file)
@@ -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 (file)
index 0000000..815acae
--- /dev/null
@@ -0,0 +1,158 @@
+-- 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
index b5e38169eecb9e65c4b2da3cda220eae2a634aae..0aebb6825b9df09e46910d4c2d826914d49affd0 100644 (file)
@@ -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
 ^^^^^^^^^^