diff options
Diffstat (limited to '')
-rw-r--r-- | modules/policy/README.rst | 774 |
1 files changed, 774 insertions, 0 deletions
diff --git a/modules/policy/README.rst b/modules/policy/README.rst new file mode 100644 index 0000000..202aaba --- /dev/null +++ b/modules/policy/README.rst @@ -0,0 +1,774 @@ +.. 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. 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 :any:`policy.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 <https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml>`_ and `locally-served <http://www.iana.org/assignments/locally-served-dns-zones>`_ domain names are applied. +These rules can be overridden by action :any:`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 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 <http://lua-users.org/wiki/PatternsTutorial>`_. + +.. 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). + + .. code-block:: lua + + policy.add(policy.suffix(policy.DENY, policy.todnames({'example.com', 'example.net'}))) + +.. 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: + +.. _IDN: + +.. note:: Non-ASCII is not supported. + + Knot Resolver does not provide any convenience support for IDN. + Therefore everywhere (all configuration, logs, RPZ files) you need to deal with the + `xn\-\- forms <https://en.wikipedia.org/wiki/Internationalized_domain_name#Example_of_IDNA_encoding>`_ + of domain name labels, instead of directly using unicode characters. + +.. function:: domains(action, domain_table) + + Like :func:`policy.suffix` match, but the queried name must match exactly, not just its suffix. + +.. 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"). + +.. :noindex: 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 <#actions>`_ 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 <https://www.lua.org/pil/6.1.html>`_. 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 :any:`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 a function which modifies DNS request, and is either of type *chain* or *non-chain*: + + * `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: + +.. py:attribute:: PASS + + Let the query pass through; it's useful to make exceptions before wider rules. For example: + + More specific whitelist rule must precede generic blacklist rule: + + .. code-block:: lua + + -- Whitelist 'good.example.com' + policy.add(policy.pattern(policy.PASS, todname('good.example.com.'))) + -- Block all names below example.com + policy.add(policy.suffix(policy.DENY, {todname('example.com.')})) + +.. py:attribute:: DENY + + Deny existence of names matching filter, i.e. reply NXDOMAIN authoritatively. + +.. function:: DENY_MSG(message, [extended_error=kres.extended_error.BLOCKED]) + + Deny existence of a given domain and add explanatory message. NXDOMAIN reply + contains an additional explanatory message as TXT record in the additional + section. + + You may override the extended DNS error to provide the user with more + information. By default, ``BLOCKED`` is returned to indicate the domain is + blocked due to the internal policy of the operator. Other suitable error + codes are ``CENSORED`` (for externally imposed policy reasons) or + ``FILTERED`` (for blocking requested by the client). For more information, + please refer to :rfc:`8914`. + +.. py:attribute:: DROP + + Terminate query resolution and return SERVFAIL to the requestor. + +.. py:attribute:: REFUSE + + Terminate query resolution and return REFUSED to the requestor. + +.. py:attribute:: NO_ANSWER + + Terminate query resolution and do not return any answer to the requestor. + + .. warning:: During normal operation, an answer should always be returned. + Deliberate query drops are indistinguishable from packet loss and may + cause problems as described in :rfc:`8906`. Only use :any:`NO_ANSWER` + on very specific occasions, e.g. as a defense mechanism during an attack, + and prefer other actions (e.g. :any:`DROP` or :any:`REFUSE`) for normal + operation. + +.. 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 <mod-renumber>` 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.all(policy.REROUTE({['192.0.2.0/24'] = '127.0.0.0'})), true) + +.. function:: ANSWER({ type = { rdata=data, [ttl=1] } }, [nodata=false]) + + Overwrite Resource Records in responses with specified values. + + * type + - RR type to be replaced, e.g. ``[kres.type.A]`` or `numeric value <https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4>`_. + * rdata + - RR data in DNS wire format, i.e. binary form specific for given RR type. Set of multiple RRs can be specified as table ``{ rdata1, rdata2, ... }``. Use helper function :func:`kres.str2ip` to generate wire format for A and AAAA records. Wire format for other record types can be generated with :func:`kres.parse_rdata`. + * ttl + - TTL in seconds. Default: 1 second. + * nodata + - If type requested by client is not configured in this policy: + + - ``true``: Return empty answer (`NODATA`). + - ``false``: Ignore this policy and continue processing other rules. + + Default: ``false``. + + .. code-block:: lua + + -- policy to change IPv4 address and TTL for example.com + policy.add( + policy.domains( + policy.ANSWER( + { [kres.type.A] = { rdata=kres.str2ip('192.0.2.7'), ttl=300 } } + ), { todname('example.com') })) + -- policy to generate two TXT records (specified in binary format) for example.net + policy.add( + policy.domains( + policy.ANSWER( + { [kres.type.TXT] = { rdata={'\005first', '\006second'}, ttl=5 } } + ), { todname('example.net') })) + + + .. function:: kres.parse_rdata({str, ...}) + + Parse string representation of RTYPE and RDATA into RDATA wire format. Expects + a table of string(s) and returns a table of wire data. + + .. code-block:: lua + + -- create wire format RDATA that can be passed to policy.ANSWER + kres.parse_rdata({'SVCB 1 resolver.example. alpn=dot'}) + kres.parse_rdata({ + 'SVCB 1 resolver.example. alpn=dot ipv4hint=192.0.2.1 ipv6hint=2001:db8::1', + 'SVCB 2 resolver.example. mandatory=key65380 alpn=h2 key65380=/dns-query{?dns}', + }) + +More complex non-chain actions are described in their own chapters, namely: + + * :ref:`forwarding` + * `Response Policy Zones`_ + +Chain actions +^^^^^^^^^^^^^ + +Following actions act on request and then processing continue until first non-chain action (specified in the previous section) is triggered: + +.. 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'))) + +.. 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. + + +.. _mod-policy-logging: + +Actions for extra logging +^^^^^^^^^^^^^^^^^^^^^^^^^ + +These are also "chain" actions, i.e. they don't stop processing the policy rule list. +Similarly to other actions, they apply during whole processing of the client's request, +i.e. including any sub-queries. + +The log lines from these policy actions are tagged by extra ``[reqdbg]`` prefix, +and they are produced regardless of your :func:`log_level()` setting. +They are marked as ``debug`` level, so e.g. with journalctl command you can use ``-p info`` to skip them. + +.. warning:: Beware of producing too much logs. + + These actions are not suitable for use on a large fraction of resolver's requests. + The extra logs have significant performance impact and might also overload your logging system + (or get rate-limited by it). + You can use `Filters`_ to further limit on which requests this happens. + +.. py:attribute:: DEBUG_ALWAYS + + Print debug-level logging for this request. + That also includes messages from client (:any:`REQTRACE`), upstream servers (:any:`QTRACE`), and stats about interesting records at the end. + + .. code-block:: lua + + -- debug requests that ask for flaky.example.net or below + policy.add(policy.suffix(policy.DEBUG_ALWAYS, + policy.todnames({'flaky.example.net'}))) + +.. py:attribute:: DEBUG_CACHE_MISS + + Same as :any:`DEBUG_ALWAYS` but only if the request required information which was not available locally, i.e. requests which forced resolver to ask upstream server(s). + Intended usage is for debugging problems with remote servers. + +.. py:function:: DEBUG_IF(test_function) + + :param test_function: Function with single argument of type :c:type:`kr_request` which returns ``true`` if debug logs for that request should be generated and ``false`` otherwise. + + Same as :any:`DEBUG_ALWAYS` but only logs if the test_function says so. + + .. note:: ``test_function`` is evaluated only when request is finished. + As a result all debug logs this request must be collected, + and at the end they get either printed or thrown away. + + Example usage which gathers verbose logs for all requests in subtree ``dnssec-failed.org.`` and prints debug logs for those finishing in a different state than ``kres.DONE`` (most importantly ``kres.FAIL``, see :c:type:`kr_layer_state`). + + .. code-block:: lua + + policy.add(policy.suffix( + policy.DEBUG_IF(function(req) + return (req.state ~= kres.DONE) + end), + policy.todnames({'dnssec-failed.org.'}))) + +.. py:attribute:: QTRACE + + Pretty-print DNS responses from upstream servers (or cache) into logs. + It's useful for debugging weird DNS servers. + + If you do not use ``QTRACE`` in combination with ``DEBUG*``, + you additionally need either ``log_groups({'iterat'})`` (possibly with other groups) + or ``log_level('debug')`` to see the output in logs. + +.. py:attribute:: REQTRACE + + Pretty-print DNS requests from clients into the verbose log. It's useful for debugging weird DNS clients. + It makes most sense together with :ref:`mod-view` (enabling per-client) + and probably with verbose logging those request (e.g. use :any:`DEBUG_ALWAYS` instead). + +.. py:attribute:: IPTRACE + + Log how the request arrived. + Most notably, this includes the client's IP address, so beware of privacy implications. + + .. code-block:: lua + + -- example usage in configuration + policy.add(policy.all(policy.IPTRACE)) + -- you might want to combine it with some other logs, e.g. + policy.add(policy.all(policy.DEBUG_ALWAYS)) + + .. code-block:: text + + -- example log lines from IPTRACE: + [reqdbg][policy][57517.00] request packet arrived from ::1#37931 to ::1#00853 (TCP + TLS) + [reqdbg][policy][65538.00] request packet arrived internally + + +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 <#actions>`_ 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:ensure_answer() + if answer == nil then return nil end + 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 <https://secure64.com/water-torture-slow-drip-dns-ddos-attack>`_ 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. + +.. _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 (without encryption), 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 does not support encryption 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')})) + +.. note:: By default, forwarding targets must support + `EDNS <https://en.wikipedia.org/wiki/Extension_mechanisms_for_DNS>`_ and + `0x20 randomization <https://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00>`_. + See example in `Replacing part of the DNS tree`_. + +.. warning:: + Limiting forwarding actions by filters (e.g. :func:`policy.suffix`) may have unexpected consequences. + Notably, forwarders can inject *any* records into your cache + even if you "restrict" them to an insignificant DNS subtree -- + except in cases where DNSSEC validation applies, of course. + + The behavior is probably best understood through the fact + that filters and actions are completely decoupled. + The forwarding actions have no clue about why they were executed, + e.g. that the user wanted to restrict the forwarder only to some subtree. + The action just selects some set of forwarders to process this whole request from the client, + and during that processing it might need some other "sub-queries" (e.g. for validation). + Some of those might not've passed the intended filter, + but policy rule-set only applies once per client's request. + +.. _tls-forwarding: + +Forwarding over TLS protocol (DNS-over-TLS) +------------------------------------------- +.. function:: TLS_FORWARD( { {ip_address, authentication}, [...] } ) + + Same as :func:`policy.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 :func:`policy.TLS_FORWARD` 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. + +To test this feature you need to either :ref:`configure Knot Resolver as DNS-over-TLS server <tls-server-config>`, or pick some public DNS-over-TLS server. Please see `DNS Privacy Project`_ homepage for list of public servers. + +.. note:: Some public DNS-over-TLS providers may apply rate-limiting which + makes their service incompatible with Knot Resolver's TLS forwarding. + Notably, `Google Public DNS + <https://developers.google.com/speed/public-dns/docs/dns-over-tls>`_ doesn't + work as of 2019-07-10. + +When multiple servers are specified, the one with the lowest round-trip time is used. + +CA+hostname authentication +^^^^^^^^^^^^^^^^^^^^^^^^^^ +Traditional PKI authentication requires server to present certificate with specified hostname, which is issued by one of trusted CAs. Example policy is: + +.. code-block:: lua + + policy.TLS_FORWARD({ + {'2001:DB8::d0c', hostname='res.example.com'}}) + +- ``hostname`` must be a valid domain name matching server's certificate. It will also be sent to the server as SNI_. +- ``ca_file`` optionally contains a path to a CA certificate (or certificate bundle) in `PEM format`_. + If you omit that, the system CA certificate store will be used instead (usually sufficient). + A list of paths is also accepted, but all of them must be valid PEMs. + +Key-pinned authentication +^^^^^^^^^^^^^^^^^^^^^^^^^ +Instead of CAs, you can specify hashes of accepted certificates in ``pin_sha256``. +They are in the usual format -- base64 from sha256. +You may still specify ``hostname`` if you want SNI_ to be sent. + +.. _tls-examples: + +TLS Examples +^^^^^^^^^^^^ + +.. code-block:: lua + + modules = { 'policy' } + -- forward all queries over TLS to the specified server + policy.add(policy.all(policy.TLS_FORWARD({{'192.0.2.1', pin_sha256='YQ=='}}))) + -- for brevity, other TLS examples omit policy.add(policy.all()) + -- single server authenticated using its certificate pin_sha256 + policy.TLS_FORWARD({{'192.0.2.1', pin_sha256='YQ=='}}) -- pin_sha256 is base64-encoded + -- single server authenticated using hostname and system-wide CA certificates + policy.TLS_FORWARD({{'192.0.2.1', hostname='res.example.com'}}) + -- single server using non-standard port + policy.TLS_FORWARD({{'192.0.2.1@443', pin_sha256='YQ=='}}) -- use @ or # to specify port + -- single server with multiple valid pins (e.g. anycast) + policy.TLS_FORWARD({{'192.0.2.1', pin_sha256={'YQ==', 'Wg=='}}) + -- multiple servers, each with own authenticator + policy.TLS_FORWARD({ -- please note that { here starts list of servers + {'192.0.2.1', pin_sha256='Wg=='}, + -- server must present certificate issued by specified CA and hostname must match + {'2001:DB8::d0c', hostname='res.example.com', ca_file='/etc/knot-resolver/tlsca.crt'} + }) + +Forwarding to multiple targets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With the use of :func:`policy.slice` function, it is possible to split the +entire DNS namespace into distinct slices. When used in conjunction with +: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 algorithm when the slicing + function is initialized. By default, the assignment 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 + + policy.add(policy.slice( + policy.slice_randomize_psl(), + policy.TLS_FORWARD({{'192.0.2.1', hostname='res.example.com'}}), + policy.TLS_FORWARD({ + -- multiple servers can be specified for a single slice + -- the one with lowest round-trip time will be used + {'193.17.47.1', hostname='odvr.nic.cz'}, + {'185.43.135.1', hostname='odvr.nic.cz'}, + }) + )) + +.. note:: The privacy implications of using this feature aren't clear. Since + websites often make requests to multiple domains, these might be forwarded + to different targets. This could result in decreased privacy (e.g. when the + remote targets are both logging or otherwise processing your DNS traffic). + The intended use-case is to use this feature with semi-trusted resolvers + which claim to do no logging (such as those listed on `dnsprivacy.org + <https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Test+Servers>`_), to + decrease the potential exposure of your DNS data to a malicious resolver + operator. + +.. _dns-graft: + +Replacing part of the DNS tree +------------------------------ + +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. + +.. code-block:: lua + :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 policy.STUB is not a chain action. + -- Flags: for "dumb" targets disabling EDNS can help (below) as DNSSEC isn't + -- validated anyway; in some of those cases adding 'NO_0X20' can also help, + -- though it also lowers defenses against off-path attacks on communication + -- between the two servers. + -- With kresd <= 5.5.3 you also needed 'NO_CACHE' flag to avoid unintentional + -- NXDOMAINs that could sometimes happen due to aggressive DNSSEC caching. + policy.add(policy.suffix(policy.FLAGS({'NO_EDNS'}), extraTrees)) + policy.add(policy.suffix(policy.STUB({'2001:db8::1'}), extraTrees)) + +Response policy zones +--------------------- + .. warning:: + + There is no published Internet Standard for RPZ_ and implementations vary. + At the moment Knot Resolver supports limited subset of RPZ format and deviates + 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: + + .. code-block:: none + + ; 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 + + The only "trigger" supported in Knot Resolver is query name, + i.e. left hand side must be a domain name which triggers the action specified + on the right hand side. + + Subset of possible RPZ actions is supported, namely: + + .. csv-table:: + :header: "RPZ Right Hand Side", "Knot Resolver Action", "BIND Compatibility" + + "``.``", "``action`` is used", "compatible if ``action`` is :any:`policy.DENY`" + "``*.``", ":func:`policy.ANSWER`", "yes" + "``rpz-passthru.``", ":any:`policy.PASS`", "yes" + "``rpz-tcp-only.``", ":any:`policy.TC`", "yes" + "``rpz-drop.``", ":any:`policy.DROP`", "no [#]_" + "fake A/AAAA", ":func:`policy.ANSWER`", "yes" + "fake CNAME", "not supported", "no" + + .. [#] Our :any:`policy.DROP` returns *SERVFAIL* answer (for historical reasons). + + + .. note:: + + To debug which domains are affected by RPZ (or other policy actions), you can enable the ``policy`` log group: + + .. code-block:: lua + + log_groups({'policy'}) + + See also :ref:`non-ASCII support note <IDN>`. + + +.. function:: rpz(action, path, [watch = true]) + + :param action: the default action for match in the zone; typically you want :any:`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 + answer with *NXDOMAIN* plus the specified additional text to queries for this domain: + + .. code-block:: lua + + policy.add( + policy.rpz(policy.DENY_MSG('domain blocked by your resolver operator'), + '/etc/knot-resolver/blocklist.rpz', + true)) + + Resolver will reload RPZ file at run-time if the RPZ file changes. + Recommended RPZ update procedure is to store new blocklist in a new file + (*newblocklist.rpz*) and then rename the new file to the original file name + (*blocklist.rpz*). This avoids problems where resolver might attempt + to re-read an incomplete file. + + + +Additional properties +--------------------- + +Most properties (actions, filters) are described above. + +.. function:: 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. + + .. code-block:: lua + + -- mirror all queries, 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) + +.. function:: del(id) + + :param id: identifier of a given rule returned by :func:`policy.add` + :return: boolean ``true`` if rule was deleted, ``false`` otherwise + + Remove a rule from policy list. + +.. function:: todnames({name, ...}) + + :param: names table of domain names in textual format + + Returns table of domain names in wire format converted from strings. + + .. code-block:: lua + + -- Convert single name + assert(todname('example.com') == '\7example\3com\0') + -- Convert table of names + policy.todnames({'example.com', 'me.cz'}) + { '\7example\3com\0', '\2me\2cz\0' } + + +.. _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 +.. _`Jan-Piet Mens's post`: http://jpmens.net/2011/04/26/how-to-configure-your-bind-resolvers-to-lie-using-response-policy-zones-rpz/ +.. _`Transport Layer Security`: https://en.wikipedia.org/wiki/Transport_Layer_Security +.. _`DNS Privacy Project`: https://dnsprivacy.org/ +.. _`IETF draft dprive-dtls-and-tls-profiles`: https://tools.ietf.org/html/draft-ietf-dprive-dtls-and-tls-profiles +.. _SNI: https://en.wikipedia.org/wiki/Server_Name_Indication +.. _`Public Suffix List`: https://publicsuffix.org |