Skip to content
Snippets Groups Projects
Commit ee9857fb authored by Marek Vavruša's avatar Marek Vavruša
Browse files

modules: 'view' that implements views and ACLs

module can identify clients based on their source address or used TSIG
key
parent cf5880f6
Branches
Tags
No related merge requests found
......@@ -185,7 +185,7 @@ local sockaddr_t = ffi.typeof('struct sockaddr')
ffi.metatype( sockaddr_t, {
__index = {
len = function(sa) return C.kr_inaddr_len(sa) end,
addr = function (sa) return ffi.string(C.kr_inaddr(sa), C.kr_inaddr_len(sa)) end,
ip = function (sa) return C.kr_inaddr(sa) end,
}
})
......
......@@ -10,11 +10,12 @@ Implemented modules
:local:
.. include:: ../modules/hints/README.rst
.. include:: ../modules/block/README.rst
.. include:: ../modules/stats/README.rst
.. include:: ../modules/policy/README.rst
.. include:: ../modules/view/README.rst
.. include:: ../modules/predict/README.rst
.. include:: ../modules/cachectl/README.rst
.. include:: ../modules/graphite/README.rst
.. include:: ../modules/ketcd/README.rst
.. include:: ../modules/kmemcached/README.rst
.. include:: ../modules/redis/README.rst
.. include:: ../modules/ketcd/README.rst
.. include:: ../modules/cachectl/README.rst
......@@ -19,6 +19,7 @@
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include "ccan/isaac/isaac.h"
......@@ -195,7 +196,7 @@ int kr_inaddr_len(const struct sockaddr *addr)
return addr->sa_family == AF_INET ? sizeof(struct in_addr) : sizeof(struct in6_addr);
}
int kr_inaddr_family(const char *addr)
int kr_straddr_family(const char *addr)
{
if (!addr) {
return kr_error(EINVAL);
......@@ -204,4 +205,55 @@ int kr_inaddr_family(const char *addr)
return AF_INET6;
}
return AF_INET;
}
int kr_straddr_subnet(void *dst, const char *addr)
{
if (!dst || !addr) {
return kr_error(EINVAL);
}
/* Parse subnet */
int bit_len = 0;
int family = kr_straddr_family(addr);
auto_free char *addr_str = strdup(addr);
char *subnet = strchr(addr_str, '/');
if (subnet) {
*subnet = '\0';
subnet += 1;
bit_len = atoi(subnet);
/* Check client subnet length */
const int max_len = (family == AF_INET6) ? 128 : 32;
if (bit_len < 0 || bit_len > max_len) {
return kr_error(ERANGE);
}
}
/* Parse address */
int ret = inet_pton(family, addr_str, dst);
if (ret < 0) {
return kr_error(EILSEQ);
}
return bit_len;
}
int kr_bitcmp(const char *a, const char *b, int bits)
{
if (!a || !b || bits == 0) {
return kr_error(ENOMEM);
}
/* Compare part byte-divisible part. */
const size_t chunk = bits / 8;
int ret = memcmp(a, b, chunk);
if (ret != 0) {
return ret;
}
a += chunk;
b += chunk;
bits -= chunk * 8;
/* Compare last partial byte address block. */
if (bits > 0) {
const size_t shift = (8 - bits);
ret = ((uint8_t)(*a >> shift) - (uint8_t)(*b >> shift));
}
return ret;
}
\ No newline at end of file
......@@ -85,4 +85,9 @@ const char *kr_inaddr(const struct sockaddr *addr);
/** Address length for given family. */
int kr_inaddr_len(const struct sockaddr *addr);
/** Return address type for string. */
int kr_inaddr_family(const char *addr);
\ No newline at end of file
int kr_straddr_family(const char *addr);
/** Parse address and return subnet length (bits).
* @warning 'dst' must be at least `sizeof(struct in6_addr)` long. */
int kr_straddr_subnet(void *dst, const char *addr);
/** Compare memory bitwise. */
int kr_bitcmp(const char *a, const char *b, int bits);
\ No newline at end of file
......@@ -17,6 +17,7 @@ ifeq ($(HAS_lua),yes)
modules_TARGETS += ketcd \
graphite \
policy \
view \
predict
endif
......
......@@ -61,30 +61,34 @@ function policy.evaluate(policy, req, query)
return policy.PASS
end
-- @function Enforce policy action
function policy.enforce(state, req, action)
if action == policy.DENY then
-- Write authority information
local answer = req.answer
answer:rcode(kres.rcode.NXDOMAIN)
answer:begin(kres.section.AUTHORITY)
answer:put('\7blocked', 900, answer:qclass(), kres.type.SOA,
'\7blocked\0\0\0\0\0\0\0\0\14\16\0\0\3\132\0\9\58\128\0\0\3\132')
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
end
return state
end
-- @function policy layer implementation
policy.layer = {
begin = function(state, req)
req = kres.request_t(req)
local action = policy:evaluate(req, req:current())
if action == policy.DENY then
-- Write authority information
local answer = req.answer
answer:rcode(kres.rcode.NXDOMAIN)
answer:begin(kres.section.AUTHORITY)
answer:put('\7blocked', 900, answer:qclass(), kres.type.SOA,
'\7blocked\0\0\0\0\0\0\0\0\14\16\0\0\3\132\0\9\58\128\0\0\3\132')
return kres.DONE
elseif action == policy.DROP then
return kres.FAIL
elseif action == policy.TC then
local answer = req.answer
print(answer.max_size)
if answer.max_size ~= 65535 then
answer:tc(1) -- ^ Only UDP queries
return kres.DONE
end
end
return state
return policy.enforce(state, req, action)
end
}
......
.. _mod-view:
Views and ACLs
--------------
The :ref:`policy <mod-policy>` module implements policies for global query matching, e.g. solves "how to react to certain query".
This module combines it with query source matching, e.g. "who asked the query". This allows you to create personalized blacklists,
filters and ACLs, sort of like ISC BIND views.
There are two identification mechanisms:
* ``subnet``
- identifies the client based on his subnet
* ``key``
- identifies the client based on a TSIG key
You can combine this information with :ref:`policy <mod-policy>` rules.
.. code-block:: lua
view:addr('10.0.0.1', policy.suffix(policy.TC, {'\7example\3com'}))
This fill force given client subnet to TCP for names in ``example.com``.
You can combine view selectors with RPZ_ to create personalized filters for example.
Example configuration
^^^^^^^^^^^^^^^^^^^^^
.. code-block:: lua
-- Load modules
modules = { 'policy', 'view' }
-- Whitelist queries identified by TSIG key
view:key('\5mykey', function (req, qry) return policy.PASS end)
-- Block local clients (ACL like)
view:addr('127.0.0.1', function (req, qry) return policy.DENY end))
-- Drop queries with suffix match for remote client
view:addr('10.0.0.0/8', policy.suffix(policy.DROP, {'\3xxx'}))
Properties
^^^^^^^^^^
.. function:: view:addr(subnet, rule)
:param subnet: client subnet, i.e. ``10.0.0.1``
:param rule: added rule, i.e. ``policy.pattern(policy.DENY, '[0-9]+\2cz')``
Apply rule to clients in given subnet.
.. function:: view:key(key_name, rule)
:param key: client TSIG key domain name, i.e. ``\5mykey``
:param rule: added rule, i.e. ``policy.pattern(policy.DENY, '[0-9]+\2cz')``
Apply rule to clients with given TSIG key.
.. warning:: This just selects rule based on the key name, it doesn't verify the key or signature yet.
.. _RPZ: https://dnsrpz.info/
local kres = require('kres')
local policy = require('policy')
local ffi = require('ffi')
local C = ffi.C
ffi.cdef[[
int kr_straddr_family(const char *addr);
int kr_straddr_subnet(void *dst, const char *addr);
int kr_bitcmp(const char *a, const char *b, int bits);
]]
-- Module declaration
local view = {
key = {},
subnet = {},
}
-- @function View based on TSIG key name.
function view.tsig(view, tsig, policy)
view.key[tsig] = policy
end
-- @function View based on source IP subnet.
function view.addr(view, subnet, policy)
local subnet_cd = ffi.new('char[16]')
local family = C.kr_straddr_family(subnet)
local bitlen = C.kr_straddr_subnet(subnet_cd, subnet)
table.insert(view.subnet, {family, subnet_cd, bitlen, policy})
end
-- @function Match IP against given subnet
local function match_subnet(family, subnet, bitlen, addr)
return (family == addr.sa_family) and (C.kr_bitcmp(subnet, addr:ip(), bitlen) == 0)
end
-- @function Find view for given request
local function evaluate(view, req)
local answer = req.answer
local client_key = req.qsource.key
local match_cb = (client_key ~= nil) and view.key[client_key:owner()] or nil
-- Search subnets otherwise
if match_cb == nil and req.qsource.addr ~= nil then
for i = 1, #view.subnet do
local pair = view.subnet[i]
if match_subnet(pair[1], pair[2], pair[3], req.qsource.addr) then
match_cb = pair[4]
break
end
end
end
return match_cb
end
-- @function Module layers
view.layer = {
begin = function(state, req)
req = kres.request_t(req)
local match_cb = evaluate(view, req)
if match_cb ~= nil then
local action = match_cb(req, req:current())
return policy.enforce(state, req, action)
end
return state
end
}
return view
\ No newline at end of file
view_SOURCES := view.lua
$(call make_lua_module,view)
......@@ -43,10 +43,46 @@ static void test_strcatdup(void **state)
(void)(null_sock);
}
static inline int test_bitcmp(const char *subnet, const char *str_addr, size_t len)
{
char addr_buf[16] = {'\0'};
kr_straddr_subnet(addr_buf, str_addr);
return kr_bitcmp(subnet, addr_buf, len);
}
static void test_straddr(void **state)
{
const char *ip4_ok = "1.2.3.0/30";
const char *ip4_bad = "1.2.3.0/33";
const char *ip4_in = "1.2.3.1";
const char *ip4_out = "1.2.3.5";
const char *ip6_ok = "7caa::/4";
const char *ip6_bad = "7caa::/129";
const char *ip6_in = "7caa::aa7c";
const char *ip6_out = "8caa::aa7c";
/* Parsing family */
assert_int_equal(kr_straddr_family(ip4_ok), AF_INET);
assert_int_equal(kr_straddr_family(ip4_in), AF_INET);
assert_int_equal(kr_straddr_family(ip6_ok), AF_INET6);
assert_int_equal(kr_straddr_family(ip6_in), AF_INET6);
/* Parsing subnet */
char ip4_sub[4], ip6_sub[16];
assert_true(kr_straddr_subnet(ip4_sub, ip4_bad) < 0);
assert_int_equal(kr_straddr_subnet(ip4_sub, ip4_ok), 30);
assert_true(kr_straddr_subnet(ip6_sub, ip6_bad) < 0);
assert_int_equal(kr_straddr_subnet(ip6_sub, ip6_ok), 4);
/* Matching subnet */
assert_int_equal(test_bitcmp(ip4_sub, ip4_in, 30), 0);
assert_int_not_equal(test_bitcmp(ip4_sub, ip4_out, 30), 0);
assert_int_equal(test_bitcmp(ip6_sub, ip6_in, 4), 0);
assert_int_not_equal(test_bitcmp(ip6_sub, ip6_out, 4), 0);
}
int main(void)
{
const UnitTest tests[] = {
unit_test(test_strcatdup),
unit_test(test_straddr),
};
return run_tests(tests);
......
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