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 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 :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 Actions. There are several policy filters available in the policy.
table:
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:
policy.suffix(policy.DENY, policy.todnames({'example.com', 'example.net'}))
It is also possible to define custom filter function with any name.
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:
-- custom filter which matches HINFO queries, action is policy.DENY
policy.add(match_query_type(policy.DENY, kres.type.HINFO))
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:
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:
Custom actions
-- 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
:
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.
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.
Forwarding over TLS protocol (DNS-over-TLS)
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.
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 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:
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
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 conjuction with :func:`policy.TLS_FORWARD`, it's possible to forward different queries to different targets.
These two functions can be used together to forward queries for names in different parts of DNS name space to different target servers:
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), to decrease the potential exposure of your DNS data to a malicious resolver operator.
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.
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.
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:
; 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:
RPZ Right Hand Side Knot Resolver Action BIND Compatibility .
action
is usedcompatible if action
is :func:`policy.DENY`*.
action
is usedgood enough [1] 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 [2] fake A/AAAA not supported no
[1] RPZ action *.
in BIND causes NODATA answer but typically our users configurepolicy.rpz(policy.DENY, ...)
which replies with NXDOMAIN. Good news is that from client's perspective it does not make a visible difference.
[2] Our :func:`policy.DROP` returns SERVFAIL answer (for historical reasons).
Additional properties
Most properties (actions, filters) are described above.