From acd019db2713fce72bbab907cc8c02244220355a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Petr=20=C5=A0pa=C4=8Dek?= Date: Mon, 16 Mar 2020 17:14:13 +0100 Subject: [PATCH] policy: doc reorganization and clarification --- doc/quickstart-config.rst | 5 +- modules/policy/README.rst | 557 +++++++++++++++++++++++++------------- 2 files changed, 368 insertions(+), 194 deletions(-) diff --git a/doc/quickstart-config.rst b/doc/quickstart-config.rst index 026ed69ef..b0417a4e1 100644 --- a/doc/quickstart-config.rst +++ b/doc/quickstart-config.rst @@ -66,7 +66,10 @@ This configuration will forward two listed domains to a DNS server with IP addre internalDomains = policy.todnames({'company.example', 'internal.example'}) -- forward all queries belonging to domains in the list above to IP address '192.0.2.44' - policy.add(policy.suffix(policy.FORWARD({'192.0.2.44'}), internalDomains)) + policy.add(policy.suffix(policy.FLAGS({'NO_CACHE'}), internalDomains)) + policy.add(policy.suffix(policy.STUB({'192.0.2.44'}), internalDomains)) + +See chapter :ref:`dns-graft` for more details. .. _ispresolver: diff --git a/modules/policy/README.rst b/modules/policy/README.rst index 0cc196ba3..6e1e966aa 100644 --- a/modules/policy/README.rst +++ b/modules/policy/README.rst @@ -1,89 +1,281 @@ .. SPDX-License-Identifier: GPL-3.0-or-later +.. default-domain:: py +.. module:: policy + .. _mod-policy: + Query policies ============== -This module can block, rewrite, or alter inbound queries based on user-defined policies. +This module can block, rewrite, or alter inbound queries based on user-defined policies. It does not affect queries generated by the resolver itself, e.g. when following CNAME chains etc. Each policy *rule* has two parts: a *filter* and an *action*. A *filter* selects which queries will be affected by the policy, and *action* which modifies queries matching the associated filter. -Typically a rule is defined as follows: ``filter(action(action parameters), filter parameters)``. For example, a filter can be ``suffix`` which matches queries whose suffix part is in specified set, and one of possible actions is ``DENY``, which denies resolution. These are combined together into ``policy.suffix(policy.DENY, {todname('badguy.example.')})``. The rule is effective when it is added into rule table using ``policy.add()``, please see `Policy examples`_. +Typically a rule is defined as follows: ``filter(action(action parameters), filter parameters)``. For example, a filter can be ``suffix`` which matches queries whose suffix part is in specified set, and one of possible actions is ``DENY``, which denies resolution. These are combined together into ``policy.suffix(policy.DENY, {todname('badguy.example.')})``. The rule is effective when it is added into rule table using ``policy.add()``, please see examples below. This module is enabled by default because it implements mandatory :rfc:`6761` logic. When no rule applies to a query, built-in rules for `special-use `_ and `locally-served `_ domain names are applied. -These rules can be overriden by action ``PASS``, see `Policy examples`_ below. For debugging purposes you can also add ``modules.unload('policy')`` to your config to unload the module. +These rules can be overriden by action :func:`policy.PASS`. For debugging purposes you can also add ``modules.unload('policy')`` to your config to unload the module. Filters ------- -A *filter* selects which queries will be affected by specified *action*. There are several policy filters available in the ``policy.`` table: - -* ``all(action)`` - - always applies the action -* ``pattern(action, pattern)`` - - applies the action if QNAME matches a `regular expression `_ -* ``suffix(action, table)`` - - applies the action if QNAME suffix matches one of suffixes in the table (useful for "is domain in zone" rules), - uses `Aho-Corasick`_ string matching algorithm `from CloudFlare `_ (BSD 3-clause) -* :any:`policy.suffix_common` -* ``rpz(default_action, path, [watch])`` - - implements a subset of RPZ_ in zonefile format. See below for details: :any:`policy.rpz`. -* ``slice(slice_func, action, action, ...)`` - splits the entire domain space - into multiple slices, uses the slicing function to determine to which slice - does the query belong, and perfroms the corresponding action. For details, see - :any:`policy.slice`. -* custom filter function +A *filter* selects which queries will be affected by specified Actions_. There are several policy filters available in the ``policy.`` table: + +.. function:: all(action) + + Always applies the action. + +.. function:: pattern(action, pattern) + + Applies the action if query name matches a `Lua regular expression `_. + +.. function:: suffix(action, suffix_table) + + Applies the action if query name suffix matches one of suffixes in the table (useful for "is domain in zone" rules). + +.. note:: For speed this filter requires domain names in DNS wire format, not textual representation, so each label in the name must be prefixed with its length. Always use convenience function :func:`policy.todnames` for automatic conversion from strings! For example: + + .. code-block:: lua + + policy.suffix(policy.DENY, policy.todnames({'example.com', 'example.net'})) + +.. function:: suffix_common(action, suffix_table[, common_suffix]) + + :param action: action if the pattern matches query name + :param suffix_table: table of valid suffixes + :param common_suffix: common suffix of entries in suffix_table + + Like :func:`policy.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:: rpz(default_action, path, [watch]) + + Implements a subset of `Response Policy Zone` (RPZ_) stored in zonefile format. See below for details: :func:`policy.rpz`. + +It is also possible to define custom filter function with any name. + +.. function:: custom_filter(state, query) + + :param state: Request processing state :c:type:`kr_layer_state`, typically not used by filter function. + :param query: Incoming DNS query as :c:type:`kr_query` structure. + :return: An `action `_ function or ``nil`` if filter did not match. + + Typically filter function is generated by another function, which allows easy parametrization - this technique is called `closure `_. An practical example of such filter generator is: + +.. code-block:: lua + + function match_query_type(action, target_qtype) + return function (state, query) + if query.stype == target_qtype then + -- filter matched the query, return action function + return action + else + -- filter did not match, continue with next filter + return nil + end + end + end + +This custom filter can be used as any other built-in filter. +For example this applies our custom filter and executes action :func:`policy.DENY` on all queries of type `HINFO`: + +.. code-block:: lua + + -- custom filter which matches HINFO queries, action is policy.DENY + policy.add(match_query_type(policy.DENY, kres.type.HINFO)) + .. _mod-policy-actions: Actions ------- -An *action* is function which modifies DNS query, and is either of type *chain* or *non-chain*. So-called *chain* actions modify the query and allow other rules to evaluate and modify the same query. *Non-chain* actions have opposite behavior, i.e. modify the query and stop rule processing. +An *action* is a function which modifies DNS request, and is either of type *chain* or *non-chain*: -Resolver comes with several actions available in the ``policy.`` table: + * `Non-chain actions`_ modify state of the request and stop rule processing. An example of such action is :ref:`forwarding`. + * `Chain actions`_ modify state of the request and allow other rules to evaluate and act on the same request. One such example is :func:`policy.MIRROR`. Non-chain actions ^^^^^^^^^^^^^^^^^ Following actions stop the policy matching on the query, i.e. other rules are not evaluated once rule with following actions matches: -* ``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 -* ``REFUSE`` - terminate query resolution and return REFUSED 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 -* ``TLS_FORWARD({{ip, authentication}})`` - resolve a query via TLS connection forwarding to an IP while validating and caching locally -* ``STUB(ip)`` - similar to ``FORWARD(ip)`` but *without* attempting DNSSEC validation. - Each request may be either answered from cache or simply sent to one of the IPs with proxying back the answer. -* ``REROUTE({{subnet,target}, ...})`` - reroute addresses in response matching given subnet to given target, e.g. ``{'192.0.2.0/24', '127.0.0.0'}`` will rewrite '192.0.2.55' to '127.0.0.55', see :ref:`renumber module ` for more information. +.. py:attribute:: PASS + + Let the query pass through; it's useful to make exceptions before wider rules. For example: + + More specific whitelist rule must preceede generic blacklist rule: + + .. code-block:: lua + + -- Whitelist 'www.badboy.cz' + policy.add(policy.pattern(policy.PASS, todname('www.badboy.cz.'))) + -- Block all names below badboy.cz + policy.add(policy.suffix(policy.DENY, {todname('badboy.cz.')})) + +.. py:attribute:: DENY + + Deny existence of names matching filter, i.e. reply NXDOMAIN authoritatively. + +.. function:: DENY_MSG(message) + + Deny existence of a given domain and add explanatory message. NXDOMAIN reply contains an additional explanatory message as TXT record in the additional section. + +.. py:attribute:: DROP + + Terminate query resolution and return SERVFAIL to the requestor. -``FORWARD``, ``TLS_FORWARD`` and ``STUB`` support up to four IP addresses "in a single call". +.. py:attribute:: REFUSE + + Terminate query resolution and return REFUSED to the requestor. + +.. py:attribute:: TC + + Force requestor to use TCP. It sets truncated bit (*TC*) in response to true if the request came through UDP, which will force standard-compliant clients to retry the request over TCP. + +.. function:: REROUTE({{subnet,target}, ...}) + + Reroute IP addresses in response matching given subnet to given target, e.g. ``{'192.0.2.0/24', '127.0.0.0'}`` will rewrite '192.0.2.55' to '127.0.0.55', see :ref:`renumber module ` for more information. See :func:`policy.add` and do not forget to specify that this is *postrule*. Quick example: + + .. code-block:: lua + + -- this policy is enforced on answers + -- therefore we have to use 'postrule' + -- (the "true" at the end of policy.add) + policy.add(policy.REROUTE({'192.0.2.0/24', '127.0.0.0'}), true) + +More complex non-chain actions are described in their own chapters, namely: + + * :ref:`forwarding` + * `Response Policy Zones`_ Chain actions ^^^^^^^^^^^^^ -Following actions allow to keep trying to match other rules, until a non-chain action is triggered: +Following actions act on request and then processing continue until first non-chain action (specified in the previous section) is triggered: -* ``MIRROR(ip)`` - mirror query to given IP and continue solving it (useful for partial snooping). -* ``QTRACE`` - pretty-print DNS response packets into the log for the query and its sub-queries. It's useful for debugging weird DNS servers. -* ``FLAGS(set, clear)`` - set and/or clear some flags for the query. There can be multiple flags to set/clear. You can just pass a single flag name (string) or a set of names. +.. function:: MIRROR(ip_address) + + Send copy of incoming DNS queries to a given IP address using DNS-over-UDP and continue resolving them as usual. This is useful for sanity testing new versions of DNS resolvers. + + .. code-block:: lua + policy.add(policy.all(policy.MIRROR('127.0.0.2'))) -Also, it is possible to write your own action (i.e. Lua function). It is possible to implement complex heuristics, e.g. to deflect `Slow drip DNS attacks `_ or gray-list resolution of misbehaving zones. +.. function:: FLAGS(set, clear) + + Set and/or clear some flags for the query. There can be multiple flags to set/clear. You can just pass a single flag name (string) or a set of names. Flag names correspond to :c:type:`kr_qflags` structure. Use only if you know what you are doing. + +.. py:attribute:: QTRACE + + Pretty-print DNS response packets from authoritative servers into the verbose log for the query and its sub-queries. It's useful for debugging weird DNS servers. Verbose logging must be enabled using :func:`verbose` for this policy to be effective. + + .. code-block:: lua + + -- log answers from all authoritative servers involved in resolving + -- requests for example.net. and its subdomains + policy.add(policy.suffix(policy.QTRACE, policy.todnames({'example.net'}))) + + +Custom actions +^^^^^^^^^^^^^^ + +.. function:: custom_action(state, request) + + :param state: Request processing state :c:type:`kr_layer_state`. + :param request: Current DNS request as :c:type:`kr_request` structure. + :return: Returning a new :c:type:`kr_layer_state` prevents evaluating other policy rules. Returning ``nil`` creates a `chain action `_ and allows to continue evaluating other rules. + + This is real example of an action function: + +.. code-block:: lua + + -- Custom action which generates fake A record + local ffi = require('ffi') + local function fake_A_record(state, req) + local answer = req.answer + local qry = req:current() + if qry.stype ~= kres.type.A then + return state + end + ffi.C.kr_pkt_make_auth_header(answer) + answer:rcode(kres.rcode.NOERROR) + answer:begin(kres.section.ANSWER) + answer:put(qry.sname, 900, answer:qclass(), kres.type.A, '\192\168\1\3') + return kres.DONE + end + +This custom action can be used as any other built-in action. +For example this applies our *fake A record action* and executes it on all queries in subtree ``example.net``: + +.. code-block:: lua + + policy.add(policy.suffix(fake_A_record, policy.todnames({'example.net'}))) + +The action function can implement arbitrary logic so it is possible to implement complex heuristics, e.g. to deflect `Slow drip DNS attacks `_ or gray-list resolution of misbehaving zones. .. warning:: The policy module currently only looks at whole DNS requests. The rules won't be re-applied e.g. when following CNAMEs. -.. note:: The module (and ``kres``) expects domain names in wire format, not textual representation. So each label in name is prefixed with its length, e.g. "example.com" equals to ``"\7example\3com"``. You can use convenience function ``todname('example.com')`` for automatic conversion. +.. _forwarding: + +Forwarding +---------- + +Forwarding action alters behavior for cache-miss events. If an information is missing in the local cache the resolver will *forward* the query to *another DNS resolver* for resolution (instead of contacting authoritative servers directly). DNS answers from the remote resolver are then processed locally and sent back to the original client. + +Actions :func:`policy.FORWARD`, :func:`policy.TLS_FORWARD` and :func:`policy.STUB` accept up to four IP addresses at once and the resolver will automatically select IP address which statistically responds the fastest. + +.. function:: FORWARD(ip_address) + FORWARD({ ip_address, [ip_address, ...] }) + + Forward cache-miss queries to specified IP addresses via DNS-over-UDP, DNSSEC validate received answers and cache them. Target IP addresses are expected to be DNS resolvers. + + .. code-block:: lua + + -- Forward all queries to public resolvers https://www.nic.cz/odvr + policy.add(policy.all( + policy.FORWARD( + {'2001:148f:fffe::1', '2001:148f:ffff::1', + '185.43.135.1', '193.14.47.1'}))) + + + + A variant which uses encrypted DNS-over-TLS transport is called :func:`policy.TLS_FORWARD`, please see section :ref:`tls-forwarding`. + +.. function:: STUB(ip_address) + STUB({ ip_address, [ip_address, ...] }) + + Similar to :func:`policy.FORWARD` but *without* attempting DNSSEC validation. + Each request may be either answered from cache or simply sent to one of the IPs with proxying back the answer. + + This mode supports only DNS-over-UDP and should be used only for `Replacing part of the DNS tree`_. + Use :func:`policy.FORWARD` mode if possible. + + .. code-block:: lua + + -- Answers for reverse queries about the 192.168.1.0/24 subnet + -- are to be obtained from IP address 192.0.2.1 port 5353 + -- This disables DNSSEC validation! + policy.add(policy.suffix( + policy.STUB('192.0.2.1@5353'), + {todname('1.168.192.in-addr.arpa')})) + .. _tls-forwarding: Forwarding over TLS protocol (DNS-over-TLS) ------------------------------------------- -Policy `TLS_FORWARD` allows you to forward queries using `Transport Layer Security`_ protocol, which hides the content of your queries from an attacker observing the network traffic. Further details about this protocol can be found in :rfc:`7858` and `IETF draft dprive-dtls-and-tls-profiles`_. +.. function:: TLS_FORWARD( { {ip_address, authentication}, [...] } ) + + Same as :func:`FORWARD` but send query over DNS-over-TLS protocol (encrypted). + Each target IP address needs explicit configuration how to validate + TLS certificate so each IP address is configured by pair: + ``{ip_address, authentication}``. See sections below for more details. + + +Policy :func:`policy.TLS_FORWARD` allows you to forward queries using `Transport Layer Security`_ protocol, which hides the content of your queries from an attacker observing the network traffic. Further details about this protocol can be found in :rfc:`7858` and `IETF draft dprive-dtls-and-tls-profiles`_. Queries affected by `TLS_FORWARD` policy will always be resolved over TLS connection. Knot Resolver does not implement fallback to non-TLS connection, so if TLS connection cannot be established or authenticated according to the configuration, the resolution will fail. @@ -146,10 +338,55 @@ TLS Examples Forwarding to multiple targets ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -With the use of :any:`policy.slice` function, it is possible to split the +With the use of :func:`policy.slice` function, it is possible to split the entire DNS namespace into distinct slices. When used in conjuction with -``policy.TLS_FORWARD``, it's possible to forward different queries to different -targets. +:func:`policy.TLS_FORWARD`, it's possible to forward different queries to +different targets. + +.. function:: slice(slice_func, action[, action[, ...]) + + :param slice_func: slicing function that returns index based on query + :param action: action to be performed for the slice + + This function splits the entire domain space into multiple slices (determined + by the number of provided ``actions``). A ``slice_func`` is called to determine + which slice a query belongs to. The corresponding ``action`` is then executed. + + +.. function:: slice_randomize_psl(seed = os.time() / (3600 * 24 * 7)) + + :param seed: seed for random assignment + + The function initializes and returns a slicing function, which + deterministically assigns ``query`` to a slice based on the query name. + + It utilizes the `Public Suffix List`_ to ensure domains under the same + registrable domain end up in a single slice. (see example below) + + ``seed`` can be used to re-shuffle the slicing algorhitm when the slicing + function is initialized. By default, the assigment is re-shuffled after one + week (when resolver restart / reloads config). To force a stable + distribution, pass a fixed value. To re-shuffle on every resolver restart, + use ``os.time()``. + + The following example demonstrates a distribution among 3 slices:: + + slice 1/3: + example.com + a.example.com + b.example.com + x.b.example.com + example3.com + + slice 2/3: + example2.co.uk + + slice 3/3: + example.co.uk + a.example.co.uk + +These two functions can be used together to forward queries for names +in different parts of DNS name space to different target servers: .. code-block:: lua @@ -174,127 +411,64 @@ targets. decrease the potential exposure of your DNS data to a malicious resolver operator. -.. _policy_examples: - -Policy examples ---------------- - -.. code-block:: lua - - -- Whitelist 'www[0-9].badboy.cz' - policy.add(policy.pattern(policy.PASS, '\4www[0-9]\6badboy\2cz')) - -- Block all names below badboy.cz - policy.add(policy.suffix(policy.DENY, {todname('badboy.cz.')})) - - -- Custom rule - local ffi = require('ffi') - local function genRR (state, req) - local answer = req.answer - local qry = req:current() - if qry.stype ~= kres.type.A then - return state - end - ffi.C.kr_pkt_make_auth_header(answer) - answer:rcode(kres.rcode.NOERROR) - answer:begin(kres.section.ANSWER) - answer:put(qry.sname, 900, answer:qclass(), kres.type.A, '\192\168\1\3') - return kres.DONE - end - policy.add(policy.suffix(genRR, { todname('my.example.cz.') })) - - -- Disallow ANY queries - policy.add(function (req, query) - if query.stype == kres.type.ANY then - return policy.DROP - end - end) - -- Enforce local RPZ - policy.add(policy.rpz(policy.DENY, 'blocklist.rpz')) - -- Forward all queries below 'company.se' to given resolver; - -- beware: typically this won't work due to DNSSEC - see "Replacing part..." below - policy.add(policy.suffix(policy.FORWARD('192.168.1.1'), {todname('company.se')})) - -- Forward reverse queries about the 192.168.1.1/24 space to .1 port 5353 - -- and do it directly without attempts to validate DNSSEC etc. - policy.add(policy.suffix(policy.STUB('192.168.1.1@5353'), {todname('1.168.192.in-addr.arpa')})) - -- Forward all queries matching pattern - policy.add(policy.pattern(policy.FORWARD('2001:DB8::1'), '\4bad[0-9]\2cz')) - -- Forward all queries (to public resolvers https://www.nic.cz/odvr) - policy.add(policy.all(policy.FORWARD({'2001:148f:fffe::1', '193.14.47.1'}))) - -- Print all responses with matching suffix - policy.add(policy.suffix(policy.QTRACE, {todname('rhybar.cz.')})) - -- Print all responses - policy.add(policy.all(policy.QTRACE)) - -- Mirror all queries and retrieve information - local rule = policy.add(policy.all(policy.MIRROR('127.0.0.2'))) - -- Print information about the rule - print(string.format('id: %d, matched queries: %d', rule.id, rule.count) - -- Reroute all addresses found in answer from 192.0.2.0/24 to 127.0.0.x - -- this policy is enforced on answers, therefore 'postrule' - local rule = policy.add(policy.REROUTE({'192.0.2.0/24', '127.0.0.0'}), true) - -- Delete rule that we just created - policy.del(rule.id) - - .. _dns-graft: Replacing part of the DNS tree ------------------------------ -You may want to resolve most of the DNS namespace by usual means while letting some other resolver solve specific subtrees. -Such data would typically be rejected by DNSSEC validation starting from the ICANN root keys. Therefore, if you trust the resolver and your link to it, you can simply use the ``STUB`` action instead of ``FORWARD`` to avoid validation only for those subtrees. - -Another issue is caused by caching, because Knot Resolver only keeps a single cache for everything. -For example, if you add an alternative top-level domain while using the ICANN root zone for the rest, at some point the cache may obtain records proving that your top-level domain does not exist, and those records could then be used when the positive records fall out of cache. The easiest work-around is to disable reading from cache for those subtrees; the other resolver is often very close anyway. - +Following procedure applies only to domains which have different content +publicly and internally. For example this applies to "your own" top-level domain +``example.`` which does not exist in the public (global) DNS namespace. + +Dealing with these internal-only domains requires extra configuration because +DNS was designed as "single namespace" and local modifications like adding +your own TLD break this assumption. + +.. warning:: Use of internal names which are not delegated from the public DNS + *is causing technical problems* with caching and DNSSEC validation + and generally makes DNS operation more costly. + We recommend **against** using these non-delegated names. + +To make such internal domain available in your resolver it is necessary to +*graft* your domain onto the public DNS namespace, +but *grafting* creates new issues: + +These *grafted* domains will be rejected by DNSSEC validation +because such domains are technically indistinguishable from an spoofing attack +against the public DNS. +Therefore, if you trust the remote resolver which hosts the internal-only domain, +and you trust your link to it, you need to use the :func:`policy.STUB` policy +instead of :func:`policy.FORWARD` to disable DNSSEC validation for those +*grafted* domains. + +Secondly, after disabling DNSSEC validation you have to solve another issue +caused by grafting. For example, if you grafted your own top-level domain +``example.`` onto the public DNS namespace, at some point the root server might +send proof-of-nonexistence proving e.g. that there are no other top-level +domain in between names ``events.`` and ``exchange.``, effectivelly proving +non-existence of ``example.``. + +These proofs-of-nonexistence protect public DNS from spoofing but break +*grafted* domains because proofs will be latter used by resolver +(when the positive records for the grafted domain timeout from cache), +effectivelly making grafted domain unavailable. +The easiest work-around is to disable reading from cache for grafted domains. .. code-block:: lua - :caption: Example configuration: graft DNS sub-trees ``faketldtest``, ``sld.example``, and ``internal.example.com`` into existing namespace - - extraTrees = policy.todnames({'faketldtest', 'sld.example', 'internal.example.com'}) + :caption: Example configuration grafting domains onto public DNS namespace + + extraTrees = policy.todnames( + {'faketldtest.', + 'sld.example.', + 'internal.example.com.', + '2.0.192.in-addr.arpa.' -- this applies to reverse DNS tree as well + }) -- Beware: the rule order is important, as STUB is not a chain action. policy.add(policy.suffix(policy.FLAGS({'NO_CACHE'}), extraTrees)) policy.add(policy.suffix(policy.STUB({'2001:db8::1'}), extraTrees)) - -Additional properties +Response policy zones --------------------- - -Most properties (actions, filters) are described above. - -.. function:: policy.add(rule, postrule) - - :param rule: added rule, i.e. ``policy.pattern(policy.DENY, '[0-9]+\2cz')`` - :param postrule: boolean, if true the rule will be evaluated on answer instead of query - :return: rule description - - Add a new policy rule that is executed either or queries or answers, depending on the ``postrule`` parameter. You can then use the returned rule description to get information and unique identifier for the rule, as well as match count. - -.. function:: policy.del(id) - - :param id: identifier of a given rule - :return: boolean - - Remove a rule from policy list. - -.. function:: policy.suffix_common(action, suffix_table[, common_suffix]) - - :param action: action if the pattern matches QNAME - :param suffix_table: table of valid suffixes - :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). - This function is faster for small suffix tables (in the order of "hundreds"). - -.. function:: policy.rpz(action, path, [watch = true]) - - :param action: the default action for match in the zone; typically you want ``policy.DENY`` - :param path: path to zone file | database - :param watch: boolean, if true, the file will be reloaded on file change - - 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. - .. warning:: There is no published Internet Standard for RPZ_ and implementations vary. @@ -302,6 +476,8 @@ Most properties (actions, filters) are described above. from implementation in BIND. Nevertheless it is good enough for blocking large lists of spam or advertising domains. + + The RPZ file format is basically a DNS zone file with *very special* semantics. For example: @@ -309,6 +485,7 @@ Most properties (actions, filters) are described above. ; left hand side ; TTL and class ; right hand side ; encodes RPZ trigger ; ignored ; encodes action + ; (i.e. filter) blocked.domain.example 600 IN CNAME . ; block main domain *.blocked.domain.example 600 IN CNAME . ; block subdomains @@ -321,19 +498,29 @@ Most properties (actions, filters) are described above. .. csv-table:: :header: "RPZ Right Hand Side", "Knot Resolver Action", "BIND Compatibility" - "``.``", "``action`` is used", "compatible if ``action`` is ``policy.DENY``" - "``*.``", "``action`` is used", "good enough [#]_ if ``action`` is ``policy.DENY``" - "``rpz-passthru.``", "``policy.PASS``", "yes" - "``rpz-tcp-only.``", "``policy.TC``", "yes" - "``rpz-drop.``", "``policy.DROP``", "no [#]_" + "``.``", "``action`` is used", "compatible if ``action`` is :func:`policy.DENY`" + "``*.``", "``action`` is used", "good enough [#]_ if ``action`` is :func:`policy.DENY`" + "``rpz-passthru.``", ":func:`policy.PASS`", "yes" + "``rpz-tcp-only.``", ":func:`policy.TC`", "yes" + "``rpz-drop.``", ":func:`policy.DROP`", "no [#]_" "fake A/AAAA", "*not supported*", "no" .. [#] RPZ action ``*.`` in BIND causes *NODATA* answer but typically our users configure ``policy.rpz(policy.DENY, ...)`` which replies with *NXDOMAIN*. Good news is that from client's perspective it does not make a visible difference. - .. [#] Our ``policy.DROP`` returns *SERVFAIL* answer (for historical reasons). + .. [#] Our :func:`policy.DROP` returns *SERVFAIL* answer (for historical reasons). + +.. function:: rpz(action, path, [watch = true]) + + :param action: the default action for match in the zone; typically you want :func:`policy.DENY` + :param path: path to zone file + :param watch: boolean, if true, the file will be reloaded on file change + + 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. For example, we can store the example snippet with domain ``blocked.domain.example`` (above) into file ``/etc/knot-resolver/blocklist.rpz`` and configure resolver to @@ -353,49 +540,35 @@ Most properties (actions, filters) are described above. to re-read an incomplete file. -.. function:: policy.slice(slice_func, action[, action[, ...]) - - :param slice_func: slicing function that returns index based on query - :param action: action to be performed for the slice - - This function splits the entire domain space into multiple slices (determined - by the number of provided ``actions``). A ``slice_func`` is called to determine - which slice a query belongs to. The corresponding ``action`` is then executed. +Additional properties +--------------------- -.. function:: policy.slice_randomize_psl(seed = os.time() / (3600 * 24 * 7)) +Most properties (actions, filters) are described above. - :param seed: seed for random assignment +.. function:: add(rule, postrule) - The function initializes and returns a slicing function, which - deterministically assigns ``query`` to a slice based on the QNAME. + :param rule: added rule, i.e. ``policy.pattern(policy.DENY, '[0-9]+\2cz')`` + :param postrule: boolean, if true the rule will be evaluated on answer instead of query + :return: rule description - It utilizes the `Public Suffix List`_ to ensure domains under the same - registrable domain end up in a single slice. (see example below) + Add a new policy rule that is executed either or queries or answers, depending on the ``postrule`` parameter. You can then use the returned rule description to get information and unique identifier for the rule, as well as match count. - ``seed`` can be used to re-shuffle the slicing algorhitm when the slicing - function is initialized. By default, the assigment is re-shuffled after one - week (when resolver restart / reloads config). To force a stable - distribution, pass a fixed value. To re-shuffle on every resolver restart, - use ``os.time()``. + .. code-block:: lua - The following example demonstrates a distribution among 3 slices:: + -- mirror all queriesm, keep handle so we can retrieve information later + local rule = policy.add(policy.all(policy.MIRROR('127.0.0.2'))) + -- we can print statistics about this rule any time later + print(string.format('id: %d, matched queries: %d', rule.id, rule.count) - slice 1/3: - example.com - a.example.com - b.example.com - x.b.example.com - example3.com +.. function:: del(id) - slice 2/3: - example2.co.uk + :param id: identifier of a given rule returned by :func:`policy.add` + :return: boolean ``true`` if rule was deleted, ``false`` otherwise - slice 3/3: - example.co.uk - a.example.co.uk + Remove a rule from policy list. -.. function:: policy.todnames({name, ...}) +.. function:: todnames({name, ...}) :param: names table of domain names in textual format @@ -410,8 +583,6 @@ Most properties (actions, filters) are described above. { '\7example\3com\0', '\2me\2cz\0' } -.. _`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/ .. _`PEM format`: https://en.wikipedia.org/wiki/Privacy-enhanced_Electronic_Mail .. _`Pro DNS and BIND`: http://www.zytrax.com/books/dns/ch7/rpz.html -- 2.47.2