Commit eabf8d64 authored by Petr Špaček's avatar Petr Špaček

Merge branch 'policy_polish' into 'master'

policy: polish policy module up

See merge request knot/knot-resolver!462
parents c2651ace a11dbf2b
......@@ -99,6 +99,20 @@ installcheck:linux:amd64:
- linux
- amd64
doc:
stage: test
script:
- SPHINXFLAGS="-W" make doc
dependencies: []
artifacts:
expire_in: 1 hour
paths:
- ./doc/*
tags:
- docker
deckard:linux:amd64:
stage: test
script:
......
......@@ -10,6 +10,8 @@ RUN apt-get upgrade -y -qqq
# Knot and Knot Resolver dependecies
RUN apt-get install -y -qqq make cmake pkg-config git build-essential bsdmainutils libtool autoconf make pkg-config liburcu-dev libgnutls28-dev libedit-dev liblmdb-dev libcap-ng-dev libsystemd-dev libidn11-dev protobuf-c-compiler libfstrm-dev pkg-config libuv1-dev libcmocka-dev libluajit-5.1-dev lua-sec lua-socket lua-http
# documentation dependecies
RUN apt-get install -y -qqq doxygen python3-sphinx python3-breathe python3-sphinx-rtd-theme
# Python packags required for Deckard CI
# Python: grab latest versions from PyPi
......
......@@ -2,7 +2,7 @@ ifeq ($(HAS_doxygen)|$(HAS_sphinx-build), yes|yes)
doc-doxygen:
@cd doc && $(doxygen_BIN)
doc-html: doc-doxygen
@cd doc && $(sphinx-build_BIN) -b html . html
@cd doc && $(sphinx-build_BIN) $(SPHINXFLAGS) -b html . html
else
doc-html:
$(error doxygen and sphinx must be installed)
......
......@@ -4,11 +4,15 @@ Query policies
--------------
This module can block, rewrite, or alter inbound queries based on user-defined policies.
By default, if no rule applies to a query, rules for special-use domain names are applied, as required by :rfc:`6761`.
You can however extend it 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.
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`_.
There are several policy filters available in the ``policy.`` table:
By default, if 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-zone>`_ domain names are applied. These built-in rules can be overriden using action ``PASS``, see `Policy examples`_ below.
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
......@@ -22,10 +26,13 @@ There are several policy filters available in the ``policy.`` table:
- implements a subset of RPZ_ in zonefile format. See below for details: :any:`policy.rpz`.
* custom filter function
There are several actions available in the ``policy.`` table:
Actions
^^^^^^^
An *action* is function which modifies DNS query. There are several actions available in the ``policy.`` table:
* ``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
* ``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;
......@@ -40,6 +47,8 @@ There are several actions available in the ``policy.`` table:
Most actions stop the policy matching on the query, but "chain actions" allow to keep trying to match other rules, until a non-chain action is triggered.
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 <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.
.. 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.
......@@ -76,14 +85,13 @@ TLS Examples
{'2001:DB8::d0c', hostname='res.example.', ca_file='/etc/knot-resolver/tlsca.crt'}
})
.. _policy_examples:
Other examples
^^^^^^^^^^^^^^
Policy examples
^^^^^^^^^^^^^^^
.. code-block:: lua
-- Load default policies
modules = { 'policy' }
-- Whitelist 'www[0-9].badboy.cz'
policy.add(policy.pattern(policy.PASS, '\4www[0-9]\6badboy\2cz'))
-- Block all names below badboy.cz
......
......@@ -57,8 +57,15 @@ local function addr2sock(target, default_port)
return sock
end
-- policy functions are defined below
local policy = {}
function policy.PASS(state, _)
return state
end
-- Mirror request elsewhere, and continue solving
local function mirror(target)
function policy.MIRROR(target)
local addr, port = addr_split_port(target, 53)
local sink, err = socket_client(addr, port)
if not sink then panic('MIRROR target %s is not a valid: %s', target, err) end
......@@ -84,7 +91,7 @@ local function set_nslist(qry, list)
end
-- Forward request, and solve as stub query
local function stub(target)
function policy.STUB(target)
local list = {}
if type(target) == 'table' then
for _, v in pairs(target) do
......@@ -105,7 +112,7 @@ local function stub(target)
end
-- Forward request and all subrequests to upstream; validate answers
local function forward(target)
function policy.FORWARD(target)
local list = {}
if type(target) == 'table' then
for _, v in pairs(target) do
......@@ -188,7 +195,7 @@ local function tls_forward_target_check_syntax(idx, list_entry)
end
-- Forward request and all subrequests to upstream over TLS; validate answers
local function tls_forward(target)
function policy.TLS_FORWARD(target)
local sockaddr_c_list = {}
local sockaddr_config = {} -- items: { string_addr=<addr string>, auth_type=<auth type> }
local ca_files = {}
......@@ -255,7 +262,7 @@ local function tls_forward(target)
end
-- Rewrite records in packet
local function reroute(tbl, names)
function policy.REROUTE(tbl, names)
-- Import renumbering rules
local ren = require('renumber')
local prefixes = {}
......@@ -267,7 +274,7 @@ local function reroute(tbl, names)
end
-- Set and clear some query flags
local function flags(opts_set, opts_clear)
function policy.FLAGS(opts_set, opts_clear)
return function(_, req)
local qry = req:current()
ffi.C.kr_qflags_set (qry.flags, kres.mk_qflags(opts_set or {}))
......@@ -280,8 +287,8 @@ local function mkauth_soa(answer, dname, mname)
if mname == nil then
mname = dname
end
return answer:put(dname, 900, answer:qclass(), kres.type.SOA,
mname .. '\6nobody\7invalid\0\0\0\0\0\0\0\14\16\0\0\3\132\0\9\58\128\0\0\3\132')
return answer:put(dname, 10800, answer:qclass(), kres.type.SOA,
mname .. '\6nobody\7invalid\0\0\0\0\1\0\0\14\16\0\0\4\176\0\9\58\128\0\0\42\48')
end
local dname_localhost = todname('localhost.')
......@@ -367,15 +374,6 @@ local function localhost_reversed(_, req)
return kres.DONE
end
local policy = {
-- Policies
PASS = 1, DENY = 2, DROP = 3, TC = 4, QTRACE = 5,
FORWARD = forward, TLS_FORWARD = tls_forward,
STUB = stub, REROUTE = reroute, MIRROR = mirror, FLAGS = flags,
-- Special values
ANY = 0,
}
-- All requests
function policy.all(action)
return function(_, _) return action end
......@@ -450,8 +448,9 @@ local function rpz_parse(action, path)
return rules
end
-- RPZ policy set
-- Create RPZ from zone file
local function rpz_zonefile(action, path)
function policy.rpz(action, path)
local rules = rpz_parse(action, path)
collectgarbage()
return function(_, query)
......@@ -465,9 +464,48 @@ local function rpz_zonefile(action, path)
end
end
-- RPZ policy set
function policy.rpz(action, path)
return rpz_zonefile(action, path)
function policy.DENY_MSG(msg)
if msg and (type(msg) ~= 'string' or #msg >= 255) then
error('DENY_MSG: optional msg must be string shorter than 256 characters')
end
return function (_, req)
-- Write authority information
local answer = req.answer
ffi.C.kr_pkt_make_auth_header(answer)
answer:rcode(kres.rcode.NXDOMAIN)
answer:begin(kres.section.AUTHORITY)
mkauth_soa(answer, answer:qname())
if msg then
answer:begin(kres.section.ADDITIONAL)
answer:put('\11explanation\7invalid', 10800, answer:qclass(), kres.type.TXT,
string.char(#msg) .. msg)
end
return kres.DONE
end
end
policy.DENY = policy.DENY_MSG() -- compatibility with < 2.0
function policy.DROP(_, _)
return kres.FAIL
end
function policy.TC(state, req)
local answer = req.answer
if answer.max_size ~= 65535 then
answer:tc(1) -- ^ Only UDP queries
return kres.DONE
else
return state
end
end
function policy.QTRACE(_, req)
local qry = req:current()
req.options.TRACE = true
qry.flags.TRACE = true
return -- this allows to continue iterating over policy list
end
-- Evaluate packet in given rules to determine policy action
......@@ -478,7 +516,7 @@ function policy.evaluate(rules, req, query, state)
local action = rule.cb(req, query)
if action ~= nil then
rule.count = rule.count + 1
local next_state = policy.enforce(state, req, action)
local next_state = action(state, req)
if next_state then -- Not a chain rule,
return next_state -- stop on first match
end
......@@ -488,35 +526,6 @@ function policy.evaluate(rules, req, query, state)
return
end
-- Enforce policy action
function policy.enforce(state, req, action)
if action == policy.DENY then
-- Write authority information
local answer = req.answer
ffi.C.kr_pkt_make_auth_header(answer)
answer:rcode(kres.rcode.NXDOMAIN)
answer:begin(kres.section.AUTHORITY)
mkauth_soa(answer, '\7blocked\0')
return kres.DONE
elseif action == policy.DROP then
return kres.FAIL
elseif action == policy.TC then
local answer = req.answer
if answer.max_size ~= 65535 then
answer:tc(1) -- ^ Only UDP queries
return kres.DONE
end
elseif action == policy.QTRACE then
local qry = req:current()
req.options.TRACE = true
qry.flags.TRACE = true
return -- this allows to continue iterating over policy list
elseif type(action) == 'function' then
return action(state, req)
end
return state
end
-- Top-down policy list walk until we hit a match
-- the caller is responsible for reordering policy list
-- from most specific to least specific.
......@@ -607,7 +616,7 @@ local private_zones = {
'100.51.198.in-addr.arpa.',
'113.0.203.in-addr.arpa.',
'255.255.255.255.in-addr.arpa.',
-- RFC7796
-- RFC7793
'64.100.in-addr.arpa.',
'65.100.in-addr.arpa.',
'66.100.in-addr.arpa.',
......@@ -690,14 +699,22 @@ policy.rules = {}
policy.postrules = {}
policy.special_names = {
{
cb=policy.suffix_common(policy.DENY, private_zones, todname('arpa.')),
cb=policy.suffix_common(policy.DENY_MSG(
'Blocking is mandated by standards, see references on '
.. 'https://www.iana.org/assignments/'
.. 'locally-served-dns-zones/locally-served-dns-zones.xhtml'),
private_zones, todname('arpa.')),
count=0
},
{
cb=policy.suffix(policy.DENY, {
todname('test.'),
todname('invalid.'),
todname('onion.'), -- RFC7686, 2.4
cb=policy.suffix(policy.DENY_MSG(
'Blocking is mandated by standards, see references on '
.. 'https://www.iana.org/assignments/'
.. 'special-use-domain-names/special-use-domain-names.xhtml'),
{
todname('test.'),
todname('onion.'),
todname('invalid.'),
}),
count=0
},
......
......@@ -7,7 +7,7 @@ This modules gathers various counters from the query resolution and server inter
and offers them as a key-value storage. Any module may update the metrics or simply hook
in new ones.
.. code-block:: lua
.. code-block:: none
-- Enumerate metrics
> stats.list()
......
local kres = require('kres')
local policy = require('policy')
local ffi = require('ffi')
local C = ffi.C
......@@ -91,7 +90,12 @@ view.layer = {
local match_cb = evaluate(view, req)
if match_cb ~= nil then
local action = match_cb(req, req:current())
return policy.enforce(state, req, action) or state
if action then
local next_state = action(state, req)
if next_state then -- Not a chain rule,
return next_state -- stop on first match
end
end
end
return state
end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment